mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge with master
This commit is contained in:
@@ -1,23 +1,32 @@
|
||||
///<reference path="headers/common.d.ts" />
|
||||
|
||||
import 'bootstrap';
|
||||
import 'vendor/filesaver';
|
||||
import 'lodash-src';
|
||||
import 'angular-strap';
|
||||
import 'babel-polyfill';
|
||||
import 'file-saver';
|
||||
import 'lodash';
|
||||
import 'jquery';
|
||||
import 'angular';
|
||||
import 'angular-route';
|
||||
import 'angular-sanitize';
|
||||
import 'angular-dragdrop';
|
||||
import 'angular-native-dragdrop';
|
||||
import 'angular-bindonce';
|
||||
import 'angular-ui';
|
||||
import 'react';
|
||||
import 'react-dom';
|
||||
import 'ngreact';
|
||||
|
||||
import 'vendor/bootstrap/bootstrap';
|
||||
import 'vendor/angular-ui/ui-bootstrap-tpls';
|
||||
import 'vendor/angular-other/angular-strap';
|
||||
|
||||
import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// add move to lodash for backward compatabiltiy
|
||||
_.move = function (array, fromIndex, toIndex) {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
return array;
|
||||
};
|
||||
|
||||
import {coreModule} from './core/core';
|
||||
|
||||
export class GrafanaApp {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
(function bootGrafana() {
|
||||
'use strict';
|
||||
|
||||
var systemLocate = System.locate;
|
||||
System.locate = function(load) {
|
||||
var System = this;
|
||||
return Promise.resolve(systemLocate.call(this, load)).then(function(address) {
|
||||
return address + System.cacheBust;
|
||||
});
|
||||
};
|
||||
System.cacheBust = '?bust=' + Date.now();
|
||||
|
||||
System.import('app/app').then(function(app) {
|
||||
app.default.init();
|
||||
}).catch(function(err) {
|
||||
console.log('Loading app module failed: ', err);
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
export interface IProps {
|
||||
@@ -15,16 +15,16 @@ export class PasswordStrength extends React.Component<IProps, any> {
|
||||
let strengthText = "strength: strong like a bull.";
|
||||
let strengthClass = "password-strength-good";
|
||||
|
||||
if (this.props.password.length < 4) {
|
||||
strengthText = "strength: weak sauce.";
|
||||
strengthClass = "password-strength-bad";
|
||||
}
|
||||
|
||||
if (this.props.password.length <= 8) {
|
||||
strengthText = "strength: you can do better.";
|
||||
strengthClass = "password-strength-ok";
|
||||
}
|
||||
|
||||
if (this.props.password.length < 4) {
|
||||
strengthText = "strength: weak sauce.";
|
||||
strengthClass = "password-strength-bad";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`password-strength small ${strengthClass}`}>
|
||||
<em>{strengthText}</em>
|
||||
@@ -36,3 +36,4 @@ export class PasswordStrength extends React.Component<IProps, any> {
|
||||
coreModule.directive('passwordStrength', function(reactDirective) {
|
||||
return reactDirective(PasswordStrength, ['password']);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,60 +26,31 @@
|
||||
* Ctrl-Enter (Command-Enter): run onChange() function
|
||||
*/
|
||||
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
import ace from 'ace';
|
||||
import ace from 'brace';
|
||||
import './theme-grafana-dark';
|
||||
import 'brace/ext/language_tools';
|
||||
import 'brace/theme/textmate';
|
||||
import 'brace/mode/text';
|
||||
import 'brace/snippets/text';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/snippets/sql';
|
||||
|
||||
const ACE_SRC_BASE = "public/vendor/npm/ace-builds/src-noconflict/";
|
||||
const DEFAULT_THEME_DARK = "grafana-dark";
|
||||
const DEFAULT_THEME_LIGHT = "textmate";
|
||||
const DEFAULT_THEME_DARK = "ace/theme/grafana-dark";
|
||||
const DEFAULT_THEME_LIGHT = "ace/theme/textmate";
|
||||
const DEFAULT_MODE = "text";
|
||||
const DEFAULT_MAX_LINES = 10;
|
||||
const DEFAULT_TAB_SIZE = 2;
|
||||
const DEFAULT_BEHAVIOURS = true;
|
||||
|
||||
const GRAFANA_MODULES = ['theme-grafana-dark'];
|
||||
const GRAFANA_MODULE_BASE = "public/app/core/components/code_editor/";
|
||||
|
||||
// Trick for loading additional modules
|
||||
function setModuleUrl(moduleType, name, pluginBaseUrl = null) {
|
||||
let baseUrl = ACE_SRC_BASE;
|
||||
let aceModeName = `ace/${moduleType}/${name}`;
|
||||
let moduleName = `${moduleType}-${name}`;
|
||||
let componentName = `${moduleName}.js`;
|
||||
|
||||
if (_.includes(GRAFANA_MODULES, moduleName)) {
|
||||
baseUrl = GRAFANA_MODULE_BASE;
|
||||
}
|
||||
|
||||
if (pluginBaseUrl) {
|
||||
baseUrl = pluginBaseUrl + '/';
|
||||
}
|
||||
|
||||
if (moduleType === 'snippets') {
|
||||
componentName = `${moduleType}/${name}.js`;
|
||||
}
|
||||
|
||||
ace.config.setModuleUrl(aceModeName, baseUrl + componentName);
|
||||
}
|
||||
|
||||
setModuleUrl("ext", "language_tools");
|
||||
setModuleUrl("mode", "text");
|
||||
setModuleUrl("snippets", "text");
|
||||
|
||||
let editorTemplate = `<div></div>`;
|
||||
|
||||
function link(scope, elem, attrs) {
|
||||
let lightTheme = config.bootData.user.lightTheme;
|
||||
let default_theme = lightTheme ? DEFAULT_THEME_LIGHT : DEFAULT_THEME_DARK;
|
||||
|
||||
// Options
|
||||
let langMode = attrs.mode || DEFAULT_MODE;
|
||||
let maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
|
||||
let showGutter = attrs.showGutter !== undefined;
|
||||
let theme = attrs.theme || default_theme;
|
||||
let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
|
||||
let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
|
||||
|
||||
@@ -103,10 +74,10 @@ function link(scope, elem, attrs) {
|
||||
// disable depreacation warning
|
||||
codeEditor.$blockScrolling = Infinity;
|
||||
// Padding hacks
|
||||
codeEditor.renderer.setScrollMargin(15, 15);
|
||||
(<any>codeEditor.renderer).setScrollMargin(15, 15);
|
||||
codeEditor.renderer.setPadding(10);
|
||||
|
||||
setThemeMode(theme);
|
||||
setThemeMode();
|
||||
setLangMode(langMode);
|
||||
setEditorContent(scope.content);
|
||||
|
||||
@@ -162,44 +133,31 @@ function link(scope, elem, attrs) {
|
||||
});
|
||||
|
||||
function setLangMode(lang) {
|
||||
let aceModeName = `ace/mode/${lang}`;
|
||||
setModuleUrl("mode", lang, scope.datasource.meta.baseUrl || null);
|
||||
setModuleUrl("snippets", lang, scope.datasource.meta.baseUrl || null);
|
||||
editorSession.setMode(aceModeName);
|
||||
|
||||
ace.config.loadModule("ace/ext/language_tools", (language_tools) => {
|
||||
codeEditor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true
|
||||
});
|
||||
|
||||
if (scope.getCompleter()) {
|
||||
// make copy of array as ace seems to share completers array between instances
|
||||
codeEditor.completers = codeEditor.completers.slice();
|
||||
codeEditor.completers.push(scope.getCompleter());
|
||||
}
|
||||
ace.acequire("ace/ext/language_tools");
|
||||
codeEditor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true
|
||||
});
|
||||
|
||||
if (scope.getCompleter()) {
|
||||
// make copy of array as ace seems to share completers array between instances
|
||||
const anyEditor = <any>codeEditor;
|
||||
anyEditor.completers = anyEditor.completers.slice();
|
||||
anyEditor.completers.push(scope.getCompleter());
|
||||
}
|
||||
|
||||
let aceModeName = `ace/mode/${lang}`;
|
||||
editorSession.setMode(aceModeName);
|
||||
}
|
||||
|
||||
function setThemeMode(theme) {
|
||||
setModuleUrl("theme", theme);
|
||||
let themeModule = `ace/theme/${theme}`;
|
||||
ace.config.loadModule(themeModule, (theme_module) => {
|
||||
// Check is theme light or dark and fix if needed
|
||||
let lightTheme = config.bootData.user.lightTheme;
|
||||
let fixedTheme = theme;
|
||||
if (lightTheme && theme_module.isDark) {
|
||||
fixedTheme = DEFAULT_THEME_LIGHT;
|
||||
} else if (!lightTheme && !theme_module.isDark) {
|
||||
fixedTheme = DEFAULT_THEME_DARK;
|
||||
}
|
||||
setModuleUrl("theme", fixedTheme);
|
||||
themeModule = `ace/theme/${fixedTheme}`;
|
||||
codeEditor.setTheme(themeModule);
|
||||
function setThemeMode() {
|
||||
let theme = DEFAULT_THEME_DARK;
|
||||
if (config.bootData.user.lightTheme) {
|
||||
theme = DEFAULT_THEME_LIGHT;
|
||||
}
|
||||
|
||||
elem.addClass("gf-code-editor--theme-loaded");
|
||||
});
|
||||
codeEditor.setTheme(theme);
|
||||
}
|
||||
|
||||
function setEditorContent(value) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* jshint ignore:start */
|
||||
|
||||
ace.define("ace/theme/grafana-dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
|
||||
ace.define("ace/theme/grafana-dark",["require","exports","module","ace/lib/dom"], function(acequire, exports, module) {
|
||||
"use strict";
|
||||
|
||||
exports.isDark = true;
|
||||
@@ -109,7 +109,7 @@ ace.define("ace/theme/grafana-dark",["require","exports","module","ace/lib/dom"]
|
||||
background: url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaaaccayaaaczgbynaaaaekleqvqimwpq0fd0zxbzd/wpaajvaoxesgneaaaaaelftksuqmcc) right repeat-y\
|
||||
}";
|
||||
|
||||
var dom = require("../lib/dom");
|
||||
var dom = acequire("../lib/dom");
|
||||
dom.importCssString(exports.cssText, exports.cssClass);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
///<reference path="./mod_defs.d.ts" />
|
||||
|
||||
import "./directives/dash_class";
|
||||
import "./directives/confirm_click";
|
||||
import "./directives/dash_edit_link";
|
||||
@@ -11,7 +8,6 @@ import "./directives/ng_model_on_blur";
|
||||
import "./directives/spectrum_picker";
|
||||
import "./directives/tags";
|
||||
import "./directives/value_select_dropdown";
|
||||
import "./directives/plugin_component";
|
||||
import "./directives/rebuild_on_change";
|
||||
import "./directives/give_focus";
|
||||
import "./directives/diff-view";
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
export default angular.module('grafana.core', ['ngRoute']);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import coreModule from '../core_module';
|
||||
|
||||
function getBlockNodes(nodes) {
|
||||
@@ -21,6 +18,7 @@ function getBlockNodes(nodes) {
|
||||
return blockNodes || nodes;
|
||||
}
|
||||
|
||||
/** @ngInject **/
|
||||
function rebuildOnChange($animate) {
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
define([
|
||||
'angular',
|
||||
'../core_module',
|
||||
'spectrum',
|
||||
'vendor/spectrum',
|
||||
],
|
||||
function (angular, coreModule) {
|
||||
'use strict';
|
||||
|
||||
@@ -2,7 +2,7 @@ define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'../core_module',
|
||||
'bootstrap-tagsinput',
|
||||
'vendor/tagsinput/bootstrap-tagsinput.js',
|
||||
],
|
||||
function (angular, $, coreModule) {
|
||||
'use strict';
|
||||
|
||||
@@ -57,7 +57,8 @@ coreModule.filter('noXml', function() {
|
||||
};
|
||||
});
|
||||
|
||||
coreModule.filter('interpolateTemplateVars', function (templateSrv) {
|
||||
/** @ngInject */
|
||||
function interpolateTemplateVars(templateSrv) {
|
||||
var filterFunc: any = function(text, scope) {
|
||||
var scopedVars;
|
||||
if (scope.ctrl) {
|
||||
@@ -71,6 +72,7 @@ coreModule.filter('interpolateTemplateVars', function (templateSrv) {
|
||||
|
||||
filterFunc.$stateful = true;
|
||||
return filterFunc;
|
||||
});
|
||||
}
|
||||
|
||||
coreModule.filter('interpolateTemplateVars', interpolateTemplateVars);
|
||||
export default {};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import {Observable} from 'vendor/npm/rxjs/Observable';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
export class LiveSrv {
|
||||
conn: any;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
define([
|
||||
], function () {});
|
||||
4
public/app/core/partials.ts
Normal file
4
public/app/core/partials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
var templates = (<any>require).context('../', true, /\.html$/);
|
||||
templates.keys().forEach(function(key) {
|
||||
templates(key);
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
|
||||
import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
export class BundleLoader {
|
||||
lazy: any;
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import './dashboard_loaders';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {BundleLoader} from './bundle_loader';
|
||||
|
||||
/** @ngInject **/
|
||||
function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
var loadOrgBundle = new BundleLoader('app/features/org/all');
|
||||
var loadPluginsBundle = new BundleLoader('app/features/plugins/all');
|
||||
var loadAdminBundle = new BundleLoader('app/features/admin/admin');
|
||||
var loadAlertingBundle = new BundleLoader('app/features/alerting/all');
|
||||
var loadOrgBundle = {
|
||||
lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
|
||||
return System.import('app/features/org/all');
|
||||
}]
|
||||
};
|
||||
|
||||
var loadAdminBundle = {
|
||||
lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
|
||||
return System.import('app/features/admin/admin');
|
||||
}]
|
||||
};
|
||||
|
||||
var loadAlertingBundle = {
|
||||
lazy: ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
|
||||
return System.import('app/features/alerting/all');
|
||||
}]
|
||||
};
|
||||
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
@@ -53,19 +62,16 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_list.html',
|
||||
controller : 'DataSourcesCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/datasources/edit/:id', {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
|
||||
controller : 'DataSourceEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/datasources/new', {
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_edit.html',
|
||||
controller : 'DataSourceEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/org', {
|
||||
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
||||
@@ -193,19 +199,16 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
|
||||
controller: 'PluginListCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/plugins/:pluginId/edit', {
|
||||
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
|
||||
controller: 'PluginEditCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/plugins/:pluginId/page/:slug', {
|
||||
templateUrl: 'public/app/features/plugins/partials/plugin_page.html',
|
||||
controller: 'AppPageCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadPluginsBundle,
|
||||
})
|
||||
.when('/styleguide/:page?', {
|
||||
controller: 'StyleGuideCtrl',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
define([
|
||||
'./alert_srv',
|
||||
'./util_srv',
|
||||
'./datasource_srv',
|
||||
'./context_srv',
|
||||
'./timer',
|
||||
'./keyboard_manager',
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
// import React from 'react';
|
||||
// import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
// import {shallow} from 'enzyme';
|
||||
//
|
||||
// import {PasswordStrength} from '../components/PasswordStrength';
|
||||
//
|
||||
// describe('PasswordStrength', () => {
|
||||
//
|
||||
// it.skip('should have class bad if length below 4', () => {
|
||||
// const wrapper = shallow(<PasswordStrength password="asd" />);
|
||||
// expect(wrapper.find(".password-strength-bad")).to.have.length(3);
|
||||
// });
|
||||
// });
|
||||
//
|
||||
import React from 'react';
|
||||
import {describe, it, expect} from 'test/lib/common';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {PasswordStrength} from '../components/PasswordStrength';
|
||||
|
||||
describe('PasswordStrength', () => {
|
||||
|
||||
it('should have class bad if length below 4', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asd" />);
|
||||
expect(wrapper.find(".password-strength-bad")).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should have class ok if length below 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdasd" />);
|
||||
expect(wrapper.find(".password-strength-ok")).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should have class good if length above 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdaasdda" />);
|
||||
expect(wrapper.find(".password-strength-good")).to.have.length(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
29
public/app/core/specs/backend_srv_specs.ts
Normal file
29
public/app/core/specs/backend_srv_specs.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import 'app/core/services/backend_srv';
|
||||
|
||||
describe('backend_srv', function() {
|
||||
var _backendSrv;
|
||||
var _http;
|
||||
var _httpBackend;
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.inject(function ($httpBackend, $http, backendSrv) {
|
||||
_httpBackend = $httpBackend;
|
||||
_http = $http;
|
||||
_backendSrv = backendSrv;
|
||||
}));
|
||||
|
||||
describe('when handling errors', function() {
|
||||
it('should return the http status code', function(done) {
|
||||
_httpBackend.whenGET('gateway-error').respond(502);
|
||||
_backendSrv.datasourceRequest({
|
||||
url: 'gateway-error'
|
||||
}).catch(function(err) {
|
||||
expect(err.status).to.be(502);
|
||||
done();
|
||||
});
|
||||
_httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
167
public/app/core/specs/value_select_dropdown_specs.ts
Normal file
167
public/app/core/specs/value_select_dropdown_specs.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks, sinon} from 'test/lib/common';
|
||||
import 'app/core/directives/value_select_dropdown';
|
||||
|
||||
describe("SelectDropdownCtrl", function() {
|
||||
var scope;
|
||||
var ctrl;
|
||||
var tagValuesMap: any = {};
|
||||
var rootScope;
|
||||
var q;
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) {
|
||||
rootScope = $rootScope;
|
||||
q = $q;
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller('ValueSelectDropdownCtrl', {$scope: scope});
|
||||
ctrl.onUpdated = sinon.spy();
|
||||
$httpBackend.when('GET', /\.html$/).respond('');
|
||||
}));
|
||||
|
||||
describe("Given simple variable", function() {
|
||||
beforeEach(function() {
|
||||
ctrl.variable = {
|
||||
current: {text: 'hej', value: 'hej' },
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
};
|
||||
ctrl.init();
|
||||
});
|
||||
|
||||
it("Should init labelText and linkText", function() {
|
||||
expect(ctrl.linkText).to.be("hej");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given variable with tags and dropdown is opened", function() {
|
||||
beforeEach(function() {
|
||||
ctrl.variable = {
|
||||
current: {text: 'server-1', value: 'server-1'},
|
||||
options: [
|
||||
{text: 'server-1', value: 'server-1', selected: true},
|
||||
{text: 'server-2', value: 'server-2'},
|
||||
{text: 'server-3', value: 'server-3'},
|
||||
],
|
||||
tags: ["key1", "key2", "key3"],
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
multi: true
|
||||
};
|
||||
tagValuesMap.key1 = ['server-1', 'server-3'];
|
||||
tagValuesMap.key2 = ['server-2', 'server-3'];
|
||||
tagValuesMap.key3 = ['server-1', 'server-2', 'server-3'];
|
||||
ctrl.init();
|
||||
ctrl.show();
|
||||
});
|
||||
|
||||
it("should init tags model", function() {
|
||||
expect(ctrl.tags.length).to.be(3);
|
||||
expect(ctrl.tags[0].text).to.be("key1");
|
||||
});
|
||||
|
||||
it("should init options model", function() {
|
||||
expect(ctrl.options.length).to.be(3);
|
||||
});
|
||||
|
||||
it("should init selected values array", function() {
|
||||
expect(ctrl.selectedValues.length).to.be(1);
|
||||
});
|
||||
|
||||
it("should set linkText", function() {
|
||||
expect(ctrl.linkText).to.be('server-1');
|
||||
});
|
||||
|
||||
describe('after adititional value is selected', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.selectValue(ctrl.options[2], {});
|
||||
ctrl.commitChanges();
|
||||
});
|
||||
|
||||
it('should update link text', function() {
|
||||
expect(ctrl.linkText).to.be('server-1 + server-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When tag is selected', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.selectTag(ctrl.tags[0]);
|
||||
rootScope.$digest();
|
||||
ctrl.commitChanges();
|
||||
});
|
||||
|
||||
it("should select tag", function() {
|
||||
expect(ctrl.selectedTags.length).to.be(1);
|
||||
});
|
||||
|
||||
it("should select values", function() {
|
||||
expect(ctrl.options[0].selected).to.be(true);
|
||||
expect(ctrl.options[2].selected).to.be(true);
|
||||
});
|
||||
|
||||
it("link text should not include tag values", function() {
|
||||
expect(ctrl.linkText).to.be('');
|
||||
});
|
||||
|
||||
describe('and then dropdown is opened and closed without changes', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.show();
|
||||
ctrl.commitChanges();
|
||||
rootScope.$digest();
|
||||
});
|
||||
|
||||
it("should still have selected tag", function() {
|
||||
expect(ctrl.selectedTags.length).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and then unselected', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.selectTag(ctrl.tags[0]);
|
||||
rootScope.$digest();
|
||||
});
|
||||
|
||||
it("should deselect tag", function() {
|
||||
expect(ctrl.selectedTags.length).to.be(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and then value is unselected', function() {
|
||||
beforeEach(function() {
|
||||
ctrl.selectValue(ctrl.options[0], {});
|
||||
});
|
||||
|
||||
it("should deselect tag", function() {
|
||||
expect(ctrl.selectedTags.length).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given variable with selected tags", function() {
|
||||
beforeEach(function() {
|
||||
ctrl.variable = {
|
||||
current: {text: 'server-1', value: 'server-1', tags: [{text: 'key1', selected: true}] },
|
||||
options: [
|
||||
{text: 'server-1', value: 'server-1'},
|
||||
{text: 'server-2', value: 'server-2'},
|
||||
{text: 'server-3', value: 'server-3'},
|
||||
],
|
||||
tags: ["key1", "key2", "key3"],
|
||||
getValuesForTag: function(key) {
|
||||
return q.when(tagValuesMap[key]);
|
||||
},
|
||||
multi: true
|
||||
};
|
||||
ctrl.init();
|
||||
ctrl.show();
|
||||
});
|
||||
|
||||
it("should set tag as selected", function() {
|
||||
expect(ctrl.tags[0].selected).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,6 +401,7 @@ function($, _, moment) {
|
||||
kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
|
||||
kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
|
||||
kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴');
|
||||
kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$');
|
||||
|
||||
// Data (Binary)
|
||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||
@@ -754,6 +755,7 @@ function($, _, moment) {
|
||||
{text: 'Yen (¥)', value: 'currencyJPY'},
|
||||
{text: 'Rubles (₽)', value: 'currencyRUB'},
|
||||
{text: 'Hryvnias (₴)', value: 'currencyUAH'},
|
||||
{text: 'Real (R$)', value: 'currencyBRL'},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,6 +49,8 @@ var reducerTypes = [
|
||||
{text: 'count()', value: 'count'},
|
||||
{text: 'last()', value: 'last'},
|
||||
{text: 'median()', value: 'median'},
|
||||
{text: 'diff()', value: 'diff'},
|
||||
{text: 'percent_diff()', value: 'percent_diff'},
|
||||
];
|
||||
|
||||
var noDataModes = [
|
||||
|
||||
@@ -34,9 +34,10 @@
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-type">
|
||||
<a class="card-item-cog" bs-tooltip="'Pausing an alert rule prevents it from executing'" ng-click="ctrl.pauseAlertRule(alert.id)">
|
||||
<i class="fa fa-pause"></i>
|
||||
</a>
|
||||
<a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'">
|
||||
<i ng-show="alert.state !== 'paused'" class="fa fa-pause"></i>
|
||||
<i ng-show="alert.state === 'paused'" class="fa fa-play"></i>
|
||||
</a>
|
||||
<a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'">
|
||||
<i class="icon-gf icon-gf-settings"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -41,10 +41,10 @@
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-6" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
@@ -53,8 +53,8 @@
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
@@ -77,13 +77,12 @@
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
@@ -91,8 +90,8 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
@@ -135,35 +134,31 @@
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<section class="card-section card-list-layout-list">
|
||||
<ol class="card-list" >
|
||||
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-type">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-sub-name">
|
||||
<span class="alert-list-item-state {{ah.stateModel.stateClass}}">
|
||||
<i class="{{ah.stateModel.iconClass}}"></i>
|
||||
{{ah.stateModel.text}}
|
||||
</span> {{ah.time}}
|
||||
</div>
|
||||
<div class="card-item-sub-name">
|
||||
{{ah.info}}
|
||||
</div>
|
||||
<div class="alert-list card-item card-item--alert">
|
||||
<div class="alert-list-body">
|
||||
<div class="alert-list-icon alert-list-item-state {{ah.stateModel.stateClass}}">
|
||||
<i class="{{ah.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-list-main alert-list-text">
|
||||
<span class="alert-list-state {{ah.stateModel.stateClass}}">{{ah.stateModel.text}}</span>
|
||||
<span class="alert-list-info">{{ah.info}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-list-footer alert-list-text">
|
||||
<span>{{ah.time}}</span>
|
||||
<span><!--Img Link--></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -3,6 +3,7 @@ define([
|
||||
'./dashlinks/module',
|
||||
'./annotations/all',
|
||||
'./templating/all',
|
||||
'./plugins/all',
|
||||
'./dashboard/all',
|
||||
'./playlist/all',
|
||||
'./snapshot/all',
|
||||
|
||||
@@ -1,211 +1,211 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {CELL_HEIGHT, CELL_VMARGIN} from '../model';
|
||||
|
||||
import 'jquery-ui';
|
||||
import 'gridstack';
|
||||
import 'gridstack.jquery-ui';
|
||||
|
||||
const template = `
|
||||
<div class="grid-stack">
|
||||
<dash-grid-item ng-repeat="panel in ctrl.dashboard.panels track by panel.id"
|
||||
class="grid-stack-item"
|
||||
grid-ctrl="ctrl"
|
||||
panel="panel">
|
||||
<plugin-component type="panel" class="grid-stack-item-content">
|
||||
</plugin-component>
|
||||
</dash-grid-item>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var rowIndex = 0;
|
||||
|
||||
export class GridCtrl {
|
||||
options: any;
|
||||
dashboard: any;
|
||||
panels: any;
|
||||
gridstack: any;
|
||||
gridElem: any;
|
||||
isInitialized: boolean;
|
||||
isDestroyed: boolean;
|
||||
index: number;
|
||||
changeRenderPromise: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $element, private $timeout) {
|
||||
console.log(this.dashboard);
|
||||
this.index = rowIndex;
|
||||
rowIndex += 1;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.gridElem = this.$element.find('.grid-stack');
|
||||
|
||||
this.gridstack = this.gridElem.gridstack({
|
||||
animate: true,
|
||||
cellHeight: CELL_HEIGHT,
|
||||
verticalMargin: CELL_VMARGIN,
|
||||
acceptWidgets: '.grid-stack-item',
|
||||
handle: '.grid-drag-handle'
|
||||
}).data('gridstack');
|
||||
|
||||
this.isInitialized = true;
|
||||
|
||||
this.gridElem.on('added', (e, items) => {
|
||||
for (let item of items) {
|
||||
this.onGridStackItemAdded(item);
|
||||
}
|
||||
});
|
||||
|
||||
this.gridElem.on('removed', (e, items) => {
|
||||
for (let item of items) {
|
||||
this.onGridStackItemRemoved(item);
|
||||
}
|
||||
});
|
||||
|
||||
this.gridElem.on('change', (e, items) => {
|
||||
this.$timeout(() => this.onGridStackItemsChanged(items), 50);
|
||||
});
|
||||
}
|
||||
|
||||
onGridStackItemAdded(item) {
|
||||
console.log('row: ' + this.index + ' item added', item);
|
||||
}
|
||||
|
||||
onGridStackItemRemoved(item) {
|
||||
console.log('row: ' + this.index + ' item removed', item.id, item);
|
||||
}
|
||||
|
||||
onGridStackItemsChanged(items) {
|
||||
console.log('onGridStackItemsChanged');
|
||||
|
||||
for (let item of items) {
|
||||
// find panel
|
||||
var panel = this.dashboard.getPanelById(parseInt(item.id));
|
||||
|
||||
if (!panel) {
|
||||
console.log('item change but no panel found for item', item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// update panel model position
|
||||
panel.x = item.x;
|
||||
panel.y = item.y;
|
||||
panel.width = item.width;
|
||||
panel.height = item.height;
|
||||
|
||||
console.log('updating panel: ' + panel.id + ' x: ' + panel.x + ' y: ' + panel.y);
|
||||
}
|
||||
|
||||
this.dashboard.panels.sort(function (a, b) {
|
||||
let aScore = a.x + (a.y * 12);
|
||||
let bScore = b.x + (b.y * 12);
|
||||
if (aScore < bScore) { return -1; }
|
||||
if (aScore > bScore) { return 1; }
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (this.changeRenderPromise) {
|
||||
this.$timeout.cancel(this.changeRenderPromise);
|
||||
}
|
||||
|
||||
this.changeRenderPromise = this.$timeout(() => {
|
||||
console.log('broadcasting render');
|
||||
this.$scope.$broadcast('render');
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.gridstack.destroy();
|
||||
this.gridstack = null;
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject **/
|
||||
export function dashGrid($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: GridCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dashboard: "=",
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
$timeout(function() {
|
||||
ctrl.init();
|
||||
});
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
ctrl.destroy();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @ngInject **/
|
||||
export function dashGridItem($timeout, $rootScope) {
|
||||
return {
|
||||
restrict: "E",
|
||||
scope: {
|
||||
panel: '=',
|
||||
gridCtrl: '='
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
let gridCtrl = scope.gridCtrl;
|
||||
let panel = scope.panel;
|
||||
let gridStackNode = null;
|
||||
|
||||
element.attr({
|
||||
'data-gs-id': panel.id,
|
||||
'data-gs-x': panel.x,
|
||||
'data-gs-y': panel.y,
|
||||
'data-gs-width': panel.width,
|
||||
'data-gs-height': panel.height,
|
||||
'data-gs-no-resize': panel.type === 'row',
|
||||
});
|
||||
|
||||
$rootScope.onAppEvent('panel-fullscreen-exit', (evt, payload) => {
|
||||
if (panel.id !== payload.panelId) {
|
||||
return;
|
||||
}
|
||||
gridCtrl.gridstack.locked(element, false);
|
||||
element.removeClass('panel-fullscreen');
|
||||
}, scope);
|
||||
|
||||
$rootScope.onAppEvent('panel-fullscreen-enter', (evt, payload) => {
|
||||
if (panel.id !== payload.panelId) {
|
||||
return;
|
||||
}
|
||||
element.addClass('panel-fullscreen');
|
||||
}, scope);
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
console.log('grid-item scope $destroy');
|
||||
if (gridCtrl.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gridStackNode) {
|
||||
console.log('grid-item scope $destroy removeWidget');
|
||||
gridStackNode._grid.removeWidget(element);
|
||||
}
|
||||
});
|
||||
|
||||
if (gridCtrl.isInitialized) {
|
||||
gridCtrl.gridstack.makeWidget(element);
|
||||
gridStackNode = element.data('_gridstack_node');
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
gridStackNode = element.data('_gridstack_node');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashGrid', dashGrid);
|
||||
coreModule.directive('dashGridItem', dashGridItem);
|
||||
// ///<reference path="../../../headers/common.d.ts" />
|
||||
//
|
||||
// import coreModule from 'app/core/core_module';
|
||||
// import {CELL_HEIGHT, CELL_VMARGIN} from '../model';
|
||||
//
|
||||
// import 'jquery-ui';
|
||||
// import 'gridstack/dist/jquery.jQueryUI';
|
||||
// import 'gridstack';
|
||||
//
|
||||
// const template = `
|
||||
// <div class="grid-stack">
|
||||
// <dash-grid-item ng-repeat="panel in ctrl.dashboard.panels track by panel.id"
|
||||
// class="grid-stack-item"
|
||||
// grid-ctrl="ctrl"
|
||||
// panel="panel">
|
||||
// <plugin-component type="panel" class="grid-stack-item-content">
|
||||
// </plugin-component>
|
||||
// </dash-grid-item>
|
||||
// </div>
|
||||
// `;
|
||||
//
|
||||
// var rowIndex = 0;
|
||||
//
|
||||
// export class GridCtrl {
|
||||
// options: any;
|
||||
// dashboard: any;
|
||||
// panels: any;
|
||||
// gridstack: any;
|
||||
// gridElem: any;
|
||||
// isInitialized: boolean;
|
||||
// isDestroyed: boolean;
|
||||
// index: number;
|
||||
// changeRenderPromise: any;
|
||||
//
|
||||
// #<{(|* @ngInject |)}>#
|
||||
// constructor(private $scope, private $element, private $timeout) {
|
||||
// console.log(this.dashboard);
|
||||
// this.index = rowIndex;
|
||||
// rowIndex += 1;
|
||||
// }
|
||||
//
|
||||
// init() {
|
||||
// this.gridElem = this.$element.find('.grid-stack');
|
||||
//
|
||||
// this.gridstack = this.gridElem.gridstack({
|
||||
// animate: true,
|
||||
// cellHeight: CELL_HEIGHT,
|
||||
// verticalMargin: CELL_VMARGIN,
|
||||
// acceptWidgets: '.grid-stack-item',
|
||||
// handle: '.grid-drag-handle'
|
||||
// }).data('gridstack');
|
||||
//
|
||||
// this.isInitialized = true;
|
||||
//
|
||||
// this.gridElem.on('added', (e, items) => {
|
||||
// for (let item of items) {
|
||||
// this.onGridStackItemAdded(item);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// this.gridElem.on('removed', (e, items) => {
|
||||
// for (let item of items) {
|
||||
// this.onGridStackItemRemoved(item);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// this.gridElem.on('change', (e, items) => {
|
||||
// this.$timeout(() => this.onGridStackItemsChanged(items), 50);
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// onGridStackItemAdded(item) {
|
||||
// console.log('row: ' + this.index + ' item added', item);
|
||||
// }
|
||||
//
|
||||
// onGridStackItemRemoved(item) {
|
||||
// console.log('row: ' + this.index + ' item removed', item.id, item);
|
||||
// }
|
||||
//
|
||||
// onGridStackItemsChanged(items) {
|
||||
// console.log('onGridStackItemsChanged');
|
||||
//
|
||||
// for (let item of items) {
|
||||
// // find panel
|
||||
// var panel = this.dashboard.getPanelById(parseInt(item.id));
|
||||
//
|
||||
// if (!panel) {
|
||||
// console.log('item change but no panel found for item', item);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // update panel model position
|
||||
// panel.x = item.x;
|
||||
// panel.y = item.y;
|
||||
// panel.width = item.width;
|
||||
// panel.height = item.height;
|
||||
//
|
||||
// console.log('updating panel: ' + panel.id + ' x: ' + panel.x + ' y: ' + panel.y);
|
||||
// }
|
||||
//
|
||||
// this.dashboard.panels.sort(function (a, b) {
|
||||
// let aScore = a.x + (a.y * 12);
|
||||
// let bScore = b.x + (b.y * 12);
|
||||
// if (aScore < bScore) { return -1; }
|
||||
// if (aScore > bScore) { return 1; }
|
||||
// return 0;
|
||||
// });
|
||||
//
|
||||
// if (this.changeRenderPromise) {
|
||||
// this.$timeout.cancel(this.changeRenderPromise);
|
||||
// }
|
||||
//
|
||||
// this.changeRenderPromise = this.$timeout(() => {
|
||||
// console.log('broadcasting render');
|
||||
// this.$scope.$broadcast('render');
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// destroy() {
|
||||
// this.gridstack.destroy();
|
||||
// this.gridstack = null;
|
||||
// this.isDestroyed = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #<{(|* @ngInject *|)}>#
|
||||
// export function dashGrid($timeout) {
|
||||
// return {
|
||||
// restrict: 'E',
|
||||
// template: template,
|
||||
// controller: GridCtrl,
|
||||
// bindToController: true,
|
||||
// controllerAs: 'ctrl',
|
||||
// scope: {
|
||||
// dashboard: "=",
|
||||
// },
|
||||
// link: function(scope, elem, attrs, ctrl) {
|
||||
// $timeout(function() {
|
||||
// ctrl.init();
|
||||
// });
|
||||
//
|
||||
// scope.$on('$destroy', () => {
|
||||
// ctrl.destroy();
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// #<{(|* @ngInject *|)}>#
|
||||
// export function dashGridItem($timeout, $rootScope) {
|
||||
// return {
|
||||
// restrict: "E",
|
||||
// scope: {
|
||||
// panel: '=',
|
||||
// gridCtrl: '='
|
||||
// },
|
||||
// link: function (scope, element, attrs) {
|
||||
// let gridCtrl = scope.gridCtrl;
|
||||
// let panel = scope.panel;
|
||||
// let gridStackNode = null;
|
||||
//
|
||||
// element.attr({
|
||||
// 'data-gs-id': panel.id,
|
||||
// 'data-gs-x': panel.x,
|
||||
// 'data-gs-y': panel.y,
|
||||
// 'data-gs-width': panel.width,
|
||||
// 'data-gs-height': panel.height,
|
||||
// 'data-gs-no-resize': panel.type === 'row',
|
||||
// });
|
||||
//
|
||||
// $rootScope.onAppEvent('panel-fullscreen-exit', (evt, payload) => {
|
||||
// if (panel.id !== payload.panelId) {
|
||||
// return;
|
||||
// }
|
||||
// gridCtrl.gridstack.locked(element, false);
|
||||
// element.removeClass('panel-fullscreen');
|
||||
// }, scope);
|
||||
//
|
||||
// $rootScope.onAppEvent('panel-fullscreen-enter', (evt, payload) => {
|
||||
// if (panel.id !== payload.panelId) {
|
||||
// return;
|
||||
// }
|
||||
// element.addClass('panel-fullscreen');
|
||||
// }, scope);
|
||||
//
|
||||
// scope.$on('$destroy', () => {
|
||||
// console.log('grid-item scope $destroy');
|
||||
// if (gridCtrl.isDestroyed) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (gridStackNode) {
|
||||
// console.log('grid-item scope $destroy removeWidget');
|
||||
// gridStackNode._grid.removeWidget(element);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (gridCtrl.isInitialized) {
|
||||
// gridCtrl.gridstack.makeWidget(element);
|
||||
// gridStackNode = element.data('_gridstack_node');
|
||||
// } else {
|
||||
// setTimeout(function() {
|
||||
// gridStackNode = element.data('_gridstack_node');
|
||||
// }, 500);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// coreModule.directive('dashGrid', dashGrid);
|
||||
// coreModule.directive('dashGridItem', dashGridItem);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.dateTimeFormat">
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Export To Excel" label-class="width-12" switch-class="max-width-6"
|
||||
label="Excel CSV Dialect" label-class="width-10" switch-class="max-width-6"
|
||||
checked="ctrl.excel">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ var template = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
coreModule.directive('dashRepeatOption', function(variableSrv) {
|
||||
/** @ngInject **/
|
||||
function dashRepeatOptionDirective(variableSrv) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
@@ -30,5 +31,6 @@ coreModule.directive('dashRepeatOption', function(variableSrv) {
|
||||
scope.variables.unshift({text: 'Disabled', value: null});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
coreModule.directive('dashRepeatOption', dashRepeatOptionDirective);
|
||||
|
||||
@@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
|
||||
|
||||
import _ from 'lodash';
|
||||
import {HistoryListCtrl} from 'app/features/dashboard/history/history';
|
||||
import { versions, compare, restore } from 'test/mocks/history-mocks';
|
||||
import {versions, compare, restore} from './history_mocks';
|
||||
|
||||
describe('HistoryListCtrl', function() {
|
||||
var RESTORE_ID = 4;
|
||||
|
||||
193
public/app/features/dashboard/specs/history_mocks.ts
Normal file
193
public/app/features/dashboard/specs/history_mocks.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
|
||||
export function versions() {
|
||||
return [{
|
||||
id: 4,
|
||||
dashboardId: 1,
|
||||
parentVersion: 3,
|
||||
restoredFrom: 0,
|
||||
version: 4,
|
||||
created: '2017-02-22T17:43:01-08:00',
|
||||
createdBy: 'admin',
|
||||
message: '',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
dashboardId: 1,
|
||||
parentVersion: 1,
|
||||
restoredFrom: 1,
|
||||
version: 3,
|
||||
created: '2017-02-22T17:43:01-08:00',
|
||||
createdBy: 'admin',
|
||||
message: '',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
dashboardId: 1,
|
||||
parentVersion: 0,
|
||||
restoredFrom: -1,
|
||||
version: 2,
|
||||
created: '2017-02-22T17:29:52-08:00',
|
||||
createdBy: 'admin',
|
||||
message: '',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
dashboardId: 1,
|
||||
parentVersion: 0,
|
||||
restoredFrom: -1,
|
||||
slug: 'history-dashboard',
|
||||
version: 1,
|
||||
created: '2017-02-22T17:06:37-08:00',
|
||||
createdBy: 'admin',
|
||||
message: '',
|
||||
}];
|
||||
}
|
||||
|
||||
export function compare(type) {
|
||||
return type === 'basic' ? '<div></div>' : '<pre><code></code></pre>';
|
||||
}
|
||||
|
||||
export function restore(version, restoredFrom?) {
|
||||
return {
|
||||
dashboard: {
|
||||
meta: {
|
||||
type: 'db',
|
||||
canSave: true,
|
||||
canEdit: true,
|
||||
canStar: true,
|
||||
slug: 'history-dashboard',
|
||||
expires: '0001-01-01T00:00:00Z',
|
||||
created: '2017-02-21T18:40:45-08:00',
|
||||
updated: '2017-04-11T21:31:22.59219665-07:00',
|
||||
updatedBy: 'admin',
|
||||
createdBy: 'admin',
|
||||
version: version,
|
||||
},
|
||||
dashboard: {
|
||||
annotations: {
|
||||
list: []
|
||||
},
|
||||
description: 'A random dashboard for implementing the history list',
|
||||
editable: true,
|
||||
gnetId: null,
|
||||
graphTooltip: 0,
|
||||
hideControls: false,
|
||||
id: 1,
|
||||
links: [],
|
||||
restoredFrom: restoredFrom,
|
||||
rows: [{
|
||||
collapse: false,
|
||||
height: '250px',
|
||||
panels: [{
|
||||
aliasColors: {},
|
||||
bars: false,
|
||||
datasource: null,
|
||||
fill: 1,
|
||||
id: 1,
|
||||
legend: {
|
||||
avg: false,
|
||||
current: false,
|
||||
max: false,
|
||||
min: false,
|
||||
show: true,
|
||||
total: false,
|
||||
values: false
|
||||
},
|
||||
lines: true,
|
||||
linewidth: 1,
|
||||
nullPointMode: "null",
|
||||
percentage: false,
|
||||
pointradius: 5,
|
||||
points: false,
|
||||
renderer: 'flot',
|
||||
seriesOverrides: [],
|
||||
span: 12,
|
||||
stack: false,
|
||||
steppedLine: false,
|
||||
targets: [{}],
|
||||
thresholds: [],
|
||||
timeFrom: null,
|
||||
timeShift: null,
|
||||
title: 'Panel Title',
|
||||
tooltip: {
|
||||
shared: true,
|
||||
sort: 0,
|
||||
value_type: 'individual'
|
||||
},
|
||||
type: 'graph',
|
||||
xaxis: {
|
||||
mode: 'time',
|
||||
name: null,
|
||||
show: true,
|
||||
values: []
|
||||
},
|
||||
yaxes: [{
|
||||
format: 'short',
|
||||
label: null,
|
||||
logBase: 1,
|
||||
max: null,
|
||||
min: null,
|
||||
show: true
|
||||
}, {
|
||||
format: 'short',
|
||||
label: null,
|
||||
logBase: 1,
|
||||
max: null,
|
||||
min: null,
|
||||
show: true
|
||||
}]
|
||||
}],
|
||||
repeat: null,
|
||||
repeatIteration: null,
|
||||
repeatRowId: null,
|
||||
showTitle: false,
|
||||
title: 'Dashboard Row',
|
||||
titleSize: 'h6'
|
||||
}
|
||||
],
|
||||
schemaVersion: 14,
|
||||
style: 'dark',
|
||||
tags: [
|
||||
'development'
|
||||
],
|
||||
templating: {
|
||||
'list': []
|
||||
},
|
||||
time: {
|
||||
from: 'now-6h',
|
||||
to: 'now'
|
||||
},
|
||||
timepicker: {
|
||||
refresh_intervals: [
|
||||
'5s',
|
||||
'10s',
|
||||
'30s',
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'2h',
|
||||
'1d',
|
||||
],
|
||||
time_options: [
|
||||
'5m',
|
||||
'15m',
|
||||
'1h',
|
||||
'6h',
|
||||
'12h',
|
||||
'24h',
|
||||
'2d',
|
||||
'7d',
|
||||
'30d'
|
||||
]
|
||||
},
|
||||
timezone: 'utc',
|
||||
title: 'History Dashboard',
|
||||
version: version,
|
||||
}
|
||||
},
|
||||
message: 'Dashboard restored to version ' + version,
|
||||
version: version
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import helpers from 'test/specs/helpers';
|
||||
import '../history/history_srv';
|
||||
import {versions, restore} from 'test/mocks/history-mocks';
|
||||
import {versions, restore} from './history_mocks';
|
||||
|
||||
describe('historySrv', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
|
||||
110
public/app/features/dashboard/specs/share_modal_ctrl_specs.ts
Normal file
110
public/app/features/dashboard/specs/share_modal_ctrl_specs.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import '../shareModalCtrl';
|
||||
import config from 'app/core/config';
|
||||
import 'app/features/panellinks/linkSrv';
|
||||
|
||||
describe('ShareModalCtrl', function() {
|
||||
var ctx = new helpers.ControllerTestContext();
|
||||
|
||||
function setTime(range) {
|
||||
ctx.timeSrv.timeRange = sinon.stub().returns(range);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
setTime({ from: new Date(1000), to: new Date(2000) });
|
||||
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($compileProvider) {
|
||||
$compileProvider.preAssignBindingsEnabled(true);
|
||||
}));
|
||||
|
||||
beforeEach(ctx.providePhase());
|
||||
|
||||
beforeEach(ctx.createControllerPhase('ShareModalCtrl'));
|
||||
|
||||
describe('shareUrl with current time range and panel', function() {
|
||||
it('should generate share url absolute time', function() {
|
||||
ctx.$location.path('/test');
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen');
|
||||
});
|
||||
|
||||
it('should generate render url', function() {
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/db/my-dash';
|
||||
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
var base = 'http://dashboards.grafana.com/render/dashboard-solo/db/my-dash';
|
||||
var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(ctx.scope.imageUrl).to.contain(base + params);
|
||||
});
|
||||
|
||||
it('should remove panel id when no panel in scope', function() {
|
||||
ctx.$location.path('/test');
|
||||
ctx.scope.options.forCurrent = true;
|
||||
ctx.scope.panel = null;
|
||||
|
||||
ctx.scope.init();
|
||||
expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1');
|
||||
});
|
||||
|
||||
it('should add theme when specified', function() {
|
||||
ctx.$location.path('/test');
|
||||
ctx.scope.options.theme = 'light';
|
||||
ctx.scope.panel = null;
|
||||
|
||||
ctx.scope.init();
|
||||
expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
|
||||
});
|
||||
|
||||
it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', function() {
|
||||
ctx.$location.url('/test?fullscreen&edit');
|
||||
ctx.scope.modeSharePanel = true;
|
||||
ctx.scope.panel = { id: 1 };
|
||||
|
||||
ctx.scope.buildUrl();
|
||||
|
||||
expect(ctx.scope.shareUrl).to.contain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1');
|
||||
expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
|
||||
|
||||
});
|
||||
|
||||
it('should remove edit from image url when is first param in querystring and modeSharePanel is true', function() {
|
||||
ctx.$location.url('/test?edit&fullscreen');
|
||||
ctx.scope.modeSharePanel = true;
|
||||
ctx.scope.panel = { id: 1 };
|
||||
|
||||
ctx.scope.buildUrl();
|
||||
|
||||
expect(ctx.scope.shareUrl).to.contain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1');
|
||||
expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
|
||||
|
||||
});
|
||||
|
||||
it('should include template variables in url', function() {
|
||||
ctx.$location.path('/test');
|
||||
ctx.scope.options.includeTemplateVars = true;
|
||||
|
||||
ctx.templateSrv.fillVariableValuesForUrl = function(params) {
|
||||
params['var-app'] = 'mupp';
|
||||
params['var-server'] = 'srv-01';
|
||||
};
|
||||
|
||||
ctx.scope.buildUrl();
|
||||
expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
|
||||
import 'app/features/dashboard/unsavedChangesSrv';
|
||||
import 'app/features/dashboard/dashboard_srv';
|
||||
|
||||
describe("unsavedChangesSrv", function() {
|
||||
var _unsavedChangesSrv;
|
||||
var _dashboardSrv;
|
||||
var _location;
|
||||
var _contextSrvStub = { isEditor: true };
|
||||
var _rootScope;
|
||||
var tracker;
|
||||
var dash;
|
||||
var scope;
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('contextSrv', _contextSrvStub);
|
||||
$provide.value('$window', {});
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) {
|
||||
_unsavedChangesSrv = unsavedChangesSrv;
|
||||
_dashboardSrv = dashboardSrv;
|
||||
_location = $location;
|
||||
_rootScope = $rootScope;
|
||||
}));
|
||||
|
||||
beforeEach(function() {
|
||||
dash = _dashboardSrv.create({
|
||||
refresh: false,
|
||||
rows: [
|
||||
{
|
||||
panels: [{ test: "asd", legend: { } }]
|
||||
}
|
||||
]
|
||||
});
|
||||
scope = _rootScope.$new();
|
||||
scope.appEvent = sinon.spy();
|
||||
scope.onAppEvent = sinon.spy();
|
||||
|
||||
tracker = new _unsavedChangesSrv.Tracker(dash, scope);
|
||||
});
|
||||
|
||||
it('No changes should not have changes', function() {
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Simple change should be registered', function() {
|
||||
dash.property = "google";
|
||||
expect(tracker.hasChanges()).to.be(true);
|
||||
});
|
||||
|
||||
it('Should ignore a lot of changes', function() {
|
||||
dash.time = {from: '1h'};
|
||||
dash.refresh = true;
|
||||
dash.schemaVersion = 10;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore row collapse change', function() {
|
||||
dash.rows[0].collapse = true;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore panel legend changes', function() {
|
||||
dash.rows[0].panels[0].legend.sortDesc = true;
|
||||
dash.rows[0].panels[0].legend.sort = "avg";
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore panel repeats', function() {
|
||||
dash.rows[0].panels.push({repeatPanelId: 10});
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore row repeats', function() {
|
||||
dash.addEmptyRow();
|
||||
dash.rows[1].repeatRowId = 10;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
});
|
||||
53
public/app/features/dashboard/specs/viewstate_srv_specs.ts
Normal file
53
public/app/features/dashboard/specs/viewstate_srv_specs.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import 'app/features/dashboard/viewStateSrv';
|
||||
import config from 'app/core/config';
|
||||
|
||||
describe('when updating view state', function() {
|
||||
var viewState, location;
|
||||
var timeSrv = {};
|
||||
var templateSrv = {};
|
||||
var contextSrv = {
|
||||
user: {
|
||||
orgId: 19
|
||||
}
|
||||
};
|
||||
beforeEach(function() {
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('timeSrv', timeSrv);
|
||||
$provide.value('templateSrv', templateSrv);
|
||||
$provide.value('contextSrv', contextSrv);
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) {
|
||||
$rootScope.onAppEvent = function() {};
|
||||
$rootScope.dashboard = {meta: {}};
|
||||
viewState = dashboardViewStateSrv.create($rootScope);
|
||||
location = $location;
|
||||
}));
|
||||
|
||||
describe('to fullscreen true and edit true', function() {
|
||||
it('should update querystring and view state', function() {
|
||||
var updateState = {fullscreen: true, edit: true, panelId: 1};
|
||||
viewState.update(updateState);
|
||||
expect(location.search()).to.eql({fullscreen: true, edit: true, panelId: 1, orgId: 1});
|
||||
expect(viewState.dashboard.meta.fullscreen).to.be(true);
|
||||
expect(viewState.state.fullscreen).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to fullscreen false', function() {
|
||||
it('should remove params from query string', function() {
|
||||
viewState.update({fullscreen: true, panelId: 1, edit: true});
|
||||
viewState.update({fullscreen: false});
|
||||
expect(viewState.dashboard.meta.fullscreen).to.be(false);
|
||||
expect(viewState.state.fullscreen).to.be(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,74 +3,78 @@
|
||||
|
||||
<div ng-repeat="link in dashboard.links">
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-group gf-form-inline">
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Type</span>
|
||||
<span class="gf-form-label width-8">Type</span>
|
||||
<div class="gf-form-select-wrapper width-10">
|
||||
<select class="gf-form-input" ng-model="link.type" ng-options="f for f in ['dashboards','link']" ng-change="updated()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards'">
|
||||
<span class="gf-form-label">With tags</span>
|
||||
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags"></bootstrap-tagsinput>
|
||||
<span class="gf-form-label width-8">With tags</span>
|
||||
<bootstrap-tagsinput ng-model="link.tags" class="width-10" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards'">
|
||||
<editor-checkbox text="As dropdown" model="link.asDropdown" change="updated()"></editor-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="gf-form max-width-30" ng-show="link.type === 'link'">
|
||||
<li class="gf-form-label width-6">Url</li>
|
||||
<input type="text" ng-model="link.url" class="gf-form-input" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse btn-mini" ng-click="moveLink($index, -1)" ng-hide="$first"><i class="fa fa-arrow-up"></i></button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse btn-mini" ng-click="moveLink($index, 1)" ng-hide="$last"><i class="fa fa-arrow-down"></i></button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse btn-mini" ng-click="deleteLink($index)"><i class="fa fa-trash" ></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
|
||||
<span class="gf-form-label width-6">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input max-width-25" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-show="link.type === 'link'">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Title</span>
|
||||
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8"></gf-form-switch>
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input max-width-10" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div ng-show="link.type === 'link'">
|
||||
<div class="gf-form">
|
||||
<li class="gf-form-label width-8">Url</li>
|
||||
<input type="text" ng-model="link.url" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Tooltip</span>
|
||||
<input type="text" ng-model="link.tooltip" class="gf-form-input max-width-10" placeholder="Open dashboard" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Icon</span>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="link.icon" ng-options="k as k for (k, v) in iconMap" ng-change="updated()"></select>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Tooltip</span>
|
||||
<input type="text" ng-model="link.tooltip" class="gf-form-input width-20" placeholder="Open dashboard" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Icon</span>
|
||||
<div class="gf-form-select-wrapper width-20">
|
||||
<select class="gf-form-input" ng-model="link.icon" ng-options="k as k for (k, v) in iconMap" ng-change="updated()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="section gf-form-inline" style="display: flex">
|
||||
<div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Include</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<gf-form-switch class="gf-form" label="Time range" checked="link.keepTime" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Variable values" checked="link.includeVars" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Open in new tab" checked="link.targetBlank" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; justify-content:flex-start">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Include</span>
|
||||
<editor-checkbox text="Time range" model="link.keepTime" change="updated()"></editor-checkbox>
|
||||
<editor-checkbox text="Variable values" model="link.includeVars" change="updated()"></editor-checkbox>
|
||||
<editor-checkbox text="Open in new tab " model="link.targetBlank" change="updated()"></editor-checkbox>
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="deleteLink($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, -1)" ng-hide="$first"><i class="fa fa-arrow-up"></i></button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, 1)" ng-hide="$last"><i class="fa fa-arrow-down"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-inverse" ng-click="addLink()"><i class="fa fa-plus"></i> Add link</button>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import {coreModule} from 'app/core/core';
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {PanelCtrl} from './panel_ctrl';
|
||||
import {PanelCtrl} from 'app/features/panel/panel_ctrl';
|
||||
|
||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
46
public/app/features/panellinks/specs/link_srv_specs.ts
Normal file
46
public/app/features/panellinks/specs/link_srv_specs.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import 'app/features/panellinks/linkSrv';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('linkSrv', function() {
|
||||
var _linkSrv;
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
|
||||
beforeEach(angularMocks.inject(function(linkSrv) {
|
||||
_linkSrv = linkSrv;
|
||||
}));
|
||||
|
||||
describe('when appending query strings', function() {
|
||||
|
||||
it('add ? to URL if not present', function() {
|
||||
var url = _linkSrv.appendToQueryString('http://example.com', 'foo=bar');
|
||||
expect(url).to.be('http://example.com?foo=bar');
|
||||
});
|
||||
|
||||
it('do not add & to URL if ? is present but query string is empty', function() {
|
||||
var url = _linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
|
||||
expect(url).to.be('http://example.com?foo=bar');
|
||||
});
|
||||
|
||||
it('add & to URL if query string is present', function() {
|
||||
var url = _linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
|
||||
expect(url).to.be('http://example.com?foo=bar&hello=world');
|
||||
});
|
||||
|
||||
it('do not change the URL if there is nothing to append', function() {
|
||||
_.each(['', undefined, null], function(toAppend) {
|
||||
var url1 = _linkSrv.appendToQueryString('http://example.com', toAppend);
|
||||
expect(url1).to.be('http://example.com');
|
||||
|
||||
var url2 = _linkSrv.appendToQueryString('http://example.com?', toAppend);
|
||||
expect(url2).to.be('http://example.com?');
|
||||
|
||||
var url3 = _linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend);
|
||||
expect(url3).to.be('http://example.com?foo=bar');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -4,3 +4,5 @@ import './plugin_list_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
import './ds_list_ctrl';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
'app/core/core_module',
|
||||
'app/core/config',
|
||||
'./plugin_loader',
|
||||
],
|
||||
function (angular, _, coreModule, config) {
|
||||
function (angular, _, coreModule, config, pluginLoader) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) {
|
||||
@@ -41,7 +42,7 @@ function (angular, _, coreModule, config) {
|
||||
var deferred = $q.defer();
|
||||
var pluginDef = dsConfig.meta;
|
||||
|
||||
System.import(pluginDef.module).then(function(plugin) {
|
||||
pluginLoader.importPluginModule(pluginDef.module).then(function(plugin) {
|
||||
// check if its in cache now
|
||||
if (self.datasources[name]) {
|
||||
deferred.resolve(self.datasources[name]);
|
||||
@@ -1,12 +1,12 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {importPluginModule} from './plugin_loader';
|
||||
|
||||
import {UnknownPanelCtrl} from 'app/plugins/panel/unknown/module';
|
||||
import {DashboardRowCtrl} from '../components/row_ctrl';
|
||||
import {DashboardRowCtrl} from './row_ctrl';
|
||||
|
||||
/** @ngInject **/
|
||||
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
|
||||
@@ -74,7 +74,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
let panelInfo = config.panels[scope.panel.type];
|
||||
var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
|
||||
if (panelInfo) {
|
||||
panelCtrlPromise = System.import(panelInfo.module).then(function(panelModule) {
|
||||
panelCtrlPromise = importPluginModule(panelInfo.module).then(function(panelModule) {
|
||||
return panelModule.PanelCtrl;
|
||||
});
|
||||
}
|
||||
@@ -114,7 +114,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
return datasourceSrv.get(datasource).then(ds => {
|
||||
scope.datasource = ds;
|
||||
|
||||
return System.import(ds.meta.module).then(dsModule => {
|
||||
return importPluginModule(ds.meta.module).then(dsModule => {
|
||||
return {
|
||||
baseUrl: ds.meta.baseUrl,
|
||||
name: 'query-ctrl-' + ds.meta.id,
|
||||
@@ -128,7 +128,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
// QueryOptionsCtrl
|
||||
case "query-options-ctrl": {
|
||||
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
|
||||
return System.import(ds.meta.module).then((dsModule): any => {
|
||||
return importPluginModule(ds.meta.module).then((dsModule): any => {
|
||||
if (!dsModule.QueryOptionsCtrl) {
|
||||
return {notFound: true};
|
||||
}
|
||||
@@ -145,7 +145,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
}
|
||||
// Annotations
|
||||
case "annotations-query-ctrl": {
|
||||
return System.import(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
|
||||
return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
|
||||
return {
|
||||
baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
|
||||
name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
|
||||
@@ -158,7 +158,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
// Datasource ConfigCtrl
|
||||
case 'datasource-config-ctrl': {
|
||||
var dsMeta = scope.ctrl.datasourceMeta;
|
||||
return System.import(dsMeta.module).then(function(dsModule): any {
|
||||
return importPluginModule(dsMeta.module).then(function(dsModule): any {
|
||||
if (!dsModule.ConfigCtrl) {
|
||||
return {notFound: true};
|
||||
}
|
||||
@@ -175,7 +175,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
// AppConfigCtrl
|
||||
case 'app-config-ctrl': {
|
||||
let model = scope.ctrl.model;
|
||||
return System.import(model.module).then(function(appModule) {
|
||||
return importPluginModule(model.module).then(function(appModule) {
|
||||
return {
|
||||
baseUrl: model.baseUrl,
|
||||
name: 'app-config-' + model.id,
|
||||
@@ -188,7 +188,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
||||
// App Page
|
||||
case 'app-page': {
|
||||
let appModel = scope.ctrl.appModel;
|
||||
return System.import(appModel.module).then(function(appModule) {
|
||||
return importPluginModule(appModel.module).then(function(appModule) {
|
||||
return {
|
||||
baseUrl: appModel.baseUrl,
|
||||
name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
|
||||
128
public/app/features/plugins/plugin_loader.ts
Normal file
128
public/app/features/plugins/plugin_loader.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import System from 'systemjs/dist/system.js';
|
||||
import _ from 'lodash';
|
||||
import * as sdk from 'app/plugins/sdk';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import moment from 'moment';
|
||||
import angular from 'angular';
|
||||
import jquery from 'jquery';
|
||||
import config from 'app/core/config';
|
||||
import TimeSeries from 'app/core/time_series2';
|
||||
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,
|
||||
};
|
||||
|
||||
System.config({
|
||||
baseURL: 'public',
|
||||
defaultExtension: 'js',
|
||||
packages: {
|
||||
'plugins': {
|
||||
defaultExtension: 'js'
|
||||
}
|
||||
},
|
||||
map: {
|
||||
text: 'vendor/plugin-text/text.js',
|
||||
css: 'vendor/plugin-css/css.js'
|
||||
},
|
||||
});
|
||||
|
||||
// add cache busting
|
||||
var systemLocate = System.locate;
|
||||
System.cacheBust = '?bust=' + Date.now();
|
||||
System.locate = function(load) {
|
||||
var System = this;
|
||||
return Promise.resolve(systemLocate.call(this, load)).then(function(address) {
|
||||
return address + System.cacheBust;
|
||||
});
|
||||
};
|
||||
|
||||
function exposeToPlugin(name: string, component: any) {
|
||||
System.registerDynamic(name, [], true, function(require, exports, module) {
|
||||
module.exports = component;
|
||||
});
|
||||
}
|
||||
|
||||
exposeToPlugin('lodash', _);
|
||||
exposeToPlugin('moment', moment);
|
||||
exposeToPlugin('jquery', jquery);
|
||||
exposeToPlugin('angular', angular);
|
||||
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);
|
||||
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.selection';
|
||||
import 'vendor/flot/jquery.flot.time';
|
||||
import 'vendor/flot/jquery.flot.stack';
|
||||
import 'vendor/flot/jquery.flot.pie';
|
||||
import 'vendor/flot/jquery.flot.stackpercent';
|
||||
import 'vendor/flot/jquery.flot.fillbelow';
|
||||
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}; });
|
||||
}
|
||||
|
||||
export function importPluginModule(path: string): Promise<any> {
|
||||
let builtIn = builtInPlugins[path];
|
||||
if (builtIn) {
|
||||
return Promise.resolve(builtIn);
|
||||
}
|
||||
return System.import(path);
|
||||
}
|
||||
|
||||
export function loadPluginCss(options) {
|
||||
if (config.bootData.user.lightTheme) {
|
||||
System.import(options.light + '!css');
|
||||
} else {
|
||||
System.import(options.dark + '!css');
|
||||
}
|
||||
}
|
||||
|
||||
60
public/app/features/plugins/specs/datasource_srv_specs.ts
Normal file
60
public/app/features/plugins/specs/datasource_srv_specs.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import config from 'app/core/config';
|
||||
import 'app/features/plugins/datasource_srv';
|
||||
|
||||
describe('datasource_srv', function() {
|
||||
var _datasourceSrv;
|
||||
var metricSources;
|
||||
var templateSrv = {};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('templateSrv', templateSrv);
|
||||
}));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.inject(function(datasourceSrv) {
|
||||
_datasourceSrv = datasourceSrv;
|
||||
}));
|
||||
|
||||
describe('when loading metric sources', function() {
|
||||
var unsortedDatasources = {
|
||||
'mmm': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
'--Grafana--': {
|
||||
type: 'grafana',
|
||||
meta: {builtIn: true, metrics: {m: 1}, id: "grafana"}
|
||||
},
|
||||
'--Mixed--': {
|
||||
type: 'test-db',
|
||||
meta: {builtIn: true, metrics: {m: 1}, id: "mixed"}
|
||||
},
|
||||
'ZZZ': {
|
||||
type: 'test-db',
|
||||
meta: {metrics: {m: 1} }
|
||||
},
|
||||
'aaa': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
'BBB': {
|
||||
type: 'test-db',
|
||||
meta: { metrics: {m: 1} }
|
||||
},
|
||||
};
|
||||
beforeEach(function() {
|
||||
config.datasources = unsortedDatasources;
|
||||
metricSources = _datasourceSrv.getMetricSources({skipVariables: true});
|
||||
});
|
||||
|
||||
it('should return a list of sources sorted case insensitively with builtin sources last', function() {
|
||||
expect(metricSources[0].name).to.be('aaa');
|
||||
expect(metricSources[1].name).to.be('BBB');
|
||||
expect(metricSources[2].name).to.be('mmm');
|
||||
expect(metricSources[3].name).to.be('ZZZ');
|
||||
expect(metricSources[4].name).to.be('--Grafana--');
|
||||
expect(metricSources[5].name).to.be('--Mixed--');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ class StyleGuideCtrl {
|
||||
}
|
||||
|
||||
loadColors() {
|
||||
this.$http.get('public/sass/styleguide.json').then(res => {
|
||||
this.$http.get('public/build/styleguide.json').then(res => {
|
||||
this.colors = _.map(res.data[this.theme], (value, key) => {
|
||||
return {name: key, value: value};
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('templateSrv', function() {
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module($provide => {
|
||||
$provide.value('timeSrv', {});
|
||||
$provide.value('datasourceSrv', {});
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(variableSrv, templateSrv) {
|
||||
|
||||
1
public/app/headers/common.d.ts
vendored
1
public/app/headers/common.d.ts
vendored
@@ -81,3 +81,4 @@ declare module 'ace' {
|
||||
var ace: any;
|
||||
export default ace;
|
||||
}
|
||||
|
||||
|
||||
3
public/app/index.ts
Normal file
3
public/app/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import app from './app';
|
||||
app.init();
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
declare var test: any;
|
||||
export default test;
|
||||
@@ -1,106 +0,0 @@
|
||||
define([
|
||||
'lodash',
|
||||
],
|
||||
function (_) {
|
||||
'use strict';
|
||||
|
||||
function CloudWatchAnnotationQuery(datasource, annotation, $q, templateSrv) {
|
||||
this.datasource = datasource;
|
||||
this.annotation = annotation;
|
||||
this.$q = $q;
|
||||
this.templateSrv = templateSrv;
|
||||
}
|
||||
|
||||
CloudWatchAnnotationQuery.prototype.process = function(from, to) {
|
||||
var self = this;
|
||||
var usePrefixMatch = this.annotation.prefixMatching;
|
||||
var region = this.templateSrv.replace(this.annotation.region);
|
||||
var namespace = this.templateSrv.replace(this.annotation.namespace);
|
||||
var metricName = this.templateSrv.replace(this.annotation.metricName);
|
||||
var dimensions = this.datasource.convertDimensionFormat(this.annotation.dimensions);
|
||||
var statistics = _.map(this.annotation.statistics, function(s) { return self.templateSrv.replace(s); });
|
||||
var defaultPeriod = usePrefixMatch ? '' : '300';
|
||||
var period = this.annotation.period || defaultPeriod;
|
||||
period = parseInt(period, 10);
|
||||
var actionPrefix = this.annotation.actionPrefix || '';
|
||||
var alarmNamePrefix = this.annotation.alarmNamePrefix || '';
|
||||
|
||||
var d = this.$q.defer();
|
||||
var allQueryPromise;
|
||||
if (usePrefixMatch) {
|
||||
allQueryPromise = [
|
||||
this.datasource.performDescribeAlarms(region, actionPrefix, alarmNamePrefix, [], '').then(function(alarms) {
|
||||
alarms.MetricAlarms = self.filterAlarms(alarms, namespace, metricName, dimensions, statistics, period);
|
||||
return alarms;
|
||||
})
|
||||
];
|
||||
} else {
|
||||
if (!region || !namespace || !metricName || _.isEmpty(statistics)) { return this.$q.when([]); }
|
||||
|
||||
allQueryPromise = _.map(statistics, function(statistic) {
|
||||
return self.datasource.performDescribeAlarmsForMetric(region, namespace, metricName, dimensions, statistic, period);
|
||||
});
|
||||
}
|
||||
this.$q.all(allQueryPromise).then(function(alarms) {
|
||||
var eventList = [];
|
||||
|
||||
var start = self.datasource.convertToCloudWatchTime(from, false);
|
||||
var end = self.datasource.convertToCloudWatchTime(to, true);
|
||||
_.chain(alarms)
|
||||
.map('MetricAlarms')
|
||||
.flatten()
|
||||
.each(function(alarm) {
|
||||
if (!alarm) {
|
||||
d.resolve(eventList);
|
||||
return;
|
||||
}
|
||||
|
||||
self.datasource.performDescribeAlarmHistory(region, alarm.AlarmName, start, end).then(function(history) {
|
||||
_.each(history.AlarmHistoryItems, function(h) {
|
||||
var event = {
|
||||
annotation: self.annotation,
|
||||
time: Date.parse(h.Timestamp),
|
||||
title: h.AlarmName,
|
||||
tags: [h.HistoryItemType],
|
||||
text: h.HistorySummary
|
||||
};
|
||||
|
||||
eventList.push(event);
|
||||
});
|
||||
|
||||
d.resolve(eventList);
|
||||
});
|
||||
})
|
||||
.value();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
};
|
||||
|
||||
CloudWatchAnnotationQuery.prototype.filterAlarms = function(alarms, namespace, metricName, dimensions, statistics, period) {
|
||||
return _.filter(alarms.MetricAlarms, function(alarm) {
|
||||
if (!_.isEmpty(namespace) && alarm.Namespace !== namespace) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isEmpty(metricName) && alarm.MetricName !== metricName) {
|
||||
return false;
|
||||
}
|
||||
var sd = function(d) {
|
||||
return d.Name;
|
||||
};
|
||||
var isSameDimensions = JSON.stringify(_.sortBy(alarm.Dimensions, sd)) === JSON.stringify(_.sortBy(dimensions, sd));
|
||||
if (!_.isEmpty(dimensions) && !isSameDimensions) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isEmpty(statistics) && !_.includes(statistics, alarm.Statistic)) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isNaN(period) && alarm.Period !== period) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return CloudWatchAnnotationQuery;
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
declare var CloudWatchDatasource: any;
|
||||
export {CloudWatchDatasource};
|
||||
export default CloudWatchDatasource;
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@ define([
|
||||
'app/core/utils/datemath',
|
||||
'app/core/utils/kbn',
|
||||
'app/features/templating/variable',
|
||||
'./annotation_query',
|
||||
],
|
||||
function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnotationQuery) {
|
||||
function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
'use strict';
|
||||
|
||||
/** @ngInject */
|
||||
function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv) {
|
||||
function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
|
||||
this.type = 'cloudwatch';
|
||||
this.name = instanceSettings.name;
|
||||
this.supportMetrics = true;
|
||||
this.proxyUrl = instanceSettings.url;
|
||||
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||
this.instanceSettings = instanceSettings;
|
||||
this.standardStatistics = [
|
||||
'Average',
|
||||
'Maximum',
|
||||
@@ -27,31 +27,30 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
|
||||
var self = this;
|
||||
this.query = function(options) {
|
||||
var start = self.convertToCloudWatchTime(options.range.from, false);
|
||||
var end = self.convertToCloudWatchTime(options.range.to, true);
|
||||
|
||||
var queries = [];
|
||||
options = angular.copy(options);
|
||||
options.targets = this.expandTemplateVariable(options.targets, options.scopedVars, templateSrv);
|
||||
_.each(options.targets, function(target) {
|
||||
if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var query = {};
|
||||
query.region = templateSrv.replace(target.region, options.scopedVars);
|
||||
query.namespace = templateSrv.replace(target.namespace, options.scopedVars);
|
||||
query.metricName = templateSrv.replace(target.metricName, options.scopedVars);
|
||||
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
|
||||
query.statistics = target.statistics;
|
||||
var queries = _.filter(options.targets, function (item) {
|
||||
return item.hide !== true &&
|
||||
!!item.region &&
|
||||
!!item.namespace &&
|
||||
!!item.metricName &&
|
||||
!_.isEmpty(item.statistics);
|
||||
}).map(function (item) {
|
||||
item.region = templateSrv.replace(item.region, options.scopedVars);
|
||||
item.namespace = templateSrv.replace(item.namespace, options.scopedVars);
|
||||
item.metricName = templateSrv.replace(item.metricName, options.scopedVars);
|
||||
item.dimensions = self.convertDimensionFormat(item.dimensions, options.scopeVars);
|
||||
item.period = self.getPeriod(item, options);
|
||||
|
||||
var now = Math.round(Date.now() / 1000);
|
||||
var period = this.getPeriod(target, query, options, start, end, now);
|
||||
target.period = period;
|
||||
query.period = period;
|
||||
|
||||
queries.push(query);
|
||||
}.bind(this));
|
||||
return _.extend({
|
||||
refId: item.refId,
|
||||
intervalMs: options.intervalMs,
|
||||
maxDataPoints: options.maxDataPoints,
|
||||
datasourceId: self.instanceSettings.id,
|
||||
type: 'timeSeriesQuery',
|
||||
}, item);
|
||||
});
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
if (_.isEmpty(queries)) {
|
||||
@@ -60,23 +59,20 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
var allQueryPromise = _.map(queries, function(query) {
|
||||
return this.performTimeSeriesQuery(query, start, end);
|
||||
}.bind(this));
|
||||
var request = {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: queries
|
||||
};
|
||||
|
||||
return $q.all(allQueryPromise).then(function(allResponse) {
|
||||
var result = [];
|
||||
|
||||
_.each(allResponse, function(response, index) {
|
||||
var metrics = transformMetricData(response, options.targets[index], options.scopedVars);
|
||||
result = result.concat(metrics);
|
||||
});
|
||||
|
||||
return {data: result};
|
||||
});
|
||||
return this.performTimeSeriesQuery(request);
|
||||
};
|
||||
|
||||
this.getPeriod = function(target, query, options, start, end, now) {
|
||||
this.getPeriod = function(target, options, now) {
|
||||
var start = this.convertToCloudWatchTime(options.range.from, false);
|
||||
var end = this.convertToCloudWatchTime(options.range.to, true);
|
||||
now = Math.round((now || Date.now()) / 1000);
|
||||
|
||||
var period;
|
||||
var range = end - start;
|
||||
|
||||
@@ -85,7 +81,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
var periodUnit = 60;
|
||||
if (!target.period) {
|
||||
if (now - start <= (daySec * 15)) { // until 15 days ago
|
||||
if (query.namespace === 'AWS/EC2') {
|
||||
if (target.namespace === 'AWS/EC2') {
|
||||
periodUnit = period = 300;
|
||||
} else {
|
||||
periodUnit = period = 60;
|
||||
@@ -114,85 +110,93 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
return period;
|
||||
};
|
||||
|
||||
this.performTimeSeriesQuery = function(query, start, end) {
|
||||
var statistics = _.filter(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
|
||||
var extendedStatistics = _.reject(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
|
||||
return this.awsRequest({
|
||||
region: query.region,
|
||||
action: 'GetMetricStatistics',
|
||||
parameters: {
|
||||
namespace: query.namespace,
|
||||
metricName: query.metricName,
|
||||
dimensions: query.dimensions,
|
||||
statistics: statistics,
|
||||
extendedStatistics: extendedStatistics,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
period: query.period
|
||||
this.performTimeSeriesQuery = function(request) {
|
||||
return backendSrv.post('/api/tsdb/query', request).then(function (res) {
|
||||
var data = [];
|
||||
|
||||
if (res.results) {
|
||||
_.forEach(res.results, function (queryRes) {
|
||||
_.forEach(queryRes.series, function (series) {
|
||||
data.push({target: series.name, datapoints: series.points});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {data: data};
|
||||
});
|
||||
};
|
||||
|
||||
this.getRegions = function() {
|
||||
return this.awsRequest({action: '__GetRegions'});
|
||||
function transformSuggestDataFromTable(suggestData) {
|
||||
return _.map(suggestData.results['metricFindQuery'].tables[0].rows, function (v) {
|
||||
return {
|
||||
text: v[0],
|
||||
value: v[1]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.doMetricQueryRequest = function (subtype, parameters) {
|
||||
var range = timeSrv.timeRange();
|
||||
return backendSrv.post('/api/tsdb/query', {
|
||||
from: range.from.valueOf().toString(),
|
||||
to: range.to.valueOf().toString(),
|
||||
queries: [
|
||||
_.extend({
|
||||
refId: 'metricFindQuery',
|
||||
intervalMs: 1, // dummy
|
||||
maxDataPoints: 1, // dummy
|
||||
datasourceId: this.instanceSettings.id,
|
||||
type: 'metricFindQuery',
|
||||
subtype: subtype
|
||||
}, parameters)
|
||||
]
|
||||
}).then(function (r) { return transformSuggestDataFromTable(r); });
|
||||
};
|
||||
|
||||
this.getRegions = function () {
|
||||
return this.doMetricQueryRequest('regions', null);
|
||||
};
|
||||
|
||||
this.getNamespaces = function() {
|
||||
return this.awsRequest({action: '__GetNamespaces'});
|
||||
return this.doMetricQueryRequest('namespaces', null);
|
||||
};
|
||||
|
||||
this.getMetrics = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetMetrics',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
this.getMetrics = function (namespace, region) {
|
||||
return this.doMetricQueryRequest('metrics', {
|
||||
region: templateSrv.replace(region),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionKeys = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetDimensions',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
return this.doMetricQueryRequest('dimension_keys', {
|
||||
region: templateSrv.replace(region),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
|
||||
var request = {
|
||||
return this.doMetricQueryRequest('dimension_values', {
|
||||
region: templateSrv.replace(region),
|
||||
action: 'ListMetrics',
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensions: this.convertDimensionFormat(filterDimensions, {}),
|
||||
}
|
||||
};
|
||||
|
||||
return this.awsRequest(request).then(function(result) {
|
||||
return _.chain(result.Metrics)
|
||||
.map('Dimensions')
|
||||
.flatten()
|
||||
.filter(function(dimension) {
|
||||
return dimension !== null && dimension.Name === dimensionKey;
|
||||
})
|
||||
.map('Value')
|
||||
.uniq()
|
||||
.sortBy()
|
||||
.map(function(value) {
|
||||
return {value: value, text: value};
|
||||
}).value();
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensionKey: templateSrv.replace(dimensionKey),
|
||||
dimensions: this.convertDimensionFormat(filterDimensions, {}),
|
||||
});
|
||||
};
|
||||
|
||||
this.performEC2DescribeInstances = function(region, filters, instanceIds) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeInstances',
|
||||
parameters: { filters: filters, instanceIds: instanceIds }
|
||||
this.getEbsVolumeIds = function(region, instanceId) {
|
||||
return this.doMetricQueryRequest('ebs_volume_ids', {
|
||||
region: templateSrv.replace(region),
|
||||
instanceId: templateSrv.replace(instanceId)
|
||||
});
|
||||
};
|
||||
|
||||
this.getEc2InstanceAttribute = function(region, attributeName, filters) {
|
||||
return this.doMetricQueryRequest('ec2_instance_attribute', {
|
||||
region: templateSrv.replace(region),
|
||||
attributeName: templateSrv.replace(attributeName),
|
||||
filters: filters
|
||||
});
|
||||
};
|
||||
|
||||
@@ -201,12 +205,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
var namespace;
|
||||
var metricName;
|
||||
|
||||
var transformSuggestData = function(suggestData) {
|
||||
return _.map(suggestData, function(v) {
|
||||
return { text: v };
|
||||
});
|
||||
};
|
||||
|
||||
var regionQuery = query.match(/^regions\(\)/);
|
||||
if (regionQuery) {
|
||||
return this.getRegions();
|
||||
@@ -219,114 +217,98 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
|
||||
var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (metricNameQuery) {
|
||||
return this.getMetrics(templateSrv.replace(metricNameQuery[1]), templateSrv.replace(metricNameQuery[3]));
|
||||
namespace = metricNameQuery[1];
|
||||
region = metricNameQuery[3];
|
||||
return this.getMetrics(namespace, region);
|
||||
}
|
||||
|
||||
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (dimensionKeysQuery) {
|
||||
return this.getDimensionKeys(templateSrv.replace(dimensionKeysQuery[1]), templateSrv.replace(dimensionKeysQuery[3]));
|
||||
namespace = dimensionKeysQuery[1];
|
||||
region = dimensionKeysQuery[3];
|
||||
return this.getDimensionKeys(namespace, region);
|
||||
}
|
||||
|
||||
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
|
||||
if (dimensionValuesQuery) {
|
||||
region = templateSrv.replace(dimensionValuesQuery[1]);
|
||||
namespace = templateSrv.replace(dimensionValuesQuery[2]);
|
||||
metricName = templateSrv.replace(dimensionValuesQuery[3]);
|
||||
var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
|
||||
region = dimensionValuesQuery[1];
|
||||
namespace = dimensionValuesQuery[2];
|
||||
metricName = dimensionValuesQuery[3];
|
||||
var dimensionKey = dimensionValuesQuery[4];
|
||||
|
||||
return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
|
||||
}
|
||||
|
||||
var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
|
||||
if (ebsVolumeIdsQuery) {
|
||||
region = templateSrv.replace(ebsVolumeIdsQuery[1]);
|
||||
var instanceId = templateSrv.replace(ebsVolumeIdsQuery[2]);
|
||||
var instanceIds = [
|
||||
instanceId
|
||||
];
|
||||
|
||||
return this.performEC2DescribeInstances(region, [], instanceIds).then(function(result) {
|
||||
var volumeIds = _.map(result.Reservations[0].Instances[0].BlockDeviceMappings, function(mapping) {
|
||||
return mapping.Ebs.VolumeId;
|
||||
});
|
||||
|
||||
return transformSuggestData(volumeIds);
|
||||
});
|
||||
region = ebsVolumeIdsQuery[1];
|
||||
var instanceId = ebsVolumeIdsQuery[2];
|
||||
return this.getEbsVolumeIds(region, instanceId);
|
||||
}
|
||||
|
||||
var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (ec2InstanceAttributeQuery) {
|
||||
region = templateSrv.replace(ec2InstanceAttributeQuery[1]);
|
||||
region = ec2InstanceAttributeQuery[1];
|
||||
var targetAttributeName = ec2InstanceAttributeQuery[2];
|
||||
var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3]));
|
||||
var filters = _.map(filterJson, function(values, name) {
|
||||
return {
|
||||
Name: name,
|
||||
Values: values
|
||||
};
|
||||
});
|
||||
var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]);
|
||||
|
||||
return this.performEC2DescribeInstances(region, filters, null).then(function(result) {
|
||||
var attributes = _.chain(result.Reservations)
|
||||
.map(function(reservations) {
|
||||
return _.map(reservations.Instances, function(instance) {
|
||||
var tags = {};
|
||||
_.each(instance.Tags, function(tag) {
|
||||
tags[tag.Key] = tag.Value;
|
||||
});
|
||||
instance.Tags = tags;
|
||||
return instance;
|
||||
});
|
||||
})
|
||||
.map(function(instances) {
|
||||
return _.map(instances, targetAttributeName);
|
||||
})
|
||||
.flatten().uniq().sortBy().value();
|
||||
return transformSuggestData(attributes);
|
||||
});
|
||||
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
|
||||
}
|
||||
|
||||
return $q.when([]);
|
||||
};
|
||||
|
||||
this.performDescribeAlarms = function(region, actionPrefix, alarmNamePrefix, alarmNames, stateValue) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarms',
|
||||
parameters: { actionPrefix: actionPrefix, alarmNamePrefix: alarmNamePrefix, alarmNames: alarmNames, stateValue: stateValue }
|
||||
this.annotationQuery = function (options) {
|
||||
var annotation = options.annotation;
|
||||
var statistics = _.map(annotation.statistics, function (s) { return templateSrv.replace(s); });
|
||||
var defaultPeriod = annotation.prefixMatching ? '' : '300';
|
||||
var period = annotation.period || defaultPeriod;
|
||||
period = parseInt(period, 10);
|
||||
var parameters = {
|
||||
prefixMatching: annotation.prefixMatching,
|
||||
region: templateSrv.replace(annotation.region),
|
||||
namespace: templateSrv.replace(annotation.namespace),
|
||||
metricName: templateSrv.replace(annotation.metricName),
|
||||
dimensions: this.convertDimensionFormat(annotation.dimensions, {}),
|
||||
statistics: statistics,
|
||||
period: period,
|
||||
actionPrefix: annotation.actionPrefix || '',
|
||||
alarmNamePrefix: annotation.alarmNamePrefix || ''
|
||||
};
|
||||
|
||||
return backendSrv.post('/api/tsdb/query', {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: [
|
||||
_.extend({
|
||||
refId: 'annotationQuery',
|
||||
intervalMs: 1, // dummy
|
||||
maxDataPoints: 1, // dummy
|
||||
datasourceId: this.instanceSettings.id,
|
||||
type: 'annotationQuery'
|
||||
}, parameters)
|
||||
]
|
||||
}).then(function (r) {
|
||||
return _.map(r.results['annotationQuery'].tables[0].rows, function (v) {
|
||||
return {
|
||||
annotation: annotation,
|
||||
time: Date.parse(v[0]),
|
||||
title: v[1],
|
||||
tags: [v[2]],
|
||||
text: v[3]
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
|
||||
var s = _.includes(self.standardStatistics, statistic) ? statistic : '';
|
||||
var es = _.includes(self.standardStatistics, statistic) ? '' : statistic;
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarmsForMetric',
|
||||
parameters: {
|
||||
namespace: namespace,
|
||||
metricName: metricName,
|
||||
dimensions: dimensions,
|
||||
statistic: s,
|
||||
extendedStatistic: es,
|
||||
period: period
|
||||
}
|
||||
this.targetContainsTemplate = function(target) {
|
||||
return templateSrv.variableExists(target.region) ||
|
||||
templateSrv.variableExists(target.namespace) ||
|
||||
templateSrv.variableExists(target.metricName) ||
|
||||
_.find(target.dimensions, function(v, k) {
|
||||
return templateSrv.variableExists(k) || templateSrv.variableExists(v);
|
||||
});
|
||||
};
|
||||
|
||||
this.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarmHistory',
|
||||
parameters: { alarmName: alarmName, startDate: startDate, endDate: endDate }
|
||||
});
|
||||
};
|
||||
|
||||
this.annotationQuery = function(options) {
|
||||
var annotationQuery = new CloudWatchAnnotationQuery(this, options.annotation, $q, templateSrv);
|
||||
return annotationQuery.process(options.range.from, options.range.to);
|
||||
};
|
||||
|
||||
this.testDatasource = function() {
|
||||
/* use billing metrics for test */
|
||||
var region = this.defaultRegion;
|
||||
@@ -355,62 +337,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
return this.defaultRegion;
|
||||
};
|
||||
|
||||
function transformMetricData(md, options, scopedVars) {
|
||||
var aliasRegex = /\{\{(.+?)\}\}/g;
|
||||
var aliasPattern = options.alias || '{{metric}}_{{stat}}';
|
||||
var aliasData = {
|
||||
region: templateSrv.replace(options.region, scopedVars),
|
||||
namespace: templateSrv.replace(options.namespace, scopedVars),
|
||||
metric: templateSrv.replace(options.metricName, scopedVars),
|
||||
};
|
||||
|
||||
var aliasDimensions = {};
|
||||
|
||||
_.each(_.keys(options.dimensions), function(origKey) {
|
||||
var key = templateSrv.replace(origKey, scopedVars);
|
||||
var value = templateSrv.replace(options.dimensions[origKey], scopedVars);
|
||||
aliasDimensions[key] = value;
|
||||
});
|
||||
|
||||
_.extend(aliasData, aliasDimensions);
|
||||
|
||||
var periodMs = options.period * 1000;
|
||||
|
||||
return _.map(options.statistics, function(stat) {
|
||||
var extended = !_.includes(self.standardStatistics, stat);
|
||||
var dps = [];
|
||||
var lastTimestamp = null;
|
||||
_.chain(md.Datapoints)
|
||||
.sortBy(function(dp) {
|
||||
return dp.Timestamp;
|
||||
})
|
||||
.each(function(dp) {
|
||||
var timestamp = new Date(dp.Timestamp).getTime();
|
||||
while (lastTimestamp && (timestamp - lastTimestamp) > periodMs) {
|
||||
dps.push([null, lastTimestamp + periodMs]);
|
||||
lastTimestamp = lastTimestamp + periodMs;
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
if (!extended) {
|
||||
dps.push([dp[stat], timestamp]);
|
||||
} else {
|
||||
dps.push([dp.ExtendedStatistics[stat], timestamp]);
|
||||
}
|
||||
})
|
||||
.value();
|
||||
|
||||
aliasData.stat = stat;
|
||||
var seriesName = aliasPattern.replace(aliasRegex, function(match, g1) {
|
||||
if (aliasData[g1]) {
|
||||
return aliasData[g1];
|
||||
}
|
||||
return g1;
|
||||
});
|
||||
|
||||
return {target: seriesName, datapoints: dps};
|
||||
});
|
||||
}
|
||||
|
||||
this.getExpandedVariables = function(target, dimensionKey, variable, templateSrv) {
|
||||
/* if the all checkbox is marked we should add all values to the targets */
|
||||
var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
|
||||
@@ -461,17 +387,14 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
};
|
||||
|
||||
this.convertDimensionFormat = function(dimensions, scopedVars) {
|
||||
return _.map(dimensions, function(value, key) {
|
||||
return {
|
||||
Name: templateSrv.replace(key, scopedVars),
|
||||
Value: templateSrv.replace(value, scopedVars)
|
||||
};
|
||||
var convertedDimensions = {};
|
||||
_.each(dimensions, function (value, key) {
|
||||
convertedDimensions[templateSrv.replace(key, scopedVars)] = templateSrv.replace(value, scopedVars);
|
||||
});
|
||||
return convertedDimensions;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
CloudWatchDatasource: CloudWatchDatasource
|
||||
};
|
||||
return CloudWatchDatasource;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './query_parameter_ctrl';
|
||||
|
||||
import {CloudWatchDatasource} from './datasource';
|
||||
import CloudWatchDatasource from './datasource';
|
||||
import {CloudWatchQueryCtrl} from './query_ctrl';
|
||||
import {CloudWatchConfigCtrl} from './config_ctrl';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"id": "cloudwatch",
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"info": {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import "../datasource";
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import moment from 'moment';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import {CloudWatchDatasource} from "../datasource";
|
||||
import CloudWatchAnnotationQuery from '../annotation_query';
|
||||
|
||||
describe('CloudWatchAnnotationQuery', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings = {
|
||||
jsonData: {defaultRegion: 'us-east-1', access: 'proxy'},
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
|
||||
|
||||
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
ctx.$rootScope = $rootScope;
|
||||
ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
|
||||
}));
|
||||
|
||||
describe('When performing annotationQuery', function() {
|
||||
var parameter = {
|
||||
annotation: {
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678'
|
||||
},
|
||||
statistics: ['Average'],
|
||||
period: 300
|
||||
},
|
||||
range: {
|
||||
from: moment(1443438674760),
|
||||
to: moment(1443460274760)
|
||||
}
|
||||
};
|
||||
var alarmResponse = {
|
||||
MetricAlarms: [
|
||||
{
|
||||
AlarmName: 'test_alarm_name'
|
||||
}
|
||||
]
|
||||
};
|
||||
var historyResponse = {
|
||||
AlarmHistoryItems: [
|
||||
{
|
||||
Timestamp: '2015-01-01T00:00:00.000Z',
|
||||
HistoryItemType: 'StateUpdate',
|
||||
AlarmName: 'test_alarm_name',
|
||||
HistoryData: '{}',
|
||||
HistorySummary: 'test_history_summary'
|
||||
}
|
||||
]
|
||||
};
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
switch (params.data.action) {
|
||||
case 'DescribeAlarmsForMetric':
|
||||
return ctx.$q.when({data: alarmResponse});
|
||||
case 'DescribeAlarmHistory':
|
||||
return ctx.$q.when({data: historyResponse});
|
||||
}
|
||||
};
|
||||
});
|
||||
it('should return annotation list', function(done) {
|
||||
var annotationQuery = new CloudWatchAnnotationQuery(ctx.ds, parameter.annotation, ctx.$q, ctx.templateSrv);
|
||||
annotationQuery.process(parameter.range.from, parameter.range.to).then(function(result) {
|
||||
expect(result[0].title).to.be('test_alarm_name');
|
||||
expect(result[0].text).to.be('test_history_summary');
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
import "../datasource";
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import {CloudWatchDatasource} from "../datasource";
|
||||
import CloudWatchDatasource from "../datasource";
|
||||
|
||||
describe('CloudWatchDatasource', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
@@ -28,6 +27,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -43,37 +43,41 @@ describe('CloudWatchDatasource', function() {
|
||||
};
|
||||
|
||||
var response = {
|
||||
Datapoints: [
|
||||
{
|
||||
Average: 1,
|
||||
Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
|
||||
},
|
||||
{
|
||||
Average: 2,
|
||||
Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
|
||||
},
|
||||
{
|
||||
Average: 5,
|
||||
Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
|
||||
timings: [null],
|
||||
results: {
|
||||
A: {
|
||||
error: '',
|
||||
refId: 'A',
|
||||
series: [
|
||||
{
|
||||
name: 'CPUUtilization_Average',
|
||||
points: [
|
||||
[1, 1483228800000],
|
||||
[2, 1483229100000],
|
||||
[5, 1483229700000],
|
||||
],
|
||||
tags: {
|
||||
InstanceId: 'i-12345678'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Label: 'CPUUtilization'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
ctx.backendSrv.post = function(path, params) {
|
||||
requestParams = params;
|
||||
return ctx.$q.when({data: response});
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should generate the correct query', function(done) {
|
||||
ctx.ds.query(query).then(function() {
|
||||
var params = requestParams.data.parameters;
|
||||
var params = requestParams.queries[0];
|
||||
expect(params.namespace).to.be(query.targets[0].namespace);
|
||||
expect(params.metricName).to.be(query.targets[0].metricName);
|
||||
expect(params.dimensions[0].Name).to.be(Object.keys(query.targets[0].dimensions)[0]);
|
||||
expect(params.dimensions[0].Value).to.be(query.targets[0].dimensions[Object.keys(query.targets[0].dimensions)[0]]);
|
||||
expect(params.dimensions['InstanceId']).to.be('i-12345678');
|
||||
expect(params.statistics).to.eql(query.targets[0].statistics);
|
||||
expect(params.period).to.be(query.targets[0].period);
|
||||
done();
|
||||
@@ -88,6 +92,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -103,7 +108,7 @@ describe('CloudWatchDatasource', function() {
|
||||
};
|
||||
|
||||
ctx.ds.query(query).then(function() {
|
||||
var params = requestParams.data.parameters;
|
||||
var params = requestParams.queries[0];
|
||||
expect(params.period).to.be(600);
|
||||
done();
|
||||
});
|
||||
@@ -112,16 +117,8 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
it('should return series list', function(done) {
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(result.data[0].target).to.be('CPUUtilization_Average');
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.Datapoints[0]['Average']);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return null for missing data point', function(done) {
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(result.data[0].datapoints[2][0]).to.be(null);
|
||||
expect(result.data[0].target).to.be(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
@@ -173,6 +170,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -189,40 +187,40 @@ describe('CloudWatchDatasource', function() {
|
||||
};
|
||||
|
||||
var response = {
|
||||
Datapoints: [
|
||||
{
|
||||
ExtendedStatistics: {
|
||||
'p90.00': 1
|
||||
},
|
||||
Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
|
||||
},
|
||||
{
|
||||
ExtendedStatistics: {
|
||||
'p90.00': 2
|
||||
},
|
||||
Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
|
||||
},
|
||||
{
|
||||
ExtendedStatistics: {
|
||||
'p90.00': 5
|
||||
},
|
||||
Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
|
||||
timings: [null],
|
||||
results: {
|
||||
A: {
|
||||
error: '',
|
||||
refId: 'A',
|
||||
series: [
|
||||
{
|
||||
name: 'TargetResponseTime_p90.00',
|
||||
points: [
|
||||
[1, 1483228800000],
|
||||
[2, 1483229100000],
|
||||
[5, 1483229700000],
|
||||
],
|
||||
tags: {
|
||||
LoadBalancer: 'lb',
|
||||
TargetGroup: 'tg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
Label: 'TargetResponseTime'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
ctx.backendSrv.post = function(path, params) {
|
||||
requestParams = params;
|
||||
return ctx.$q.when({data: response});
|
||||
return ctx.$q.when(response);
|
||||
};
|
||||
});
|
||||
|
||||
it('should return series list', function(done) {
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(result.data[0].target).to.be('TargetResponseTime_p90.00');
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.Datapoints[0].ExtendedStatistics['p90.00']);
|
||||
expect(result.data[0].target).to.be(response.results.A.series[0].name);
|
||||
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
@@ -237,7 +235,11 @@ describe('CloudWatchDatasource', function() {
|
||||
setupCallback();
|
||||
ctx.backendSrv.datasourceRequest = args => {
|
||||
scenario.request = args;
|
||||
return ctx.$q.when({data: scenario.requestResponse });
|
||||
return ctx.$q.when({ data: scenario.requestResponse });
|
||||
};
|
||||
ctx.backendSrv.post = (path, args) => {
|
||||
scenario.request = args;
|
||||
return ctx.$q.when(scenario.requestResponse);
|
||||
};
|
||||
ctx.ds.metricFindQuery(query).then(args => {
|
||||
scenario.result = args;
|
||||
@@ -252,135 +254,178 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
describeMetricFindQuery('regions()', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'us-east-1'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['us-east-1', 'us-east-1']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetRegions and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('us-east-1');
|
||||
expect(scenario.request.data.action).to.be('__GetRegions');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('regions');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('namespaces()', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'AWS/EC2'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['AWS/EC2', 'AWS/EC2']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetNamespaces and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('AWS/EC2');
|
||||
expect(scenario.request.data.action).to.be('__GetNamespaces');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('namespaces');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('metrics(AWS/EC2)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'CPUUtilization'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['CPUUtilization', 'CPUUtilization']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('CPUUtilization');
|
||||
expect(scenario.request.data.action).to.be('__GetMetrics');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('metrics');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('dimension_keys(AWS/EC2)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'InstanceId'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['InstanceId', 'InstanceId']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetDimensions and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('InstanceId');
|
||||
expect(scenario.request.data.action).to.be('__GetDimensions');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_keys');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
Metrics: [
|
||||
{
|
||||
Namespace: 'AWS/EC2',
|
||||
MetricName: 'CPUUtilization',
|
||||
Dimensions: [
|
||||
{
|
||||
Name: 'InstanceId',
|
||||
Value: 'i-12345678'
|
||||
}
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['i-12345678', 'i-12345678']] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('i-12345678');
|
||||
expect(scenario.request.data.action).to.be('ListMetrics');
|
||||
expect(scenario.result[0].text).to.contain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_values');
|
||||
});
|
||||
});
|
||||
|
||||
it('should caclculate the correct period', function () {
|
||||
var hourSec = 60 * 60;
|
||||
var daySec = hourSec * 24;
|
||||
var start = 1483196400;
|
||||
var start = 1483196400 * 1000;
|
||||
var testData: any[] = [
|
||||
[{ period: 60 }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 300],
|
||||
[{ period: 60 }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 1440 - 1, (hourSec * 3 - 1), 1],
|
||||
[{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
|
||||
[{ period: 60 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 15), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 63), 300],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 455), 3600]
|
||||
[
|
||||
{ period: 60, namespace: 'AWS/EC2' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'AWS/EC2' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 300
|
||||
],
|
||||
[
|
||||
{ period: 60, namespace: 'AWS/ELB' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'AWS/ELB' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: 1, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + (1440 - 1) * 1000) } },
|
||||
(hourSec * 3 - 1), 1
|
||||
],
|
||||
[
|
||||
{ period: 1, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3 - 1), 60
|
||||
],
|
||||
[
|
||||
{ period: 60, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3 - 1), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 15), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 63), 300
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 455), 3600
|
||||
]
|
||||
];
|
||||
for (let t of testData) {
|
||||
let target = t[0];
|
||||
let query = t[1];
|
||||
let options = t[2];
|
||||
let start = t[3];
|
||||
let end = t[4];
|
||||
let now = start + t[5];
|
||||
let expected = t[6];
|
||||
let actual = ctx.ds.getPeriod(target, query, options, start, end, now);
|
||||
let options = t[1];
|
||||
let now = new Date(options.range.from.valueOf() + t[2] * 1000);
|
||||
let expected = t[3];
|
||||
let actual = ctx.ds.getPeriod(target, options, now);
|
||||
expect(actual).to.be(expected);
|
||||
}
|
||||
});
|
||||
|
||||
describeMetricFindQuery('ec2_instance_attribute(us-east-1, Tags.Name, { "tag:team": [ "sysops" ] })', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
Reservations: [
|
||||
{
|
||||
Instances: [
|
||||
{
|
||||
Tags: [
|
||||
{ Key: 'InstanceId', Value: 'i-123456' },
|
||||
{ Key: 'Name', Value: 'Sysops Dev Server' },
|
||||
{ Key: 'env', Value: 'dev' },
|
||||
{ Key: 'team', Value: 'sysops' }
|
||||
]
|
||||
},
|
||||
{
|
||||
Tags: [
|
||||
{ Key: 'InstanceId', Value: 'i-789012' },
|
||||
{ Key: 'Name', Value: 'Sysops Staging Server' },
|
||||
{ Key: 'env', Value: 'staging' },
|
||||
{ Key: 'team', Value: 'sysops' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
it('should return the "Name" tag for each instance', function() {
|
||||
expect(scenario.result[0].text).to.be('Sysops Dev Server');
|
||||
expect(scenario.result[1].text).to.be('Sysops Staging Server');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
declare var ElasticDatasource: any;
|
||||
export {ElasticDatasource};
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'moment',
|
||||
'app/core/utils/kbn',
|
||||
'./query_builder',
|
||||
'./index_pattern',
|
||||
'./elastic_response',
|
||||
'./query_ctrl',
|
||||
],
|
||||
function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticResponse) {
|
||||
'use strict';
|
||||
|
||||
ElasticResponse = ElasticResponse.ElasticResponse;
|
||||
|
||||
/** @ngInject */
|
||||
function ElasticDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.url = instanceSettings.url;
|
||||
this.name = instanceSettings.name;
|
||||
this.index = instanceSettings.index;
|
||||
this.timeField = instanceSettings.jsonData.timeField;
|
||||
this.esVersion = instanceSettings.jsonData.esVersion;
|
||||
this.indexPattern = new IndexPattern(instanceSettings.index, instanceSettings.jsonData.interval);
|
||||
this.interval = instanceSettings.jsonData.timeInterval;
|
||||
this.queryBuilder = new ElasticQueryBuilder({
|
||||
timeField: this.timeField,
|
||||
esVersion: this.esVersion,
|
||||
});
|
||||
|
||||
this._request = function(method, url, data) {
|
||||
var options = {
|
||||
url: this.url + "/" + url,
|
||||
method: method,
|
||||
data: data
|
||||
};
|
||||
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
}
|
||||
if (this.basicAuth) {
|
||||
options.headers = {
|
||||
"Authorization": this.basicAuth
|
||||
};
|
||||
}
|
||||
|
||||
return backendSrv.datasourceRequest(options);
|
||||
};
|
||||
|
||||
this._get = function(url) {
|
||||
var range = timeSrv.timeRange();
|
||||
var index_list = this.indexPattern.getIndexList(range.from.valueOf(), range.to.valueOf());
|
||||
if (_.isArray(index_list) && index_list.length) {
|
||||
return this._request('GET', index_list[0] + url).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
} else {
|
||||
return this._request('GET', this.indexPattern.getIndexForToday() + url).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this._post = function(url, data) {
|
||||
return this._request('POST', url, data).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
};
|
||||
|
||||
this.annotationQuery = function(options) {
|
||||
var annotation = options.annotation;
|
||||
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 = {};
|
||||
range[timeField]= {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
format: "epoch_millis",
|
||||
};
|
||||
|
||||
var queryInterpolated = templateSrv.replace(queryString, {}, 'lucene');
|
||||
var query = {
|
||||
"bool": {
|
||||
"filter": [
|
||||
{ "range": range },
|
||||
{
|
||||
"query_string": {
|
||||
"query": queryInterpolated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var data = {
|
||||
"query" : query,
|
||||
"size": 10000
|
||||
};
|
||||
|
||||
// fields field not supported on ES 5.x
|
||||
if (this.esVersion < 5) {
|
||||
data["fields"] = [timeField, "_source"];
|
||||
}
|
||||
|
||||
var header = {search_type: "query_then_fetch", "ignore_unavailable": true};
|
||||
|
||||
// old elastic annotations had index specified on them
|
||||
if (annotation.index) {
|
||||
header.index = annotation.index;
|
||||
} else {
|
||||
header.index = this.indexPattern.getIndexList(options.range.from, options.range.to);
|
||||
}
|
||||
|
||||
var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n';
|
||||
|
||||
return this._post('_msearch', payload).then(function(res) {
|
||||
var list = [];
|
||||
var hits = res.responses[0].hits.hits;
|
||||
|
||||
var getFieldFromSource = function(source, fieldName) {
|
||||
if (!fieldName) { return; }
|
||||
|
||||
var fieldNames = fieldName.split('.');
|
||||
var fieldValue = source;
|
||||
|
||||
for (var i = 0; i < fieldNames.length; i++) {
|
||||
fieldValue = fieldValue[fieldNames[i]];
|
||||
if (!fieldValue) {
|
||||
console.log('could not find field in annotation: ', fieldName);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isArray(fieldValue)) {
|
||||
fieldValue = fieldValue.join(', ');
|
||||
}
|
||||
return fieldValue;
|
||||
};
|
||||
|
||||
for (var i = 0; i < hits.length; i++) {
|
||||
var source = hits[i]._source;
|
||||
var time = source[timeField];
|
||||
if (typeof hits[i].fields !== 'undefined') {
|
||||
var fields = hits[i].fields;
|
||||
if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) {
|
||||
time = fields[timeField];
|
||||
}
|
||||
}
|
||||
|
||||
var event = {
|
||||
annotation: annotation,
|
||||
time: moment.utc(time).valueOf(),
|
||||
title: getFieldFromSource(source, titleField),
|
||||
tags: getFieldFromSource(source, tagsField),
|
||||
text: getFieldFromSource(source, textField)
|
||||
};
|
||||
|
||||
list.push(event);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
};
|
||||
|
||||
this.testDatasource = function() {
|
||||
timeSrv.setTime({ from: 'now-1m', to: 'now' }, true);
|
||||
// validate that the index exist and has date field
|
||||
return this.getFields({type: 'date'}).then(function(dateFields) {
|
||||
var timeField = _.find(dateFields, {text: this.timeField});
|
||||
if (!timeField) {
|
||||
return { status: "error", message: "No date field named " + this.timeField + ' found' };
|
||||
}
|
||||
return { status: "success", message: "Index OK. Time field name OK." };
|
||||
}.bind(this), function(err) {
|
||||
console.log(err);
|
||||
if (err.data && err.data.error) {
|
||||
var message = angular.toJson(err.data.error);
|
||||
if (err.data.error.reason) {
|
||||
message = err.data.error.reason;
|
||||
}
|
||||
return { status: "error", message: message };
|
||||
} else {
|
||||
return { status: "error", message: err.status };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.getQueryHeader = function(searchType, timeFrom, timeTo) {
|
||||
var header = {search_type: searchType, "ignore_unavailable": true};
|
||||
header.index = this.indexPattern.getIndexList(timeFrom, timeTo);
|
||||
return angular.toJson(header);
|
||||
};
|
||||
|
||||
this.query = function(options) {
|
||||
var payload = "";
|
||||
var target;
|
||||
var sentTargets = [];
|
||||
|
||||
// add global adhoc filters to timeFilter
|
||||
var adhocFilters = templateSrv.getAdhocFilters(this.name);
|
||||
|
||||
for (var i = 0; i < options.targets.length; i++) {
|
||||
target = options.targets[i];
|
||||
if (target.hide) {continue;}
|
||||
|
||||
var queryString = templateSrv.replace(target.query || '*', options.scopedVars, 'lucene');
|
||||
var queryObj = this.queryBuilder.build(target, adhocFilters, queryString);
|
||||
var esQuery = angular.toJson(queryObj);
|
||||
|
||||
var searchType = (queryObj.size === 0 && this.esVersion < 5) ? 'count' : 'query_then_fetch';
|
||||
var header = this.getQueryHeader(searchType, options.range.from, options.range.to);
|
||||
payload += header + '\n';
|
||||
|
||||
payload += esQuery + '\n';
|
||||
sentTargets.push(target);
|
||||
}
|
||||
|
||||
if (sentTargets.length === 0) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf());
|
||||
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
|
||||
payload = templateSrv.replace(payload, options.scopedVars);
|
||||
|
||||
return this._post('_msearch', payload).then(function(res) {
|
||||
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
||||
});
|
||||
};
|
||||
|
||||
this.getFields = function(query) {
|
||||
return this._get('/_mapping').then(function(result) {
|
||||
|
||||
var typeMap = {
|
||||
'float': 'number',
|
||||
'double': 'number',
|
||||
'integer': 'number',
|
||||
'long': 'number',
|
||||
'date': 'date',
|
||||
'string': 'string',
|
||||
'text': 'string',
|
||||
'scaled_float': 'number',
|
||||
'nested': 'nested'
|
||||
};
|
||||
|
||||
function shouldAddField(obj, key, query) {
|
||||
if (key[0] === '_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!query.type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// equal query type filter, or via typemap translation
|
||||
return query.type === obj.type || query.type === typeMap[obj.type];
|
||||
}
|
||||
|
||||
// Store subfield names: [system, process, cpu, total] -> system.process.cpu.total
|
||||
var fieldNameParts = [];
|
||||
var fields = {};
|
||||
|
||||
function getFieldsRecursively(obj) {
|
||||
for (var key in obj) {
|
||||
var subObj = obj[key];
|
||||
|
||||
// Check mapping field for nested fields
|
||||
if (_.isObject(subObj.properties)) {
|
||||
fieldNameParts.push(key);
|
||||
getFieldsRecursively(subObj.properties);
|
||||
}
|
||||
|
||||
if (_.isObject(subObj.fields)) {
|
||||
fieldNameParts.push(key);
|
||||
getFieldsRecursively(subObj.fields);
|
||||
}
|
||||
|
||||
if (_.isString(subObj.type)) {
|
||||
var fieldName = fieldNameParts.concat(key).join('.');
|
||||
|
||||
// Hide meta-fields and check field type
|
||||
if (shouldAddField(subObj, key, query)) {
|
||||
fields[fieldName] = {
|
||||
text: fieldName,
|
||||
type: subObj.type
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
fieldNameParts.pop();
|
||||
}
|
||||
|
||||
for (var indexName in result) {
|
||||
var index = result[indexName];
|
||||
if (index && index.mappings) {
|
||||
var mappings = index.mappings;
|
||||
for (var typeName in mappings) {
|
||||
var properties = mappings[typeName].properties;
|
||||
getFieldsRecursively(properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transform to array
|
||||
return _.map(fields, function(value) {
|
||||
return value;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.getTerms = function(queryDef) {
|
||||
var range = timeSrv.timeRange();
|
||||
var searchType = this.esVersion >= 5 ? 'query_then_fetch' : 'count' ;
|
||||
var header = this.getQueryHeader(searchType, range.from, range.to);
|
||||
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
|
||||
|
||||
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
|
||||
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
|
||||
esQuery = header + '\n' + esQuery + '\n';
|
||||
|
||||
return this._post('_msearch?search_type=' + searchType, esQuery).then(function(res) {
|
||||
if (!res.responses[0].aggregations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var buckets = res.responses[0].aggregations["1"].buckets;
|
||||
return _.map(buckets, function(bucket) {
|
||||
return {
|
||||
text: bucket.key_as_string || bucket.key,
|
||||
value: bucket.key
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.metricFindQuery = function(query) {
|
||||
query = angular.fromJson(query);
|
||||
if (!query) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
if (query.find === 'fields') {
|
||||
query.field = templateSrv.replace(query.field, {}, 'lucene');
|
||||
return this.getFields(query);
|
||||
}
|
||||
|
||||
if (query.find === 'terms') {
|
||||
query.query = templateSrv.replace(query.query || '*', {}, 'lucene');
|
||||
return this.getTerms(query);
|
||||
}
|
||||
};
|
||||
|
||||
this.getTagKeys = function() {
|
||||
return this.getFields({});
|
||||
};
|
||||
|
||||
this.getTagValues = function(options) {
|
||||
return this.getTerms({field: options.key, query: '*'});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ElasticDatasource: ElasticDatasource
|
||||
};
|
||||
});
|
||||
376
public/app/plugins/datasource/elasticsearch/datasource.ts
Normal file
376
public/app/plugins/datasource/elasticsearch/datasource.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {ElasticQueryBuilder} from './query_builder';
|
||||
import {IndexPattern} from './index_pattern';
|
||||
import {ElasticResponse} from './elastic_response';
|
||||
|
||||
export class ElasticDatasource {
|
||||
basicAuth: string;
|
||||
withCredentials: boolean;
|
||||
url: string;
|
||||
name: string;
|
||||
index: string;
|
||||
timeField: string;
|
||||
esVersion: number;
|
||||
interval: string;
|
||||
queryBuilder: ElasticQueryBuilder;
|
||||
indexPattern: IndexPattern;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) {
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.url = instanceSettings.url;
|
||||
this.name = instanceSettings.name;
|
||||
this.index = instanceSettings.index;
|
||||
this.timeField = instanceSettings.jsonData.timeField;
|
||||
this.esVersion = instanceSettings.jsonData.esVersion;
|
||||
this.indexPattern = new IndexPattern(instanceSettings.index, instanceSettings.jsonData.interval);
|
||||
this.interval = instanceSettings.jsonData.timeInterval;
|
||||
this.queryBuilder = new ElasticQueryBuilder({
|
||||
timeField: this.timeField,
|
||||
esVersion: this.esVersion,
|
||||
});
|
||||
}
|
||||
|
||||
private request(method, url, data?) {
|
||||
var options: any = {
|
||||
url: this.url + "/" + url,
|
||||
method: method,
|
||||
data: data
|
||||
};
|
||||
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
}
|
||||
if (this.basicAuth) {
|
||||
options.headers = {
|
||||
"Authorization": this.basicAuth
|
||||
};
|
||||
}
|
||||
|
||||
return this.backendSrv.datasourceRequest(options);
|
||||
}
|
||||
|
||||
private get(url) {
|
||||
var range = this.timeSrv.timeRange();
|
||||
var index_list = this.indexPattern.getIndexList(range.from.valueOf(), range.to.valueOf());
|
||||
if (_.isArray(index_list) && index_list.length) {
|
||||
return this.request('GET', index_list[0] + url).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
} else {
|
||||
return this.request('GET', this.indexPattern.getIndexForToday() + url).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private post(url, data) {
|
||||
return this.request('POST', url, data).then(function(results) {
|
||||
results.data.$$config = results.config;
|
||||
return results.data;
|
||||
});
|
||||
}
|
||||
|
||||
annotationQuery(options) {
|
||||
var annotation = options.annotation;
|
||||
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 = {};
|
||||
range[timeField]= {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
format: "epoch_millis",
|
||||
};
|
||||
|
||||
var queryInterpolated = this.templateSrv.replace(queryString, {}, 'lucene');
|
||||
var query = {
|
||||
"bool": {
|
||||
"filter": [
|
||||
{ "range": range },
|
||||
{
|
||||
"query_string": {
|
||||
"query": queryInterpolated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var data = {
|
||||
"query" : query,
|
||||
"size": 10000
|
||||
};
|
||||
|
||||
// fields field not supported on ES 5.x
|
||||
if (this.esVersion < 5) {
|
||||
data["fields"] = [timeField, "_source"];
|
||||
}
|
||||
|
||||
var header: any = {search_type: "query_then_fetch", "ignore_unavailable": true};
|
||||
|
||||
// old elastic annotations had index specified on them
|
||||
if (annotation.index) {
|
||||
header.index = annotation.index;
|
||||
} else {
|
||||
header.index = this.indexPattern.getIndexList(options.range.from, options.range.to);
|
||||
}
|
||||
|
||||
var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n';
|
||||
|
||||
return this.post('_msearch', payload).then(res => {
|
||||
var list = [];
|
||||
var hits = res.responses[0].hits.hits;
|
||||
|
||||
var getFieldFromSource = function(source, fieldName) {
|
||||
if (!fieldName) { return; }
|
||||
|
||||
var fieldNames = fieldName.split('.');
|
||||
var fieldValue = source;
|
||||
|
||||
for (var i = 0; i < fieldNames.length; i++) {
|
||||
fieldValue = fieldValue[fieldNames[i]];
|
||||
if (!fieldValue) {
|
||||
console.log('could not find field in annotation: ', fieldName);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isArray(fieldValue)) {
|
||||
fieldValue = fieldValue.join(', ');
|
||||
}
|
||||
return fieldValue;
|
||||
};
|
||||
|
||||
for (var i = 0; i < hits.length; i++) {
|
||||
var source = hits[i]._source;
|
||||
var time = source[timeField];
|
||||
if (typeof hits[i].fields !== 'undefined') {
|
||||
var fields = hits[i].fields;
|
||||
if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) {
|
||||
time = fields[timeField];
|
||||
}
|
||||
}
|
||||
|
||||
var event = {
|
||||
annotation: annotation,
|
||||
time: moment.utc(time).valueOf(),
|
||||
title: getFieldFromSource(source, titleField),
|
||||
tags: getFieldFromSource(source, tagsField),
|
||||
text: getFieldFromSource(source, textField)
|
||||
};
|
||||
|
||||
list.push(event);
|
||||
}
|
||||
return list;
|
||||
});
|
||||
};
|
||||
|
||||
testDatasource() {
|
||||
this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true);
|
||||
// validate that the index exist and has date field
|
||||
return this.getFields({type: 'date'}).then(function(dateFields) {
|
||||
var timeField = _.find(dateFields, {text: this.timeField});
|
||||
if (!timeField) {
|
||||
return { status: "error", message: "No date field named " + this.timeField + ' found' };
|
||||
}
|
||||
return { status: "success", message: "Index OK. Time field name OK." };
|
||||
}.bind(this), function(err) {
|
||||
console.log(err);
|
||||
if (err.data && err.data.error) {
|
||||
var message = angular.toJson(err.data.error);
|
||||
if (err.data.error.reason) {
|
||||
message = err.data.error.reason;
|
||||
}
|
||||
return { status: "error", message: message };
|
||||
} else {
|
||||
return { status: "error", message: err.status };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getQueryHeader(searchType, timeFrom, timeTo) {
|
||||
return angular.toJson({
|
||||
search_type: searchType,
|
||||
"ignore_unavailable": true,
|
||||
index: this.indexPattern.getIndexList(timeFrom, timeTo),
|
||||
});
|
||||
}
|
||||
|
||||
query(options) {
|
||||
var payload = "";
|
||||
var target;
|
||||
var sentTargets = [];
|
||||
|
||||
// add global adhoc filters to timeFilter
|
||||
var adhocFilters = this.templateSrv.getAdhocFilters(this.name);
|
||||
|
||||
for (var i = 0; i < options.targets.length; i++) {
|
||||
target = options.targets[i];
|
||||
if (target.hide) {continue;}
|
||||
|
||||
var queryString = this.templateSrv.replace(target.query || '*', options.scopedVars, 'lucene');
|
||||
var queryObj = this.queryBuilder.build(target, adhocFilters, queryString);
|
||||
var esQuery = angular.toJson(queryObj);
|
||||
|
||||
var searchType = (queryObj.size === 0 && this.esVersion < 5) ? 'count' : 'query_then_fetch';
|
||||
var header = this.getQueryHeader(searchType, options.range.from, options.range.to);
|
||||
payload += header + '\n';
|
||||
|
||||
payload += esQuery + '\n';
|
||||
sentTargets.push(target);
|
||||
}
|
||||
|
||||
if (sentTargets.length === 0) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf());
|
||||
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
|
||||
payload = this.templateSrv.replace(payload, options.scopedVars);
|
||||
|
||||
return this.post('_msearch', payload).then(function(res) {
|
||||
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
||||
});
|
||||
};
|
||||
|
||||
getFields(query) {
|
||||
return this.get('/_mapping').then(function(result) {
|
||||
|
||||
var typeMap = {
|
||||
'float': 'number',
|
||||
'double': 'number',
|
||||
'integer': 'number',
|
||||
'long': 'number',
|
||||
'date': 'date',
|
||||
'string': 'string',
|
||||
'text': 'string',
|
||||
'scaled_float': 'number',
|
||||
'nested': 'nested'
|
||||
};
|
||||
|
||||
function shouldAddField(obj, key, query) {
|
||||
if (key[0] === '_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!query.type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// equal query type filter, or via typemap translation
|
||||
return query.type === obj.type || query.type === typeMap[obj.type];
|
||||
}
|
||||
|
||||
// Store subfield names: [system, process, cpu, total] -> system.process.cpu.total
|
||||
var fieldNameParts = [];
|
||||
var fields = {};
|
||||
|
||||
function getFieldsRecursively(obj) {
|
||||
for (var key in obj) {
|
||||
var subObj = obj[key];
|
||||
|
||||
// Check mapping field for nested fields
|
||||
if (_.isObject(subObj.properties)) {
|
||||
fieldNameParts.push(key);
|
||||
getFieldsRecursively(subObj.properties);
|
||||
}
|
||||
|
||||
if (_.isObject(subObj.fields)) {
|
||||
fieldNameParts.push(key);
|
||||
getFieldsRecursively(subObj.fields);
|
||||
}
|
||||
|
||||
if (_.isString(subObj.type)) {
|
||||
var fieldName = fieldNameParts.concat(key).join('.');
|
||||
|
||||
// Hide meta-fields and check field type
|
||||
if (shouldAddField(subObj, key, query)) {
|
||||
fields[fieldName] = {
|
||||
text: fieldName,
|
||||
type: subObj.type
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
fieldNameParts.pop();
|
||||
}
|
||||
|
||||
for (var indexName in result) {
|
||||
var index = result[indexName];
|
||||
if (index && index.mappings) {
|
||||
var mappings = index.mappings;
|
||||
for (var typeName in mappings) {
|
||||
var properties = mappings[typeName].properties;
|
||||
getFieldsRecursively(properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transform to array
|
||||
return _.map(fields, function(value) {
|
||||
return value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getTerms(queryDef) {
|
||||
var range = this.timeSrv.timeRange();
|
||||
var searchType = this.esVersion >= 5 ? 'query_then_fetch' : 'count' ;
|
||||
var header = this.getQueryHeader(searchType, range.from, range.to);
|
||||
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
|
||||
|
||||
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
|
||||
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
|
||||
esQuery = header + '\n' + esQuery + '\n';
|
||||
|
||||
return this.post('_msearch?search_type=' + searchType, esQuery).then(function(res) {
|
||||
if (!res.responses[0].aggregations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var buckets = res.responses[0].aggregations["1"].buckets;
|
||||
return _.map(buckets, function(bucket) {
|
||||
return {
|
||||
text: bucket.key_as_string || bucket.key,
|
||||
value: bucket.key
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
metricFindQuery(query) {
|
||||
query = angular.fromJson(query);
|
||||
if (!query) {
|
||||
return this.$q.when([]);
|
||||
}
|
||||
|
||||
if (query.find === 'fields') {
|
||||
query.field = this.templateSrv.replace(query.field, {}, 'lucene');
|
||||
return this.getFields(query);
|
||||
}
|
||||
|
||||
if (query.find === 'terms') {
|
||||
query.query = this.templateSrv.replace(query.query || '*', {}, 'lucene');
|
||||
return this.getTerms(query);
|
||||
}
|
||||
}
|
||||
|
||||
getTagKeys() {
|
||||
return this.getFields({});
|
||||
}
|
||||
|
||||
getTagValues(options) {
|
||||
return this.getTerms({field: options.key, query: '*'});
|
||||
}
|
||||
}
|
||||
@@ -1,130 +1,55 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import queryDef from "./query_def";
|
||||
import * as queryDef from "./query_def";
|
||||
import TableModel from 'app/core/table_model';
|
||||
|
||||
export function ElasticResponse(targets, response) {
|
||||
this.targets = targets;
|
||||
this.response = response;
|
||||
}
|
||||
export class ElasticResponse {
|
||||
|
||||
ElasticResponse.prototype.processMetrics = function(esAgg, target, seriesList, props) {
|
||||
var metric, y, i, newSeries, bucket, value;
|
||||
constructor(private targets, private response) {
|
||||
this.targets = targets;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
for (y = 0; y < target.metrics.length; y++) {
|
||||
metric = target.metrics[y];
|
||||
if (metric.hide) {
|
||||
continue;
|
||||
}
|
||||
processMetrics(esAgg, target, seriesList, props) {
|
||||
var metric, y, i, newSeries, bucket, value;
|
||||
|
||||
switch (metric.type) {
|
||||
case 'count': {
|
||||
newSeries = { datapoints: [], metric: 'count', props: props};
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
value = bucket.doc_count;
|
||||
newSeries.datapoints.push([value, bucket.key]);
|
||||
}
|
||||
seriesList.push(newSeries);
|
||||
break;
|
||||
for (y = 0; y < target.metrics.length; y++) {
|
||||
metric = target.metrics[y];
|
||||
if (metric.hide) {
|
||||
continue;
|
||||
}
|
||||
case 'percentiles': {
|
||||
if (esAgg.buckets.length === 0) {
|
||||
|
||||
switch (metric.type) {
|
||||
case 'count': {
|
||||
newSeries = { datapoints: [], metric: 'count', props: props};
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
value = bucket.doc_count;
|
||||
newSeries.datapoints.push([value, bucket.key]);
|
||||
}
|
||||
seriesList.push(newSeries);
|
||||
break;
|
||||
}
|
||||
|
||||
var firstBucket = esAgg.buckets[0];
|
||||
var percentiles = firstBucket[metric.id].values;
|
||||
|
||||
for (var percentileName in percentiles) {
|
||||
newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field};
|
||||
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
var values = bucket[metric.id].values;
|
||||
newSeries.datapoints.push([values[percentileName], bucket.key]);
|
||||
}
|
||||
seriesList.push(newSeries);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'extended_stats': {
|
||||
for (var statName in metric.meta) {
|
||||
if (!metric.meta[statName]) {
|
||||
continue;
|
||||
case 'percentiles': {
|
||||
if (esAgg.buckets.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
newSeries = {datapoints: [], metric: statName, props: props, field: metric.field};
|
||||
var firstBucket = esAgg.buckets[0];
|
||||
var percentiles = firstBucket[metric.id].values;
|
||||
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
var stats = bucket[metric.id];
|
||||
for (var percentileName in percentiles) {
|
||||
newSeries = {datapoints: [], metric: 'p' + percentileName, props: props, field: metric.field};
|
||||
|
||||
// add stats that are in nested obj to top level obj
|
||||
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
|
||||
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
|
||||
|
||||
newSeries.datapoints.push([stats[statName], bucket.key]);
|
||||
}
|
||||
|
||||
seriesList.push(newSeries);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
|
||||
value = bucket[metric.id];
|
||||
if (value !== undefined) {
|
||||
if (value.normalized_value) {
|
||||
newSeries.datapoints.push([value.normalized_value, bucket.key]);
|
||||
} else {
|
||||
newSeries.datapoints.push([value.value, bucket.key]);
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
var values = bucket[metric.id].values;
|
||||
newSeries.datapoints.push([values[percentileName], bucket.key]);
|
||||
}
|
||||
seriesList.push(newSeries);
|
||||
}
|
||||
|
||||
}
|
||||
seriesList.push(newSeries);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, target, table, props) {
|
||||
// add columns
|
||||
if (table.columns.length === 0) {
|
||||
for (let propKey of _.keys(props)) {
|
||||
table.addColumn({text: propKey, filterable: true});
|
||||
}
|
||||
table.addColumn({text: aggDef.field, filterable: true});
|
||||
}
|
||||
|
||||
// helper func to add values to value array
|
||||
let addMetricValue = (values, metricName, value) => {
|
||||
table.addColumn({text: metricName});
|
||||
values.push(value);
|
||||
};
|
||||
|
||||
for (let bucket of esAgg.buckets) {
|
||||
let values = [];
|
||||
|
||||
for (let propValues of _.values(props)) {
|
||||
values.push(propValues);
|
||||
}
|
||||
|
||||
// add bucket key (value)
|
||||
values.push(bucket.key);
|
||||
|
||||
for (let metric of target.metrics) {
|
||||
switch (metric.type) {
|
||||
case "count": {
|
||||
addMetricValue(values, this._getMetricName(metric.type), bucket.doc_count);
|
||||
break;
|
||||
}
|
||||
case 'extended_stats': {
|
||||
@@ -133,228 +58,304 @@ ElasticResponse.prototype.processAggregationDocs = function(esAgg, aggDef, targe
|
||||
continue;
|
||||
}
|
||||
|
||||
var stats = bucket[metric.id];
|
||||
// add stats that are in nested obj to top level obj
|
||||
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
|
||||
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
|
||||
newSeries = {datapoints: [], metric: statName, props: props, field: metric.field};
|
||||
|
||||
addMetricValue(values, this._getMetricName(statName), stats[statName]);
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
var stats = bucket[metric.id];
|
||||
|
||||
// add stats that are in nested obj to top level obj
|
||||
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
|
||||
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
|
||||
|
||||
newSeries.datapoints.push([stats[statName], bucket.key]);
|
||||
}
|
||||
|
||||
seriesList.push(newSeries);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let metricName = this._getMetricName(metric.type);
|
||||
let otherMetrics = _.filter(target.metrics, {type: metric.type});
|
||||
default: {
|
||||
newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props};
|
||||
for (i = 0; i < esAgg.buckets.length; i++) {
|
||||
bucket = esAgg.buckets[i];
|
||||
|
||||
value = bucket[metric.id];
|
||||
if (value !== undefined) {
|
||||
if (value.normalized_value) {
|
||||
newSeries.datapoints.push([value.normalized_value, bucket.key]);
|
||||
} else {
|
||||
newSeries.datapoints.push([value.value, bucket.key]);
|
||||
}
|
||||
}
|
||||
|
||||
// if more of the same metric type include field field name in property
|
||||
if (otherMetrics.length > 1) {
|
||||
metricName += ' ' + metric.field;
|
||||
}
|
||||
|
||||
addMetricValue(values, metricName, bucket[metric.id].value);
|
||||
seriesList.push(newSeries);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.rows.push(values);
|
||||
}
|
||||
};
|
||||
|
||||
// This is quite complex
|
||||
// neeed to recurise down the nested buckets to build series
|
||||
ElasticResponse.prototype.processBuckets = function(aggs, target, seriesList, table, props, depth) {
|
||||
var bucket, aggDef, esAgg, aggId;
|
||||
var maxDepth = target.bucketAggs.length-1;
|
||||
|
||||
for (aggId in aggs) {
|
||||
aggDef = _.find(target.bucketAggs, {id: aggId});
|
||||
esAgg = aggs[aggId];
|
||||
|
||||
if (!aggDef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth === maxDepth) {
|
||||
if (aggDef.type === 'date_histogram') {
|
||||
this.processMetrics(esAgg, target, seriesList, props);
|
||||
} else {
|
||||
this.processAggregationDocs(esAgg, aggDef, target, table, props);
|
||||
}
|
||||
} else {
|
||||
for (var nameIndex in esAgg.buckets) {
|
||||
bucket = esAgg.buckets[nameIndex];
|
||||
props = _.clone(props);
|
||||
if (bucket.key !== void 0) {
|
||||
props[aggDef.field] = bucket.key;
|
||||
} else {
|
||||
props["filter"] = nameIndex;
|
||||
}
|
||||
if (bucket.key_as_string) {
|
||||
props[aggDef.field] = bucket.key_as_string;
|
||||
}
|
||||
this.processBuckets(bucket, target, seriesList, table, props, depth+1);
|
||||
processAggregationDocs(esAgg, aggDef, target, table, props) {
|
||||
// add columns
|
||||
if (table.columns.length === 0) {
|
||||
for (let propKey of _.keys(props)) {
|
||||
table.addColumn({text: propKey, filterable: true});
|
||||
}
|
||||
table.addColumn({text: aggDef.field, filterable: true});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ElasticResponse.prototype._getMetricName = function(metric) {
|
||||
var metricDef = _.find(queryDef.metricAggTypes, {value: metric});
|
||||
if (!metricDef) {
|
||||
metricDef = _.find(queryDef.extendedStats, {value: metric});
|
||||
}
|
||||
|
||||
return metricDef ? metricDef.text : metric;
|
||||
};
|
||||
|
||||
ElasticResponse.prototype._getSeriesName = function(series, target, metricTypeCount) {
|
||||
var metricName = this._getMetricName(series.metric);
|
||||
|
||||
if (target.alias) {
|
||||
var regex = /\{\{([\s\S]+?)\}\}/g;
|
||||
|
||||
return target.alias.replace(regex, function(match, g1, g2) {
|
||||
var group = g1 || g2;
|
||||
|
||||
if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; }
|
||||
if (series.props[group] !== void 0) { return series.props[group]; }
|
||||
if (group === 'metric') { return metricName; }
|
||||
if (group === 'field') { return series.field; }
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
if (series.field && queryDef.isPipelineAgg(series.metric)) {
|
||||
var appliedAgg = _.find(target.metrics, { id: series.field });
|
||||
if (appliedAgg) {
|
||||
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
||||
} else {
|
||||
metricName = 'Unset';
|
||||
}
|
||||
} else if (series.field) {
|
||||
metricName += ' ' + series.field;
|
||||
}
|
||||
|
||||
var propKeys = _.keys(series.props);
|
||||
if (propKeys.length === 0) {
|
||||
return metricName;
|
||||
}
|
||||
|
||||
var name = '';
|
||||
for (var propName in series.props) {
|
||||
name += series.props[propName] + ' ';
|
||||
}
|
||||
|
||||
if (metricTypeCount === 1) {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
return name.trim() + ' ' + metricName;
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.nameSeries = function(seriesList, target) {
|
||||
var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length;
|
||||
var fieldNameCount = _.uniq(_.map(seriesList, 'field')).length;
|
||||
|
||||
for (var i = 0; i < seriesList.length; i++) {
|
||||
var series = seriesList[i];
|
||||
series.target = this._getSeriesName(series, target, metricTypeCount, fieldNameCount);
|
||||
}
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.processHits = function(hits, seriesList) {
|
||||
var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total, filterable: true};
|
||||
var propName, hit, doc, i;
|
||||
|
||||
for (i = 0; i < hits.hits.length; i++) {
|
||||
hit = hits.hits[i];
|
||||
doc = {
|
||||
_id: hit._id,
|
||||
_type: hit._type,
|
||||
_index: hit._index
|
||||
// helper func to add values to value array
|
||||
let addMetricValue = (values, metricName, value) => {
|
||||
table.addColumn({text: metricName});
|
||||
values.push(value);
|
||||
};
|
||||
|
||||
if (hit._source) {
|
||||
for (propName in hit._source) {
|
||||
doc[propName] = hit._source[propName];
|
||||
}
|
||||
}
|
||||
for (let bucket of esAgg.buckets) {
|
||||
let values = [];
|
||||
|
||||
for (propName in hit.fields) {
|
||||
doc[propName] = hit.fields[propName];
|
||||
}
|
||||
series.datapoints.push(doc);
|
||||
}
|
||||
|
||||
seriesList.push(series);
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.trimDatapoints = function(aggregations, target) {
|
||||
var histogram = _.find(target.bucketAggs, { type: 'date_histogram'});
|
||||
|
||||
var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges;
|
||||
if (shouldDropFirstAndLast) {
|
||||
var trim = histogram.settings.trimEdges;
|
||||
for (var prop in aggregations) {
|
||||
var points = aggregations[prop];
|
||||
if (points.datapoints.length > trim * 2) {
|
||||
points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.getErrorFromElasticResponse = function(response, err) {
|
||||
var result: any = {};
|
||||
result.data = JSON.stringify(err, null, 4);
|
||||
if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) {
|
||||
result.message = err.root_cause[0].reason;
|
||||
} else {
|
||||
result.message = err.reason || 'Unkown elatic error response';
|
||||
}
|
||||
|
||||
if (response.$$config) {
|
||||
result.config = response.$$config;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.getTimeSeries = function() {
|
||||
var seriesList = [];
|
||||
|
||||
for (var i = 0; i < this.response.responses.length; i++) {
|
||||
var response = this.response.responses[i];
|
||||
if (response.error) {
|
||||
throw this.getErrorFromElasticResponse(this.response, response.error);
|
||||
}
|
||||
|
||||
if (response.hits && response.hits.hits.length > 0) {
|
||||
this.processHits(response.hits, seriesList);
|
||||
}
|
||||
|
||||
if (response.aggregations) {
|
||||
var aggregations = response.aggregations;
|
||||
var target = this.targets[i];
|
||||
var tmpSeriesList = [];
|
||||
var table = new TableModel();
|
||||
|
||||
this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0);
|
||||
this.trimDatapoints(tmpSeriesList, target);
|
||||
this.nameSeries(tmpSeriesList, target);
|
||||
|
||||
for (var y = 0; y < tmpSeriesList.length; y++) {
|
||||
seriesList.push(tmpSeriesList[y]);
|
||||
for (let propValues of _.values(props)) {
|
||||
values.push(propValues);
|
||||
}
|
||||
|
||||
if (table.rows.length > 0) {
|
||||
seriesList.push(table);
|
||||
// add bucket key (value)
|
||||
values.push(bucket.key);
|
||||
|
||||
for (let metric of target.metrics) {
|
||||
switch (metric.type) {
|
||||
case "count": {
|
||||
addMetricValue(values, this.getMetricName(metric.type), bucket.doc_count);
|
||||
break;
|
||||
}
|
||||
case 'extended_stats': {
|
||||
for (var statName in metric.meta) {
|
||||
if (!metric.meta[statName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var stats = bucket[metric.id];
|
||||
// add stats that are in nested obj to top level obj
|
||||
stats.std_deviation_bounds_upper = stats.std_deviation_bounds.upper;
|
||||
stats.std_deviation_bounds_lower = stats.std_deviation_bounds.lower;
|
||||
|
||||
addMetricValue(values, this.getMetricName(statName), stats[statName]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let metricName = this.getMetricName(metric.type);
|
||||
let otherMetrics = _.filter(target.metrics, {type: metric.type});
|
||||
|
||||
// if more of the same metric type include field field name in property
|
||||
if (otherMetrics.length > 1) {
|
||||
metricName += ' ' + metric.field;
|
||||
}
|
||||
|
||||
addMetricValue(values, metricName, bucket[metric.id].value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.rows.push(values);
|
||||
}
|
||||
}
|
||||
|
||||
// This is quite complex
|
||||
// neeed to recurise down the nested buckets to build series
|
||||
processBuckets(aggs, target, seriesList, table, props, depth) {
|
||||
var bucket, aggDef, esAgg, aggId;
|
||||
var maxDepth = target.bucketAggs.length-1;
|
||||
|
||||
for (aggId in aggs) {
|
||||
aggDef = _.find(target.bucketAggs, {id: aggId});
|
||||
esAgg = aggs[aggId];
|
||||
|
||||
if (!aggDef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth === maxDepth) {
|
||||
if (aggDef.type === 'date_histogram') {
|
||||
this.processMetrics(esAgg, target, seriesList, props);
|
||||
} else {
|
||||
this.processAggregationDocs(esAgg, aggDef, target, table, props);
|
||||
}
|
||||
} else {
|
||||
for (var nameIndex in esAgg.buckets) {
|
||||
bucket = esAgg.buckets[nameIndex];
|
||||
props = _.clone(props);
|
||||
if (bucket.key !== void 0) {
|
||||
props[aggDef.field] = bucket.key;
|
||||
} else {
|
||||
props["filter"] = nameIndex;
|
||||
}
|
||||
if (bucket.key_as_string) {
|
||||
props[aggDef.field] = bucket.key_as_string;
|
||||
}
|
||||
this.processBuckets(bucket, target, seriesList, table, props, depth+1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data: seriesList };
|
||||
};
|
||||
private getMetricName(metric) {
|
||||
var metricDef = _.find(queryDef.metricAggTypes, {value: metric});
|
||||
if (!metricDef) {
|
||||
metricDef = _.find(queryDef.extendedStats, {value: metric});
|
||||
}
|
||||
|
||||
return metricDef ? metricDef.text : metric;
|
||||
}
|
||||
|
||||
private getSeriesName(series, target, metricTypeCount) {
|
||||
var metricName = this.getMetricName(series.metric);
|
||||
|
||||
if (target.alias) {
|
||||
var regex = /\{\{([\s\S]+?)\}\}/g;
|
||||
|
||||
return target.alias.replace(regex, function(match, g1, g2) {
|
||||
var group = g1 || g2;
|
||||
|
||||
if (group.indexOf('term ') === 0) { return series.props[group.substring(5)]; }
|
||||
if (series.props[group] !== void 0) { return series.props[group]; }
|
||||
if (group === 'metric') { return metricName; }
|
||||
if (group === 'field') { return series.field; }
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
if (series.field && queryDef.isPipelineAgg(series.metric)) {
|
||||
var appliedAgg = _.find(target.metrics, { id: series.field });
|
||||
if (appliedAgg) {
|
||||
metricName += ' ' + queryDef.describeMetric(appliedAgg);
|
||||
} else {
|
||||
metricName = 'Unset';
|
||||
}
|
||||
} else if (series.field) {
|
||||
metricName += ' ' + series.field;
|
||||
}
|
||||
|
||||
var propKeys = _.keys(series.props);
|
||||
if (propKeys.length === 0) {
|
||||
return metricName;
|
||||
}
|
||||
|
||||
var name = '';
|
||||
for (var propName in series.props) {
|
||||
name += series.props[propName] + ' ';
|
||||
}
|
||||
|
||||
if (metricTypeCount === 1) {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
return name.trim() + ' ' + metricName;
|
||||
}
|
||||
|
||||
nameSeries(seriesList, target) {
|
||||
var metricTypeCount = _.uniq(_.map(seriesList, 'metric')).length;
|
||||
|
||||
for (var i = 0; i < seriesList.length; i++) {
|
||||
var series = seriesList[i];
|
||||
series.target = this.getSeriesName(series, target, metricTypeCount);
|
||||
}
|
||||
}
|
||||
|
||||
processHits(hits, seriesList) {
|
||||
var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total, filterable: true};
|
||||
var propName, hit, doc, i;
|
||||
|
||||
for (i = 0; i < hits.hits.length; i++) {
|
||||
hit = hits.hits[i];
|
||||
doc = {
|
||||
_id: hit._id,
|
||||
_type: hit._type,
|
||||
_index: hit._index
|
||||
};
|
||||
|
||||
if (hit._source) {
|
||||
for (propName in hit._source) {
|
||||
doc[propName] = hit._source[propName];
|
||||
}
|
||||
}
|
||||
|
||||
for (propName in hit.fields) {
|
||||
doc[propName] = hit.fields[propName];
|
||||
}
|
||||
series.datapoints.push(doc);
|
||||
}
|
||||
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
trimDatapoints(aggregations, target) {
|
||||
var histogram = _.find(target.bucketAggs, { type: 'date_histogram'});
|
||||
|
||||
var shouldDropFirstAndLast = histogram && histogram.settings && histogram.settings.trimEdges;
|
||||
if (shouldDropFirstAndLast) {
|
||||
var trim = histogram.settings.trimEdges;
|
||||
for (var prop in aggregations) {
|
||||
var points = aggregations[prop];
|
||||
if (points.datapoints.length > trim * 2) {
|
||||
points.datapoints = points.datapoints.slice(trim, points.datapoints.length - trim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getErrorFromElasticResponse(response, err) {
|
||||
var result: any = {};
|
||||
result.data = JSON.stringify(err, null, 4);
|
||||
if (err.root_cause && err.root_cause.length > 0 && err.root_cause[0].reason) {
|
||||
result.message = err.root_cause[0].reason;
|
||||
} else {
|
||||
result.message = err.reason || 'Unkown elatic error response';
|
||||
}
|
||||
|
||||
if (response.$$config) {
|
||||
result.config = response.$$config;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getTimeSeries() {
|
||||
var seriesList = [];
|
||||
|
||||
for (var i = 0; i < this.response.responses.length; i++) {
|
||||
var response = this.response.responses[i];
|
||||
if (response.error) {
|
||||
throw this.getErrorFromElasticResponse(this.response, response.error);
|
||||
}
|
||||
|
||||
if (response.hits && response.hits.hits.length > 0) {
|
||||
this.processHits(response.hits, seriesList);
|
||||
}
|
||||
|
||||
if (response.aggregations) {
|
||||
var aggregations = response.aggregations;
|
||||
var target = this.targets[i];
|
||||
var tmpSeriesList = [];
|
||||
var table = new TableModel();
|
||||
|
||||
this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0);
|
||||
this.trimDatapoints(tmpSeriesList, target);
|
||||
this.nameSeries(tmpSeriesList, target);
|
||||
|
||||
for (var y = 0; y < tmpSeriesList.length; y++) {
|
||||
seriesList.push(tmpSeriesList[y]);
|
||||
}
|
||||
|
||||
if (table.rows.length > 0) {
|
||||
seriesList.push(table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data: seriesList };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
declare var test: any;
|
||||
export default test;
|
||||
@@ -1,48 +0,0 @@
|
||||
define([
|
||||
'lodash',
|
||||
'moment',
|
||||
],
|
||||
function (_, moment) {
|
||||
'use strict';
|
||||
|
||||
function IndexPattern(pattern, interval) {
|
||||
this.pattern = pattern;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
IndexPattern.intervalMap = {
|
||||
"Hourly": { startOf: 'hour', amount: 'hours'},
|
||||
"Daily": { startOf: 'day', amount: 'days'},
|
||||
"Weekly": { startOf: 'isoWeek', amount: 'weeks'},
|
||||
"Monthly": { startOf: 'month', amount: 'months'},
|
||||
"Yearly": { startOf: 'year', amount: 'years'},
|
||||
};
|
||||
|
||||
IndexPattern.prototype.getIndexForToday = function() {
|
||||
if (this.interval) {
|
||||
return moment.utc().format(this.pattern);
|
||||
} else {
|
||||
return this.pattern;
|
||||
}
|
||||
};
|
||||
|
||||
IndexPattern.prototype.getIndexList = function(from, to) {
|
||||
if (!this.interval) {
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
var intervalInfo = IndexPattern.intervalMap[this.interval];
|
||||
var start = moment(from).utc().startOf(intervalInfo.startOf);
|
||||
var end = moment(to).utc().startOf(intervalInfo.startOf).valueOf();
|
||||
var indexList = [];
|
||||
|
||||
while (start <= end) {
|
||||
indexList.push(start.format(this.pattern));
|
||||
start.add(1, intervalInfo.amount);
|
||||
}
|
||||
|
||||
return indexList;
|
||||
};
|
||||
|
||||
return IndexPattern;
|
||||
});
|
||||
43
public/app/plugins/datasource/elasticsearch/index_pattern.ts
Normal file
43
public/app/plugins/datasource/elasticsearch/index_pattern.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
const intervalMap = {
|
||||
"Hourly": { startOf: 'hour', amount: 'hours'},
|
||||
"Daily": { startOf: 'day', amount: 'days'},
|
||||
"Weekly": { startOf: 'isoWeek', amount: 'weeks'},
|
||||
"Monthly": { startOf: 'month', amount: 'months'},
|
||||
"Yearly": { startOf: 'year', amount: 'years'},
|
||||
};
|
||||
|
||||
export class IndexPattern {
|
||||
|
||||
constructor(private pattern, private interval: string | null) { }
|
||||
|
||||
getIndexForToday() {
|
||||
if (this.interval) {
|
||||
return moment.utc().format(this.pattern);
|
||||
} else {
|
||||
return this.pattern;
|
||||
}
|
||||
};
|
||||
|
||||
getIndexList(from, to) {
|
||||
if (!this.interval) {
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
var intervalInfo = intervalMap[this.interval];
|
||||
var start = moment(from).utc().startOf(intervalInfo.startOf);
|
||||
var end = moment(to).utc().startOf(intervalInfo.startOf).valueOf();
|
||||
var indexList = [];
|
||||
|
||||
while (start <= end) {
|
||||
indexList.push(start.format(this.pattern));
|
||||
start.add(1, intervalInfo.amount);
|
||||
}
|
||||
|
||||
return indexList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
declare var test: any;
|
||||
export default test;
|
||||
@@ -1,15 +1,15 @@
|
||||
define([
|
||||
'./query_def',
|
||||
],
|
||||
function (queryDef) {
|
||||
'use strict';
|
||||
import * as queryDef from './query_def';
|
||||
|
||||
function ElasticQueryBuilder(options) {
|
||||
export class ElasticQueryBuilder {
|
||||
timeField: string;
|
||||
esVersion: number;
|
||||
|
||||
constructor(options) {
|
||||
this.timeField = options.timeField;
|
||||
this.esVersion = options.esVersion;
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getRangeFilter = function() {
|
||||
getRangeFilter() {
|
||||
var filter = {};
|
||||
filter[this.timeField] = {
|
||||
gte: "$timeFrom",
|
||||
@@ -18,9 +18,9 @@ function (queryDef) {
|
||||
};
|
||||
|
||||
return filter;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.buildTermsAgg = function(aggDef, queryNode, target) {
|
||||
buildTermsAgg(aggDef, queryNode, target) {
|
||||
var metricRef, metric, y;
|
||||
queryNode.terms = { "field": aggDef.field };
|
||||
|
||||
@@ -57,10 +57,10 @@ function (queryDef) {
|
||||
}
|
||||
|
||||
return queryNode;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getDateHistogramAgg = function(aggDef) {
|
||||
var esAgg = {};
|
||||
getDateHistogramAgg(aggDef) {
|
||||
var esAgg: any = {};
|
||||
var settings = aggDef.settings || {};
|
||||
esAgg.interval = settings.interval;
|
||||
esAgg.field = this.timeField;
|
||||
@@ -77,10 +77,10 @@ function (queryDef) {
|
||||
}
|
||||
|
||||
return esAgg;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getHistogramAgg = function(aggDef) {
|
||||
var esAgg = {};
|
||||
getHistogramAgg(aggDef) {
|
||||
var esAgg: any = {};
|
||||
var settings = aggDef.settings || {};
|
||||
esAgg.interval = settings.interval;
|
||||
esAgg.field = aggDef.field;
|
||||
@@ -90,9 +90,9 @@ function (queryDef) {
|
||||
esAgg.missing = settings.missing;
|
||||
}
|
||||
return esAgg;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getFiltersAgg = function(aggDef) {
|
||||
getFiltersAgg(aggDef) {
|
||||
var filterObj = {};
|
||||
for (var i = 0; i < aggDef.settings.filters.length; i++) {
|
||||
var query = aggDef.settings.filters[i].query;
|
||||
@@ -107,9 +107,9 @@ function (queryDef) {
|
||||
}
|
||||
|
||||
return filterObj;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.documentQuery = function(query, size) {
|
||||
documentQuery(query, size) {
|
||||
query.size = size;
|
||||
query.sort = {};
|
||||
query.sort[this.timeField] = {order: 'desc', unmapped_type: 'boolean'};
|
||||
@@ -126,9 +126,9 @@ function (queryDef) {
|
||||
query.docvalue_fields = [this.timeField];
|
||||
}
|
||||
return query;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.addAdhocFilters = function(query, adhocFilters) {
|
||||
addAdhocFilters(query, adhocFilters) {
|
||||
if (!adhocFilters) {
|
||||
return;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ function (queryDef) {
|
||||
queryCondition = {};
|
||||
queryCondition[filter.key] = {query: filter.value};
|
||||
|
||||
switch(filter.operator){
|
||||
switch (filter.operator){
|
||||
case "=":
|
||||
if (!query.query.bool.must) { query.query.bool.must = []; }
|
||||
query.query.bool.must.push({match_phrase: queryCondition});
|
||||
@@ -169,7 +169,7 @@ function (queryDef) {
|
||||
}
|
||||
};
|
||||
|
||||
ElasticQueryBuilder.prototype.build = function(target, adhocFilters, queryString) {
|
||||
build(target, adhocFilters?, queryString?) {
|
||||
// make sure query has defaults;
|
||||
target.metrics = target.metrics || [{ type: 'count', id: '1' }];
|
||||
target.dsType = 'elasticsearch';
|
||||
@@ -213,7 +213,7 @@ function (queryDef) {
|
||||
var aggDef = target.bucketAggs[i];
|
||||
var esAgg = {};
|
||||
|
||||
switch(aggDef.type) {
|
||||
switch (aggDef.type) {
|
||||
case 'date_histogram': {
|
||||
esAgg["date_histogram"] = this.getDateHistogramAgg(aggDef);
|
||||
break;
|
||||
@@ -273,10 +273,10 @@ function (queryDef) {
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
}
|
||||
|
||||
ElasticQueryBuilder.prototype.getTermsQuery = function(queryDef) {
|
||||
var query = {
|
||||
getTermsQuery(queryDef) {
|
||||
var query: any = {
|
||||
"size": 0,
|
||||
"query": {
|
||||
"bool": {
|
||||
@@ -311,7 +311,5 @@ function (queryDef) {
|
||||
}
|
||||
};
|
||||
return query;
|
||||
};
|
||||
|
||||
return ElasticQueryBuilder;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import './metric_agg';
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import queryDef from './query_def';
|
||||
import * as queryDef from './query_def';
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
|
||||
export class ElasticQueryCtrl extends QueryCtrl {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
declare var test: any;
|
||||
export default test;
|
||||
@@ -1,199 +0,0 @@
|
||||
define([
|
||||
'lodash'
|
||||
],
|
||||
function (_) {
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
metricAggTypes: [
|
||||
{text: "Count", value: 'count', requiresField: false},
|
||||
{text: "Average", value: 'avg', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Sum", value: 'sum', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Max", value: 'max', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Min", value: 'min', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Extended Stats", value: 'extended_stats', requiresField: true, supportsMissing: true, supportsInlineScript: true},
|
||||
{text: "Percentiles", value: 'percentiles', requiresField: true, supportsMissing: true, supportsInlineScript: true},
|
||||
{text: "Unique Count", value: "cardinality", requiresField: true, supportsMissing: true},
|
||||
{text: "Moving Average", value: 'moving_avg', requiresField: false, isPipelineAgg: true, minVersion: 2},
|
||||
{text: "Derivative", value: 'derivative', requiresField: false, isPipelineAgg: true, minVersion: 2 },
|
||||
{text: "Raw Document", value: "raw_document", requiresField: false}
|
||||
],
|
||||
|
||||
bucketAggTypes: [
|
||||
{text: "Terms", value: 'terms', requiresField: true},
|
||||
{text: "Filters", value: 'filters' },
|
||||
{text: "Geo Hash Grid", value: 'geohash_grid', requiresField: true},
|
||||
{text: "Date Histogram", value: 'date_histogram', requiresField: true},
|
||||
{text: "Histogram", value: 'histogram', requiresField: true},
|
||||
],
|
||||
|
||||
orderByOptions: [
|
||||
{text: "Doc Count", value: '_count' },
|
||||
{text: "Term value", value: '_term' },
|
||||
],
|
||||
|
||||
orderOptions: [
|
||||
{text: "Top", value: 'desc' },
|
||||
{text: "Bottom", value: 'asc' },
|
||||
],
|
||||
|
||||
sizeOptions: [
|
||||
{text: "No limit", value: '0' },
|
||||
{text: "1", value: '1' },
|
||||
{text: "2", value: '2' },
|
||||
{text: "3", value: '3' },
|
||||
{text: "5", value: '5' },
|
||||
{text: "10", value: '10' },
|
||||
{text: "15", value: '15' },
|
||||
{text: "20", value: '20' },
|
||||
],
|
||||
|
||||
extendedStats: [
|
||||
{text: 'Avg', value: 'avg'},
|
||||
{text: 'Min', value: 'min'},
|
||||
{text: 'Max', value: 'max'},
|
||||
{text: 'Sum', value: 'sum'},
|
||||
{text: 'Count', value: 'count'},
|
||||
{text: 'Std Dev', value: 'std_deviation'},
|
||||
{text: 'Std Dev Upper', value: 'std_deviation_bounds_upper'},
|
||||
{text: 'Std Dev Lower', value: 'std_deviation_bounds_lower'},
|
||||
],
|
||||
|
||||
intervalOptions: [
|
||||
{text: 'auto', value: 'auto'},
|
||||
{text: '10s', value: '10s'},
|
||||
{text: '1m', value: '1m'},
|
||||
{text: '5m', value: '5m'},
|
||||
{text: '10m', value: '10m'},
|
||||
{text: '20m', value: '20m'},
|
||||
{text: '1h', value: '1h'},
|
||||
{text: '1d', value: '1d'},
|
||||
],
|
||||
|
||||
movingAvgModelOptions: [
|
||||
{text: 'Simple', value: 'simple'},
|
||||
{text: 'Linear', value: 'linear'},
|
||||
{text: 'Exponentially Weighted', value: 'ewma'},
|
||||
{text: 'Holt Linear', value: 'holt'},
|
||||
{text: 'Holt Winters', value: 'holt_winters'},
|
||||
],
|
||||
|
||||
pipelineOptions: {
|
||||
'moving_avg' : [
|
||||
{text: 'window', default: 5},
|
||||
{text: 'model', default: 'simple'},
|
||||
{text: 'predict', default: undefined},
|
||||
{text: 'minimize', default: false},
|
||||
],
|
||||
'derivative': [
|
||||
{text: 'unit', default: undefined},
|
||||
]
|
||||
},
|
||||
|
||||
movingAvgModelSettings: {
|
||||
'simple' : [],
|
||||
'linear' : [],
|
||||
'ewma' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined}],
|
||||
'holt' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
],
|
||||
'holt_winters' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
{text: "Gamma", value: "gamma", default: undefined},
|
||||
{text: "Period", value: "period", default: undefined},
|
||||
{text: "Pad", value: "pad", default: undefined, isCheckbox: true},
|
||||
],
|
||||
},
|
||||
|
||||
getMetricAggTypes: function(esVersion) {
|
||||
return _.filter(this.metricAggTypes, function(f) {
|
||||
if (f.minVersion) {
|
||||
return f.minVersion <= esVersion;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getPipelineOptions: function(metric) {
|
||||
if (!this.isPipelineAgg(metric.type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.pipelineOptions[metric.type];
|
||||
},
|
||||
|
||||
isPipelineAgg: function(metricType) {
|
||||
if (metricType) {
|
||||
var po = this.pipelineOptions[metricType];
|
||||
return po !== null && po !== undefined;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getPipelineAggOptions: function(targets) {
|
||||
var self = this;
|
||||
var result = [];
|
||||
_.each(targets.metrics, function(metric) {
|
||||
if (!self.isPipelineAgg(metric.type)) {
|
||||
result.push({text: self.describeMetric(metric), value: metric.id });
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getMovingAvgSettings: function(model, filtered) {
|
||||
var filteredResult = [];
|
||||
if (filtered) {
|
||||
_.each(this.movingAvgModelSettings[model], function(setting) {
|
||||
if (!(setting.isCheckbox)) {
|
||||
filteredResult.push(setting);
|
||||
}
|
||||
});
|
||||
return filteredResult;
|
||||
}
|
||||
return this.movingAvgModelSettings[model];
|
||||
},
|
||||
|
||||
getOrderByOptions: function(target) {
|
||||
var self = this;
|
||||
var metricRefs = [];
|
||||
_.each(target.metrics, function(metric) {
|
||||
if (metric.type !== 'count') {
|
||||
metricRefs.push({text: self.describeMetric(metric), value: metric.id});
|
||||
}
|
||||
});
|
||||
|
||||
return this.orderByOptions.concat(metricRefs);
|
||||
},
|
||||
|
||||
describeOrder: function(order) {
|
||||
var def = _.find(this.orderOptions, {value: order});
|
||||
return def.text;
|
||||
},
|
||||
|
||||
describeMetric: function(metric) {
|
||||
var def = _.find(this.metricAggTypes, {value: metric.type});
|
||||
return def.text + ' ' + metric.field;
|
||||
},
|
||||
|
||||
describeOrderBy: function(orderBy, target) {
|
||||
var def = _.find(this.orderByOptions, {value: orderBy});
|
||||
if (def) {
|
||||
return def.text;
|
||||
}
|
||||
var metric = _.find(target.metrics, {id: orderBy});
|
||||
if (metric) {
|
||||
return this.describeMetric(metric);
|
||||
} else {
|
||||
return "metric not found";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
191
public/app/plugins/datasource/elasticsearch/query_def.ts
Normal file
191
public/app/plugins/datasource/elasticsearch/query_def.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export const metricAggTypes = [
|
||||
{text: "Count", value: 'count', requiresField: false},
|
||||
{text: "Average", value: 'avg', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Sum", value: 'sum', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Max", value: 'max', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Min", value: 'min', requiresField: true, supportsInlineScript: true, supportsMissing: true},
|
||||
{text: "Extended Stats", value: 'extended_stats', requiresField: true, supportsMissing: true, supportsInlineScript: true},
|
||||
{text: "Percentiles", value: 'percentiles', requiresField: true, supportsMissing: true, supportsInlineScript: true},
|
||||
{text: "Unique Count", value: "cardinality", requiresField: true, supportsMissing: true},
|
||||
{text: "Moving Average", value: 'moving_avg', requiresField: false, isPipelineAgg: true, minVersion: 2},
|
||||
{text: "Derivative", value: 'derivative', requiresField: false, isPipelineAgg: true, minVersion: 2 },
|
||||
{text: "Raw Document", value: "raw_document", requiresField: false}
|
||||
];
|
||||
|
||||
export const bucketAggTypes = [
|
||||
{text: "Terms", value: 'terms', requiresField: true},
|
||||
{text: "Filters", value: 'filters' },
|
||||
{text: "Geo Hash Grid", value: 'geohash_grid', requiresField: true},
|
||||
{text: "Date Histogram", value: 'date_histogram', requiresField: true},
|
||||
{text: "Histogram", value: 'histogram', requiresField: true},
|
||||
];
|
||||
|
||||
export const orderByOptions = [
|
||||
{text: "Doc Count", value: '_count' },
|
||||
{text: "Term value", value: '_term' },
|
||||
];
|
||||
|
||||
export const orderOptions = [
|
||||
{text: "Top", value: 'desc' },
|
||||
{text: "Bottom", value: 'asc' },
|
||||
];
|
||||
|
||||
export const sizeOptions = [
|
||||
{text: "No limit", value: '0' },
|
||||
{text: "1", value: '1' },
|
||||
{text: "2", value: '2' },
|
||||
{text: "3", value: '3' },
|
||||
{text: "5", value: '5' },
|
||||
{text: "10", value: '10' },
|
||||
{text: "15", value: '15' },
|
||||
{text: "20", value: '20' },
|
||||
];
|
||||
|
||||
export const extendedStats = [
|
||||
{text: 'Avg', value: 'avg'},
|
||||
{text: 'Min', value: 'min'},
|
||||
{text: 'Max', value: 'max'},
|
||||
{text: 'Sum', value: 'sum'},
|
||||
{text: 'Count', value: 'count'},
|
||||
{text: 'Std Dev', value: 'std_deviation'},
|
||||
{text: 'Std Dev Upper', value: 'std_deviation_bounds_upper'},
|
||||
{text: 'Std Dev Lower', value: 'std_deviation_bounds_lower'},
|
||||
];
|
||||
|
||||
export const intervalOptions = [
|
||||
{text: 'auto', value: 'auto'},
|
||||
{text: '10s', value: '10s'},
|
||||
{text: '1m', value: '1m'},
|
||||
{text: '5m', value: '5m'},
|
||||
{text: '10m', value: '10m'},
|
||||
{text: '20m', value: '20m'},
|
||||
{text: '1h', value: '1h'},
|
||||
{text: '1d', value: '1d'},
|
||||
];
|
||||
|
||||
export const movingAvgModelOptions = [
|
||||
{text: 'Simple', value: 'simple'},
|
||||
{text: 'Linear', value: 'linear'},
|
||||
{text: 'Exponentially Weighted', value: 'ewma'},
|
||||
{text: 'Holt Linear', value: 'holt'},
|
||||
{text: 'Holt Winters', value: 'holt_winters'},
|
||||
];
|
||||
|
||||
export const pipelineOptions = {
|
||||
'moving_avg' : [
|
||||
{text: 'window', default: 5},
|
||||
{text: 'model', default: 'simple'},
|
||||
{text: 'predict', default: undefined},
|
||||
{text: 'minimize', default: false},
|
||||
],
|
||||
'derivative': [
|
||||
{text: 'unit', default: undefined},
|
||||
]
|
||||
};
|
||||
|
||||
export const movingAvgModelSettings = {
|
||||
'simple' : [],
|
||||
'linear' : [],
|
||||
'ewma' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined}],
|
||||
'holt' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
],
|
||||
'holt_winters' : [
|
||||
{text: "Alpha", value: "alpha", default: undefined},
|
||||
{text: "Beta", value: "beta", default: undefined},
|
||||
{text: "Gamma", value: "gamma", default: undefined},
|
||||
{text: "Period", value: "period", default: undefined},
|
||||
{text: "Pad", value: "pad", default: undefined, isCheckbox: true},
|
||||
],
|
||||
};
|
||||
|
||||
export function getMetricAggTypes(esVersion) {
|
||||
return _.filter(metricAggTypes, function(f) {
|
||||
if (f.minVersion) {
|
||||
return f.minVersion <= esVersion;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPipelineOptions(metric) {
|
||||
if (!isPipelineAgg(metric.type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return pipelineOptions[metric.type];
|
||||
}
|
||||
|
||||
export function isPipelineAgg(metricType) {
|
||||
if (metricType) {
|
||||
var po = pipelineOptions[metricType];
|
||||
return po !== null && po !== undefined;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPipelineAggOptions(targets) {
|
||||
var result = [];
|
||||
_.each(targets.metrics, function(metric) {
|
||||
if (!isPipelineAgg(metric.type)) {
|
||||
result.push({text: describeMetric(metric), value: metric.id });
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getMovingAvgSettings(model, filtered) {
|
||||
var filteredResult = [];
|
||||
if (filtered) {
|
||||
_.each(movingAvgModelSettings[model], function(setting) {
|
||||
if (!(setting.isCheckbox)) {
|
||||
filteredResult.push(setting);
|
||||
}
|
||||
});
|
||||
return filteredResult;
|
||||
}
|
||||
return movingAvgModelSettings[model];
|
||||
}
|
||||
|
||||
export function getOrderByOptions(target) {
|
||||
var metricRefs = [];
|
||||
_.each(target.metrics, function(metric) {
|
||||
if (metric.type !== 'count') {
|
||||
metricRefs.push({text: describeMetric(metric), value: metric.id});
|
||||
}
|
||||
});
|
||||
|
||||
return orderByOptions.concat(metricRefs);
|
||||
}
|
||||
|
||||
export function describeOrder(order) {
|
||||
var def = _.find(orderOptions, {value: order});
|
||||
return def.text;
|
||||
}
|
||||
|
||||
export function describeMetric(metric) {
|
||||
var def = _.find(metricAggTypes, {value: metric.type});
|
||||
return def.text + ' ' + metric.field;
|
||||
}
|
||||
|
||||
export function describeOrderBy(orderBy, target) {
|
||||
var def = _.find(orderByOptions, {value: orderBy});
|
||||
if (def) {
|
||||
return def.text;
|
||||
}
|
||||
var metric = _.find(target.metrics, {id: orderBy});
|
||||
if (metric) {
|
||||
return describeMetric(metric);
|
||||
} else {
|
||||
return "metric not found";
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {describe, it, expect} from 'test/lib/common';
|
||||
import moment from 'moment';
|
||||
import IndexPattern from '../index_pattern';
|
||||
import {IndexPattern} from '../index_pattern';
|
||||
|
||||
describe('IndexPattern', function() {
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('IndexPattern', function() {
|
||||
|
||||
describe('no interval', function() {
|
||||
it('should return correct index', function() {
|
||||
var pattern = new IndexPattern('my-metrics');
|
||||
var pattern = new IndexPattern('my-metrics', null);
|
||||
var from = new Date(2015, 4, 30, 1, 2, 3);
|
||||
var to = new Date(2015, 5, 1, 12, 5 , 6);
|
||||
expect(pattern.getIndexList(from, to)).to.eql('my-metrics');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import {describe, beforeEach, it, expect} from 'test/lib/common';
|
||||
import ElasticQueryBuilder from '../query_builder';
|
||||
import {ElasticQueryBuilder} from '../query_builder';
|
||||
|
||||
describe('ElasticQueryBuilder', function() {
|
||||
var builder;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import {describe, it, expect} from 'test/lib/common';
|
||||
|
||||
import queryDef from '../query_def';
|
||||
import * as queryDef from '../query_def';
|
||||
|
||||
describe('ElasticQueryDef', function() {
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import './add_graphite_func';
|
||||
import './func_editor';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('graphiteDatasource', function() {
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(ctx.providePhase(['backendSrv']));
|
||||
beforeEach(ctx.providePhase(['backendSrv', 'templateSrv']));
|
||||
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
import '../query_ctrl';
|
||||
import 'app/core/services/segment_srv';
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
// jshint ignore: start
|
||||
// jscs: disable
|
||||
|
||||
ace.define("ace/mode/sql_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
|
||||
|
||||
var SqlHighlightRules = function() {
|
||||
|
||||
var keywords = (
|
||||
"select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|" +
|
||||
"when|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|" +
|
||||
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant"
|
||||
);
|
||||
|
||||
var builtinConstants = (
|
||||
"true|false"
|
||||
);
|
||||
|
||||
var builtinFunctions = (
|
||||
"avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|" +
|
||||
"coalesce|ifnull|isnull|nvl"
|
||||
);
|
||||
|
||||
var dataTypes = (
|
||||
"int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" +
|
||||
"money|real|number|integer"
|
||||
);
|
||||
|
||||
var keywordMapper = this.createKeywordMapper({
|
||||
"support.function": builtinFunctions,
|
||||
"keyword": keywords,
|
||||
"constant.language": builtinConstants,
|
||||
"storage.type": dataTypes
|
||||
}, "identifier", true);
|
||||
|
||||
this.$rules = {
|
||||
"start" : [ {
|
||||
token : "comment",
|
||||
regex : "--.*$"
|
||||
}, {
|
||||
token : "comment",
|
||||
start : "/\\*",
|
||||
end : "\\*/"
|
||||
}, {
|
||||
token : "string", // " string
|
||||
regex : '".*?"'
|
||||
}, {
|
||||
token : "string", // ' string
|
||||
regex : "'.*?'"
|
||||
}, {
|
||||
token : "string", // ` string (apache drill)
|
||||
regex : "`.*?`"
|
||||
}, {
|
||||
token : "constant.numeric", // float
|
||||
regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
|
||||
}, {
|
||||
token : keywordMapper,
|
||||
regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
|
||||
}, {
|
||||
token : "keyword.operator",
|
||||
regex : "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|="
|
||||
}, {
|
||||
token : "paren.lparen",
|
||||
regex : "[\\(]"
|
||||
}, {
|
||||
token : "paren.rparen",
|
||||
regex : "[\\)]"
|
||||
}, {
|
||||
token : "text",
|
||||
regex : "\\s+"
|
||||
} ]
|
||||
};
|
||||
this.normalizeRules();
|
||||
};
|
||||
|
||||
oop.inherits(SqlHighlightRules, TextHighlightRules);
|
||||
|
||||
exports.SqlHighlightRules = SqlHighlightRules;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/sql",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/sql_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextMode = require("./text").Mode;
|
||||
var SqlHighlightRules = require("./sql_highlight_rules").SqlHighlightRules;
|
||||
|
||||
var Mode = function() {
|
||||
this.HighlightRules = SqlHighlightRules;
|
||||
this.$behaviour = this.$defaultBehaviour;
|
||||
};
|
||||
oop.inherits(Mode, TextMode);
|
||||
|
||||
(function() {
|
||||
|
||||
this.lineCommentStart = "--";
|
||||
|
||||
this.$id = "ace/mode/sql";
|
||||
}).call(Mode.prototype);
|
||||
|
||||
exports.Mode = Mode;
|
||||
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
declare var OpenTsDatasource: any;
|
||||
export {OpenTsDatasource};
|
||||
export default OpenTsDatasource;
|
||||
|
||||
|
||||
@@ -472,7 +472,5 @@ function (angular, _, dateMath) {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
OpenTsDatasource: OpenTsDatasource
|
||||
};
|
||||
return OpenTsDatasource;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenTsDatasource} from './datasource';
|
||||
import OpenTsDatasource from './datasource';
|
||||
import {OpenTsQueryCtrl} from './query_ctrl';
|
||||
import {OpenTsConfigCtrl} from './config_ctrl';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import {OpenTsDatasource} from "../datasource";
|
||||
import OpenTsDatasource from "../datasource";
|
||||
|
||||
describe('opentsdb', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
import {PromCompleter} from './completer';
|
||||
import './mode-prometheus';
|
||||
import './snippets/prometheus';
|
||||
|
||||
class PrometheusQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
@@ -44,12 +43,6 @@ class PrometheusQueryCtrl extends QueryCtrl {
|
||||
|
||||
getCompleter(query) {
|
||||
return new PromCompleter(this.datasource);
|
||||
// console.log('getquery);
|
||||
// return this.datasource.performSuggestQuery(query).then(res => {
|
||||
// return res.map(item => {
|
||||
// return {word: item, type: 'metric'};
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
getDefaultFormat() {
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<div class="panel-alert-list" style="{{ctrl.contentHeight}}">
|
||||
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
|
||||
<ol class="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-notice">
|
||||
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
|
||||
{{alert.name}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-item-sub-name">
|
||||
<span class="alert-list-item-state {{alert.stateModel.stateClass}}">
|
||||
<i class="{{alert.stateModel.iconClass}}"></i>
|
||||
{{alert.stateModel.text}}
|
||||
</span> for {{alert.newStateDateAgo}}
|
||||
</div>
|
||||
<div class="small muted" ng-show="alert.executionError">
|
||||
{{alert.executionError}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'current'">
|
||||
<ol class="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.currentAlerts">
|
||||
<div class="alert-list card-item card-item--alert">
|
||||
<div class="alert-list-body">
|
||||
<div class="alert-list-icon alert-list-item-state {{alert.stateModel.stateClass}}">
|
||||
<i class="{{alert.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-list-main">
|
||||
<p class="alert-list-title">
|
||||
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
|
||||
{{alert.name}}
|
||||
</a>
|
||||
</p>
|
||||
<p class="alert-list-text">
|
||||
<span class="alert-list-state {{alert.stateModel.stateClass}}">{{alert.stateModel.text}}</span>
|
||||
for {{alert.newStateDateAgo}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'changes'">
|
||||
<ol class="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-sub-name">{{al.time}}</div>
|
||||
</div>
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-notice">{{al.title}}</div>
|
||||
<div class="card-item-sub-name">
|
||||
<span class="alert-list-item-state {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
{{al.stateModel.text}}
|
||||
</span> {{al.info}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<section class="card-section card-list-layout-list" ng-if="ctrl.panel.show === 'changes'">
|
||||
<ol class ="card-list">
|
||||
<li class="card-item-wrapper" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="alert-list card-item card-item--alert">
|
||||
<div class="alert-list-body">
|
||||
<div class="alert-list-icon alert-list-item-state {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-list-main">
|
||||
<p class="alert-list-title">{{al.title}}</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-list-footer">
|
||||
<span class="alert-list-text">{{al.time}}</span>
|
||||
<span class="alert-list-text"><!--Img Link--></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import 'jquery.flot';
|
||||
import 'jquery.flot.selection';
|
||||
import 'jquery.flot.time';
|
||||
import 'jquery.flot.stack';
|
||||
import 'jquery.flot.stackpercent';
|
||||
import 'jquery.flot.fillbelow';
|
||||
import 'jquery.flot.crosshair';
|
||||
import 'jquery.flot.dashes';
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.selection';
|
||||
import 'vendor/flot/jquery.flot.time';
|
||||
import 'vendor/flot/jquery.flot.stack';
|
||||
import 'vendor/flot/jquery.flot.stackpercent';
|
||||
import 'vendor/flot/jquery.flot.fillbelow';
|
||||
import 'vendor/flot/jquery.flot.crosshair';
|
||||
import 'vendor/flot/jquery.flot.dashes';
|
||||
import './jquery.flot.events';
|
||||
|
||||
import $ from 'jquery';
|
||||
@@ -21,7 +21,8 @@ import {ThresholdManager} from './threshold_manager';
|
||||
import {EventManager} from 'app/features/annotations/all';
|
||||
import {convertValuesToHistogram, getSeriesValues} from './histogram';
|
||||
|
||||
coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
/** @ngInject **/
|
||||
function graphDirective($rootScope, timeSrv, popoverSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
@@ -683,4 +684,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
coreModule.directive('grafanaGraph', graphDirective);
|
||||
|
||||
@@ -2,8 +2,6 @@ define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery',
|
||||
'jquery.flot',
|
||||
'jquery.flot.time',
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
|
||||
import '../series_overrides_ctrl';
|
||||
import helpers from 'test/specs/helpers';
|
||||
|
||||
describe('SeriesOverridesCtrl', function() {
|
||||
var ctx = new helpers.ControllerTestContext();
|
||||
var popoverSrv = {};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
|
||||
beforeEach(ctx.providePhase({
|
||||
popoverSrv: popoverSrv
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function($rootScope, $controller) {
|
||||
ctx.scope = $rootScope.$new();
|
||||
ctx.scope.ctrl = {
|
||||
refresh: sinon.spy(),
|
||||
render: sinon.spy(),
|
||||
seriesList: []
|
||||
};
|
||||
ctx.scope.render = function() {};
|
||||
ctx.controller = $controller('SeriesOverridesCtrl', {
|
||||
$scope: ctx.scope
|
||||
});
|
||||
}));
|
||||
|
||||
describe('When setting an override', function() {
|
||||
beforeEach(function() {
|
||||
ctx.scope.setOverride({propertyName: 'lines'}, {value: true});
|
||||
});
|
||||
|
||||
it('should set override property', function() {
|
||||
expect(ctx.scope.override.lines).to.be(true);
|
||||
});
|
||||
|
||||
it('should update view model', function() {
|
||||
expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
|
||||
expect(ctx.scope.currentOverrides[0].value).to.be('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When removing overide', function() {
|
||||
it('click should include option and value index', function() {
|
||||
ctx.scope.setOverride(1,0);
|
||||
ctx.scope.removeOverride({ propertyName: 'lines' });
|
||||
expect(ctx.scope.currentOverrides.length).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import 'jquery.flot';
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import d3 from 'd3';
|
||||
import d3 from 'vendor/d3/d3';
|
||||
import {contextSrv} from 'app/core/core';
|
||||
import {tickStep} from 'app/core/utils/ticks';
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import d3 from 'd3';
|
||||
import d3 from 'vendor/d3/d3';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
import './color_legend';
|
||||
import {HeatmapCtrl} from './heatmap_ctrl';
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {appEvents, contextSrv} from 'app/core/core';
|
||||
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
|
||||
import d3 from 'd3';
|
||||
import d3 from 'vendor/d3/d3';
|
||||
import {HeatmapTooltip} from './heatmap_tooltip';
|
||||
import {mergeZeroBuckets} from './heatmap_data_converter';
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'jquery.flot';
|
||||
import 'jquery.flot.gauge';
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.gauge';
|
||||
import 'app/features/panellinks/linkSrv';
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import config from 'app/core/config';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
import {describe, beforeEach, afterEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import {describe, beforeEach, afterEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
|
||||
|
||||
import helpers from '../../../../../test/specs/helpers';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import {SingleStatCtrl} from '../module';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {PanelCtrl} from 'app/plugins/sdk';
|
||||
import {PanelCtrl} from 'app/features/panel/panel_ctrl';
|
||||
|
||||
export class UnknownPanelCtrl extends PanelCtrl {
|
||||
static templateUrl = 'public/app/plugins/panel/unknown/module.html';
|
||||
@@ -9,7 +8,6 @@ export class UnknownPanelCtrl extends PanelCtrl {
|
||||
constructor($scope, $injector) {
|
||||
super($scope, $injector);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Plugin API
|
||||
|
||||
### 3.0 changes to plugin api changes
|
||||
|
||||
There has been big changes to both data source and plugin schema (plugin.json) and how
|
||||
you write the plugin main module.
|
||||
|
||||
#### Datasource plugin
|
||||
|
||||
Now data source plugins AMD/SystemJS module should return:
|
||||
|
||||
```javascript
|
||||
return {
|
||||
Datasource: ElasticDatasource,
|
||||
configView: editView.default,
|
||||
annotationsQueryEditor: annotationsQueryEditor,
|
||||
metricsQueryEditor: metricsQueryEditor,
|
||||
metricsQueryOptions: metricsQueryOptions,
|
||||
};
|
||||
```
|
||||
|
||||
Where ElasticDatasource is a constructor function to a javascript. The constructor
|
||||
function can take angular services and `instanceSettings` as parameters.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
function ElasticDatasource(instanceSettings, templateSrv) {
|
||||
this.instanceSettings = this.instanceSettings;
|
||||
///...
|
||||
};
|
||||
```
|
||||
|
||||
A datasource module can optionally return a configView directive function, metricsQueryEditor directive function, etc.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
function metricsQueryEditor() {
|
||||
return {controller: 'ElasticQueryCtrl', templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html'};
|
||||
}
|
||||
```
|
||||
|
||||
#### Panel plugin
|
||||
|
||||
The panel plugin AMD/SystemJS module should return an object with a property named `panel`. This needs to be
|
||||
a directive function.
|
||||
|
||||
### 2.5.1 changes
|
||||
datasource annotationQuery changed. now single options parameter with:
|
||||
- range
|
||||
- rangeRaw
|
||||
- annotation
|
||||
|
||||
2.5 changed the `range` parameter in the `datasource.query` function's options parameter. This
|
||||
parameter now holds a parsed range with `moment` dates `form` and `to`. To get
|
||||
millisecond epoch from a `moment` you the function `valueOf`. The raw date range as represented
|
||||
internally in grafana (which may be relative expressions like `now-5h`) is included in the
|
||||
new property `rangeRaw` (on the options object).
|
||||
@@ -2,20 +2,12 @@ import {PanelCtrl} from 'app/features/panel/panel_ctrl';
|
||||
import {MetricsPanelCtrl} from 'app/features/panel/metrics_panel_ctrl';
|
||||
import {QueryCtrl} from 'app/features/panel/query_ctrl';
|
||||
import {alertTab} from 'app/features/alerting/alert_tab_ctrl';
|
||||
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function loadPluginCss(options) {
|
||||
if (config.bootData.user.lightTheme) {
|
||||
System.import(options.light + '!css');
|
||||
} else {
|
||||
System.import(options.dark + '!css');
|
||||
}
|
||||
}
|
||||
import {loadPluginCss} from 'app/features/plugins/plugin_loader';
|
||||
|
||||
export {
|
||||
PanelCtrl,
|
||||
MetricsPanelCtrl,
|
||||
QueryCtrl,
|
||||
alertTab,
|
||||
loadPluginCss,
|
||||
};
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
System.config({
|
||||
defaultJSExtenions: true,
|
||||
baseURL: 'public',
|
||||
paths: {
|
||||
'gemini-scrollbar': 'vendor/npm/gemini-scrollbar/index.js',
|
||||
'react': 'vendor/npm/react/dist/react.js',
|
||||
'react-dom': 'vendor/npm/react-dom/dist/react-dom.js',
|
||||
'ngreact': 'vendor/npm/ngreact/ngReact.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/npm/jquery/dist/jquery.js",
|
||||
'lodash-src': 'vendor/npm/lodash/lodash.js',
|
||||
"lodash": 'app/core/lodash_extended.js',
|
||||
"angular": "vendor/npm/angular/angular.js",
|
||||
"bootstrap": "vendor/bootstrap/bootstrap.js",
|
||||
'angular-route': 'vendor/npm/angular-route/angular-route.js',
|
||||
'angular-sanitize': 'vendor/npm/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/npm/angular-native-dragdrop/draganddrop.js",
|
||||
"angular-bindonce": "vendor/npm/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",
|
||||
"jquery-ui": "vendor/jquery-ui/custom.js",
|
||||
"gridstack": "vendor/npm/gridstack/dist/gridstack.js",
|
||||
"gridstack.jquery-ui": "vendor/npm/gridstack/dist/gridstack.jQueryUI.js",
|
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
|
||||
"clipboard": "vendor/npm/clipboard/dist/clipboard.js"
|
||||
},
|
||||
|
||||
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/jquery-ui/jquery-ui.js': {
|
||||
format: 'amd',
|
||||
deps: ['jquery'],
|
||||
},
|
||||
'vendor/npm/gridstack/dist/gridstack.js': {
|
||||
format: 'global',
|
||||
deps: ['jquery', 'jquery-ui', 'lodash'],
|
||||
},
|
||||
"vendor/npm/gridstack/dist/gridstack.jQueryUI.js": {
|
||||
format: 'global',
|
||||
deps: ['gridstack'],
|
||||
},
|
||||
'vendor/npm/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'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
// vendor
|
||||
@import "../vendor/css/timepicker.css";
|
||||
@import "../vendor/css/spectrum.css";
|
||||
|
||||
// MIXINS
|
||||
@import "mixins/mixins";
|
||||
@import "mixins/animations";
|
||||
@@ -15,8 +19,7 @@
|
||||
@import "base/type";
|
||||
@import "base/forms";
|
||||
@import "base/grid";
|
||||
@import "base/font_awesome";
|
||||
@import "base/grafana_icons";
|
||||
@import "base/fonts";
|
||||
@import "base/code";
|
||||
|
||||
// UTILS
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
/* icon fonts
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
src: url('../fonts/fontawesome-webfont.eot?v=4.2.0');
|
||||
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'grafana-icons';
|
||||
src: url('../fonts/grafana-icons.eot?h6rv8b');
|
||||
src: url('../fonts/grafana-icons.eot?h6rv8b#iefix') format('embedded-opentype'),
|
||||
url('../fonts/grafana-icons.ttf?h6rv8b') format('truetype'),
|
||||
url('../fonts/grafana-icons.woff?h6rv8b') format('woff'),
|
||||
url('../fonts/grafana-icons.svg?h6rv8b#grafana-icons') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}*/
|
||||
|
||||
@import "base/font_awesome";
|
||||
@import "base/grafana_icons";
|
||||
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user