mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into develop
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<div class="graph-legend-popover">
|
||||
<div ng-show="ctrl.series" class="p-b-1">
|
||||
<label>Y Axis:</label>
|
||||
<button ng-click="ctrl.toggleAxis(yaxis);" class="btn btn-small"
|
||||
ng-class="{'btn-success': ctrl.series.yaxis === 1,
|
||||
'btn-inverse': ctrl.series.yaxis === 2}">
|
||||
Left
|
||||
</button>
|
||||
<button ng-click="ctrl.toggleAxis(yaxis);"
|
||||
class="btn btn-small"
|
||||
ng-class="{'btn-success': ctrl.series.yaxis === 2,
|
||||
'btn-inverse': ctrl.series.yaxis === 1}">
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="m-b-0">
|
||||
<i ng-repeat="color in ctrl.colors" class="pointer fa fa-circle"
|
||||
ng-style="{color:color}"
|
||||
ng-click="ctrl.colorSelected(color);"> </i>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class ColorPickerCtrl {
|
||||
colors: any;
|
||||
autoClose: boolean;
|
||||
series: any;
|
||||
showAxisControls: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, $rootScope) {
|
||||
this.colors = $rootScope.colors;
|
||||
this.autoClose = $scope.autoClose;
|
||||
this.series = $scope.series;
|
||||
}
|
||||
|
||||
toggleAxis(yaxis) {
|
||||
this.$scope.toggleAxis();
|
||||
|
||||
if (this.$scope.autoClose) {
|
||||
this.$scope.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
colorSelected(color) {
|
||||
this.$scope.colorSelected(color);
|
||||
if (this.$scope.autoClose) {
|
||||
this.$scope.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function colorPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: ColorPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
template: template,
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPicker', colorPicker);
|
||||
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
45
public/app/core/components/colorpicker/ColorPalette.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { sortedColors } from 'app/core/utils/colors';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class GfColorPalette extends React.Component<IProps, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
return () => {
|
||||
this.props.onColorSelect(color);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const colorPaletteItems = this.paletteColors.map((paletteColor) => {
|
||||
const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle';
|
||||
return (
|
||||
<i key={paletteColor} className={"pointer fa " + cssClass}
|
||||
style={{'color': paletteColor}}
|
||||
onClick={this.onColorSelect(paletteColor)}>
|
||||
</i>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
<p className="m-b-0">{colorPaletteItems}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPalette', function (reactDirective) {
|
||||
return reactDirective(GfColorPalette, ['color', 'onColorSelect']);
|
||||
});
|
||||
84
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
84
public/app/core/components/colorpicker/ColorPicker.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onChange: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<IProps, any> {
|
||||
pickerElem: any;
|
||||
colorPickerDrop: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.openColorPicker = this.openColorPicker.bind(this);
|
||||
this.closeColorPicker = this.closeColorPicker.bind(this);
|
||||
this.setPickerElem = this.setPickerElem.bind(this);
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
setPickerElem(elem) {
|
||||
this.pickerElem = $(elem);
|
||||
}
|
||||
|
||||
openColorPicker() {
|
||||
const dropContent = (
|
||||
<ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />
|
||||
);
|
||||
|
||||
let dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
let drop = new Drop({
|
||||
target: this.pickerElem[0],
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover drop-popover--form',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: "none both" }]
|
||||
}
|
||||
});
|
||||
|
||||
drop.on('close', this.closeColorPicker);
|
||||
|
||||
this.colorPickerDrop = drop;
|
||||
this.colorPickerDrop.open();
|
||||
}
|
||||
|
||||
closeColorPicker() {
|
||||
setTimeout(() => {
|
||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||
this.colorPickerDrop.destroy();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
this.props.onChange(color);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
|
||||
<div className="sp-preview">
|
||||
<div className="sp-preview-inner" style={{backgroundColor: this.props.color}}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('colorPicker', function (reactDirective) {
|
||||
return reactDirective(ColorPicker, [
|
||||
'color',
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }]
|
||||
]);
|
||||
});
|
||||
121
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
121
public/app/core/components/colorpicker/ColorPickerPopover.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { GfColorPalette } from './ColorPalette';
|
||||
import { GfSpectrumPicker } from './SpectrumPicker';
|
||||
|
||||
const DEFAULT_COLOR = '#000000';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPickerPopover extends React.Component<IProps, any> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
color: this.props.color || DEFAULT_COLOR,
|
||||
colorString: this.props.color || DEFAULT_COLOR
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
let newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
colorString: newColor.toString()
|
||||
});
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color) {
|
||||
let rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e) {
|
||||
let colorString = e.target.value;
|
||||
this.setState({
|
||||
colorString: colorString
|
||||
});
|
||||
|
||||
let newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
});
|
||||
this.props.onColorSelect(newColor);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e) {
|
||||
let colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', (e) => {
|
||||
// use href attr (#name => name)
|
||||
let tab = e.target.hash.slice(1);
|
||||
this.setState({
|
||||
tab: tab
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const paletteTab = (
|
||||
<div id="palette">
|
||||
<GfColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
const spectrumTab = (
|
||||
<div id="spectrum">
|
||||
<GfSpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} options={{}} />
|
||||
</div>
|
||||
);
|
||||
const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
|
||||
|
||||
return (
|
||||
<div className="gf-color-picker">
|
||||
<ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#palette" data-toggle="tab">Colors</a>
|
||||
</li>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#spectrum" data-toggle="tab">Custom</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="gf-color-picker__body">
|
||||
{currentTab}
|
||||
</div>
|
||||
<div>
|
||||
<input className="gf-form-input gf-form-input--small" value={this.state.colorString}
|
||||
onChange={this.onColorStringChange.bind(this)} onBlur={this.onColorStringBlur.bind(this)}>
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfColorPickerPopover', function (reactDirective) {
|
||||
return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']);
|
||||
});
|
||||
55
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
55
public/app/core/components/colorpicker/SeriesColorPicker.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {ColorPickerPopover} from './ColorPickerPopover';
|
||||
|
||||
export interface IProps {
|
||||
series: any;
|
||||
onColorChange: (color: string) => void;
|
||||
onToggleAxis: () => void;
|
||||
}
|
||||
|
||||
export class SeriesColorPicker extends React.Component<IProps, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
this.onToggleAxis = this.onToggleAxis.bind(this);
|
||||
}
|
||||
|
||||
onColorChange(color) {
|
||||
this.props.onColorChange(color);
|
||||
}
|
||||
|
||||
onToggleAxis() {
|
||||
this.props.onToggleAxis();
|
||||
}
|
||||
|
||||
renderAxisSelection() {
|
||||
const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||
const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
|
||||
|
||||
return (
|
||||
<div className="p-b-1">
|
||||
<label className="small p-r-1">Y Axis:</label>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
|
||||
Left
|
||||
</button>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
{this.props.series && this.renderAxisSelection()}
|
||||
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('seriesColorPicker', function(reactDirective) {
|
||||
return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
|
||||
});
|
||||
76
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
76
public/app/core/components/colorpicker/SpectrumPicker.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
|
||||
export interface IProps {
|
||||
color: string;
|
||||
options: object;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class GfSpectrumPicker extends React.Component<IProps, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let spectrumOptions = _.assignIn({
|
||||
flat: true,
|
||||
showAlpha: true,
|
||||
showButtons: false,
|
||||
color: this.props.color,
|
||||
appendTo: this.elem,
|
||||
move: this.onSpectrumMove,
|
||||
}, this.props.options);
|
||||
|
||||
this.elem.spectrum(spectrumOptions);
|
||||
this.elem.spectrum('show');
|
||||
this.elem.spectrum('set', this.props.color);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
// If user move pointer over spectrum field this produce 'move' event and component
|
||||
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
||||
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
||||
// is called after updating occurs (when user finished moving).
|
||||
if (!this.isMoving) {
|
||||
this.elem.spectrum('set', nextProps.color);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.isMoving) {
|
||||
this.isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.elem.spectrum('destroy');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="spectrum-container" ref={this.setComponentElem}></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('gfSpectrumPicker', function (reactDirective) {
|
||||
return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']);
|
||||
});
|
||||
@@ -4,9 +4,6 @@ import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
Not finding dashboard you want? Star it first, then it should appear in this select box.
|
||||
</info-popover>
|
||||
`;
|
||||
|
||||
export class DashboardSelectorCtrl {
|
||||
|
||||
@@ -7,6 +7,7 @@ import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {profiler} from 'app/core/profiler';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
export class GrafanaCtrl {
|
||||
|
||||
@@ -105,6 +106,11 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (data.params.kiosk) {
|
||||
appEvents.emit('toggle-kiosk-mode');
|
||||
}
|
||||
|
||||
// close all drops
|
||||
for (let drop of Drop.drops) {
|
||||
drop.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// handle kiosk mode
|
||||
|
||||
@@ -27,6 +27,8 @@ export function infoPopover() {
|
||||
|
||||
transclude(function(clone, newScope) {
|
||||
var content = document.createElement("div");
|
||||
content.className = 'markdown-html';
|
||||
|
||||
_.each(clone, (node) => {
|
||||
content.appendChild(node);
|
||||
});
|
||||
|
||||
@@ -1,201 +1,22 @@
|
||||
|
||||
/** Created by: Alex Wendland (me@alexwendland.com), 2014-08-06
|
||||
*
|
||||
* angular-json-tree
|
||||
*
|
||||
* Directive for creating a tree-view out of a JS Object. Only loads
|
||||
* sub-nodes on demand in order to improve performance of rendering large
|
||||
* objects.
|
||||
*
|
||||
* Attributes:
|
||||
* - object (Object, 2-way): JS object to build the tree from
|
||||
* - start-expanded (Boolean, 1-way, ?=true): should the tree default to expanded
|
||||
*
|
||||
* Usage:
|
||||
* // In the controller
|
||||
* scope.someObject = {
|
||||
* test: 'hello',
|
||||
* array: [1,1,2,3,5,8]
|
||||
* };
|
||||
* // In the html
|
||||
* <json-tree object="someObject"></json-tree>
|
||||
*
|
||||
* Dependencies:
|
||||
* - utils (json-tree.js)
|
||||
* - ajsRecursiveDirectiveHelper (json-tree.js)
|
||||
*
|
||||
* Test: json-tree-test.js
|
||||
*/
|
||||
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var utils = {
|
||||
/* See link for possible type values to check against.
|
||||
* http://stackoverflow.com/questions/4622952/json-object-containing-array
|
||||
*
|
||||
* Value Class Type
|
||||
* -------------------------------------
|
||||
* "foo" String string
|
||||
* new String("foo") String object
|
||||
* 1.2 Number number
|
||||
* new Number(1.2) Number object
|
||||
* true Boolean boolean
|
||||
* new Boolean(true) Boolean object
|
||||
* new Date() Date object
|
||||
* new Error() Error object
|
||||
* [1,2,3] Array object
|
||||
* new Array(1, 2, 3) Array object
|
||||
* new Function("") Function function
|
||||
* /abc/g RegExp object (function in Nitro/V8)
|
||||
* new RegExp("meow") RegExp object (function in Nitro/V8)
|
||||
* {} Object object
|
||||
* new Object() Object object
|
||||
*/
|
||||
is: function is(obj, clazz) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1) === clazz;
|
||||
},
|
||||
|
||||
// See above for possible values
|
||||
whatClass: function whatClass(obj) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1);
|
||||
},
|
||||
|
||||
// Iterate over an objects keyset
|
||||
forKeys: function forKeys(obj, f) {
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key) && typeof obj[key] !== 'function') {
|
||||
if (f(key, obj[key])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
import {JsonExplorer} from '../json_explorer/json_explorer';
|
||||
|
||||
coreModule.directive('jsonTree', [function jsonTreeDirective() {
|
||||
return {
|
||||
return{
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
object: '=',
|
||||
startExpanded: '@',
|
||||
rootName: '@',
|
||||
},
|
||||
template: '<json-node key="rootName" value="object" start-expanded="startExpanded"></json-node>'
|
||||
};
|
||||
}]);
|
||||
link: function(scope, elem) {
|
||||
|
||||
coreModule.directive('jsonNode', ['ajsRecursiveDirectiveHelper', function jsonNodeDirective(ajsRecursiveDirectiveHelper) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
key: '=',
|
||||
value: '=',
|
||||
startExpanded: '@'
|
||||
},
|
||||
compile: function jsonNodeDirectiveCompile(elem) {
|
||||
return ajsRecursiveDirectiveHelper.compile(elem, this);
|
||||
},
|
||||
template: ' <span class="json-tree-key" ng-click="toggleExpanded()">{{key}}</span>' +
|
||||
' <span class="json-tree-leaf-value" ng-if="!isExpandable">{{value}}</span>' +
|
||||
' <span class="json-tree-branch-preview" ng-if="isExpandable" ng-show="!isExpanded" ng-click="toggleExpanded()">' +
|
||||
' {{preview}}</span>' +
|
||||
' <ul class="json-tree-branch-value" ng-if="isExpandable && shouldRender" ng-show="isExpanded">' +
|
||||
' <li ng-repeat="(subkey,subval) in value">' +
|
||||
' <json-node key="subkey" value="subval"></json-node>' +
|
||||
' </li>' +
|
||||
' </ul>',
|
||||
pre: function jsonNodeDirectiveLink(scope, elem, attrs) {
|
||||
// Set value's type as Class for CSS styling
|
||||
elem.addClass(utils.whatClass(scope.value).toLowerCase());
|
||||
// If the value is an Array or Object, use expandable view type
|
||||
if (utils.is(scope.value, 'Object') || utils.is(scope.value, 'Array')) {
|
||||
scope.isExpandable = true;
|
||||
// Add expandable class for CSS usage
|
||||
elem.addClass('expandable');
|
||||
// Setup preview text
|
||||
var isArray = utils.is(scope.value, 'Array');
|
||||
scope.preview = isArray ? '[ ' : '{ ';
|
||||
utils.forKeys(scope.value, function jsonNodeDirectiveLinkForKeys(key, value) {
|
||||
if (value === null) { scope.value[key] = 'null'; }
|
||||
if (isArray) {
|
||||
scope.preview += value + ', ';
|
||||
} else {
|
||||
scope.preview += key + ': ' + value + ', ';
|
||||
}
|
||||
});
|
||||
scope.preview = scope.preview.substring(0, scope.preview.length - (scope.preview.length > 2 ? 2 : 0)) + (isArray ? ' ]' : ' }');
|
||||
// If directive initially has isExpanded set, also set shouldRender to true
|
||||
if (scope.startExpanded) {
|
||||
scope.shouldRender = true;
|
||||
elem.addClass('expanded');
|
||||
}
|
||||
// Setup isExpanded state handling
|
||||
scope.isExpanded = scope.startExpanded;
|
||||
scope.toggleExpanded = function jsonNodeDirectiveToggleExpanded() {
|
||||
scope.isExpanded = !scope.isExpanded;
|
||||
if (scope.isExpanded) {
|
||||
elem.addClass('expanded');
|
||||
} else {
|
||||
elem.removeClass('expanded');
|
||||
}
|
||||
// For delaying subnode render until requested
|
||||
scope.shouldRender = true;
|
||||
};
|
||||
} else {
|
||||
scope.isExpandable = false;
|
||||
// Add expandable class for CSS usage
|
||||
elem.addClass('not-expandable');
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/** Added by: Alex Wendland (me@alexwendland.com), 2014-08-09
|
||||
* Source: http://stackoverflow.com/questions/14430655/recursion-in-angular-directives
|
||||
*
|
||||
* Used to allow for recursion within directives
|
||||
*/
|
||||
coreModule.factory('ajsRecursiveDirectiveHelper', ['$compile', function RecursiveDirectiveHelper($compile) {
|
||||
return {
|
||||
/**
|
||||
* Manually compiles the element, fixing the recursion loop.
|
||||
* @param element
|
||||
* @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
|
||||
* @returns An object containing the linking functions.
|
||||
*/
|
||||
compile: function RecursiveDirectiveHelperCompile(element, link) {
|
||||
// Normalize the link parameter
|
||||
if (angular.isFunction(link)) {
|
||||
link = {
|
||||
post: link
|
||||
};
|
||||
}
|
||||
|
||||
// Break the recursion loop by removing the contents
|
||||
var contents = element.contents().remove();
|
||||
var compiledContents;
|
||||
return {
|
||||
pre: (link && link.pre) ? link.pre : null,
|
||||
/**
|
||||
* Compiles and re-adds the contents
|
||||
*/
|
||||
post: function RecursiveDirectiveHelperCompilePost(scope, element) {
|
||||
// Compile the contents
|
||||
if (!compiledContents) {
|
||||
compiledContents = $compile(contents);
|
||||
}
|
||||
// Re-add the compiled contents to the element
|
||||
compiledContents(scope, function (clone) {
|
||||
element.append(clone);
|
||||
});
|
||||
|
||||
// Call the post-linking function, if any
|
||||
if (link && link.post) {
|
||||
link.post.apply(null, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
var jsonExp = new JsonExplorer(scope.object, 3, {
|
||||
animateOpen: true
|
||||
});
|
||||
|
||||
const html = jsonExp.render(true);
|
||||
elem.html(html);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./directives/dropdown_typeahead";
|
||||
import "./directives/metric_segment";
|
||||
import "./directives/misc";
|
||||
import "./directives/ng_model_on_blur";
|
||||
import "./directives/spectrum_picker";
|
||||
import "./directives/tags";
|
||||
import "./directives/value_select_dropdown";
|
||||
import "./directives/rebuild_on_change";
|
||||
@@ -16,12 +15,13 @@ import './partials';
|
||||
import './components/jsontree/jsontree';
|
||||
import './components/code_editor/code_editor';
|
||||
import './utils/outline';
|
||||
import './components/colorpicker/ColorPicker';
|
||||
import './components/colorpicker/SeriesColorPicker';
|
||||
|
||||
import {grafanaAppDirective} from './components/grafana_app';
|
||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||
import {searchDirective} from './components/search/search';
|
||||
import {infoPopover} from './components/info_popover';
|
||||
import {colorPicker} from './components/colorpicker';
|
||||
import {navbarDirective} from './components/navbar/navbar';
|
||||
import {arrayJoin} from './directives/array_join';
|
||||
import {liveSrv} from './live/live_srv';
|
||||
@@ -58,7 +58,6 @@ export {
|
||||
sideMenuDirective,
|
||||
navbarDirective,
|
||||
searchDirective,
|
||||
colorPicker,
|
||||
liveSrv,
|
||||
layoutSelector,
|
||||
switchDirective,
|
||||
|
||||
@@ -79,7 +79,9 @@ function (_, $, coreModule) {
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions({ $query: query }).then(function(altSegments) {
|
||||
$scope.altSegments = altSegments;
|
||||
options = _.map($scope.altSegments, function(alt) { return alt.value; });
|
||||
options = _.map($scope.altSegments, function(alt) {
|
||||
return _.escape(alt.value);
|
||||
});
|
||||
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'../core_module',
|
||||
'vendor/spectrum',
|
||||
],
|
||||
function (angular, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('spectrumPicker', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: 'ngModel',
|
||||
scope: false,
|
||||
replace: true,
|
||||
template: "<span><input class='input-small' /></span>",
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
var input = element.find('input');
|
||||
var options = angular.extend({
|
||||
showAlpha: true,
|
||||
showButtons: false,
|
||||
color: ngModel.$viewValue,
|
||||
change: function(color) {
|
||||
scope.$apply(function() {
|
||||
ngModel.$setViewValue(color.toRgbString());
|
||||
});
|
||||
}
|
||||
}, scope.$eval(attrs.options));
|
||||
|
||||
ngModel.$render = function() {
|
||||
input.spectrum('set', ngModel.$viewValue || '');
|
||||
};
|
||||
|
||||
input.spectrum(options);
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
input.spectrum('destroy');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -88,6 +88,7 @@ function (angular, $, coreModule) {
|
||||
typeahead: {
|
||||
source: angular.isFunction(scope.$parent[attrs.typeaheadSource]) ? scope.$parent[attrs.typeaheadSource] : null
|
||||
},
|
||||
widthClass: attrs.widthClass,
|
||||
itemValue: getItemProperty(scope, attrs.itemvalue),
|
||||
itemText : getItemProperty(scope, attrs.itemtext),
|
||||
tagClass : angular.isFunction(scope.$parent[attrs.tagclass]) ?
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export class NavModelSrv {
|
||||
|
||||
menu.push({
|
||||
title: 'Annotations',
|
||||
icon: 'fa fa-fw fa-bolt',
|
||||
icon: 'fa fa-fw fa-comment',
|
||||
clickHandler: () => dashNavCtrl.openEditView('annotations')
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = "rgba(11, 237, 50, 1)";
|
||||
export const ALERTING_COLOR = "rgba(237, 46, 24, 1)";
|
||||
export const NO_DATA_COLOR = "rgba(150, 150, 150, 1)";
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
export default [
|
||||
let colors = [
|
||||
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
|
||||
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
|
||||
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
|
||||
@@ -10,3 +19,26 @@ export default [
|
||||
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
|
||||
];
|
||||
|
||||
export function sortColorsByHue(hexColors) {
|
||||
let hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
let sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
sortedHSLColors = _.map(sortedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors));
|
||||
|
||||
return _.map(sortedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
export function hexToHsl(color) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
export function hslToHex(color) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
export default colors;
|
||||
|
||||
@@ -5,7 +5,7 @@ import moment from 'moment';
|
||||
|
||||
var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
|
||||
|
||||
export function parse(text, roundUp?) {
|
||||
export function parse(text, roundUp?, timezone?) {
|
||||
if (!text) { return undefined; }
|
||||
if (moment.isMoment(text)) { return text; }
|
||||
if (_.isDate(text)) { return moment(text); }
|
||||
@@ -16,7 +16,11 @@ export function parse(text, roundUp?) {
|
||||
var parseString;
|
||||
|
||||
if (text.substring(0, 3) === 'now') {
|
||||
time = moment();
|
||||
if (timezone === 'utc') {
|
||||
time = moment.utc();
|
||||
} else {
|
||||
time = moment();
|
||||
}
|
||||
mathString = text.substring('now'.length);
|
||||
} else {
|
||||
index = text.indexOf('||');
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
declare var window: any;
|
||||
|
||||
const DEFAULT_DATETIME_FORMAT: String = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
|
||||
export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = excel ? 'sep=;\n' : '' + 'Series;Time;Value\n';
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
|
||||
@@ -18,7 +16,7 @@ export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATET
|
||||
}
|
||||
|
||||
export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
|
||||
var text = excel ? 'sep=;\n' : '' + 'Time;';
|
||||
var text = (excel ? 'sep=;\n' : '') + 'Time;';
|
||||
// add header
|
||||
_.each(seriesList, function(series) {
|
||||
text += series.alias + ';';
|
||||
|
||||
@@ -402,6 +402,10 @@ function($, _, moment) {
|
||||
kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
|
||||
kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴');
|
||||
kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$');
|
||||
kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
|
||||
kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
||||
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@@ -756,6 +760,10 @@ function($, _, moment) {
|
||||
{text: 'Rubles (₽)', value: 'currencyRUB'},
|
||||
{text: 'Hryvnias (₴)', value: 'currencyUAH'},
|
||||
{text: 'Real (R$)', value: 'currencyBRL'},
|
||||
{text: 'Danish Krone (kr)', value: 'currencyDKK'},
|
||||
{text: 'Icelandic Krone (kr)', value: 'currencyISK'},
|
||||
{text: 'Norwegian Krone (kr)', value: 'currencyNOK'},
|
||||
{text: 'Swedish Krone (kr)', value: 'currencySEK'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<form name="passwordForm" class="gf-form-group">
|
||||
<div class="gf-form" >
|
||||
<editor-checkbox text="Grafana Admin" model="permissions.isGrafanaAdmin" style="line-height: 1.5rem;"></editor-checkbox>
|
||||
<gf-form-switch class="gf-form" label="Grafana Admin" checked="permissions.isGrafanaAdmin" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
|
||||
@@ -128,7 +128,6 @@ function joinEvalMatches(matches, separator: string) {
|
||||
}
|
||||
|
||||
function getAlertAnnotationInfo(ah) {
|
||||
|
||||
// backward compatability, 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
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import alertDef from '../alerting/alert_def';
|
||||
|
||||
/** @ngInject **/
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, popoverSrv, $compile) {
|
||||
|
||||
function sanitizeString(str) {
|
||||
try {
|
||||
@@ -21,6 +19,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
"event": "=",
|
||||
"onEdit": "&"
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var event = scope.event;
|
||||
@@ -31,33 +30,46 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
var tooltip = '<div class="graph-annotation">';
|
||||
var titleStateClass = '';
|
||||
|
||||
if (event.source.name === 'panel-alert') {
|
||||
if (event.alertId) {
|
||||
var stateModel = alertDef.getStateDisplayModel(event.newState);
|
||||
titleStateClass = stateModel.stateClass;
|
||||
title = `<i class="icon-gf ${stateModel.iconClass}"></i> ${stateModel.text}`;
|
||||
text = alertDef.getAlertAnnotationInfo(event);
|
||||
if (event.text) {
|
||||
text = text + '<br />' + event.text;
|
||||
}
|
||||
} else if (title) {
|
||||
text = title + '<br />' + text;
|
||||
title = '';
|
||||
}
|
||||
|
||||
tooltip += `
|
||||
<div class="graph-annotation-header">
|
||||
<span class="graph-annotation-title ${titleStateClass}">${sanitizeString(title)}</span>
|
||||
<span class="graph-annotation-time">${dashboard.formatDate(event.min)}</span>
|
||||
</div>
|
||||
var header = `<div class="graph-annotation__header">`;
|
||||
if (event.login) {
|
||||
header += `<div class="graph-annotation__user" bs-tooltip="'Created by ${event.login}'"><img src="${event.avatarUrl}" /></div>`;
|
||||
}
|
||||
header += `
|
||||
<span class="graph-annotation__title ${titleStateClass}">${sanitizeString(title)}</span>
|
||||
<span class="graph-annotation__time">${dashboard.formatDate(event.min)}</span>
|
||||
`;
|
||||
|
||||
tooltip += '<div class="graph-annotation-body">';
|
||||
// Show edit icon only for users with at least Editor role
|
||||
if (event.id && contextSrv.isEditor) {
|
||||
header += `
|
||||
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
||||
<i class="fa fa-pencil-square"></i>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
header += `</div>`;
|
||||
tooltip += header;
|
||||
tooltip += '<div class="graph-annotation__body">';
|
||||
|
||||
if (text) {
|
||||
tooltip += sanitizeString(text).replace(/\n/g, '<br>') + '<br>';
|
||||
tooltip += '<div>' + sanitizeString(text).replace(/\n/g, '<br>') + '</div>';
|
||||
}
|
||||
|
||||
var tags = event.tags;
|
||||
if (_.isString(event.tags)) {
|
||||
tags = event.tags.split(',');
|
||||
if (tags.length === 1) {
|
||||
tags = event.tags.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
if (tags && tags.length) {
|
||||
scope.tags = tags;
|
||||
@@ -65,6 +77,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, $compile) {
|
||||
}
|
||||
|
||||
tooltip += "</div>";
|
||||
tooltip += '</div>';
|
||||
|
||||
var $tooltip = $(tooltip);
|
||||
$tooltip.appendTo(element);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import './editor_ctrl';
|
||||
|
||||
import angular from 'angular';
|
||||
@@ -11,11 +9,7 @@ export class AnnotationsSrv {
|
||||
alertStatesPromise: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope,
|
||||
private $q,
|
||||
private datasourceSrv,
|
||||
private backendSrv,
|
||||
private timeSrv) {
|
||||
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
|
||||
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
|
||||
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
|
||||
}
|
||||
@@ -26,64 +20,40 @@ export class AnnotationsSrv {
|
||||
}
|
||||
|
||||
getAnnotations(options) {
|
||||
return this.$q.all([
|
||||
this.getGlobalAnnotations(options),
|
||||
this.getPanelAnnotations(options),
|
||||
this.getAlertStates(options)
|
||||
]).then(results => {
|
||||
return this.$q
|
||||
.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||
.then(results => {
|
||||
// combine the annotations and flatten results
|
||||
var annotations = _.flattenDeep(results[0]);
|
||||
|
||||
// combine the annotations and flatten results
|
||||
var annotations = _.flattenDeep([results[0], results[1]]);
|
||||
|
||||
// filter out annotations that do not belong to requesting panel
|
||||
annotations = _.filter(annotations, item => {
|
||||
// shownIn === 1 requires annotation matching panel id
|
||||
if (item.source.showIn === 1) {
|
||||
if (item.panelId && options.panel.id === item.panelId) {
|
||||
return true;
|
||||
// filter out annotations that do not belong to requesting panel
|
||||
annotations = _.filter(annotations, item => {
|
||||
// if event has panel id and query is of type dashboard then panel and requesting panel id must match
|
||||
if (item.panelId && item.source.type === 'dashboard') {
|
||||
return item.panelId === options.panel.id;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
annotations = dedupAnnotations(annotations);
|
||||
annotations = makeRegions(annotations, options);
|
||||
|
||||
// look for alert state for this panel
|
||||
var alertState = _.find(results[1], {panelId: options.panel.id});
|
||||
|
||||
return {
|
||||
annotations: annotations,
|
||||
alertState: alertState,
|
||||
};
|
||||
})
|
||||
.catch(err => {
|
||||
if (!err.message && err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
return true;
|
||||
console.log('AnnotationSrv.query error', err);
|
||||
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', err.message || err]);
|
||||
return [];
|
||||
});
|
||||
|
||||
// look for alert state for this panel
|
||||
var alertState = _.find(results[2], {panelId: options.panel.id});
|
||||
|
||||
return {
|
||||
annotations: annotations,
|
||||
alertState: alertState,
|
||||
};
|
||||
|
||||
}).catch(err => {
|
||||
if (!err.message && err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
this.$rootScope.appEvent('alert-error', ['Annotation Query Failed', (err.message || err)]);
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
getPanelAnnotations(options) {
|
||||
var panel = options.panel;
|
||||
var dashboard = options.dashboard;
|
||||
|
||||
if (dashboard.id && panel && panel.alert) {
|
||||
return this.backendSrv.get('/api/annotations', {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
limit: 100,
|
||||
panelId: panel.id,
|
||||
dashboardId: dashboard.id,
|
||||
}).then(results => {
|
||||
// this built in annotation source name `panel-alert` is used in annotation tooltip
|
||||
// to know that this annotation is from panel alert
|
||||
return this.translateQueryResult({iconColor: '#AA0000', name: 'panel-alert'}, results);
|
||||
});
|
||||
}
|
||||
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
getAlertStates(options) {
|
||||
@@ -104,43 +74,55 @@ export class AnnotationsSrv {
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
|
||||
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {
|
||||
dashboardId: options.dashboard.id,
|
||||
});
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
getGlobalAnnotations(options) {
|
||||
var dashboard = options.dashboard;
|
||||
|
||||
if (dashboard.annotations.list.length === 0) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
if (this.globalAnnotationsPromise) {
|
||||
return this.globalAnnotationsPromise;
|
||||
}
|
||||
|
||||
var annotations = _.filter(dashboard.annotations.list, {enable: true});
|
||||
var range = this.timeSrv.timeRange();
|
||||
var promises = [];
|
||||
|
||||
for (let annotation of dashboard.annotations.list) {
|
||||
if (!annotation.enable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => {
|
||||
if (annotation.snapshotData) {
|
||||
return this.translateQueryResult(annotation, annotation.snapshotData);
|
||||
}
|
||||
|
||||
return this.datasourceSrv.get(annotation.datasource).then(datasource => {
|
||||
// issue query against data source
|
||||
return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation});
|
||||
})
|
||||
.then(results => {
|
||||
// store response in annotation object if this is a snapshot call
|
||||
if (dashboard.snapshot) {
|
||||
annotation.snapshotData = angular.copy(results);
|
||||
}
|
||||
// translate result
|
||||
return this.translateQueryResult(annotation, results);
|
||||
});
|
||||
}));
|
||||
promises.push(
|
||||
this.datasourceSrv
|
||||
.get(annotation.datasource)
|
||||
.then(datasource => {
|
||||
// issue query against data source
|
||||
return datasource.annotationQuery({
|
||||
range: range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
// store response in annotation object if this is a snapshot call
|
||||
if (dashboard.snapshot) {
|
||||
annotation.snapshotData = angular.copy(results);
|
||||
}
|
||||
// translate result
|
||||
return this.translateQueryResult(annotation, results);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.globalAnnotationsPromise = this.$q.all(promises);
|
||||
return this.globalAnnotationsPromise;
|
||||
}
|
||||
|
||||
@@ -149,6 +131,21 @@ export class AnnotationsSrv {
|
||||
return this.backendSrv.post('/api/annotations', annotation);
|
||||
}
|
||||
|
||||
updateAnnotationEvent(annotation) {
|
||||
this.globalAnnotationsPromise = null;
|
||||
return this.backendSrv.put(`/api/annotations/${annotation.id}`, annotation);
|
||||
}
|
||||
|
||||
deleteAnnotationEvent(annotation) {
|
||||
this.globalAnnotationsPromise = null;
|
||||
let deleteUrl = `/api/annotations/${annotation.id}`;
|
||||
if (annotation.isRegion) {
|
||||
deleteUrl = `/api/annotations/region/${annotation.regionId}`;
|
||||
}
|
||||
|
||||
return this.backendSrv.delete(deleteUrl);
|
||||
}
|
||||
|
||||
translateQueryResult(annotation, results) {
|
||||
// if annotation has snapshotData
|
||||
// make clone and remove it
|
||||
@@ -159,13 +156,88 @@ export class AnnotationsSrv {
|
||||
|
||||
for (var item of results) {
|
||||
item.source = annotation;
|
||||
item.min = item.time;
|
||||
item.max = item.time;
|
||||
item.scope = 1;
|
||||
item.eventType = annotation.name;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function converts annotation events into set
|
||||
* of single events and regions (event consist of two)
|
||||
* @param annotations
|
||||
* @param options
|
||||
*/
|
||||
function makeRegions(annotations, options) {
|
||||
let [regionEvents, singleEvents] = _.partition(annotations, 'regionId');
|
||||
let regions = getRegions(regionEvents, options.range);
|
||||
annotations = _.concat(regions, singleEvents);
|
||||
return annotations;
|
||||
}
|
||||
|
||||
function getRegions(events, range) {
|
||||
let region_events = _.filter(events, event => {
|
||||
return event.regionId;
|
||||
});
|
||||
let regions = _.groupBy(region_events, 'regionId');
|
||||
regions = _.compact(
|
||||
_.map(regions, region_events => {
|
||||
let region_obj = _.head(region_events);
|
||||
if (region_events && region_events.length > 1) {
|
||||
region_obj.timeEnd = region_events[1].time;
|
||||
region_obj.isRegion = true;
|
||||
return region_obj;
|
||||
} else {
|
||||
if (region_events && region_events.length) {
|
||||
// Don't change proper region object
|
||||
if (!region_obj.time || !region_obj.timeEnd) {
|
||||
// This is cut region
|
||||
if (isStartOfRegion(region_obj)) {
|
||||
region_obj.timeEnd = range.to.valueOf() - 1;
|
||||
} else {
|
||||
// Start time = null
|
||||
region_obj.timeEnd = region_obj.time;
|
||||
region_obj.time = range.from.valueOf() + 1;
|
||||
}
|
||||
region_obj.isRegion = true;
|
||||
}
|
||||
|
||||
return region_obj;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
function isStartOfRegion(event): boolean {
|
||||
return event.id && event.id === event.regionId;
|
||||
}
|
||||
|
||||
function dedupAnnotations(annotations) {
|
||||
let dedup = [];
|
||||
|
||||
// Split events by annotationId property existance
|
||||
let events = _.partition(annotations, 'id');
|
||||
|
||||
let eventsById = _.groupBy(events[0], 'id');
|
||||
dedup = _.map(eventsById, eventGroup => {
|
||||
if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) {
|
||||
// Get first non-panel alert
|
||||
return _.find(eventGroup, event => {
|
||||
return event.eventType !== 'panel-alert';
|
||||
});
|
||||
} else {
|
||||
return _.head(eventGroup);
|
||||
}
|
||||
});
|
||||
|
||||
dedup = _.concat(dedup, events[1]);
|
||||
return dedup;
|
||||
}
|
||||
|
||||
function isPanelAlert(event) {
|
||||
return event.eventType === 'panel-alert';
|
||||
}
|
||||
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
@@ -36,11 +34,7 @@ export class AnnotationsEditorCtrl {
|
||||
this.annotations = $scope.dashboard.annotations.list;
|
||||
this.reset();
|
||||
|
||||
$scope.$watch('mode', newVal => {
|
||||
if (newVal === 'new') {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
}
|
||||
|
||||
datasourceChanged() {
|
||||
@@ -71,6 +65,11 @@ export class AnnotationsEditorCtrl {
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
setupNew() {
|
||||
this.mode = 'new';
|
||||
this.reset();
|
||||
}
|
||||
|
||||
add() {
|
||||
this.annotations.push(this.currentAnnotation);
|
||||
this.reset();
|
||||
@@ -85,6 +84,18 @@ export class AnnotationsEditorCtrl {
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
onColorChange(newColor) {
|
||||
this.currentAnnotation.iconColor = newColor;
|
||||
}
|
||||
|
||||
annotationEnabledChange() {
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
annotationHiddenChanged() {
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
export class AnnotationEvent {
|
||||
dashboardId: number;
|
||||
panelId: number;
|
||||
userId: number;
|
||||
time: any;
|
||||
timeEnd: any;
|
||||
isRegion: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
type: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {coreModule} from 'app/core/core';
|
||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||
import {AnnotationEvent} from './event';
|
||||
@@ -11,11 +10,20 @@ export class EventEditorCtrl {
|
||||
timeRange: {from: number, to: number};
|
||||
form: any;
|
||||
close: any;
|
||||
timeFormated: string;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private annotationsSrv) {
|
||||
this.event.panelId = this.panelCtrl.panel.id;
|
||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||
|
||||
// Annotations query returns time as Unix timestamp in milliseconds
|
||||
this.event.time = tryEpochToMoment(this.event.time);
|
||||
if (this.event.isRegion) {
|
||||
this.event.timeEnd = tryEpochToMoment(this.event.timeEnd);
|
||||
}
|
||||
|
||||
this.timeFormated = this.panelCtrl.dashboard.formatDate(this.event.time);
|
||||
}
|
||||
|
||||
save() {
|
||||
@@ -28,7 +36,7 @@ export class EventEditorCtrl {
|
||||
saveModel.timeEnd = 0;
|
||||
|
||||
if (saveModel.isRegion) {
|
||||
saveModel.timeEnd = saveModel.timeEnd.valueOf();
|
||||
saveModel.timeEnd = this.event.timeEnd.valueOf();
|
||||
|
||||
if (saveModel.timeEnd < saveModel.time) {
|
||||
console.log('invalid time');
|
||||
@@ -36,14 +44,48 @@ export class EventEditorCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => {
|
||||
if (saveModel.id) {
|
||||
this.annotationsSrv.updateAnnotationEvent(saveModel)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
} else {
|
||||
this.annotationsSrv.saveAnnotationEvent(saveModel)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
return this.annotationsSrv.deleteAnnotationEvent(this.event)
|
||||
.then(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
})
|
||||
.catch(() => {
|
||||
this.panelCtrl.refresh();
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
timeChanged() {
|
||||
this.panelCtrl.render();
|
||||
function tryEpochToMoment(timestamp) {
|
||||
if (timestamp && _.isNumber(timestamp)) {
|
||||
let epoch = Number(timestamp);
|
||||
return moment(epoch);
|
||||
} else {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {MetricsPanelCtrl} from 'app/plugins/sdk';
|
||||
import {AnnotationEvent} from './event';
|
||||
import {OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA} from 'app/core/utils/colors';
|
||||
|
||||
export class EventManager {
|
||||
event: AnnotationEvent;
|
||||
editorOpen: boolean;
|
||||
|
||||
constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) {
|
||||
}
|
||||
constructor(private panelCtrl: MetricsPanelCtrl) {}
|
||||
|
||||
editorClosed() {
|
||||
console.log('editorClosed');
|
||||
this.event = null;
|
||||
this.editorOpen = false;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
updateTime(range) {
|
||||
let newEvent = true;
|
||||
editorOpened() {
|
||||
this.editorOpen = true;
|
||||
}
|
||||
|
||||
if (this.event) {
|
||||
newEvent = false;
|
||||
} else {
|
||||
// init new event
|
||||
updateTime(range) {
|
||||
if (!this.event) {
|
||||
this.event = new AnnotationEvent();
|
||||
this.event.dashboardId = this.panelCtrl.dashboard.id;
|
||||
this.event.panelId = this.panelCtrl.panel.id;
|
||||
@@ -35,25 +36,11 @@ export class EventManager {
|
||||
this.event.isRegion = true;
|
||||
}
|
||||
|
||||
// newEvent means the editor is not visible
|
||||
if (!newEvent) {
|
||||
this.panelCtrl.render();
|
||||
return;
|
||||
}
|
||||
|
||||
this.popoverSrv.show({
|
||||
element: this.elem[0],
|
||||
classNames: 'drop-popover drop-popover--form',
|
||||
position: 'bottom center',
|
||||
openOn: null,
|
||||
template: '<event-editor panel-ctrl="panelCtrl" event="event" close="dismiss()"></event-editor>',
|
||||
onClose: this.editorClosed.bind(this),
|
||||
model: {
|
||||
event: this.event,
|
||||
panelCtrl: this.panelCtrl,
|
||||
},
|
||||
});
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
editEvent(event, elem?) {
|
||||
this.event = event;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
@@ -63,36 +50,60 @@ export class EventManager {
|
||||
}
|
||||
|
||||
var types = {
|
||||
'$__alerting': {
|
||||
color: 'rgba(237, 46, 24, 1)',
|
||||
$__alerting: {
|
||||
color: ALERTING_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
'$__ok': {
|
||||
color: 'rgba(11, 237, 50, 1)',
|
||||
$__ok: {
|
||||
color: OK_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
'$__no_data': {
|
||||
color: 'rgba(150, 150, 150, 1)',
|
||||
$__no_data: {
|
||||
color: NO_DATA_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
$__editing: {
|
||||
color: DEFAULT_ANNOTATION_COLOR,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.event) {
|
||||
annotations = [
|
||||
{
|
||||
min: this.event.time.valueOf(),
|
||||
title: this.event.title,
|
||||
text: this.event.text,
|
||||
eventType: '$__alerting',
|
||||
}
|
||||
];
|
||||
if (this.event.isRegion) {
|
||||
annotations = [
|
||||
{
|
||||
isRegion: true,
|
||||
min: this.event.time.valueOf(),
|
||||
timeEnd: this.event.timeEnd.valueOf(),
|
||||
text: this.event.text,
|
||||
eventType: '$__editing',
|
||||
editModel: this.event,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
annotations = [
|
||||
{
|
||||
min: this.event.time.valueOf(),
|
||||
text: this.event.text,
|
||||
editModel: this.event,
|
||||
eventType: '$__editing',
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// annotations from query
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var item = annotations[i];
|
||||
|
||||
// add properties used by jquery flot events
|
||||
item.min = item.time;
|
||||
item.max = item.time;
|
||||
item.eventType = item.source.name;
|
||||
|
||||
if (item.newState) {
|
||||
item.eventType = '$__' + item.newState;
|
||||
continue;
|
||||
@@ -108,10 +119,50 @@ export class EventManager {
|
||||
}
|
||||
}
|
||||
|
||||
let regions = getRegions(annotations);
|
||||
addRegionMarking(regions, flotOptions);
|
||||
|
||||
let eventSectionHeight = 20;
|
||||
let eventSectionMargin = 7;
|
||||
flotOptions.grid.eventSectionHeight = eventSectionMargin;
|
||||
flotOptions.xaxis.eventSectionHeight = eventSectionHeight;
|
||||
|
||||
flotOptions.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types,
|
||||
manager: this,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getRegions(events) {
|
||||
return _.filter(events, 'isRegion');
|
||||
}
|
||||
|
||||
function addRegionMarking(regions, flotOptions) {
|
||||
let markings = flotOptions.grid.markings;
|
||||
let defaultColor = DEFAULT_ANNOTATION_COLOR;
|
||||
let fillColor;
|
||||
|
||||
_.each(regions, region => {
|
||||
if (region.source) {
|
||||
fillColor = region.source.iconColor || defaultColor;
|
||||
} else {
|
||||
fillColor = defaultColor;
|
||||
}
|
||||
|
||||
fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA);
|
||||
markings.push({xaxis: {from: region.min, to: region.timeEnd}, color: fillColor});
|
||||
});
|
||||
}
|
||||
|
||||
function addAlphaToRGB(colorString: string, alpha: number): string {
|
||||
let color = tinycolor(colorString);
|
||||
if (color.isValid()) {
|
||||
color.setAlpha(alpha);
|
||||
return color.toRgbString();
|
||||
} else {
|
||||
return colorString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,11 @@
|
||||
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
||||
on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event.
|
||||
In the <i>Queries</i> tab you can add queries that return annotation events.
|
||||
<br>
|
||||
<br>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</p>
|
||||
<p>
|
||||
You can add annotations directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.
|
||||
</p>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,13 +54,16 @@
|
||||
</div>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="annotation in ctrl.annotations">
|
||||
<td style="width:90%">
|
||||
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
|
||||
<td style="width:90%" ng-hide="annotation.builtIn">
|
||||
<i class="fa fa-comment" style="color:{{annotation.iconColor}}"></i>
|
||||
{{annotation.name}}
|
||||
</td>
|
||||
<td style="width:90%" ng-show="annotation.builtIn">
|
||||
<i class="fa fa-comment"></i>
|
||||
<em class="muted">{{annotation.name}} (Built-in)</em>
|
||||
</td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
@@ -67,7 +71,7 @@
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini" ng-hide="annotation.builtIn">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
@@ -77,60 +81,65 @@
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
||||
<div class="gf-form-button-row">
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.setupNew()"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Name</span>
|
||||
<input type="text" class="gf-form-input width-12" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Data source</span>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<!-- <div class="gf-form"> -->
|
||||
<!-- <span class="gf-form-label width-7">Show in</span> -->
|
||||
<!-- <div class="gf-form-select-wrapper width-12"> -->
|
||||
<!-- <select class="gf-form-input" ng-model="ctrl.currentAnnotation.showIn" ng-options="f.value as f.text for f in ctrl.showOptions"></select> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hide toggle"
|
||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||
checked="ctrl.currentAnnotation.hide"
|
||||
label-class="width-9">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Enabled"
|
||||
checked="ctrl.currentAnnotation.enable"
|
||||
on-change="ctrl.annotationEnabledChange()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hidden"
|
||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||
checked="ctrl.currentAnnotation.hide"
|
||||
on-change="ctrl.annotationHiddenChanged()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Color</label>
|
||||
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="ctrl.currentAnnotation.iconColor" onChange="ctrl.onColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
|
||||
<h5 class="section-heading text-center">Add annotation</h5>
|
||||
|
||||
<form name="ctrl.form" class="text-center">
|
||||
<div style="display: inline-block">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Title</span>
|
||||
<input type="text" ng-model="ctrl.event.title" class="gf-form-input max-width-20" required>
|
||||
<div class="graph-annotation">
|
||||
<div class="graph-annotation__header">
|
||||
<div class="graph-annotation__user" bs-tooltip="'Created by {{ctrl.login}}'">
|
||||
</div>
|
||||
<!-- single event -->
|
||||
<div ng-if="!ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Time</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<!-- region event -->
|
||||
<div ng-if="ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Start</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">End</span>
|
||||
<input type="text" ng-model="ctrl.event.timeEnd" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-20" rows="3" ng-model="ctrl.event.text" placeholder="Event description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn gf-form-btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="graph-annotation__title">
|
||||
<span ng-if="!ctrl.event.id">Add Annotation</span>
|
||||
<span ng-if="ctrl.event.id">Edit Annotation</span>
|
||||
</div>
|
||||
|
||||
<div class="graph-annotation__time">{{ctrl.timeFormated}}</div>
|
||||
</div>
|
||||
|
||||
<form name="ctrl.form" class="graph-annotation__body text-center">
|
||||
<div style="display: inline-block">
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-20" rows="2" ng-model="ctrl.event.text" placeholder="Description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Tags</span>
|
||||
<bootstrap-tagsinput ng-model="ctrl.event.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<button ng-if="ctrl.event.id" type="submit" class="btn btn-danger" ng-click="ctrl.delete()">Delete</button>
|
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import '../annotations_srv';
|
||||
import helpers from 'test/specs/helpers';
|
||||
|
||||
describe('AnnotationsSrv', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(() => {
|
||||
ctx.createService('annotationsSrv');
|
||||
});
|
||||
describe('When translating the query result', () => {
|
||||
const annotationSource = {
|
||||
datasource: '-- Grafana --',
|
||||
enable: true,
|
||||
hide: false,
|
||||
limit: 200,
|
||||
name: 'test',
|
||||
scope: 'global',
|
||||
tags: [
|
||||
'test'
|
||||
],
|
||||
type: 'event',
|
||||
};
|
||||
|
||||
const time = 1507039543000;
|
||||
const annotations = [{id: 1, panelId: 1, text: 'text', time: time}];
|
||||
let translatedAnnotations;
|
||||
|
||||
beforeEach(() => {
|
||||
translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
|
||||
});
|
||||
|
||||
it('should set defaults', () => {
|
||||
expect(translatedAnnotations[0].source).to.eql(annotationSource);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import * as fileExport from 'app/core/utils/file_export';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -8,7 +6,7 @@ export class ExportDataModalCtrl {
|
||||
private data: any[];
|
||||
private panel: string;
|
||||
asRows: Boolean = true;
|
||||
dateTimeFormat: String = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
dateTimeFormat = 'YYYY-MM-DDTHH:mm:ssZ';
|
||||
excel: false;
|
||||
|
||||
export() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
|
||||
import {Emitter, contextSrv, appEvents} from 'app/core/core';
|
||||
import {DashboardRow} from './row/row_model';
|
||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||
@@ -82,10 +83,35 @@ export class DashboardModel {
|
||||
this.panels = data.panels || [];
|
||||
this.rows = [];
|
||||
|
||||
this.addBuiltInAnnotationQuery();
|
||||
this.initMeta(meta);
|
||||
this.updateSchema(data);
|
||||
}
|
||||
|
||||
addBuiltInAnnotationQuery() {
|
||||
let found = false;
|
||||
for (let item of this.annotations.list) {
|
||||
if (item.builtIn === 1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.annotations.list.unshift({
|
||||
datasource: '-- Grafana --',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
iconColor: DEFAULT_ANNOTATION_COLOR,
|
||||
enable: true,
|
||||
hide: true,
|
||||
builtIn: 1,
|
||||
});
|
||||
}
|
||||
|
||||
private initMeta(meta) {
|
||||
meta = meta || {};
|
||||
|
||||
|
||||
@@ -53,9 +53,10 @@ describe('DashImportCtrl', function() {
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
json: {}
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
return ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
@@ -69,9 +70,10 @@ describe('DashImportCtrl', function() {
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
json: {}
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
return ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
|
||||
@@ -46,8 +46,8 @@ describe('DashboardModel', function() {
|
||||
var saveModel = model.getSaveModelClone();
|
||||
var keys = _.keys(saveModel);
|
||||
|
||||
expect(keys[0]).to.be('addEmptyRow');
|
||||
expect(keys[1]).to.be('addPanel');
|
||||
expect(keys[0]).to.be('addBuiltInAnnotationQuery');
|
||||
expect(keys[1]).to.be('addEmptyRow');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,26 +220,6 @@ describe('DashboardModel', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating dashboard model with missing list for annoations or templating', function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new DashboardModel({
|
||||
annotations: {
|
||||
enable: true,
|
||||
},
|
||||
templating: {
|
||||
enable: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given editable false dashboard', function() {
|
||||
var model;
|
||||
|
||||
@@ -339,7 +319,12 @@ describe('DashboardModel', function() {
|
||||
});
|
||||
|
||||
it('should add empty list', function() {
|
||||
expect(model.annotations.list.length).to.be(0);
|
||||
expect(model.annotations.list.length).to.be(1);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should add builtin annotation query', function() {
|
||||
expect(model.annotations.list[0].builtIn).to.be(1);
|
||||
expect(model.templating.list.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,10 @@ describe('given dashboard with repeated panels', function() {
|
||||
name: 'mixed',
|
||||
meta: {id: "mixed", info: {version: "1.2.1"}, name: "Mixed", builtIn: true}
|
||||
}));
|
||||
datasourceSrvStub.get.withArgs('-- Grafana --').returns(Promise.resolve({
|
||||
name: '-- Grafana --',
|
||||
meta: {id: "grafana", info: {version: "1.2.1"}, name: "grafana", builtIn: true}
|
||||
}));
|
||||
|
||||
config.panels['graph'] = {
|
||||
id: "graph",
|
||||
@@ -116,7 +120,7 @@ describe('given dashboard with repeated panels', function() {
|
||||
});
|
||||
|
||||
it('should replace datasource in annotation query', function() {
|
||||
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
|
||||
expect(exported.annotations.list[1].datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
it('should add datasource as input', function() {
|
||||
|
||||
@@ -21,50 +21,48 @@ describe('historySrv', function() {
|
||||
return [200, restoreResponse(parsedData.version)];
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(ctx.createService('historySrv'));
|
||||
|
||||
function wrapPromise(ctx, angularPromise) {
|
||||
return new Promise((resolve, reject) => {
|
||||
angularPromise.then(resolve, reject);
|
||||
ctx.$httpBackend.flush();
|
||||
});
|
||||
}
|
||||
|
||||
describe('getHistoryList', function() {
|
||||
it('should return a versions array for the given dashboard id', function(done) {
|
||||
ctx.service.getHistoryList({ id: 1 }).then(function(versions) {
|
||||
it('should return a versions array for the given dashboard id', function() {
|
||||
return wrapPromise(ctx, ctx.service.getHistoryList({ id: 1 }).then(function(versions) {
|
||||
expect(versions).to.eql(versionsResponse);
|
||||
done();
|
||||
});
|
||||
ctx.$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return an empty array when not given an id', function(done) {
|
||||
ctx.service.getHistoryList({ }).then(function(versions) {
|
||||
it('should return an empty array when not given an id', function() {
|
||||
return wrapPromise(ctx, ctx.service.getHistoryList({ }).then(function(versions) {
|
||||
expect(versions).to.eql([]);
|
||||
done();
|
||||
});
|
||||
ctx.$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return an empty array when not given a dashboard', function(done) {
|
||||
ctx.service.getHistoryList().then(function(versions) {
|
||||
it('should return an empty array when not given a dashboard', function() {
|
||||
return wrapPromise(ctx, ctx.service.getHistoryList().then(function(versions) {
|
||||
expect(versions).to.eql([]);
|
||||
done();
|
||||
});
|
||||
ctx.$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreDashboard', function() {
|
||||
it('should return a success response given valid parameters', function(done) {
|
||||
var version = 6;
|
||||
ctx.service.restoreDashboard({ id: 1 }, version).then(function(response) {
|
||||
it('should return a success response given valid parameters', function() {
|
||||
let version = 6;
|
||||
return wrapPromise(ctx, ctx.service.restoreDashboard({ id: 1 }, version).then(function(response) {
|
||||
expect(response).to.eql(restoreResponse(version));
|
||||
done();
|
||||
});
|
||||
ctx.$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should return an empty object when not given an id', function(done) {
|
||||
ctx.service.restoreDashboard({}, 6).then(function(response) {
|
||||
it('should return an empty object when not given an id', function() {
|
||||
return wrapPromise(ctx, ctx.service.restoreDashboard({}, 6).then(function(response) {
|
||||
expect(response).to.eql({});
|
||||
done();
|
||||
});
|
||||
ctx.$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
|
||||
|
||||
import helpers from 'test/specs/helpers';
|
||||
import '../time_srv';
|
||||
@@ -8,6 +8,7 @@ describe('timeSrv', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var _dashboard: any = {
|
||||
time: {from: 'now-6h', to: 'now'},
|
||||
getTimezone: sinon.stub().returns('browser')
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
||||
<div ng-repeat="annotation in ctrl.dashboard.annotations.list" ng-hide="annotation.hide" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
|
||||
<gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -196,9 +196,11 @@ class TimeSrv {
|
||||
to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to,
|
||||
};
|
||||
|
||||
var timezone = this.dashboard && this.dashboard.getTimezone();
|
||||
|
||||
return {
|
||||
from: dateMath.parse(raw.from, false),
|
||||
to: dateMath.parse(raw.to, true),
|
||||
from: dateMath.parse(raw.from, false, timezone),
|
||||
to: dateMath.parse(raw.to, true, timezone),
|
||||
raw: raw
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,15 +41,13 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline gf-form-group">
|
||||
<div class="gf-form">
|
||||
<a class="btn btn-inverse btn-small" ng-click="addInvite()">
|
||||
<div class="gf-form" style="margin-right:.25rem">
|
||||
<a class="btn btn-inverse gf-form-button" ng-click="addInvite()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Invite another
|
||||
</a>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<editor-checkbox text="Skip sending invite email" model="options.skipEmails" change="targetBlur()"></editor-checkbox>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Skip sending invite email" checked="options.skipEmails" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
|
||||
@@ -59,9 +59,13 @@ var template = `
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Home Dashboard</span>
|
||||
<dashboard-selector class="gf-form-select-wrapper max-width-20 gf-form-select-wrapper--has-help-icon"
|
||||
model="ctrl.prefs.homeDashboardId">
|
||||
<span class="gf-form-label width-10">
|
||||
Home Dashboard
|
||||
<info-popover mode="right-normal">
|
||||
Not finding dashboard you want? Star it first, then it should appear in this select box.
|
||||
</info-popover>
|
||||
</span>
|
||||
<dashboard-selector class="gf-form-select-wrapper max-width-20" model="ctrl.prefs.homeDashboardId">
|
||||
</dashboard-selector>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
this.timeSrv = $injector.get('timeSrv');
|
||||
this.templateSrv = $injector.get('templateSrv');
|
||||
this.scope = $scope;
|
||||
this.panel.datasource = this.panel.datasource || null;
|
||||
|
||||
if (!this.panel.targets) {
|
||||
this.panel.targets = [{}];
|
||||
@@ -218,6 +219,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
});
|
||||
|
||||
var metricsQuery = {
|
||||
timezone: this.dashboard.getTimezone(),
|
||||
panelId: this.panel.id,
|
||||
range: this.range,
|
||||
rangeRaw: this.range.raw,
|
||||
|
||||
47
public/app/features/plugins/buit_in_plugins.ts
Normal file
47
public/app/features/plugins/buit_in_plugins.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as graphitePlugin from 'app/plugins/datasource/graphite/module';
|
||||
import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module';
|
||||
import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/module';
|
||||
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
|
||||
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
|
||||
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
|
||||
import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
|
||||
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
|
||||
import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
|
||||
|
||||
import * as textPanel from 'app/plugins/panel/text/module';
|
||||
import * as graphPanel from 'app/plugins/panel/graph/module';
|
||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||
import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
|
||||
|
||||
const builtInPlugins = {
|
||||
"app/plugins/datasource/graphite/module": graphitePlugin,
|
||||
"app/plugins/datasource/cloudwatch/module": cloudwatchPlugin,
|
||||
"app/plugins/datasource/elasticsearch/module": elasticsearchPlugin,
|
||||
"app/plugins/datasource/opentsdb/module": opentsdbPlugin,
|
||||
"app/plugins/datasource/grafana/module": grafanaPlugin,
|
||||
"app/plugins/datasource/influxdb/module": influxdbPlugin,
|
||||
"app/plugins/datasource/mixed/module": mixedPlugin,
|
||||
"app/plugins/datasource/mysql/module": mysqlPlugin,
|
||||
"app/plugins/datasource/prometheus/module": prometheusPlugin,
|
||||
"app/plugins/app/testdata/module": testDataAppPlugin,
|
||||
"app/plugins/app/testdata/datasource/module": testDataDSPlugin,
|
||||
|
||||
"app/plugins/panel/text/module": textPanel,
|
||||
"app/plugins/panel/graph/module": graphPanel,
|
||||
"app/plugins/panel/dashlist/module": dashListPanel,
|
||||
"app/plugins/panel/pluginlist/module": pluginsListPanel,
|
||||
"app/plugins/panel/alertlist/module": alertListPanel,
|
||||
"app/plugins/panel/heatmap/module": heatmapPanel,
|
||||
"app/plugins/panel/table/module": tablePanel,
|
||||
"app/plugins/panel/singlestat/module": singlestatPanel,
|
||||
"app/plugins/panel/gettingstarted/module": gettingStartedPanel,
|
||||
};
|
||||
|
||||
export default builtInPlugins;
|
||||
@@ -7,53 +7,12 @@ import angular from 'angular';
|
||||
import jquery from 'jquery';
|
||||
import config from 'app/core/config';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
import TableModel from 'app/core/table_model';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import * as datemath from 'app/core/utils/datemath';
|
||||
|
||||
import * as graphitePlugin from 'app/plugins/datasource/graphite/module';
|
||||
import * as cloudwatchPlugin from 'app/plugins/datasource/cloudwatch/module';
|
||||
import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/module';
|
||||
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
|
||||
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
|
||||
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
|
||||
import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
|
||||
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
|
||||
import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
|
||||
|
||||
import * as textPanel from 'app/plugins/panel/text/module';
|
||||
import * as graphPanel from 'app/plugins/panel/graph/module';
|
||||
import * as dashListPanel from 'app/plugins/panel/dashlist/module';
|
||||
import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
|
||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||
import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
|
||||
import * as tablePanel from 'app/plugins/panel/table/module';
|
||||
import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
|
||||
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
|
||||
import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
|
||||
import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
|
||||
|
||||
let builtInPlugins = {
|
||||
"app/plugins/datasource/graphite/module": graphitePlugin,
|
||||
"app/plugins/datasource/cloudwatch/module": cloudwatchPlugin,
|
||||
"app/plugins/datasource/elasticsearch/module": elasticsearchPlugin,
|
||||
"app/plugins/datasource/opentsdb/module": opentsdbPlugin,
|
||||
"app/plugins/datasource/grafana/module": grafanaPlugin,
|
||||
"app/plugins/datasource/influxdb/module": influxdbPlugin,
|
||||
"app/plugins/datasource/mixed/module": mixedPlugin,
|
||||
"app/plugins/datasource/mysql/module": mysqlPlugin,
|
||||
"app/plugins/datasource/prometheus/module": prometheusPlugin,
|
||||
"app/plugins/app/testdata/module": testDataAppPlugin,
|
||||
"app/plugins/app/testdata/datasource/module": testDataDSPlugin,
|
||||
|
||||
"app/plugins/panel/text/module": textPanel,
|
||||
"app/plugins/panel/graph/module": graphPanel,
|
||||
"app/plugins/panel/dashlist/module": dashListPanel,
|
||||
"app/plugins/panel/pluginlist/module": pluginsListPanel,
|
||||
"app/plugins/panel/alertlist/module": alertListPanel,
|
||||
"app/plugins/panel/heatmap/module": heatmapPanel,
|
||||
"app/plugins/panel/table/module": tablePanel,
|
||||
"app/plugins/panel/singlestat/module": singlestatPanel,
|
||||
"app/plugins/panel/gettingstarted/module": gettingStartedPanel,
|
||||
};
|
||||
import builtInPlugins from './buit_in_plugins';
|
||||
|
||||
System.config({
|
||||
baseURL: 'public',
|
||||
@@ -89,12 +48,17 @@ exposeToPlugin('lodash', _);
|
||||
exposeToPlugin('moment', moment);
|
||||
exposeToPlugin('jquery', jquery);
|
||||
exposeToPlugin('angular', angular);
|
||||
exposeToPlugin('rxjs/Subject', Subject);
|
||||
exposeToPlugin('rxjs/Observable', Observable);
|
||||
|
||||
exposeToPlugin('app/plugins/sdk', sdk);
|
||||
exposeToPlugin('app/core/utils/datemath', datemath);
|
||||
exposeToPlugin('app/core/utils/kbn', kbn);
|
||||
exposeToPlugin('app/core/config', config);
|
||||
exposeToPlugin('app/core/time_series', TimeSeries);
|
||||
exposeToPlugin('app/core/time_series2', TimeSeries);
|
||||
exposeToPlugin('app/core/table_model', TableModel);
|
||||
exposeToPlugin('app/core/app_events', appEvents);
|
||||
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.selection';
|
||||
@@ -107,7 +71,7 @@ import 'vendor/flot/jquery.flot.crosshair';
|
||||
import 'vendor/flot/jquery.flot.dashes';
|
||||
|
||||
for (let flotDep of ['jquery.flot', 'jquery.flot.pie', 'jquery.flot.time']) {
|
||||
System.registerDynamic(flotDep, [], true, function(require, exports, module) { module.exports = {fakeDep: 1}; });
|
||||
exposeToPlugin(flotDep, {fakeDep: 1});
|
||||
}
|
||||
|
||||
export function importPluginModule(path: string): Promise<any> {
|
||||
|
||||
68
public/app/headers/common.d.ts
vendored
68
public/app/headers/common.d.ts
vendored
@@ -1,72 +1,10 @@
|
||||
|
||||
declare var System: any;
|
||||
|
||||
// dummy modules
|
||||
declare module 'app/core/config' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'lodash' {
|
||||
var lodash: any;
|
||||
export default lodash;
|
||||
}
|
||||
|
||||
declare module 'moment' {
|
||||
var moment: any;
|
||||
export default moment;
|
||||
}
|
||||
|
||||
declare module 'angular' {
|
||||
var angular: any;
|
||||
export default angular;
|
||||
}
|
||||
|
||||
declare module 'jquery' {
|
||||
var jquery: any;
|
||||
export default jquery;
|
||||
}
|
||||
|
||||
declare module 'app/core/utils/kbn' {
|
||||
var kbn: any;
|
||||
export default kbn;
|
||||
}
|
||||
|
||||
declare module 'app/core/store' {
|
||||
var store: any;
|
||||
export default store;
|
||||
}
|
||||
|
||||
declare module 'tether' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'tether-drop' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'eventemitter3' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'mousetrap' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'remarkable' {
|
||||
var config: any;
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'd3' {
|
||||
var d3: any;
|
||||
export default d3;
|
||||
}
|
||||
|
||||
declare module 'gridstack' {
|
||||
var gridstack: any;
|
||||
export default gridstack;
|
||||
@@ -76,9 +14,3 @@ declare module 'gemini-scrollbar' {
|
||||
var d3: any;
|
||||
export default d3;
|
||||
}
|
||||
|
||||
declare module 'ace' {
|
||||
var ace: any;
|
||||
export default ace;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
<i class="fa fa-minus error-minus"></i>
|
||||
<div class="error-column error-space-between">
|
||||
<div class="error-row error-space-between">
|
||||
<p>Chance your are one the page your'e looking for.</p>
|
||||
<p>Chances you are on the page you are looking for.</p>
|
||||
<p class="left-margin">0%</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Sorry for the inconvenience</h3>
|
||||
<p>Please go back to your <a href="#" class="error-link">home dashboard</a> and try again.</p>
|
||||
<p>If the error persists, seek help att the <a href="#" class="error-link">community site</a>.</p>
|
||||
<p>If the error persists, seek help on the <a href="#" class="error-link">community site</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
Row settings
|
||||
</h2>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tabbed-view-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="page-heading">
|
||||
<h5>Row details</h5>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Title</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='row.title'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Height</span>
|
||||
<input type="text" class="gf-form-input max-width-8" ng-model='row.height'></input>
|
||||
<editor-checkbox text="Show Title" model="row.showTitle"></editor-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="page-heading">
|
||||
<h5>Templating options</h5>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Repeat Row</span>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="row.repeat" ng-options="f.name as f.name for f in dashboard.templating.list">
|
||||
<option value=""></option>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,18 +3,16 @@
|
||||
<div class="editor-row" style="padding: 2rem 0">
|
||||
<div class="section">
|
||||
<h5>Prefix matching</h5>
|
||||
<div class="editor-option">
|
||||
<editor-checkbox text="Enable" model="ctrl.annotation.prefixMatching"></editor-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="editor-option" ng-if="ctrl.annotation.prefixMatching">
|
||||
<label class="small">Action</label>
|
||||
<input type="text" class="input-small" ng-model='ctrl.annotation.actionPrefix'></input>
|
||||
</div>
|
||||
|
||||
<div class="editor-option" ng-if="ctrl.annotation.prefixMatching">
|
||||
<label class="small">Alarm Name</label>
|
||||
<input type="text" class="input-small" ng-model='ctrl.annotation.alarmNamePrefix'></input>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Enable" checked="ctrl.annotation.prefixMatching" switch-class="max-width-6"></gf-form-switch>
|
||||
<div class="gf-form" ng-if="ctrl.annotation.prefixMatching">
|
||||
<span class="gf-form-label">Action</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.actionPrefix'></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.annotation.prefixMatching">
|
||||
<span class="gf-form-label">Alarm Name</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.alarmNamePrefix'></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,6 @@ export class ElasticDatasource {
|
||||
var timeField = annotation.timeField || '@timestamp';
|
||||
var queryString = annotation.query || '*';
|
||||
var tagsField = annotation.tagsField || 'tags';
|
||||
var titleField = annotation.titleField || 'desc';
|
||||
var textField = annotation.textField || null;
|
||||
|
||||
var range = {};
|
||||
@@ -146,9 +145,6 @@ export class ElasticDatasource {
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isArray(fieldValue)) {
|
||||
fieldValue = fieldValue.join(', ');
|
||||
}
|
||||
return fieldValue;
|
||||
};
|
||||
|
||||
@@ -165,16 +161,27 @@ export class ElasticDatasource {
|
||||
var event = {
|
||||
annotation: annotation,
|
||||
time: moment.utc(time).valueOf(),
|
||||
title: getFieldFromSource(source, titleField),
|
||||
text: getFieldFromSource(source, textField),
|
||||
tags: getFieldFromSource(source, tagsField),
|
||||
text: getFieldFromSource(source, textField)
|
||||
};
|
||||
|
||||
// legacy support for title tield
|
||||
if (annotation.titleField) {
|
||||
const title = getFieldFromSource(source, annotation.titleField);
|
||||
if (title) {
|
||||
event.text = title + '\n' + event.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof event.tags === 'string') {
|
||||
event.tags = event.tags.split(',');
|
||||
}
|
||||
|
||||
list.push(event);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true);
|
||||
@@ -242,7 +249,7 @@ export class ElasticDatasource {
|
||||
return this.post('_msearch', payload).then(function(res) {
|
||||
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
getFields(query) {
|
||||
return this.get('/_mapping').then(function(result) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
const intervalMap = {
|
||||
@@ -20,7 +18,7 @@ export class IndexPattern {
|
||||
} else {
|
||||
return this.pattern;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getIndexList(from, to) {
|
||||
if (!this.interval) {
|
||||
@@ -29,10 +27,10 @@ export class IndexPattern {
|
||||
|
||||
var intervalInfo = intervalMap[this.interval];
|
||||
var start = moment(from).utc().startOf(intervalInfo.startOf);
|
||||
var end = moment(to).utc().startOf(intervalInfo.startOf).valueOf();
|
||||
var endEpoch = moment(to).utc().startOf(intervalInfo.startOf).valueOf();
|
||||
var indexList = [];
|
||||
|
||||
while (start <= end) {
|
||||
while (start.valueOf() <= endEpoch) {
|
||||
indexList.push(start.format(this.pattern));
|
||||
start.add(1, intervalInfo.amount);
|
||||
}
|
||||
|
||||
@@ -15,24 +15,20 @@
|
||||
<h6>Field mappings</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Time</span>
|
||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
|
||||
<span class="gf-form-label">Time</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Title</span>
|
||||
<span class="gf-form-label">Text</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.annotation.textField' placeholder=""></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Tags</span>
|
||||
<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>
|
||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Tags</span>
|
||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Text</span>
|
||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.textField' placeholder=""></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@ export class ElasticQueryBuilder {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
build(target, adhocFilters?, queryString?) {
|
||||
// make sure query has defaults;
|
||||
|
||||
@@ -188,4 +188,4 @@ export function describeOrderBy(orderBy, target) {
|
||||
} else {
|
||||
return "metric not found";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
class GrafanaDatasource {
|
||||
@@ -8,42 +6,62 @@ class GrafanaDatasource {
|
||||
constructor(private backendSrv, private $q) {}
|
||||
|
||||
query(options) {
|
||||
return this.backendSrv.get('/api/tsdb/testdata/random-walk', {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
}).then(res => {
|
||||
var data = [];
|
||||
return this.backendSrv
|
||||
.get('/api/tsdb/testdata/random-walk', {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
})
|
||||
.then(res => {
|
||||
var data = [];
|
||||
|
||||
if (res.results) {
|
||||
_.forEach(res.results, queryRes => {
|
||||
for (let series of queryRes.series) {
|
||||
data.push({
|
||||
target: series.name,
|
||||
datapoints: series.points
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (res.results) {
|
||||
_.forEach(res.results, queryRes => {
|
||||
for (let series of queryRes.series) {
|
||||
data.push({
|
||||
target: series.name,
|
||||
datapoints: series.points,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {data: data};
|
||||
});
|
||||
return {data: data};
|
||||
});
|
||||
}
|
||||
|
||||
metricFindQuery(options) {
|
||||
return this.$q.when({data: []});
|
||||
}
|
||||
|
||||
|
||||
annotationQuery(options) {
|
||||
return this.backendSrv.get('/api/annotations', {
|
||||
const params: any = {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
limit: options.limit,
|
||||
type: options.type,
|
||||
});
|
||||
}
|
||||
limit: options.annotation.limit,
|
||||
tags: options.annotation.tags,
|
||||
};
|
||||
|
||||
if (options.annotation.type === 'dashboard') {
|
||||
// if no dashboard id yet return
|
||||
if (!options.dashboard.id) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
// filter by dashboard id
|
||||
params.dashboardId = options.dashboard.id;
|
||||
// remove tags filter if any
|
||||
delete params.tags;
|
||||
} else {
|
||||
// require at least one tag
|
||||
if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
}
|
||||
|
||||
return this.backendSrv.get('/api/annotations', params);
|
||||
}
|
||||
}
|
||||
|
||||
export {GrafanaDatasource};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {GrafanaDatasource} from './datasource';
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
|
||||
@@ -10,19 +8,22 @@ class GrafanaQueryCtrl extends QueryCtrl {
|
||||
class GrafanaAnnotationsQueryCtrl {
|
||||
annotation: any;
|
||||
|
||||
types = [
|
||||
{text: 'Dashboard', value: 'dashboard'},
|
||||
{text: 'Tags', value: 'tags'}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.annotation.type = this.annotation.type || 'alert';
|
||||
this.annotation.type = this.annotation.type || 'tags';
|
||||
this.annotation.limit = this.annotation.limit || 100;
|
||||
}
|
||||
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
GrafanaDatasource,
|
||||
GrafanaDatasource as Datasource,
|
||||
GrafanaQueryCtrl as QueryCtrl,
|
||||
GrafanaAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,29 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Type</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Event', value: 'event'}, {text: 'Alert', value: 'alert'}]">
|
||||
<span class="gf-form-label width-8">
|
||||
Filter by
|
||||
<info-popover mode="right-normal">
|
||||
<ul>
|
||||
<li>Dashboard: This will fetch annotation and alert state changes for whole dashboard and show them only on the event's originating panel.</li>
|
||||
<li>All: This will fetch any annotation events that match the tags filter.</li>
|
||||
</ul>
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in ctrl.types">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
|
||||
<span class="gf-form-label">Tags</span>
|
||||
<bootstrap-tagsinput ng-model="ctrl.annotation.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Max limit</span>
|
||||
<span class="gf-form-label">Max limit</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
||||
</select>
|
||||
@@ -17,3 +32,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,18 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
return result;
|
||||
};
|
||||
|
||||
this.parseTags = function(tagString) {
|
||||
let tags = [];
|
||||
tags = tagString.split(',');
|
||||
if (tags.length === 1) {
|
||||
tags = tagString.split(' ');
|
||||
if (tags[0] === '') {
|
||||
tags = [];
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
|
||||
this.annotationQuery = function(options) {
|
||||
// Graphite metric as annotation
|
||||
if (options.annotation.target) {
|
||||
@@ -102,19 +114,25 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
} else {
|
||||
// Graphite event as annotation
|
||||
var tags = templateSrv.replace(options.annotation.tags);
|
||||
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
|
||||
return this.events({range: options.rangeRaw, tags: tags}).then(results => {
|
||||
var list = [];
|
||||
for (var i = 0; i < results.data.length; i++) {
|
||||
var e = results.data[i];
|
||||
|
||||
var tags = e.tags;
|
||||
if (_.isString(e.tags)) {
|
||||
tags = this.parseTags(e.tags);
|
||||
}
|
||||
|
||||
list.push({
|
||||
annotation: options.annotation,
|
||||
time: e.when * 1000,
|
||||
title: e.what,
|
||||
tags: e.tags,
|
||||
tags: tags,
|
||||
text: e.data
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
}
|
||||
@@ -126,7 +144,6 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
if (options.tags) {
|
||||
tags = '&tags=' + options.tags;
|
||||
}
|
||||
|
||||
return this.doGraphiteRequest({
|
||||
method: 'GET',
|
||||
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import {GraphiteDatasource} from "../datasource";
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('graphiteDatasource', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}};
|
||||
let ctx = new helpers.ServiceTestContext();
|
||||
let instanceSettings: any = {url: [''], name: 'graphiteProd', jsonData: {}};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
@@ -22,16 +24,16 @@ describe('graphiteDatasource', function() {
|
||||
ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
|
||||
});
|
||||
|
||||
describe('When querying influxdb with one target using query editor target spec', function() {
|
||||
var query = {
|
||||
describe('When querying graphite with one target using query editor target spec', function() {
|
||||
let query = {
|
||||
panelId: 3,
|
||||
rangeRaw: { from: 'now-1h', to: 'now' },
|
||||
targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}],
|
||||
maxDataPoints: 500,
|
||||
};
|
||||
|
||||
var results;
|
||||
var requestOptions;
|
||||
let results;
|
||||
let requestOptions;
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
@@ -52,7 +54,7 @@ describe('graphiteDatasource', function() {
|
||||
});
|
||||
|
||||
it('should query correctly', function() {
|
||||
var params = requestOptions.data.split('&');
|
||||
let params = requestOptions.data.split('&');
|
||||
expect(params).to.contain('target=prod1.count');
|
||||
expect(params).to.contain('target=prod2.count');
|
||||
expect(params).to.contain('from=-1h');
|
||||
@@ -60,7 +62,7 @@ describe('graphiteDatasource', function() {
|
||||
});
|
||||
|
||||
it('should exclude undefined params', function() {
|
||||
var params = requestOptions.data.split('&');
|
||||
let params = requestOptions.data.split('&');
|
||||
expect(params).to.not.contain('cacheTimeout=undefined');
|
||||
});
|
||||
|
||||
@@ -75,58 +77,130 @@ describe('graphiteDatasource', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('when fetching Graphite Events as annotations', () => {
|
||||
let results;
|
||||
|
||||
const options = {
|
||||
annotation: {
|
||||
tags: 'tag1'
|
||||
},
|
||||
range: {
|
||||
from: moment(1432288354),
|
||||
to: moment(1432288401)
|
||||
},
|
||||
rangeRaw: {from: "now-24h", to: "now"}
|
||||
};
|
||||
|
||||
describe('and tags are returned as string', () => {
|
||||
const response = {
|
||||
data: [
|
||||
{
|
||||
when: 1507222850,
|
||||
tags: 'tag1 tag2',
|
||||
data: 'some text',
|
||||
id: 2,
|
||||
what: 'Event - deploy'
|
||||
}
|
||||
]};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
|
||||
ctx.ds.annotationQuery(options).then(function(data) { results = data; });
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should parse the tags string into an array', () => {
|
||||
expect(_.isArray(results[0].tags)).to.eql(true);
|
||||
expect(results[0].tags.length).to.eql(2);
|
||||
expect(results[0].tags[0]).to.eql('tag1');
|
||||
expect(results[0].tags[1]).to.eql('tag2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and tags are returned as an array', () => {
|
||||
const response = {
|
||||
data: [
|
||||
{
|
||||
when: 1507222850,
|
||||
tags: ['tag1', 'tag2'],
|
||||
data: 'some text',
|
||||
id: 2,
|
||||
what: 'Event - deploy'
|
||||
}
|
||||
]};
|
||||
beforeEach(() => {
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
|
||||
ctx.ds.annotationQuery(options).then(function(data) { results = data; });
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should parse the tags string into an array', () => {
|
||||
expect(_.isArray(results[0].tags)).to.eql(true);
|
||||
expect(results[0].tags.length).to.eql(2);
|
||||
expect(results[0].tags[0]).to.eql('tag1');
|
||||
expect(results[0].tags[1]).to.eql('tag2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('building graphite params', function() {
|
||||
it('should return empty array if no targets', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{}]
|
||||
});
|
||||
expect(results.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should uri escape targets', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}]
|
||||
});
|
||||
expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D');
|
||||
});
|
||||
|
||||
it('should replace target placeholder', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}]
|
||||
});
|
||||
expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
|
||||
});
|
||||
|
||||
it('should replace target placeholder for hidden series', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: 'series1', hide: true}, {target: 'sumSeries(#A)', hide: true}, {target: 'asPercent(#A,#B)'}]
|
||||
});
|
||||
expect(results[0]).to.be('target=' + encodeURIComponent('asPercent(series1,sumSeries(series1))'));
|
||||
});
|
||||
|
||||
it('should replace target placeholder when nesting query references', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: 'series1'}, {target: 'sumSeries(#A)'}, {target: 'asPercent(#A,#B)'}]
|
||||
});
|
||||
expect(results[2]).to.be('target=' + encodeURIComponent("asPercent(series1,sumSeries(series1))"));
|
||||
});
|
||||
|
||||
it('should fix wrong minute interval parameters', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }]
|
||||
});
|
||||
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')"));
|
||||
});
|
||||
|
||||
it('should fix wrong month interval parameters', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: "summarize(prod.5M.count, '5M', 'sum')" }]
|
||||
});
|
||||
expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')"));
|
||||
});
|
||||
|
||||
it('should ignore empty targets', function() {
|
||||
var results = ctx.ds.buildGraphiteParams({
|
||||
let results = ctx.ds.buildGraphiteParams({
|
||||
targets: [{target: 'series1'}, {target: ''}]
|
||||
});
|
||||
expect(results.length).to.be(2);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
||||
@@ -9,18 +9,16 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-4">Title</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
|
||||
<span class="gf-form-label width-4">Text</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-4">Tags</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-4">Text</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.textColumn' placeholder=""></input>
|
||||
<div class="gf-form" ng-show="ctrl.annotation.titleColumn">
|
||||
<span class="gf-form-label width-4">Title <em class="muted">(depricated)</em></span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,5 +5,9 @@
|
||||
|
||||
"builtIn": true,
|
||||
"mixed": true,
|
||||
"metrics": true
|
||||
"metrics": true,
|
||||
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ class MysqlConfigCtrl {
|
||||
|
||||
const defaultQuery = `SELECT
|
||||
UNIX_TIMESTAMP(<time_column>) as time_sec,
|
||||
<title_column> as title,
|
||||
<text_column> as text,
|
||||
<tags_column> as tags
|
||||
FROM <table name>
|
||||
|
||||
@@ -106,7 +106,6 @@ export default class ResponseParser {
|
||||
const table = data.data.results[options.annotation.name].tables[0];
|
||||
|
||||
let timeColumnIndex = -1;
|
||||
let titleColumnIndex = -1;
|
||||
let textColumnIndex = -1;
|
||||
let tagsColumnIndex = -1;
|
||||
|
||||
@@ -114,7 +113,7 @@ export default class ResponseParser {
|
||||
if (table.columns[i].text === 'time_sec') {
|
||||
timeColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'title') {
|
||||
titleColumnIndex = i;
|
||||
return this.$q.reject({message: 'Title return column on annotations are depricated, return only a column named text'});
|
||||
} else if (table.columns[i].text === 'text') {
|
||||
textColumnIndex = i;
|
||||
} else if (table.columns[i].text === 'tags') {
|
||||
@@ -132,7 +131,6 @@ export default class ResponseParser {
|
||||
list.push({
|
||||
annotation: options.annotation,
|
||||
time: Math.floor(row[timeColumnIndex]) * 1000,
|
||||
title: row[titleColumnIndex],
|
||||
text: row[textColumnIndex],
|
||||
tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : []
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('MySQLDatasource', function() {
|
||||
const options = {
|
||||
annotation: {
|
||||
name: annotationName,
|
||||
rawQuery: 'select time_sec, title, text, tags from table;'
|
||||
rawQuery: 'select time_sec, text, tags from table;'
|
||||
},
|
||||
range: {
|
||||
from: moment(1432288354),
|
||||
@@ -41,11 +41,11 @@ describe('MySQLDatasource', function() {
|
||||
refId: annotationName,
|
||||
tables: [
|
||||
{
|
||||
columns: [{text: 'time_sec'}, {text: 'title'}, {text: 'text'}, {text: 'tags'}],
|
||||
columns: [{text: 'time_sec'}, {text: 'text'}, {text: 'tags'}],
|
||||
rows: [
|
||||
[1432288355, 'aTitle', 'some text', 'TagA,TagB'],
|
||||
[1432288390, 'aTitle2', 'some text2', ' TagB , TagC'],
|
||||
[1432288400, 'aTitle3', 'some text3']
|
||||
[1432288355, 'some text', 'TagA,TagB'],
|
||||
[1432288390, 'some text2', ' TagB , TagC'],
|
||||
[1432288400, 'some text3']
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -64,7 +64,6 @@ describe('MySQLDatasource', function() {
|
||||
it('should return annotation list', function() {
|
||||
expect(results.length).to.be(3);
|
||||
|
||||
expect(results[0].title).to.be('aTitle');
|
||||
expect(results[0].text).to.be('some text');
|
||||
expect(results[0].tags[0]).to.be('TagA');
|
||||
expect(results[0].tags[1]).to.be('TagB');
|
||||
|
||||
@@ -91,9 +91,8 @@ function (angular, _, dateMath) {
|
||||
if(annotationObject) {
|
||||
_.each(annotationObject, function(annotation) {
|
||||
var event = {
|
||||
title: annotation.description,
|
||||
text: annotation.description,
|
||||
time: Math.floor(annotation.startTime) * 1000,
|
||||
text: annotation.notes,
|
||||
annotation: options.annotation
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,5 @@
|
||||
<span class="gf-form-label width-13">OpenTSDB metrics query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder="events.eventname"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-13">Show Global Annotations?</span>
|
||||
<editor-checkbox text="" model="ctrl.annotation.isGlobal"></editor-checkbox>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Show Global Annotations?" checked="ctrl.annotation.isGlobal" switch-class="max-width-6" label-class="width-13"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,76 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {PrometheusDatasource} from "./datasource";
|
||||
import _ from 'lodash';
|
||||
|
||||
export class PromCompleter {
|
||||
labelQueryCache: any;
|
||||
labelNameCache: any;
|
||||
labelValueCache: any;
|
||||
|
||||
identifierRegexps = [/[\[\]a-zA-Z_0-9=]/];
|
||||
|
||||
constructor(private datasource: PrometheusDatasource) {
|
||||
this.labelQueryCache = {};
|
||||
this.labelNameCache = {};
|
||||
this.labelValueCache = {};
|
||||
}
|
||||
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
let token = session.getTokenAt(pos.row, pos.column);
|
||||
|
||||
var metricName;
|
||||
switch (token.type) {
|
||||
case 'label.name':
|
||||
callback(null, ['instance', 'job'].map(function (key) {
|
||||
return {
|
||||
caption: key,
|
||||
value: key,
|
||||
meta: "label name",
|
||||
score: Number.MAX_VALUE
|
||||
};
|
||||
}));
|
||||
return;
|
||||
case 'label.value':
|
||||
callback(null, []);
|
||||
return;
|
||||
case 'entity.name.tag':
|
||||
metricName = this.findMetricName(session, pos.row, pos.column);
|
||||
if (!metricName) {
|
||||
callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.labelNameCache[metricName]) {
|
||||
callback(null, this.labelNameCache[metricName]);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.getLabelNameAndValueForMetric(metricName).then(result => {
|
||||
var labelNames = this.transformToCompletions(
|
||||
_.uniq(_.flatten(result.map(r => {
|
||||
return Object.keys(r.metric);
|
||||
})))
|
||||
, 'label name');
|
||||
this.labelNameCache[metricName] = labelNames;
|
||||
callback(null, labelNames);
|
||||
});
|
||||
case 'string.quoted':
|
||||
metricName = this.findMetricName(session, pos.row, pos.column);
|
||||
if (!metricName) {
|
||||
callback(null, []);
|
||||
return;
|
||||
}
|
||||
|
||||
var labelNameToken = this.findToken(session, pos.row, pos.column, 'entity.name.tag', null, 'paren.lparen');
|
||||
if (!labelNameToken) {
|
||||
callback(null, []);
|
||||
return;
|
||||
}
|
||||
var labelName = labelNameToken.value;
|
||||
|
||||
if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) {
|
||||
callback(null, this.labelValueCache[metricName][labelName]);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.getLabelNameAndValueForMetric(metricName).then(result => {
|
||||
var labelValues = this.transformToCompletions(
|
||||
_.uniq(result.map(r => {
|
||||
return r.metric[labelName];
|
||||
}))
|
||||
, 'label value');
|
||||
this.labelValueCache[metricName] = this.labelValueCache[metricName] || {};
|
||||
this.labelValueCache[metricName][labelName] = labelValues;
|
||||
callback(null, labelValues);
|
||||
});
|
||||
}
|
||||
|
||||
if (prefix === '[') {
|
||||
@@ -56,4 +102,87 @@ export class PromCompleter {
|
||||
});
|
||||
}
|
||||
|
||||
getLabelNameAndValueForMetric(metricName) {
|
||||
if (this.labelQueryCache[metricName]) {
|
||||
return Promise.resolve(this.labelQueryCache[metricName]);
|
||||
}
|
||||
var op = '=~';
|
||||
if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) {
|
||||
op = '=';
|
||||
}
|
||||
var expr = '{__name__' + op + '"' + metricName + '"}';
|
||||
return this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => {
|
||||
this.labelQueryCache[metricName] = response.data.data.result;
|
||||
return response.data.data.result;
|
||||
});
|
||||
}
|
||||
|
||||
transformToCompletions(words, meta) {
|
||||
return words.map(name => {
|
||||
return {
|
||||
caption: name,
|
||||
value: name,
|
||||
meta: meta,
|
||||
score: Number.MAX_VALUE
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
findMetricName(session, row, column) {
|
||||
var metricName = '';
|
||||
|
||||
var tokens;
|
||||
var nameLabelNameToken = this.findToken(session, row, column, 'entity.name.tag', '__name__', 'paren.lparen');
|
||||
if (nameLabelNameToken) {
|
||||
tokens = session.getTokens(nameLabelNameToken.row);
|
||||
var nameLabelValueToken = tokens[nameLabelNameToken.index + 2];
|
||||
if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted') {
|
||||
metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation
|
||||
}
|
||||
} else {
|
||||
var metricNameToken = this.findToken(session, row, column, 'identifier', null, null);
|
||||
if (metricNameToken) {
|
||||
tokens = session.getTokens(metricNameToken.row);
|
||||
if (tokens[metricNameToken.index + 1].type === 'paren.lparen') {
|
||||
metricName = metricNameToken.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metricName;
|
||||
}
|
||||
|
||||
findToken(session, row, column, target, value, guard) {
|
||||
var tokens, idx;
|
||||
for (var r = row; r >= 0; r--) {
|
||||
tokens = session.getTokens(r);
|
||||
if (r === row) { // current row
|
||||
var c = 0;
|
||||
for (idx = 0; idx < tokens.length; idx++) {
|
||||
c += tokens[idx].value.length;
|
||||
if (c >= column) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx = tokens.length - 1;
|
||||
}
|
||||
|
||||
for (; idx >= 0; idx--) {
|
||||
if (tokens[idx].type === guard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tokens[idx].type === target
|
||||
&& (!value || tokens[idx].value === value)) {
|
||||
tokens[idx].row = r;
|
||||
tokens[idx].index = idx;
|
||||
return tokens[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import PrometheusMetricFindQuery from './metric_find_query';
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/;
|
||||
|
||||
function prometheusSpecialRegexEscape(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
||||
}
|
||||
@@ -83,6 +80,7 @@ export class PrometheusDatasource {
|
||||
var self = this;
|
||||
var start = this.getPrometheusTime(options.range.from, false);
|
||||
var end = this.getPrometheusTime(options.range.to, true);
|
||||
var range = Math.ceil(end - start);
|
||||
|
||||
var queries = [];
|
||||
var activeTargets = [];
|
||||
@@ -95,18 +93,7 @@ export class PrometheusDatasource {
|
||||
}
|
||||
|
||||
activeTargets.push(target);
|
||||
|
||||
var query: any = {};
|
||||
query.expr = this.templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
||||
query.requestId = options.panelId + target.refId;
|
||||
query.instant = target.instant;
|
||||
|
||||
var interval = this.templateSrv.replace(target.interval, options.scopedVars) || options.interval;
|
||||
var intervalFactor = target.intervalFactor || 1;
|
||||
target.step = query.step = this.calculateInterval(interval, intervalFactor);
|
||||
var range = Math.ceil(end - start);
|
||||
target.step = query.step = this.adjustStep(query.step, this.intervalSeconds(options.interval), range);
|
||||
queries.push(query);
|
||||
queries.push(this.createQuery(target, options, range));
|
||||
}
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
@@ -147,13 +134,41 @@ export class PrometheusDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
adjustStep(step, autoStep, range) {
|
||||
// Prometheus drop query if range/step > 11000
|
||||
// calibrate step if it is too big
|
||||
if (step !== 0 && range / step > 11000) {
|
||||
step = Math.ceil(range / 11000);
|
||||
createQuery(target, options, range) {
|
||||
var query: any = {};
|
||||
query.instant = target.instant;
|
||||
|
||||
var interval = kbn.interval_to_seconds(options.interval);
|
||||
// Minimum interval ("Min step"), if specified for the query. or same as interval otherwise
|
||||
var minInterval = kbn.interval_to_seconds(this.templateSrv.replace(target.interval, options.scopedVars) || options.interval);
|
||||
var intervalFactor = target.intervalFactor || 1;
|
||||
// Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
|
||||
var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
|
||||
|
||||
var scopedVars = options.scopedVars;
|
||||
// If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
|
||||
if (interval !== adjustedInterval) {
|
||||
interval = adjustedInterval;
|
||||
scopedVars = Object.assign({}, options.scopedVars, {
|
||||
"__interval": {text: interval + "s", value: interval + "s"},
|
||||
"__interval_ms": {text: interval * 1000, value: interval * 1000},
|
||||
});
|
||||
}
|
||||
return Math.max(step, autoStep);
|
||||
target.step = query.step = interval;
|
||||
|
||||
// Only replace vars in expression after having (possibly) updated interval vars
|
||||
query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr);
|
||||
query.requestId = options.panelId + target.refId;
|
||||
return query;
|
||||
}
|
||||
|
||||
adjustInterval(interval, minInterval, range, intervalFactor) {
|
||||
// Prometheus will drop queries that might return more than 11000 data points.
|
||||
// Calibrate interval if it is too small.
|
||||
if (interval !== 0 && range / intervalFactor / interval > 11000) {
|
||||
interval = Math.ceil(range / intervalFactor / 11000);
|
||||
}
|
||||
return Math.max(interval * intervalFactor, minInterval);
|
||||
}
|
||||
|
||||
performTimeSeriesQuery(query, start, end) {
|
||||
@@ -218,7 +233,7 @@ export class PrometheusDatasource {
|
||||
var end = this.getPrometheusTime(options.range.to, true);
|
||||
var query = {
|
||||
expr: interpolated,
|
||||
step: this.adjustStep(kbn.interval_to_seconds(step), 0, Math.ceil(end - start)) + 's'
|
||||
step: this.adjustInterval(kbn.interval_to_seconds(step), 0, Math.ceil(end - start), 1) + 's'
|
||||
};
|
||||
|
||||
var self = this;
|
||||
@@ -257,21 +272,6 @@ export class PrometheusDatasource {
|
||||
});
|
||||
}
|
||||
|
||||
calculateInterval(interval, intervalFactor) {
|
||||
return Math.ceil(this.intervalSeconds(interval) * intervalFactor);
|
||||
}
|
||||
|
||||
intervalSeconds(interval) {
|
||||
var m = interval.match(durationSplitRegexp);
|
||||
var dur = moment.duration(parseInt(m[1]), m[2]);
|
||||
var sec = dur.asSeconds();
|
||||
if (sec < 1) {
|
||||
sec = 1;
|
||||
}
|
||||
|
||||
return sec;
|
||||
}
|
||||
|
||||
transformMetricData(md, options, start, end) {
|
||||
var dps = [],
|
||||
metricLabel = null;
|
||||
|
||||
@@ -65,13 +65,13 @@ var PrometheusHighlightRules = function() {
|
||||
regex : "\\s+"
|
||||
} ],
|
||||
"start-label-matcher" : [ {
|
||||
token : "keyword",
|
||||
token : "entity.name.tag",
|
||||
regex : '[a-zA-Z_][a-zA-Z0-9_]*'
|
||||
}, {
|
||||
token : "keyword.operator",
|
||||
regex : '=~|=|!~|!='
|
||||
}, {
|
||||
token : "string",
|
||||
token : "string.quoted",
|
||||
regex : '"[^"]*"|\'[^\']*\''
|
||||
}, {
|
||||
token : "punctuation.operator",
|
||||
@@ -401,7 +401,7 @@ var PrometheusCompletions = function() {};
|
||||
(function() {
|
||||
this.getCompletions = function(state, session, pos, prefix, callback) {
|
||||
var token = session.getTokenAt(pos.row, pos.column);
|
||||
if (token.type === 'label.name' || token.type === 'label.value') {
|
||||
if (token.type === 'entity.name.tag' || token.type === 'string.quoted') {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,115 @@ import {PromCompleter} from '../completer';
|
||||
import {PrometheusDatasource} from '../datasource';
|
||||
|
||||
describe('Prometheus editor completer', function() {
|
||||
function getSessionStub(data) {
|
||||
return {
|
||||
getTokenAt: sinon.stub().returns(data.currentToken),
|
||||
getTokens: sinon.stub().returns(data.tokens),
|
||||
getLine: sinon.stub().returns(data.line),
|
||||
};
|
||||
}
|
||||
|
||||
let editor = {};
|
||||
let session = {
|
||||
getTokenAt: sinon.stub().returns({}),
|
||||
getLine: sinon.stub().returns(""),
|
||||
let datasourceStub = <PrometheusDatasource>{
|
||||
performInstantQuery: sinon
|
||||
.stub()
|
||||
.withArgs({expr: '{__name__="node_cpu"'})
|
||||
.returns(
|
||||
Promise.resolve({
|
||||
data: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {
|
||||
job: 'node',
|
||||
instance: 'localhost:9100',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
performSuggestQuery: sinon
|
||||
.stub()
|
||||
.withArgs('node', true)
|
||||
.returns(Promise.resolve(['node_cpu'])),
|
||||
};
|
||||
|
||||
let datasourceStub = <PrometheusDatasource>{};
|
||||
let completer = new PromCompleter(datasourceStub);
|
||||
|
||||
describe("When inside brackets", () => {
|
||||
|
||||
it("Should return range vectors", () => {
|
||||
completer.getCompletions(editor, session, 10, "[", (s, res) => {
|
||||
describe('When inside brackets', () => {
|
||||
it('Should return range vectors', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {},
|
||||
tokens: [],
|
||||
line: '',
|
||||
});
|
||||
completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => {
|
||||
expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label name', () => {
|
||||
it('Should return label name list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {type: 'entity.name.tag', value: 'j', index: 2, start: 9},
|
||||
tokens: [
|
||||
{type: 'identifier', value: 'node_cpu'},
|
||||
{type: 'paren.lparen', value: '{'},
|
||||
{type: 'entity.name.tag', value: 'j', index: 2, start: 9},
|
||||
{type: 'paren.rparen', value: '}'},
|
||||
],
|
||||
line: 'node_cpu{j}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, {row: 0, column: 10}, 'j', (s, res) => {
|
||||
expect(res[0].meta).to.eql('label name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label name with __name__ match', () => {
|
||||
it('Should return label name list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {type: 'entity.name.tag', value: 'j', index: 5, start: 22},
|
||||
tokens: [
|
||||
{type: 'paren.lparen', value: '{'},
|
||||
{type: 'entity.name.tag', value: '__name__'},
|
||||
{type: 'keyword.operator', value: '=~'},
|
||||
{type: 'string.quoted', value: '"node_cpu"'},
|
||||
{type: 'punctuation.operator', value: ','},
|
||||
{type: 'entity.name.tag', value: 'j', index: 5, start: 22},
|
||||
{type: 'paren.rparen', value: '}'},
|
||||
],
|
||||
line: '{__name__=~"node_cpu",j}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, {row: 0, column: 23}, 'j', (s, res) => {
|
||||
expect(res[0].meta).to.eql('label name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When inside label matcher, and located at label value', () => {
|
||||
it('Should return label value list', () => {
|
||||
const session = getSessionStub({
|
||||
currentToken: {type: 'string.quoted', value: '"n"', index: 4, start: 13},
|
||||
tokens: [
|
||||
{type: 'identifier', value: 'node_cpu'},
|
||||
{type: 'paren.lparen', value: '{'},
|
||||
{type: 'entity.name.tag', value: 'job'},
|
||||
{type: 'keyword.operator', value: '='},
|
||||
{type: 'string.quoted', value: '"n"', index: 4, start: 13},
|
||||
{type: 'paren.rparen', value: '}'},
|
||||
],
|
||||
line: 'node_cpu{job="n"}',
|
||||
});
|
||||
|
||||
return completer.getCompletions(editor, session, {row: 0, column: 15}, 'n', (s, res) => {
|
||||
expect(res[0].meta).to.eql('label value');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -269,4 +269,311 @@ describe('PrometheusDatasource', function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('The "step" query parameter', function() {
|
||||
var response = {
|
||||
status: "success",
|
||||
data: {
|
||||
resultType: "matrix",
|
||||
result: []
|
||||
}
|
||||
};
|
||||
|
||||
it('should be min interval when greater than auto interval', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
interval: '10s'
|
||||
}],
|
||||
interval: '5s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=10';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should be auto interval when greater than min interval', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
interval: '5s'
|
||||
}],
|
||||
interval: '10s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=10';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should result in querying fewer than 11000 data points', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{ expr: 'test' }],
|
||||
interval: '1s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=2';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should not apply min interval when interval * intervalFactor greater', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
interval: '10s',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '5s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=50';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should apply min interval when interval * intervalFactor smaller', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
interval: '15s',
|
||||
intervalFactor: 2
|
||||
}],
|
||||
interval: '5s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=15';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should apply intervalFactor to auto interval when greater', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
interval: '5s',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '10s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1443460275&step=100';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should not not be affected by the 11000 data points limit when large enough', function() {
|
||||
var query = {
|
||||
// 1 week range
|
||||
range: { from: moment(1443438674760), to: moment(1444043474760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '10s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1444043475&step=100';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
it('should be determined by the 11000 data points limit when too small', function() {
|
||||
var query = {
|
||||
// 1 week range
|
||||
range: { from: moment(1443438674760), to: moment(1444043474760) },
|
||||
targets: [{
|
||||
expr: 'test',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '5s'
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=test' +
|
||||
'&start=1443438675&end=1444043475&step=60';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
});
|
||||
describe('The __interval and __interval_ms template variables', function() {
|
||||
var response = {
|
||||
status: "success",
|
||||
data: {
|
||||
resultType: "matrix",
|
||||
result: []
|
||||
}
|
||||
};
|
||||
|
||||
it('should be unchanged when auto interval is greater than min interval', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
interval: '5s'
|
||||
}],
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "10s", value: "10s"},
|
||||
"__interval_ms": {text: 10 * 1000, value: 10 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[10s])') +
|
||||
'&start=1443438675&end=1443460275&step=10';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("10s");
|
||||
expect(query.scopedVars.__interval.value).to.be("10s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
});
|
||||
it('should be min interval when it is greater than auto interval', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
interval: '10s'
|
||||
}],
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "5s", value: "5s"},
|
||||
"__interval_ms": {text: 5 * 1000, value: 5 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[10s])') +
|
||||
'&start=1443438675&end=1443460275&step=10';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("5s");
|
||||
expect(query.scopedVars.__interval.value).to.be("5s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should account for intervalFactor', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
interval: '5s',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '10s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "10s", value: "10s"},
|
||||
"__interval_ms": {text: 10 * 1000, value: 10 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[100s])') +
|
||||
'&start=1443438675&end=1443460275&step=100';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("10s");
|
||||
expect(query.scopedVars.__interval.value).to.be("10s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
|
||||
});
|
||||
it('should be interval * intervalFactor when greater than min interval', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
interval: '10s',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "5s", value: "5s"},
|
||||
"__interval_ms": {text: 5 * 1000, value: 5 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[50s])') +
|
||||
'&start=1443438675&end=1443460275&step=50';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("5s");
|
||||
expect(query.scopedVars.__interval.value).to.be("5s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should be min interval when greater than interval * intervalFactor', function() {
|
||||
var query = {
|
||||
// 6 hour range
|
||||
range: { from: moment(1443438674760), to: moment(1443460274760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
interval: '15s',
|
||||
intervalFactor: 2
|
||||
}],
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "5s", value: "5s"},
|
||||
"__interval_ms": {text: 5 * 1000, value: 5 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[15s])') +
|
||||
'&start=1443438675&end=1443460275&step=15';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("5s");
|
||||
expect(query.scopedVars.__interval.value).to.be("5s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
|
||||
var query = {
|
||||
// 1 week range
|
||||
range: { from: moment(1443438674760), to: moment(1444043474760) },
|
||||
targets: [{
|
||||
expr: 'rate(test[$__interval])',
|
||||
intervalFactor: 10
|
||||
}],
|
||||
interval: '5s',
|
||||
scopedVars: {
|
||||
"__interval": {text: "5s", value: "5s"},
|
||||
"__interval_ms": {text: 5 * 1000, value: 5 * 1000},
|
||||
}
|
||||
};
|
||||
var urlExpected = 'proxied/api/v1/query_range?query=' +
|
||||
encodeURIComponent('rate(test[60s])') +
|
||||
'&start=1443438675&end=1444043475&step=60';
|
||||
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
|
||||
ctx.ds.query(query);
|
||||
ctx.$httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
expect(query.scopedVars.__interval.text).to.be("5s");
|
||||
expect(query.scopedVars.__interval.value).to.be("5s");
|
||||
expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
|
||||
expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-list-main">
|
||||
<p class="alert-list-title">{{al.title}}</p>
|
||||
<p class="alert-list-title">{{al.alertName}}</p>
|
||||
<div class="alert-list-text">
|
||||
<span class="alert-list-state {{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||
<span class="alert-list-info alert-list-info-left">{{al.info}}</span>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all';
|
||||
import {convertValuesToHistogram, getSeriesValues} from './histogram';
|
||||
|
||||
/** @ngInject **/
|
||||
function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
@@ -37,7 +37,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
var legendSideLastValue = null;
|
||||
var rootScope = scope.$root;
|
||||
var panelWidth = 0;
|
||||
var eventManager = new EventManager(ctrl, elem, popoverSrv);
|
||||
var eventManager = new EventManager(ctrl);
|
||||
var thresholdManager = new ThresholdManager(ctrl);
|
||||
var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
@@ -268,6 +268,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
clickable: true,
|
||||
color: '#c8c8c8',
|
||||
margin: { left: 0, right: 0 },
|
||||
labelMarginX: 0,
|
||||
},
|
||||
selection: {
|
||||
mode: "x",
|
||||
@@ -651,10 +652,10 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
}
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
if (ranges.ctrlKey || ranges.metaKey) {
|
||||
// scope.$apply(() => {
|
||||
// eventManager.updateTime(ranges.xaxis);
|
||||
// });
|
||||
if ((ranges.ctrlKey || ranges.metaKey) && contextSrv.isEditor) {
|
||||
setTimeout(() => {
|
||||
eventManager.updateTime(ranges.xaxis);
|
||||
}, 100);
|
||||
} else {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
@@ -666,13 +667,13 @@ function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
});
|
||||
|
||||
elem.bind("plotclick", function (event, pos, item) {
|
||||
if (pos.ctrlKey || pos.metaKey || eventManager.event) {
|
||||
if ((pos.ctrlKey || pos.metaKey) && contextSrv.isEditor) {
|
||||
// Skip if range selected (added in "plotselected" event handler)
|
||||
let isRangeSelection = pos.x !== pos.x1;
|
||||
if (!isRangeSelection) {
|
||||
// scope.$apply(() => {
|
||||
// eventManager.updateTime({from: pos.x, to: null});
|
||||
// });
|
||||
setTimeout(() => {
|
||||
eventManager.updateTime({from: pos.x, to: null});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,14 +7,18 @@ define([
|
||||
function ($, _, angular, Drop) {
|
||||
'use strict';
|
||||
|
||||
function createAnnotationToolip(element, event) {
|
||||
function createAnnotationToolip(element, event, plot) {
|
||||
var injector = angular.element(document).injector();
|
||||
var content = document.createElement('div');
|
||||
content.innerHTML = '<annotation-tooltip event="event"></annotation-tooltip>';
|
||||
content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
|
||||
|
||||
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||
var eventManager = plot.getOptions().events.manager;
|
||||
var tmpScope = $rootScope.$new(true);
|
||||
tmpScope.event = event;
|
||||
tmpScope.onEdit = function() {
|
||||
eventManager.editEvent(event);
|
||||
};
|
||||
|
||||
$compile(content)(tmpScope);
|
||||
tmpScope.$digest();
|
||||
@@ -42,6 +46,69 @@ function ($, _, angular, Drop) {
|
||||
}]);
|
||||
}
|
||||
|
||||
var markerElementToAttachTo = null;
|
||||
|
||||
function createEditPopover(element, event, plot) {
|
||||
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)
|
||||
markerElementToAttachTo = element;
|
||||
return;
|
||||
}
|
||||
|
||||
// mark as openend
|
||||
eventManager.editorOpened();
|
||||
// set marker elment to attache to
|
||||
markerElementToAttachTo = element;
|
||||
|
||||
// wait for element to be attached and positioned
|
||||
setTimeout(function() {
|
||||
|
||||
var injector = angular.element(document).injector();
|
||||
var content = document.createElement('div');
|
||||
content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
|
||||
|
||||
injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
|
||||
var scope = $rootScope.$new(true);
|
||||
var drop;
|
||||
|
||||
scope.event = event;
|
||||
scope.panelCtrl = eventManager.panelCtrl;
|
||||
scope.close = function() {
|
||||
drop.close();
|
||||
};
|
||||
|
||||
$compile(content)(scope);
|
||||
scope.$digest();
|
||||
|
||||
drop = new Drop({
|
||||
target: markerElementToAttachTo[0],
|
||||
content: content,
|
||||
position: "bottom center",
|
||||
classes: 'drop-popover drop-popover--form',
|
||||
openOn: 'click',
|
||||
tetherOptions: {
|
||||
constraints: [{to: 'window', pin: true, attachment: "both"}]
|
||||
}
|
||||
});
|
||||
|
||||
drop.open();
|
||||
eventManager.editorOpened();
|
||||
|
||||
drop.on('close', function() {
|
||||
// need timeout here in order call drop.destroy
|
||||
setTimeout(function() {
|
||||
eventManager.editorClosed();
|
||||
scope.$destroy();
|
||||
drop.destroy();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/*
|
||||
* jquery.flot.events
|
||||
*
|
||||
@@ -121,11 +188,20 @@ function ($, _, angular, Drop) {
|
||||
*/
|
||||
this.setupEvents = function(events) {
|
||||
var that = this;
|
||||
var parts = _.partition(events, 'isRegion');
|
||||
var regions = parts[0];
|
||||
events = parts[1];
|
||||
|
||||
$.each(events, function(index, event) {
|
||||
var ve = new VisualEvent(event, that._buildDiv(event));
|
||||
_events.push(ve);
|
||||
});
|
||||
|
||||
$.each(regions, function (index, event) {
|
||||
var vre = new VisualEvent(event, that._buildRegDiv(event));
|
||||
_events.push(vre);
|
||||
});
|
||||
|
||||
_events.sort(function(a, b) {
|
||||
var ao = a.getOptions(), bo = b.getOptions();
|
||||
if (ao.min > bo.min) { return 1; }
|
||||
@@ -232,7 +308,10 @@ function ($, _, angular, Drop) {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
top = o.top + this._plot.height();
|
||||
var topOffset = xaxis.options.eventSectionHeight || 0;
|
||||
topOffset = topOffset / 3;
|
||||
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
left = xaxis.p2c(event.min) + o.left;
|
||||
|
||||
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||
@@ -241,25 +320,27 @@ function ($, _, angular, Drop) {
|
||||
"left": left + 'px',
|
||||
"top": 8,
|
||||
"width": lineWidth + "px",
|
||||
"height": this._plot.height(),
|
||||
"height": this._plot.height() + topOffset * 0.8,
|
||||
"border-left-width": lineWidth + "px",
|
||||
"border-left-style": lineStyle,
|
||||
"border-left-color": color
|
||||
"border-left-color": color,
|
||||
"color": color
|
||||
})
|
||||
.appendTo(container);
|
||||
|
||||
if (markerShow) {
|
||||
var marker = $('<div class="events_marker"></div>').css({
|
||||
"position": "absolute",
|
||||
"left": (-markerSize-Math.round(lineWidth/2)) + "px",
|
||||
"left": (-markerSize - Math.round(lineWidth / 2)) + "px",
|
||||
"font-size": 0,
|
||||
"line-height": 0,
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"border-left": markerSize+"px solid transparent",
|
||||
"border-right": markerSize+"px solid transparent"
|
||||
})
|
||||
.appendTo(line);
|
||||
});
|
||||
|
||||
marker.appendTo(line);
|
||||
|
||||
if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
|
||||
marker.css({
|
||||
@@ -280,9 +361,13 @@ function ($, _, angular, Drop) {
|
||||
});
|
||||
|
||||
var mouseenter = function() {
|
||||
createAnnotationToolip(marker, $(this).data("event"));
|
||||
createAnnotationToolip(marker, $(this).data("event"), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(marker, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
var mouseleave = function() {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
@@ -312,6 +397,127 @@ function ($, _, angular, Drop) {
|
||||
return drawableEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* create a DOM element for the given region
|
||||
*/
|
||||
this._buildRegDiv = function (event) {
|
||||
var that = this;
|
||||
|
||||
var container = this._plot.getPlaceholder();
|
||||
var o = this._plot.getPlotOffset();
|
||||
var axes = this._plot.getAxes();
|
||||
var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
|
||||
var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
|
||||
|
||||
// determine the y axis used
|
||||
if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
|
||||
if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
|
||||
|
||||
// map the eventType to a types object
|
||||
var eventTypeId = event.eventType;
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
|
||||
color = '#666';
|
||||
} else {
|
||||
color = this._types[eventTypeId].color;
|
||||
}
|
||||
|
||||
if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
|
||||
markerTooltip = true;
|
||||
} else {
|
||||
markerTooltip = this._types[eventTypeId].markerTooltip;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
|
||||
lineWidth = 1; //default line width
|
||||
} else {
|
||||
lineWidth = this._types[eventTypeId].lineWidth;
|
||||
}
|
||||
|
||||
if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
|
||||
lineStyle = 'dashed'; //default line style
|
||||
} else {
|
||||
lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
|
||||
}
|
||||
|
||||
var topOffset = 2;
|
||||
top = o.top + this._plot.height() + topOffset;
|
||||
|
||||
var timeFrom = Math.min(event.min, event.timeEnd);
|
||||
var timeTo = Math.max(event.min, event.timeEnd);
|
||||
left = xaxis.p2c(timeFrom) + o.left;
|
||||
var right = xaxis.p2c(timeTo) + o.left;
|
||||
regionWidth = right - left;
|
||||
|
||||
_.each([left, right], function(position) {
|
||||
var line = $('<div class="events_line flot-temp-elem"></div>').css({
|
||||
"position": "absolute",
|
||||
"opacity": 0.8,
|
||||
"left": position + 'px',
|
||||
"top": 8,
|
||||
"width": lineWidth + "px",
|
||||
"height": that._plot.height() + topOffset,
|
||||
"border-left-width": lineWidth + "px",
|
||||
"border-left-style": lineStyle,
|
||||
"border-left-color": color,
|
||||
"color": color
|
||||
});
|
||||
line.appendTo(container);
|
||||
});
|
||||
|
||||
var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
|
||||
"position": "absolute",
|
||||
"opacity": 0.5,
|
||||
"left": left + 'px',
|
||||
"top": top,
|
||||
"width": Math.round(regionWidth + lineWidth) + "px",
|
||||
"height": "0.5rem",
|
||||
"border-left-color": color,
|
||||
"color": color,
|
||||
"background-color": color
|
||||
});
|
||||
region.appendTo(container);
|
||||
|
||||
region.data({
|
||||
"event": event
|
||||
});
|
||||
|
||||
var mouseenter = function () {
|
||||
createAnnotationToolip(region, $(this).data("event"), that._plot);
|
||||
};
|
||||
|
||||
if (event.editModel) {
|
||||
createEditPopover(region, event.editModel, that._plot);
|
||||
}
|
||||
|
||||
var mouseleave = function () {
|
||||
that._plot.clearSelection();
|
||||
};
|
||||
|
||||
if (markerTooltip) {
|
||||
region.css({ "cursor": "help" });
|
||||
region.hover(mouseenter, mouseleave);
|
||||
}
|
||||
|
||||
var drawableEvent = new DrawableEvent(
|
||||
region,
|
||||
function drawFunc(obj) { obj.show(); },
|
||||
function (obj) { obj.remove(); },
|
||||
function (obj, position) {
|
||||
obj.css({
|
||||
top: position.top,
|
||||
left: position.left
|
||||
});
|
||||
},
|
||||
left,
|
||||
top,
|
||||
region.width(),
|
||||
region.height()
|
||||
);
|
||||
|
||||
return drawableEvent;
|
||||
};
|
||||
|
||||
/**
|
||||
* check if the event is inside visible range
|
||||
*/
|
||||
@@ -395,5 +601,4 @@ function ($, _, angular, Drop) {
|
||||
name: "events",
|
||||
version: "0.2.5"
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -45,7 +45,8 @@ function (angular, _, $) {
|
||||
popoverSrv.show({
|
||||
element: el[0],
|
||||
position: 'bottom center',
|
||||
template: '<gf-color-picker></gf-color-picker>',
|
||||
template: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
|
||||
'</series-color-picker>',
|
||||
openOn: 'hover',
|
||||
model: {
|
||||
series: series,
|
||||
|
||||
@@ -57,7 +57,7 @@ define([
|
||||
element: $element.find(".dropdown")[0],
|
||||
position: 'top center',
|
||||
openOn: 'click',
|
||||
template: '<gf-color-picker></gf-color-picker>',
|
||||
template: '<series-color-picker onColorChange="colorSelected" />',
|
||||
model: {
|
||||
autoClose: true,
|
||||
colorSelected: $scope.colorSelected,
|
||||
|
||||
@@ -37,6 +37,20 @@ export class ThresholdFormCtrl {
|
||||
render() {
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
onFillColorChange(index) {
|
||||
return (newColor) => {
|
||||
this.panel.thresholds[index].fillColor = newColor;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
|
||||
onLineColorChange(index) {
|
||||
return (newColor) => {
|
||||
this.panel.thresholds[index].lineColor = newColor;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var template = `
|
||||
@@ -77,7 +91,7 @@ var template = `
|
||||
<div class="gf-form" ng-if="threshold.fill && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Fill color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="threshold.fillColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<color-picker color="threshold.fillColor" onChange="ctrl.onFillColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +101,7 @@ var template = `
|
||||
<div class="gf-form" ng-if="threshold.line && threshold.colorMode === 'custom'">
|
||||
<label class="gf-form-label">Line color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="threshold.lineColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<color-picker color="threshold.lineColor" onChange="ctrl.onLineColorChange($index)"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.events.on('data-error', this.onDataError.bind(this));
|
||||
this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
|
||||
this.onCardColorChange = this.onCardColorChange.bind(this);
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
@@ -236,6 +238,11 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
onCardColorChange(newColor) {
|
||||
this.panel.color.cardColor = newColor;
|
||||
this.render();
|
||||
}
|
||||
|
||||
seriesHandler(seriesData) {
|
||||
let series = new TimeSeries({
|
||||
datapoints: seriesData.datapoints,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.color.cardColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<color-picker color="ctrl.panel.color.cardColor" onChange="ctrl.onCardColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
|
||||
@@ -68,14 +68,8 @@
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Colors</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.colors[0]" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
</span>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.colors[1]" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
</span>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.colors[2]" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<span class="gf-form-label" ng-repeat="color in ctrl.panel.colors track by $index">
|
||||
<color-picker color="color" onChange="ctrl.onColorChange($index)"></color-picker>
|
||||
</span>
|
||||
<span class="gf-form-label">
|
||||
<a ng-click="ctrl.invertColorOrder()">
|
||||
@@ -93,13 +87,13 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Line Color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.sparkline.lineColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<color-picker color="ctrl.panel.sparkline.lineColor" onChange="ctrl.onSparklineColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Fill Color</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.sparkline.fillColor" ng-change="ctrl.render()" ></spectrum-picker>
|
||||
<color-picker color="ctrl.panel.sparkline.fillColor" onChange="ctrl.onSparklineFillChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,6 +92,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
this.events.on('data-error', this.onDataError.bind(this));
|
||||
this.events.on('data-snapshot-load', this.onDataReceived.bind(this));
|
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||
|
||||
this.onSparklineColorChange = this.onSparklineColorChange.bind(this);
|
||||
this.onSparklineFillChange = this.onSparklineFillChange.bind(this);
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
@@ -214,6 +217,23 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
}
|
||||
|
||||
onColorChange(panelColorIndex) {
|
||||
return (color) => {
|
||||
this.panel.colors[panelColorIndex] = color;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
|
||||
onSparklineColorChange(newColor) {
|
||||
this.panel.sparkline.lineColor = newColor;
|
||||
this.render();
|
||||
}
|
||||
|
||||
onSparklineFillChange(newColor) {
|
||||
this.panel.sparkline.fillColor = newColor;
|
||||
this.render();
|
||||
}
|
||||
|
||||
getDecimalsForValue(value) {
|
||||
if (_.isNumber(this.panel.decimals)) {
|
||||
return {decimals: this.panel.decimals, scaledDecimals: null};
|
||||
@@ -425,7 +445,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
function addGauge() {
|
||||
var width = elem.width();
|
||||
var height = elem.height();
|
||||
var dimension = Math.min(width, height);
|
||||
// Allow to use a bit more space for wide gauges
|
||||
var dimension = Math.min(width, height * 1.3);
|
||||
|
||||
ctrl.invalidGaugeRange = false;
|
||||
if (panel.gauge.minValue > panel.gauge.maxValue) {
|
||||
@@ -462,8 +483,11 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
|
||||
var fontScale = parseInt(panel.valueFontSize) / 100;
|
||||
var fontSize = Math.min(dimension/5, 100) * fontScale;
|
||||
var gaugeWidth = Math.min(dimension/6, 60);
|
||||
// Reduce gauge width if threshold labels enabled
|
||||
var gaugeWidthReduceRatio = panel.gauge.thresholdLabels ? 1.5 : 1;
|
||||
var gaugeWidth = Math.min(dimension/6, 60) / gaugeWidthReduceRatio;
|
||||
var thresholdMarkersWidth = gaugeWidth/5;
|
||||
var thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
var options = {
|
||||
series: {
|
||||
@@ -484,8 +508,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
values: thresholds,
|
||||
label: {
|
||||
show: panel.gauge.thresholdLabels,
|
||||
margin: 8,
|
||||
font: { size: 18 }
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
font: { size: thresholdLabelFontSize }
|
||||
},
|
||||
show: panel.gauge.thresholdMarkers,
|
||||
width: thresholdMarkersWidth,
|
||||
|
||||
@@ -35,13 +35,15 @@
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">Type</label>
|
||||
<div class="gf-form-select-wrapper width-10">
|
||||
<div class="gf-form-select-wrapper width-16">
|
||||
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="style.type === 'date'">
|
||||
<label class="gf-form-label width-11">Date Format</label>
|
||||
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
|
||||
<div class="gf-form-select-wrapper width-16">
|
||||
<select class="gf-form-input" ng-model="style.dateFormat" ng-options="c.value as c.text for c in editor.dateFormats" ng-change="editor.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="style.type === 'string'">
|
||||
@@ -54,7 +56,7 @@
|
||||
<div ng-if="style.type === 'number'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">Unit</label>
|
||||
<div class="gf-form-dropdown-typeahead width-10" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
|
||||
<div class="gf-form-dropdown-typeahead width-16" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">Decimals</label>
|
||||
@@ -67,7 +69,7 @@
|
||||
<h5 class="section-heading">Thresholds</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label>
|
||||
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur>
|
||||
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Color Mode</label>
|
||||
@@ -78,13 +80,13 @@
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Colors</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker>
|
||||
<color-picker color="style.colors[0]" onChange="editor.onColorChange($index, 0)"></color-picker>
|
||||
</span>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
|
||||
<color-picker color="style.colors[1]" onChange="editor.onColorChange($index, 1)"></color-picker>
|
||||
</span>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
|
||||
<color-picker color="style.colors[2]" onChange="editor.onColorChange($index, 2)"></color-picker>
|
||||
</span>
|
||||
<div class="gf-form-label">
|
||||
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
|
||||
|
||||
@@ -40,6 +40,7 @@ export class ColumnOptionsCtrl {
|
||||
this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
|
||||
this.dateFormats = [
|
||||
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
|
||||
{text: 'YYYY-MM-DD HH:mm:ss.SSS', value: 'YYYY-MM-DD HH:mm:ss.SSS'},
|
||||
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
|
||||
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
|
||||
];
|
||||
@@ -52,6 +53,8 @@ export class ColumnOptionsCtrl {
|
||||
return col.text;
|
||||
});
|
||||
};
|
||||
|
||||
this.onColorChange = this.onColorChange.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -103,6 +106,13 @@ export class ColumnOptionsCtrl {
|
||||
ref[2] = copy;
|
||||
this.panelCtrl.render();
|
||||
}
|
||||
|
||||
onColorChange(styleIndex, colorIndex) {
|
||||
return (newColor) => {
|
||||
this.panel.styles[styleIndex].colors[colorIndex] = newColor;
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
|
||||
83
public/app/system.conf.js
Normal file
83
public/app/system.conf.js
Normal file
@@ -0,0 +1,83 @@
|
||||
System.config({
|
||||
defaultJSExtenions: true,
|
||||
baseURL: 'public',
|
||||
paths: {
|
||||
'virtual-scroll': 'vendor/npm/virtual-scroll/src/index.js',
|
||||
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js',
|
||||
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
|
||||
'tether': 'vendor/npm/tether/dist/js/tether.js',
|
||||
'eventemitter3': 'vendor/npm/eventemitter3/index.js',
|
||||
'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js',
|
||||
'moment': 'vendor/moment.js',
|
||||
"jquery": "vendor/jquery/dist/jquery.js",
|
||||
'lodash-src': 'vendor/lodash/dist/lodash.js',
|
||||
"lodash": 'app/core/lodash_extended.js',
|
||||
"angular": "vendor/angular/angular.js",
|
||||
"bootstrap": "vendor/bootstrap/bootstrap.js",
|
||||
'angular-route': 'vendor/angular-route/angular-route.js',
|
||||
'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js',
|
||||
"angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js",
|
||||
"angular-strap": "vendor/angular-other/angular-strap.js",
|
||||
"angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js",
|
||||
"angular-bindonce": "vendor/angular-bindonce/bindonce.js",
|
||||
"spectrum": "vendor/spectrum.js",
|
||||
"bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js",
|
||||
"jquery.flot": "vendor/flot/jquery.flot",
|
||||
"jquery.flot.pie": "vendor/flot/jquery.flot.pie",
|
||||
"jquery.flot.selection": "vendor/flot/jquery.flot.selection",
|
||||
"jquery.flot.stack": "vendor/flot/jquery.flot.stack",
|
||||
"jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent",
|
||||
"jquery.flot.time": "vendor/flot/jquery.flot.time",
|
||||
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
|
||||
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
|
||||
"d3": "vendor/d3/d3.js",
|
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
|
||||
"twemoji": "vendor/npm/twemoji/2/twemoji.amd.js",
|
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
|
||||
},
|
||||
|
||||
packages: {
|
||||
app: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
vendor: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
plugins: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
test: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
},
|
||||
|
||||
map: {
|
||||
text: 'vendor/plugin-text/text.js',
|
||||
css: 'app/core/utils/css_loader.js'
|
||||
},
|
||||
|
||||
meta: {
|
||||
'vendor/npm/virtual-scroll/src/indx.js': {
|
||||
format: 'cjs',
|
||||
exports: 'VirtualScroll',
|
||||
},
|
||||
'vendor/angular/angular.js': {
|
||||
format: 'global',
|
||||
deps: ['jquery'],
|
||||
exports: 'angular',
|
||||
},
|
||||
'vendor/npm/eventemitter3/index.js': {
|
||||
format: 'cjs',
|
||||
exports: 'EventEmitter'
|
||||
},
|
||||
'vendor/npm/mousetrap/mousetrap.js': {
|
||||
format: 'global',
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': {
|
||||
format: 'global',
|
||||
exports: 'ace'
|
||||
}
|
||||
}
|
||||
});
|
||||
BIN
public/img/mixed_styles.png
Normal file
BIN
public/img/mixed_styles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -269,7 +269,8 @@ $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a);
|
||||
// popover
|
||||
$popover-bg: $panel-bg;
|
||||
$popover-color: $text-color;
|
||||
$popover-border-color: $gray-1;
|
||||
$popover-border-color: $dark-4;
|
||||
$popover-shadow: 0 0 20px black;
|
||||
|
||||
$popover-help-bg: $btn-secondary-bg;
|
||||
$popover-help-color: $text-color;
|
||||
|
||||
@@ -284,9 +284,11 @@ $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d);
|
||||
$alert-info-bg: $blue-dark;
|
||||
|
||||
// popover
|
||||
$popover-bg: $gray-5;
|
||||
$popover-bg: $panel-bg;
|
||||
$popover-color: $text-color;
|
||||
$popover-border-color: $gray-3;
|
||||
$popover-border-color: $gray-5;
|
||||
$popover-shadow: 0 0 20px $white;
|
||||
|
||||
$popover-help-bg: $blue-dark;
|
||||
$popover-help-color: $gray-6;
|
||||
$popover-error-bg: $btn-danger-bg;
|
||||
|
||||
@@ -35,3 +35,14 @@
|
||||
float: left;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gf-color-picker__body {
|
||||
padding-bottom: 10px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.drop-popover.gf-color-picker {
|
||||
.drop-content {
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,16 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00);
|
||||
}
|
||||
}
|
||||
|
||||
.drop-element.drop-popover {
|
||||
.drop-content {
|
||||
box-shadow: $popover-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-element.drop-popover--form {
|
||||
.drop-content {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,12 @@ $gf-form-margin: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-hint {
|
||||
|
||||
26
public/sass/components/_icon-picker.scss
Normal file
26
public/sass/components/_icon-picker.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.gf-icon-picker {
|
||||
width: 400px;
|
||||
height: 450px;
|
||||
|
||||
.icon-filter {
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
|
||||
.gf-event-icon {
|
||||
margin: 0.4rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gf-icon-picker-button {
|
||||
.gf-event-icon {
|
||||
height: 1.2rem;
|
||||
}
|
||||
}
|
||||
@@ -287,19 +287,27 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.graph-annotation-header {
|
||||
background-color: $input-label-bg;
|
||||
.graph-annotation__header {
|
||||
background-color: $popover-border-color;
|
||||
padding: 0.40rem 0.65rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.graph-annotation-title {
|
||||
.graph-annotation__title {
|
||||
font-weight: $font-weight-semi-bold;
|
||||
padding-right: $spacer;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.graph-annotation-time {
|
||||
.graph-annotation__edit-icon {
|
||||
padding-left: $spacer;
|
||||
}
|
||||
|
||||
.graph-annotation__time {
|
||||
color: $text-muted;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
@@ -308,15 +316,22 @@
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.graph-annotation-body {
|
||||
.graph-annotation__body {
|
||||
padding: 0.65rem;
|
||||
}
|
||||
|
||||
a {
|
||||
.graph-annotation__user {
|
||||
img {
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
a[href] {
|
||||
color: $blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.left-yaxis-label {
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
max-width: 20rem;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@if $theme-bg != $border-color {
|
||||
box-shadow: 0 0 15px $border-color;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
|
||||
@@ -46,6 +46,14 @@ describe("DateMath", () => {
|
||||
expect(startOfDay).to.be(expected.getTime());
|
||||
});
|
||||
|
||||
it("now/d on a utc dashboard should be start of the current day in UTC time", () => {
|
||||
var today = new Date();
|
||||
var expected = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0));
|
||||
|
||||
var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf();
|
||||
expect(startOfDay).to.be(expected.getTime());
|
||||
});
|
||||
|
||||
describe('subtraction', () => {
|
||||
var now;
|
||||
var anchored;
|
||||
|
||||
@@ -21,7 +21,7 @@ angular.module('grafana.directives', []);
|
||||
angular.module('grafana.filters', []);
|
||||
angular.module('grafana.routes', ['ngRoute']);
|
||||
|
||||
const context = (<any>require).context('../', true, /specs/);
|
||||
const context = (<any>require).context('../', true, /specs\.(tsx?|js)/);
|
||||
for (let key of context.keys()) {
|
||||
context(key);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ define([
|
||||
};
|
||||
|
||||
this.createService = function(name) {
|
||||
return window.inject(function($q, $rootScope, $httpBackend, $injector, $location) {
|
||||
return window.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) {
|
||||
self.$q = $q;
|
||||
self.$rootScope = $rootScope;
|
||||
self.$httpBackend = $httpBackend;
|
||||
@@ -111,6 +111,7 @@ define([
|
||||
|
||||
self.$rootScope.onAppEvent = function() {};
|
||||
self.$rootScope.appEvent = function() {};
|
||||
self.$timeout = $timeout;
|
||||
|
||||
self.service = $injector.get(name);
|
||||
});
|
||||
|
||||
130
public/test/test-main.js
Normal file
130
public/test/test-main.js
Normal file
@@ -0,0 +1,130 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
// Tun on full stack traces in errors to help debugging
|
||||
Error.stackTraceLimit=Infinity;
|
||||
|
||||
window.__karma__.loaded = function() {};
|
||||
|
||||
System.config({
|
||||
baseURL: '/base/',
|
||||
defaultJSExtensions: true,
|
||||
paths: {
|
||||
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js',
|
||||
'eventemitter3': 'vendor/npm/eventemitter3/index.js',
|
||||
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
|
||||
'tether': 'vendor/npm/tether/dist/js/tether.js',
|
||||
'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js',
|
||||
'moment': 'vendor/moment.js',
|
||||
"jquery": "vendor/jquery/dist/jquery.js",
|
||||
'lodash-src': 'vendor/lodash/dist/lodash.js',
|
||||
"lodash": 'app/core/lodash_extended.js',
|
||||
"angular": 'vendor/angular/angular.js',
|
||||
'angular-mocks': 'vendor/angular-mocks/angular-mocks.js',
|
||||
"bootstrap": "vendor/bootstrap/bootstrap.js",
|
||||
'angular-route': 'vendor/angular-route/angular-route.js',
|
||||
'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js',
|
||||
"angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js",
|
||||
"angular-strap": "vendor/angular-other/angular-strap.js",
|
||||
"angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js",
|
||||
"angular-bindonce": "vendor/angular-bindonce/bindonce.js",
|
||||
"spectrum": "vendor/spectrum.js",
|
||||
"bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js",
|
||||
"jquery.flot": "vendor/flot/jquery.flot",
|
||||
"jquery.flot.pie": "vendor/flot/jquery.flot.pie",
|
||||
"jquery.flot.selection": "vendor/flot/jquery.flot.selection",
|
||||
"jquery.flot.stack": "vendor/flot/jquery.flot.stack",
|
||||
"jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent",
|
||||
"jquery.flot.time": "vendor/flot/jquery.flot.time",
|
||||
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
|
||||
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
|
||||
"d3": "vendor/d3/d3.js",
|
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
|
||||
"twemoji": "vendor/npm/twemoji/2/twemoji.amd.js",
|
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
|
||||
},
|
||||
|
||||
packages: {
|
||||
app: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
vendor: {
|
||||
defaultExtension: 'js',
|
||||
},
|
||||
},
|
||||
|
||||
map: {
|
||||
},
|
||||
|
||||
meta: {
|
||||
'vendor/angular/angular.js': {
|
||||
format: 'global',
|
||||
deps: ['jquery'],
|
||||
exports: 'angular',
|
||||
},
|
||||
'vendor/angular-mocks/angular-mocks.js': {
|
||||
format: 'global',
|
||||
deps: ['angular'],
|
||||
},
|
||||
'vendor/npm/eventemitter3/index.js': {
|
||||
format: 'cjs',
|
||||
exports: 'EventEmitter'
|
||||
},
|
||||
'vendor/npm/mousetrap/mousetrap.js': {
|
||||
format: 'global',
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': {
|
||||
format: 'global',
|
||||
exports: 'ace'
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
function file2moduleName(filePath) {
|
||||
return filePath.replace(/\\/g, '/')
|
||||
.replace(/^\/base\//, '')
|
||||
.replace(/\.\w*$/, '');
|
||||
}
|
||||
|
||||
function onlySpecFiles(path) {
|
||||
return /specs.*/.test(path);
|
||||
}
|
||||
|
||||
window.grafanaBootData = {settings: {}};
|
||||
|
||||
var modules = ['angular', 'angular-mocks', 'app/app'];
|
||||
var promises = modules.map(function(name) {
|
||||
return System.import(name);
|
||||
});
|
||||
|
||||
Promise.all(promises).then(function(deps) {
|
||||
var angular = deps[0];
|
||||
|
||||
angular.module('grafana', ['ngRoute']);
|
||||
angular.module('grafana.services', ['ngRoute', '$strap.directives']);
|
||||
angular.module('grafana.panels', []);
|
||||
angular.module('grafana.controllers', []);
|
||||
angular.module('grafana.directives', []);
|
||||
angular.module('grafana.filters', []);
|
||||
angular.module('grafana.routes', ['ngRoute']);
|
||||
|
||||
// load specs
|
||||
return Promise.all(
|
||||
Object.keys(window.__karma__.files) // All files served by Karma.
|
||||
.filter(onlySpecFiles)
|
||||
.map(file2moduleName)
|
||||
.map(function(path) {
|
||||
// console.log(path);
|
||||
return System.import(path);
|
||||
}));
|
||||
}).then(function() {
|
||||
window.__karma__.start();
|
||||
}, function(error) {
|
||||
window.__karma__.error(error.stack || error);
|
||||
}).catch(function(error) {
|
||||
window.__karma__.error(error.stack || error);
|
||||
});
|
||||
|
||||
})();
|
||||
7
public/vendor/flot/jquery.flot.js
vendored
7
public/vendor/flot/jquery.flot.js
vendored
@@ -602,6 +602,7 @@ Licensed under the MIT license.
|
||||
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
|
||||
margin: 0, // distance from the canvas edge to the grid
|
||||
labelMargin: 5, // in pixels
|
||||
eventSectionHeight: 0, // space for event section
|
||||
axisMargin: 8, // in pixels
|
||||
borderWidth: 2, // in pixels
|
||||
minBorderMargin: null, // in pixels, null means taken from points radius
|
||||
@@ -1450,6 +1451,7 @@ Licensed under the MIT license.
|
||||
tickLength = axis.options.tickLength,
|
||||
axisMargin = options.grid.axisMargin,
|
||||
padding = options.grid.labelMargin,
|
||||
eventSectionPadding = options.grid.eventSectionHeight,
|
||||
innermost = true,
|
||||
outermost = true,
|
||||
first = true,
|
||||
@@ -1490,7 +1492,9 @@ Licensed under the MIT license.
|
||||
padding += +tickLength;
|
||||
|
||||
if (isXAxis) {
|
||||
// Add space for event section
|
||||
lh += padding;
|
||||
lh += eventSectionPadding;
|
||||
|
||||
if (pos == "bottom") {
|
||||
plotOffset.bottom += lh + axisMargin;
|
||||
@@ -1518,6 +1522,7 @@ Licensed under the MIT license.
|
||||
axis.position = pos;
|
||||
axis.tickLength = tickLength;
|
||||
axis.box.padding = padding;
|
||||
axis.box.eventSectionPadding = eventSectionPadding;
|
||||
axis.innermost = innermost;
|
||||
}
|
||||
|
||||
@@ -2225,7 +2230,7 @@ Licensed under the MIT license.
|
||||
halign = "center";
|
||||
x = plotOffset.left + axis.p2c(tick.v);
|
||||
if (axis.position == "bottom") {
|
||||
y = box.top + box.padding;
|
||||
y = box.top + box.padding + box.eventSectionPadding;
|
||||
} else {
|
||||
y = box.top + box.height - box.padding;
|
||||
valign = "bottom";
|
||||
|
||||
12
public/vendor/tagsinput/bootstrap-tagsinput.js
vendored
12
public/vendor/tagsinput/bootstrap-tagsinput.js
vendored
@@ -28,15 +28,14 @@
|
||||
this.$element = $(element);
|
||||
this.$element.hide();
|
||||
|
||||
this.widthClass = options.widthClass || 'width-9';
|
||||
this.isSelect = (element.tagName === 'SELECT');
|
||||
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
||||
this.objectItems = options && options.itemValue;
|
||||
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
|
||||
this.inputSize = Math.max(1, this.placeholderText.length);
|
||||
|
||||
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
||||
this.$input = $('<input class="gf-form-input" size="' +
|
||||
this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
||||
this.$input = $('<input class="gf-form-input ' + this.widthClass + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
|
||||
|
||||
this.$element.after(this.$container);
|
||||
|
||||
@@ -292,6 +291,13 @@
|
||||
self.$input.focus();
|
||||
}, self));
|
||||
|
||||
self.$container.on('blur', 'input', $.proxy(function(event) {
|
||||
var $input = $(event.target);
|
||||
self.add($input.val());
|
||||
$input.val('');
|
||||
event.preventDefault();
|
||||
}, self));
|
||||
|
||||
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
||||
var $input = $(event.target),
|
||||
$inputWrapper = self.findInputWrapper();
|
||||
|
||||
Reference in New Issue
Block a user