Merge branch 'dynamic-directives'

This commit is contained in:
Torkel Ödegaard 2016-01-14 18:57:34 +01:00
commit 88a132b878
89 changed files with 1505 additions and 1353 deletions

View File

@ -18,10 +18,10 @@ Press `Shift`+`?` to open the keyboard shortcut dialog from anywhere within the
|---|---|
|`Esc`|Exit fullscreen edit/view mode, close search or any editor view|
|`F`|Open dashboard search view (also contains import/playlist controls)|
|`R`|Refresh (Fetches new data and rerenders panels)|
|`CTRL`+`S`|Save dashboard|
|`CTRL`+`H`|Hide row controls|
|`CTRL`+`Z`|Zoom out|
|`CTRL`+`R`|Refresh (Fetches new data and rerenders panels)|
|`CTRL`+`O`|Enable/Disable shared graph crosshair|

View File

@ -34,7 +34,7 @@
"grunt-ng-annotate": "^1.0.1",
"grunt-string-replace": "~1.2.1",
"grunt-systemjs-builder": "^0.2.5",
"grunt-tslint": "^2.5.0",
"grunt-tslint": "^3.0.1",
"grunt-typescript": "^0.8.0",
"grunt-usemin": "3.0.0",
"jshint-stylish": "~0.1.5",
@ -70,7 +70,7 @@
"lodash": "^2.4.1",
"sinon": "1.16.1",
"systemjs-builder": "^0.14.15",
"tslint": "^3.2.0",
"tslint": "^3.2.1",
"typescript": "^1.7.5"
}
}

View File

@ -10,6 +10,7 @@ type AppSettings struct {
AppId string `json:"appId"`
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
Module string `json:"module"`
Info *plugins.PluginInfo `json:"info"`
Pages []*plugins.AppPluginPage `json:"pages"`
JsonData map[string]interface{} `json:"jsonData"`
@ -17,10 +18,11 @@ type AppSettings struct {
func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSettings {
dto := &AppSettings{
AppId: def.Id,
Name: def.Name,
Info: &def.Info,
Pages: def.Pages,
AppId: def.Id,
Name: def.Name,
Info: &def.Info,
Module: def.Module,
Pages: def.Pages,
}
if data != nil {

View File

@ -47,13 +47,13 @@ coreModule.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, $rootSc
};
$rootScope.colors = [
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
"#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93",
"#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7",
"#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B",
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
];
$scope.getTotalWatcherCount = function() {
@ -85,6 +85,7 @@ coreModule.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, $rootSc
$scope.$watch(function digestCounter() {
count++;
}, function() {
// something
});
$rootScope.performance.panels = [];

View File

@ -1,6 +1,5 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
import coreModule from '../core_module';
@ -8,10 +7,10 @@ export class SignUpCtrl {
/** @ngInject */
constructor(
private $scope : any,
private $location : any,
private contextSrv : any,
private backendSrv : any) {
private $scope: any,
private $location: any,
private contextSrv: any,
private backendSrv: any) {
contextSrv.sidemenu = false;
$scope.ctrl = this;

View File

@ -17,15 +17,15 @@ import "./directives/spectrum_picker";
import "./directives/tags";
import "./directives/topnav";
import "./directives/value_select_dropdown";
import "./directives/give_focus";
import './jquery_extended';
import './partials';
import {arrayJoin} from './directives/array_join';
import * as controllers from 'app/core/controllers/all';
import * as services from 'app/core/services/all';
import * as routes from 'app/core/routes/all';
import 'app/core/controllers/all';
import 'app/core/services/all';
import 'app/core/routes/all';
import './filters/filters';
import coreModule from './core_module';
// export * from './directives/give_focus'
export {arrayJoin, controllers, services, routes};
export {arrayJoin, coreModule};

View File

@ -1,7 +1,6 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
import coreModule from '../core_module';
export function arrayJoin() {

View File

@ -1,9 +1,8 @@
///<reference path="../../headers/common.d.ts" />
import angular = require('angular');
import coreModule from '../core_module';
coreModule.default.directive('giveFocus', function() {
coreModule.directive('giveFocus', function() {
return function(scope, element, attrs) {
element.click(function(e) {
e.stopPropagation();

View File

@ -1,6 +1,5 @@
///<reference path="../../headers/common.d.ts" />
import jquery from 'jquery';
import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
@ -59,7 +58,7 @@ coreModule.filter('noXml', function() {
});
coreModule.filter('interpolateTemplateVars', function (templateSrv) {
var filterFunc : any = function (text, scope) {
var filterFunc: any = function(text, scope) {
if (scope.panel) {
return templateSrv.replaceWithText(text, scope.panel.scopedVars);
} else {

View File

@ -9,5 +9,6 @@ define([
'./popover_srv',
'./segment_srv',
'./backend_srv',
'./dynamic_directive_srv',
],
function () {});

View File

@ -0,0 +1,64 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import coreModule from '../core_module';
class DynamicDirectiveSrv {
/** @ngInject */
constructor(private $compile, private $parse, private $rootScope) {}
addDirective(element, name, scope) {
var child = angular.element(document.createElement(name));
this.$compile(child)(scope);
element.empty();
element.append(child);
}
link(scope, elem, attrs, options) {
options.directive(scope).then(directiveInfo => {
if (!directiveInfo || !directiveInfo.fn) {
elem.empty();
return;
}
if (!directiveInfo.fn.registered) {
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
directiveInfo.fn.registered = true;
}
this.addDirective(elem, directiveInfo.name, scope);
}).catch(err => {
console.log('Plugin load:', err);
this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
});
}
create(options) {
let directiveDef = {
restrict: 'E',
scope: options.scope,
link: (scope, elem, attrs) => {
if (options.watch) {
let childScope = null;
scope.$watch(options.watch, () => {
if (childScope) {
childScope.$destroy();
}
childScope = scope.$new();
this.link(childScope, elem, attrs, options);
});
} else {
this.link(scope, elem, attrs, options);
}
}
};
return directiveDef;
}
}
coreModule.service('dynamicDirectiveSrv', DynamicDirectiveSrv);

View File

@ -132,7 +132,7 @@ export default class TimeSeries {
}
}
if (currentValue != 0) {
if (currentValue !== 0) {
this.allIsZero = false;
}

View File

@ -4,11 +4,6 @@ import _ from 'lodash';
import moment from 'moment';
var units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
var unitsAsc = _.sortBy(units, function (unit) {
return moment.duration(1, unit).valueOf();
});
var unitsDesc = unitsAsc.reverse();
export function parse(text, roundUp?) {
if (!text) { return undefined; }
@ -104,8 +99,7 @@ export function parseDateMath(mathString, time, roundUp?) {
if (type === 0) {
if (roundUp) {
dateTime.endOf(unit);
}
else {
} else {
dateTime.startOf(unit);
}
} else if (type === 1) {

View File

@ -1,7 +1,6 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
import * as dateMath from './datemath';

View File

@ -1,7 +1,7 @@
define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotationsSrv',
'./annotations/annotations_srv',
'./templating/templateSrv',
'./dashboard/all',
'./playlist/all',

View File

@ -1,7 +1,8 @@
define([
'angular',
'lodash',
'./editorCtrl'
'./editor_ctrl',
'./query_editor'
], function (angular, _) {
'use strict';

View File

@ -31,10 +31,10 @@ function (angular, _, $) {
};
$scope.datasourceChanged = function() {
$scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
if (!$scope.currentDatasource) {
$scope.currentDatasource = $scope.datasources[0];
}
datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
$scope.currentDatasource = ds;
$scope.currentAnnotation.datasource = ds.name;
});
};
$scope.edit = function(annotation) {
@ -50,7 +50,6 @@ function (angular, _, $) {
$scope.currentAnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
$scope.datasourceChanged();
$scope.currentAnnotation.datasource = $scope.currentDatasource.name;
};
$scope.update = function() {

View File

@ -91,7 +91,8 @@
</div>
</div>
<datasource-editor-view datasource="currentAnnotation.datasource" name="annotations-query-editor"></datasource-editor-view>
<annotations-query-editor datasource="currentDatasource" annotation="currentAnnotation">
</annotations-query-editor>
<br>
<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>

View File

@ -0,0 +1,25 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function annotationsQueryEditor(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
annotation: "=",
datasource: "="
},
watch: "datasource.type",
directive: scope => {
return System.import(scope.datasource.meta.module).then(function(dsModule) {
return {
name: 'annotation-query-editor-' + scope.datasource.meta.id,
fn: dsModule.annotationsQueryEditor,
};
});
},
});
}
angular.module('grafana.directives').directive('annotationsQueryEditor', annotationsQueryEditor);

View File

@ -1,2 +1,3 @@
import './edit_ctrl';
import './list_ctrl';
import './config_view';

View File

@ -1,43 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
export class AppSrv {
apps: any = {};
/** @ngInject */
constructor(
private $rootScope,
private $timeout,
private $q,
private backendSrv) {
}
get(type) {
return this.getAll().then(() => {
return this.apps[type];
});
}
getAll() {
if (!_.isEmpty(this.apps)) {
return this.$q.when(this.apps);
}
return this.backendSrv.get('api/org/apps').then(results => {
return results.reduce((prev, current) => {
prev[current.type] = current;
return prev;
}, this.apps);
});
}
update(app) {
return this.backendSrv.post('api/org/apps', app).then(resp => {
});
}
}
angular.module('grafana.services').service('appSrv', AppSrv);

View File

@ -0,0 +1,23 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function appConfigView(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
appModel: "="
},
directive: scope => {
return System.import(scope.appModel.module).then(function(appModule) {
return {
name: 'app-config-' + scope.appModel.appId,
fn: appModule.configView,
};
});
},
});
}
angular.module('grafana.directives').directive('appConfigView', appConfigView);

View File

@ -1,6 +1,5 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import _ from 'lodash';

View File

@ -1,6 +1,5 @@
///<reference path="../../headers/common.d.ts" />
import config = require('app/core/config');
import angular from 'angular';
export class AppListCtrl {

View File

@ -29,7 +29,6 @@
<span style="small">
Version: {{ctrl.appModel.info.version}} &nbsp; &nbsp; Updated: {{ctrl.appModel.info.updated}}
</span>
</em>
<br><br>
@ -94,9 +93,12 @@
<section class="simple-box">
<h3 class="simple-box-header">Configuration:</h3>
<div class="simple-box-body">
<div ng-if="ctrl.appModel.appId">
<app-config-view app-model="ctrl.appModel"></app-config-view>
</div>
</div>
</section>
<app-config-loader></app-config-loader>
</div>
</div>

View File

@ -52,7 +52,7 @@ function(angular, $) {
scope.appEvent('save-dashboard', evt);
}, { inputDisabled: true });
keyboardManager.bind('ctrl+r', function() {
keyboardManager.bind('r', function() {
scope.broadcastRefresh();
}, { inputDisabled: true });

View File

@ -1,7 +1,5 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
export function inputDateDirective() {

View File

@ -1,19 +1,17 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
import angular from 'angular';
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
import * as rangeUtil from 'app/core/utils/rangeutil';
export class TimePickerCtrl {
static tooltipFormat = 'MMM D, YYYY HH:mm:ss';
static defaults = {
time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
};
dashboard: any;

View File

@ -1,4 +1,5 @@
define([
'./list_ctrl',
'./edit_ctrl',
'./config_view',
], function () {});

View File

@ -0,0 +1,25 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function dsConfigView(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
dsMeta: "=",
current: "="
},
watch: "dsMeta.module",
directive: scope => {
return System.import(scope.dsMeta.module).then(function(dsModule) {
return {
name: 'ds-config-' + scope.dsMeta.id,
fn: dsModule.configView,
};
});
},
});
}
angular.module('grafana.directives').directive('dsConfigView', dsConfigView);

View File

@ -42,7 +42,7 @@
<div class="clearfix"></div>
</div>
<datasource-custom-settings-view ds-meta="datasourceMeta" current="current"></datasource-custom-settings-view>
<ds-config-view ng-if="datasourceMeta.id" ds-meta="datasourceMeta" current="current"></ds-config-view>
<div ng-if="testing" style="margin-top: 25px">
<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

View File

@ -4,4 +4,6 @@ define([
'./panel_srv',
'./panel_helper',
'./solo_panel_ctrl',
'./panel_loader',
'./query_editor',
], function () {});

View File

@ -1,32 +1,12 @@
define([
'angular',
'jquery',
'app/core/config',
],
function (angular, $, config) {
function (angular, $) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('panelLoader', function($compile, $parse) {
return {
restrict: 'E',
link: function(scope, elem, attr) {
var getter = $parse(attr.type), panelType = getter(scope);
var module = config.panels[panelType].module;
System.import(module).then(function() {
var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
elem.append(panelEl);
$compile(panelEl)(scope);
}).catch(function(err) {
console.log('Failed to load panel:', err);
scope.appEvent('alert-error', ['Panel Load Error', 'Failed to load panel ' + panelType + ', ' + err]);
});
}
};
});
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
@ -43,103 +23,6 @@ function (angular, $, config) {
};
});
module.directive('datasourceCustomSettingsView', function($compile) {
return {
restrict: 'E',
scope: {
dsMeta: "=",
current: "=",
},
link: function(scope, elem) {
scope.$watch("dsMeta.module", function() {
if (!scope.dsMeta) {
return;
}
System.import(scope.dsMeta.module).then(function() {
elem.empty();
var panelEl = angular.element(document.createElement('datasource-custom-settings-view-' + scope.dsMeta.id));
elem.append(panelEl);
$compile(panelEl)(scope);
}).catch(function(err) {
console.log('Failed to load plugin:', err);
scope.appEvent('alert-error', ['Plugin Load Error', 'Failed to load plugin ' + scope.dsMeta.id + ', ' + err]);
});
});
}
};
});
module.service('dynamicDirectiveSrv', function($compile, $parse, datasourceSrv) {
var self = this;
this.addDirective = function(options, type, editorScope) {
var panelEl = angular.element(document.createElement(options.name + '-' + type));
options.parentElem.append(panelEl);
$compile(panelEl)(editorScope);
};
this.define = function(options) {
var editorScope;
options.scope.$watch(options.datasourceProperty, function(newVal) {
if (editorScope) {
editorScope.$destroy();
options.parentElem.empty();
}
editorScope = options.scope.$new();
datasourceSrv.get(newVal).then(function(ds) {
self.addDirective(options, ds.meta.id, editorScope);
});
});
};
});
module.directive('datasourceEditorView', function(dynamicDirectiveSrv) {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
dynamicDirectiveSrv.define({
datasourceProperty: attrs.datasource,
name: attrs.name,
scope: scope,
parentElem: elem,
});
}
};
});
module.directive('queryEditorLoader', function($compile, $parse, datasourceSrv) {
return {
restrict: 'E',
link: function(scope, elem) {
var editorScope;
scope.$watch("panel.datasource", function() {
var datasource = scope.target.datasource || scope.panel.datasource;
datasourceSrv.get(datasource).then(function(ds) {
if (editorScope) {
editorScope.$destroy();
elem.empty();
}
editorScope = scope.$new();
editorScope.datasource = ds;
if (!scope.target.refId) {
scope.target.refId = 'A';
}
var panelEl = angular.element(document.createElement('metric-query-editor-' + ds.meta.id));
elem.append(panelEl);
$compile(panelEl)(editorScope);
});
});
}
};
});
module.directive('panelResizer', function($rootScope) {
return {
restrict: 'E',

View File

@ -0,0 +1,22 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
/** @ngInject */
function panelLoader($parse, dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
directive: scope => {
let modulePath = config.panels[scope.panel.type].module;
return System.import(modulePath).then(function(panelModule) {
return {
name: 'panel-directive-' + scope.panel.type,
fn: panelModule.panel,
};
});
},
});
}
angular.module('grafana.directives').directive('panelLoader', panelLoader);

View File

@ -0,0 +1,48 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watch: "panel.datasource",
directive: scope => {
let datasource = scope.target.datasource || scope.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
if (!scope.target.refId) {
scope.target.refId = 'A';
}
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'metrics-query-editor-' + ds.meta.id,
fn: dsModule.metricsQueryEditor,
};
});
});
}
});
}
/** @ngInject */
function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watch: "panel.datasource",
directive: scope => {
return datasourceSrv.get(scope.panel.datasource).then(ds => {
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'metrics-query-options-' + ds.meta.id,
fn: dsModule.metricsQueryOptions
};
});
});
}
});
}
angular.module('grafana.directives')
.directive('metricsQueryEditor', metricsQueryEditor)
.directive('metricsQueryOptions', metricsQueryOptions);

View File

@ -1,5 +1,5 @@
import '../playlist_edit_ctrl';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
describe('PlaylistEditCtrl', function() {
@ -83,4 +83,4 @@ describe('PlaylistEditCtrl', function() {
});
});
});
});
});

View File

@ -8,12 +8,12 @@ import 'angular-sanitize';
import 'angular-dragdrop';
import 'angular-bindonce';
import 'angular-ui';
import 'app/core/core';
import $ from 'jquery';
import angular from 'angular';
import config from 'app/core/config';
import _ from 'lodash';
import {coreModule} from './core/core';
export class GrafanaApp {
registerFunctions: any;
@ -67,6 +67,9 @@ export class GrafanaApp {
this.useModule(angular.module(moduleName, []));
});
// makes it possible to add dynamic stuff
this.useModule(coreModule);
var preBootRequires = [System.import('app/features/all')];
var pluginModules = config.bootData.pluginModules || [];

View File

@ -1,220 +0,0 @@
<div class="editor-row" style="margin-bottom: 20px;">
<span style="float: right; font-size: 12px;"><i>Last updated by Grafana October 4, 2015 12:15:04 by $username</i></span>
<div class="section">
<h5>General Alerting Options</h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
Alert Title
</li>
<li>
<input type="text" class="input-xlarge tight-form-input"></input>
</li>
<li class="tight-form-item">
Alerting Backend
</li>
<li>
<select class="input-medium tight-form-input">
<option>Grafana Alerting</option>
</select>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="alerting-enabled">Enabled</label>
<input class="cr1" id="alerting-enabled" type="checkbox">
<label for="alerting-enabled" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<h5>Choose your query:</h5>
<p>Select an exising query to alert on:</p>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item">None</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item last">aliasByNode(2)</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">B</li>
<li class="tight-form-item last"><span class="query-keyword">Metric:</span> us-west-2 AWS/EC2 CPUUtilization <span class="query-keyword">Stats:</span> Minimum Maximum <span class="query-keyword">Dimensions</span> InstanceIS <span class="query-segment-operator">=</span> i-b0e8a447 <span class="query-keyword">Alias</span> {{stat}} <span class="query-keyword">Period</span> 60</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">C</li>
<li class="tight-form-item last"><span class="query-keyword">Query:</span> avg(counters_logins) by(server) <span class="query-keyword">Legend Format:</span> {{app}} - {{server}} <span class="query-keyword">Step:</span> 1s <span class="query-keyword">Resolution:</span> 1/2</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">D</li>
<li class="tight-form-item last"><span class="query-keyword">SELECT</span> mean(value) <span class="query-keyword">FROM</span> logins.count <span class="query-keyword">WHERE</span> hostname <span class="query-segment-operator">=</span> /$Hostname$/ <span class="query-keyword">GROUP BY</span> time($internal) hostname</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" checked /></li>
<li class="tight-form-item" style="min-width: 15px; text-align: center">E</li>
<li class="tight-form-item last"><span class="query-keyword">Metric:</span> apps.backend.backend_01.counters.requests.count <span class="query-keyword">Alias:</span> Bristow <span class="query-keyword">Aggregator:</span> Sum <span class="query-keyword">Downsample:</span> 1m <span class="query-keyword">Aggregator</span> Sum <span class="query-keyword">Tags</span> host = test</li>
<li><div class="copy-query" bs-tooltip="'Copy to custom query'" data-placement="top"></div></li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<p>Or write a new custom alerting query:</p>
<div class="section">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item"><input type="radio" class="radio input-small" name="query" style="margin: 0 4px 4px;" /></li>
<li class="tight-form-item">
<a class="pointer">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
select metric
</li>
<li>
<a class="tight-form-item tight-form-func last dropdown-toggle"><i class="fa fa-plus"></i></a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 10px;">
<div class="section">
<h5>Define Your States</h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">
by
</li>
<li>
<select class="input-medium tight-form-input">
<option>Averaging</option>
</select>
</li>
<li class="tight-form-item">
the values in the query over the last
</li>
<li>
<input type="text" class="input-mini tight-form-input last"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row" style="margin-bottom: 20px;">
<div class="section">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
<span class="alert-state alert-state-warning">Warn</span>
</li>
<li>
<input type="text" class="input-mini tight-form-input" value=">" style="text-align: center;"></input>
</li>
<li>
<input type="text" class="input-mini tight-form-input" value="#B" style="text-align: center;"></input>
</li>
<li class="tight-form-item">
.notify
</li>
<li class="alert-notify-emails">
<bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="state-enabled">Enabled</label>
<input class="cr1" id="state-enabled" type="checkbox">
<label for="state-enabled" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
<span class="alert-state alert-state-critical">Critical</span>
</li>
<li>
<input type="text" class="input-mini tight-form-input"></input>
</li>
<li>
<input type="text" class="input-mini tight-form-input"></input>
</li>
<li class="tight-form-item">
.notify
</li>
<li class="alert-notify-emails">
<bootstrap-tagsinput tagclass="label label-tag label-tag-email"></bootstrap-tagsinput>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="state-enabled2">Enabled</label>
<input class="cr1" id="state-enabled2" type="checkbox">
<label for="state-enabled2" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>What to Say <span style="float: right; font-size: 12px; font-weight: normal;"><a href="#">Variables</a> | <a href="#">Preview</a></span></h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Summary
</li>
<li>
<input type="text" class="input-xxlarge tight-form-input last"></input>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px;">
Description
</li>
<li>
<textarea class="tight-form-textarea input-xxlarge last"></textarea>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -83,7 +83,7 @@
<div ng-repeat="(name, panel) in row.panels track by panel.id" class="panel" ui-draggable="!dashboardViewState.fullscreen" drag="panel.id"
ui-on-Drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
<panel-loader type="panel.type" class="panel-margin"></panel-loader>
<panel-loader class="panel-margin"></panel-loader>
</div>
<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">

View File

@ -24,6 +24,10 @@
<td><span class="label label-info">F</span></td>
<td>Open dashboard search view (also contains import/playlist controls)</td>
</tr>
<tr>
<td><span class="label label-info">R</span></td>
<td>Refresh (Fetches new data and rerenders panels)</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+S</span></td>
<td>Save dashboard</td>
@ -36,10 +40,6 @@
<td><span class="label label-info">CTRL+Z</span></td>
<td>Zoom out</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+R</span></td>
<td>Refresh (Fetches new data and rerenders panels)</td>
</tr>
<tr>
<td><span class="label label-info">CTRL+O</span></td>
<td>Enable/Disable shared graph crosshair</td>

View File

@ -1,8 +1,8 @@
<div class="editor-row">
<div class="tight-form-container">
<query-editor-loader ng-repeat="target in panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
</query-editor-loader>
<metrics-query-editor ng-repeat="target in panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
</metrics-query-editor>
</div>
<div style="margin: 20px 0 0 0">
@ -26,7 +26,7 @@
</div>
<datasource-editor-view datasource="panel.datasource" name="metric-query-options"></datasource-editor-view>
<metrics-query-options></metrics-query-options>
</div>
<div class="editor-row" style="margin-top: 30px">

View File

@ -3,7 +3,6 @@ define([
'lodash',
'moment',
'app/core/utils/datemath',
'./query_ctrl',
],
function (angular, _, moment, dateMath) {
'use strict';

View File

@ -1,39 +1,27 @@
define([
'angular',
'./datasource',
'./query_parameter_ctrl',
'./query_ctrl',
],
function (angular, CloudWatchDatasource) {
function (CloudWatchDatasource) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorCloudwatch', function() {
function metricsQueryEditor() {
return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
});
}
module.directive('annotationsQueryEditorCloudwatch', function() {
function annotationsQueryEditor() {
return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
});
}
module.directive('cloudwatchQueryParameter', function() {
return {
templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
controller: 'CloudWatchQueryParameterCtrl',
restrict: 'E',
scope: {
target: "=",
datasourceName: "@",
onChange: "&",
}
};
});
module.directive('datasourceCustomSettingsViewCloudwatch', function() {
function configView() {
return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/edit_view.html'};
});
}
return {
Datasource: CloudWatchDatasource
Datasource: CloudWatchDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
};
});

View File

@ -1 +1 @@
<cloudwatch-query-parameter target="currentAnnotation" datasource-name="{{currentAnnotation.datasource}}"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>

View File

@ -35,4 +35,4 @@
<div class="clearfix"></div>
</div>
<cloudwatch-query-parameter target="target" datasource-name="{{datasource.name}}" on-change="refreshMetricData()"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="target" datasource="datasource" on-change="refreshMetricData()"></cloudwatch-query-parameter>

View File

@ -7,6 +7,19 @@ function (angular, _) {
var module = angular.module('grafana.controllers');
module.directive('cloudwatchQueryParameter', function() {
return {
templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
controller: 'CloudWatchQueryParameterCtrl',
restrict: 'E',
scope: {
target: "=",
datasource: "=",
onChange: "&",
}
};
});
module.controller('CloudWatchQueryParameterCtrl', function($scope, templateSrv, uiSegmentSrv, datasourceSrv, $q) {
$scope.init = function() {
@ -38,12 +51,9 @@ function (angular, _) {
$scope.removeDimSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove dimension --'});
$scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
datasourceSrv.get($scope.datasourceName).then(function(datasource) {
$scope.datasource = datasource;
if (_.isEmpty($scope.target.region)) {
$scope.target.region = $scope.datasource.getDefaultRegion();
}
});
if (_.isEmpty($scope.target.region)) {
$scope.target.region = $scope.datasource.getDefaultRegion();
}
if (!$scope.onChange) {
$scope.onChange = function() {};

View File

@ -228,10 +228,8 @@ describe('CloudWatchDatasource', function() {
switch (params.data.action) {
case 'DescribeAlarmsForMetric':
return ctx.$q.when({data: alarmResponse});
break;
case 'DescribeAlarmHistory':
return ctx.$q.when({data: historyResponse});
break;
}
};
});

View File

@ -8,6 +8,20 @@ function (angular, _, queryDef) {
var module = angular.module('grafana.directives');
module.directive('elasticBucketAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
module.controller('ElasticBucketAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
var bucketAggs = $scope.target.bucketAggs;

View File

@ -8,6 +8,21 @@ function (angular, _, queryDef) {
var module = angular.module('grafana.directives');
module.directive('elasticMetricAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
esVersion: '='
}
};
});
module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) {
var metricAggs = $scope.target.metrics;

View File

@ -1,60 +1,30 @@
define([
'angular',
'./datasource',
'./edit_view',
'./bucket_agg',
'./metric_agg',
],
function (angular, ElasticDatasource, editView) {
function (ElasticDatasource, editView) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorElasticsearch', function() {
function metricsQueryEditor() {
return {controller: 'ElasticQueryCtrl', templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.editor.html'};
});
}
module.directive('metricQueryOptionsElasticsearch', function() {
function metricsQueryOptions() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/query.options.html'};
});
}
module.directive('annotationsQueryEditorElasticsearch', function() {
function annotationsQueryEditor() {
return {templateUrl: 'app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
});
module.directive('elasticMetricAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html',
controller: 'ElasticMetricAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
esVersion: '='
}
};
});
module.directive('elasticBucketAgg', function() {
return {
templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html',
controller: 'ElasticBucketAggCtrl',
restrict: 'E',
scope: {
target: "=",
index: "=",
onChange: "&",
getFields: "&",
}
};
});
module.directive('datasourceCustomSettingsViewElasticsearch', editView.default);
}
return {
Datasource: ElasticDatasource,
configView: editView.default,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -1,14 +1,14 @@
<div class="editor-row">
<div class="section" ng-if="currentAnnotation.index">
<div class="section" ng-if="annotation.index">
<h5>Index name</h5>
<div class="editor-option">
<input type="text" class="span4" ng-model='currentAnnotation.index' placeholder="events-*"></input>
<input type="text" class="span4" ng-model='annotation.index' placeholder="events-*"></input>
</div>
</div>
<div class="section">
<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
<div class="editor-option">
<input type="text" class="span6" ng-model='currentAnnotation.query' placeholder="tags:deploy"></input>
<input type="text" class="span6" ng-model='annotation.query' placeholder="tags:deploy"></input>
</div>
</div>
</div>
@ -18,22 +18,22 @@
<h5>Field mappings</h5>
<div class="editor-option">
<label class="small">Time</label>
<input type="text" class="input-small" ng-model='currentAnnotation.timeField' placeholder="@timestamp"></input>
<input type="text" class="input-small" ng-model='annotation.timeField' placeholder="@timestamp"></input>
</div>
<div class="editor-option">
<label class="small">Title</label>
<input type="text" class="input-small" ng-model='currentAnnotation.titleField' placeholder="desc"></input>
<input type="text" class="input-small" ng-model='annotation.titleField' placeholder="desc"></input>
</div>
<div class="editor-option">
<label class="small">Tags</label>
<input type="text" class="input-small" ng-model='currentAnnotation.tagsField' placeholder="tags"></input>
<input type="text" class="input-small" ng-model='annotation.tagsField' placeholder="tags"></input>
</div>
<div class="editor-option">
<label class="small">Text</label>
<input type="text" class="input-small" ng-model='currentAnnotation.textField' placeholder=""></input>
<input type="text" class="input-small" ng-model='annotation.textField' placeholder=""></input>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {describe, beforeEach, it, expect} from 'test/lib/common';
import ElasticResponse from '../elastic_response';
describe('ElasticResponse', function() {
@ -497,11 +497,6 @@ describe('ElasticResponse', function() {
});
});
describe('', function() {
});
describe('Raw documents query', function() {
beforeEach(function() {
targets = [{ refId: 'A', metrics: [{type: 'raw_document', id: '1'}], bucketAggs: [] }];

View File

@ -5,10 +5,14 @@ import {GrafanaDatasource} from './datasource';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorGrafana', function() {
function grafanaMetricsQueryEditor() {
return {templateUrl: 'app/plugins/datasource/grafana/partials/query.editor.html'};
});
}
export {GrafanaDatasource, GrafanaDatasource as Datasource};
export {
GrafanaDatasource,
GrafanaDatasource as Datasource,
grafanaMetricsQueryEditor as metricsQueryEditor
};

View File

@ -1,29 +1,30 @@
define([
'angular',
'./datasource',
],
function (angular, GraphiteDatasource) {
function (GraphiteDatasource) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorGraphite', function() {
function metricsQueryEditor() {
return {controller: 'GraphiteQueryCtrl', templateUrl: 'app/plugins/datasource/graphite/partials/query.editor.html'};
});
}
module.directive('metricQueryOptionsGraphite', function() {
function metricsQueryOptions() {
return {templateUrl: 'app/plugins/datasource/graphite/partials/query.options.html'};
});
}
module.directive('annotationsQueryEditorGraphite', function() {
function annotationsQueryEditor() {
return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
});
}
module.directive('datasourceCustomSettingsViewGraphite', function() {
function configView() {
return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
});
}
return {
Datasource: GraphiteDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -1,14 +1,14 @@
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite target expression</label>
<input type="text" class="span10" ng-model='currentAnnotation.target' placeholder=""></input>
<input type="text" class="span10" ng-model='annotation.target' placeholder=""></input>
</div>
</div>
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite event tags</label>
<input type="text" ng-model='currentAnnotation.tags' placeholder=""></input>
<input type="text" ng-model='annotation.tags' placeholder=""></input>
</div>
</div>

View File

@ -1,6 +1,5 @@
///<amd-dependency path="app/plugins/datasource/graphite/gfunc" name="gfunc" />
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import {describe, it, expect} from 'test/lib/common';
import gfunc from '../gfunc';
describe('when creating func instance from func names', function() {

View File

@ -1,29 +1,30 @@
define([
'angular',
'./datasource',
],
function (angular, InfluxDatasource) {
function (InfluxDatasource) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorInfluxdb', function() {
function influxMetricsQueryEditor() {
return {controller: 'InfluxQueryCtrl', templateUrl: 'app/plugins/datasource/influxdb/partials/query.editor.html'};
});
}
module.directive('metricQueryOptionsInfluxdb', function() {
function influxMetricsQueryOptions() {
return {templateUrl: 'app/plugins/datasource/influxdb/partials/query.options.html'};
});
}
module.directive('annotationsQueryEditorInfluxdb', function() {
function influxAnnotationsQueryEditor() {
return {templateUrl: 'app/plugins/datasource/influxdb/partials/annotations.editor.html'};
});
}
module.directive('datasourceCustomSettingsViewInfluxdb', function() {
function influxConfigView() {
return {templateUrl: 'app/plugins/datasource/influxdb/partials/config.html'};
});
}
return {
Datasource: InfluxDatasource
Datasource: InfluxDatasource,
metricsQueryEditor: influxMetricsQueryEditor,
metricsQueryOptions: influxMetricsQueryOptions,
annotationsQueryEditor: influxAnnotationsQueryEditor,
configView: influxConfigView,
};
});

View File

@ -2,7 +2,7 @@
<div class="section">
<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
<div class="editor-option">
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where $timeFilter"></input>
<input type="text" class="span10" ng-model='annotation.query' placeholder="select text from events where $timeFilter"></input>
</div>
</div>
</div>
@ -12,17 +12,17 @@
<h5>Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
<div class="editor-option">
<label class="small">Title</label>
<input type="text" class="input-small" ng-model='currentAnnotation.titleColumn' placeholder=""></input>
<input type="text" class="input-small" ng-model='annotation.titleColumn' placeholder=""></input>
</div>
<div class="editor-option">
<label class="small">Tags</label>
<input type="text" class="input-small" ng-model='currentAnnotation.tagsColumn' placeholder=""></input>
<input type="text" class="input-small" ng-model='annotation.tagsColumn' placeholder=""></input>
</div>
<div class="editor-option">
<label class="small">Text</label>
<input type="text" class="input-small" ng-model='currentAnnotation.textColumn' placeholder=""></input>
<input type="text" class="input-small" ng-model='annotation.textColumn' placeholder=""></input>
</div>
</div>
</div>

View File

@ -126,9 +126,7 @@ function addMathStrategy(selectParts, partModel) {
if (selectParts[partCount-2].def.type === 'math') {
selectParts[partCount-2] = partModel;
return;
}
// if last is alias add it before
else if (selectParts[partCount-1].def.type === 'alias') {
} else if (selectParts[partCount-1].def.type === 'alias') { // if last is alias add it before
selectParts.splice(partCount-1, 0, partModel);
return;
}
@ -399,8 +397,7 @@ class QueryPart {
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
} else {
this.params[index] = strValue;
}

View File

@ -3,12 +3,5 @@
import angular from 'angular';
import {MixedDatasource} from './datasource';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorMixed', function() {
return {templateUrl: 'app/plugins/datasource/mixed/partials/query.editor.html'};
});
export {MixedDatasource, MixedDatasource as Datasource};

View File

@ -1,24 +1,23 @@
define([
'angular',
'./datasource',
],
function (angular, OpenTsDatasource) {
function (OpenTsDatasource) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorOpentsdb', function() {
function metricsQueryEditor() {
return {
controller: 'OpenTSDBQueryCtrl',
templateUrl: 'app/plugins/datasource/opentsdb/partials/query.editor.html',
};
});
}
module.directive('datasourceCustomSettingsViewOpentsdb', function() {
function configView() {
return {templateUrl: 'app/plugins/datasource/opentsdb/partials/config.html'};
});
}
return {
Datasource: OpenTsDatasource
Datasource: OpenTsDatasource,
metricsQueryEditor: metricsQueryEditor,
configView: configView,
};
});

View File

@ -1,21 +1,20 @@
define([
'angular',
'./datasource',
],
function (angular, PromDatasource) {
function (PromDatasource) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorPrometheus', function() {
function metricsQueryEditor() {
return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
});
}
module.directive('datasourceCustomSettingsViewPrometheus', function() {
function configView() {
return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'};
});
}
return {
Datasource: PromDatasource
Datasource: PromDatasource,
metricsQueryEditor: metricsQueryEditor,
configView: configView,
};
});

View File

@ -11,14 +11,8 @@ function (angular, app, _, config, PanelMeta) {
var module = angular.module('grafana.panels.dashlist', []);
app.useModule(module);
module.directive('grafanaPanelDashlist', function() {
return {
controller: 'DashListPanelCtrl',
templateUrl: 'app/plugins/panel/dashlist/module.html',
};
});
module.controller('DashListPanelCtrl', function($scope, panelSrv, backendSrv) {
/** @ngInject */
function DashListPanelCtrl($scope, panelSrv, backendSrv) {
$scope.panelMeta = new PanelMeta({
panelName: 'Dashboard list',
@ -73,5 +67,16 @@ function (angular, app, _, config, PanelMeta) {
};
$scope.init();
});
}
function dashListPanelDirective() {
return {
controller: DashListPanelCtrl,
templateUrl: 'app/plugins/panel/dashlist/module.html',
};
}
return {
panel: dashListPanelDirective
};
});

View File

@ -4,7 +4,7 @@ define([
'moment',
'lodash',
'app/core/utils/kbn',
'./graph.tooltip',
'./graph_tooltip',
'jquery.flot',
'jquery.flot.events',
'jquery.flot.selection',

View File

@ -0,0 +1,2 @@
declare var GraphTooltip: any;
export default GraphTooltip;

View File

@ -0,0 +1,3 @@
declare var panel: any;
declare var GraphCtrl: any;
export {panel, GraphCtrl};

View File

@ -12,16 +12,8 @@ define([
function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
'use strict';
var module = angular.module('grafana.panels.graph');
module.directive('grafanaPanelGraph', function() {
return {
controller: 'GraphCtrl',
templateUrl: 'app/plugins/panel/graph/module.html',
};
});
module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) {
/** @ngInject */
function GraphCtrl($scope, $rootScope, panelSrv, annotationsSrv, panelHelper) {
$scope.panelMeta = new PanelMeta({
panelName: 'Graph',
@ -294,7 +286,17 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
};
panelSrv.init($scope);
}
});
function graphPanelDirective() {
return {
controller: GraphCtrl,
templateUrl: 'app/plugins/panel/graph/module.html',
};
}
return {
GraphCtrl: GraphCtrl,
panel: graphPanelDirective,
};
});

View File

@ -0,0 +1,53 @@
///<reference path="../../../../headers/common.d.ts" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import 'app/features/panel/panel_srv';
import 'app/features/panel/panel_helper';
import angular from 'angular';
import {GraphCtrl} from '../module';
import helpers from '../../../../../test/specs/helpers';
angular.module('grafana.controllers').controller('GraphCtrl', GraphCtrl);
describe('GraphCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('GraphCtrl'));
describe('get_data with 2 series', function() {
beforeEach(function() {
ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([]));
ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
data: [
{ target: 'test.cpu1', datapoints: [[1, 10]]},
{ target: 'test.cpu2', datapoints: [[1, 10]]}
]
}));
ctx.scope.render = sinon.spy();
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});
it('should send time series to render', function() {
var data = ctx.scope.render.getCall(0).args[0];
expect(data.length).to.be(2);
});
describe('get_data failure following success', function() {
beforeEach(function() {
ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});
});
});
});

View File

@ -0,0 +1,230 @@
///<reference path="../../../../headers/common.d.ts" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import '../module';
import angular from 'angular';
import $ from 'jquery';
import helpers from '../../../../../test/specs/helpers';
import TimeSeries from '../../../../core/time_series2';
describe('grafanaGraph', function() {
beforeEach(angularMocks.module('grafana.directives'));
function graphScenario(desc, func) {
describe(desc, function() {
var ctx: any = {};
ctx.setup = function(setupFunc) {
beforeEach(angularMocks.module(function($provide) {
$provide.value("timeSrv", new helpers.TimeSrvStub());
}));
beforeEach(angularMocks.inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
var element = angular.element("<div style='width:500px' grafana-graph><div>");
scope.height = '200px';
scope.panel = {
legend: {},
grid: { },
y_formats: [],
seriesOverrides: [],
tooltip: {
shared: true
}
};
scope.panelRenderingComplete = sinon.spy();
scope.appEvent = sinon.spy();
scope.onAppEvent = sinon.spy();
scope.hiddenSeries = {};
scope.dashboard = { timezone: 'browser' };
scope.range = {
from: new Date('2014-08-09 10:00:00'),
to: new Date('2014-09-09 13:00:00')
};
ctx.data = [];
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
alias: 'series1'
}));
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
alias: 'series2'
}));
setupFunc(scope, ctx.data);
$compile(element)(scope);
scope.$digest();
$.plot = ctx.plotSpy = sinon.spy();
scope.$emit('render', ctx.data);
ctx.plotData = ctx.plotSpy.getCall(0).args[1];
ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
}));
};
func(ctx);
});
}
graphScenario('simple lines options', function(ctx) {
ctx.setup(function(scope) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.linewidth = 3;
scope.panel.steppedLine = true;
});
it('should configure plot with correct options', function() {
expect(ctx.plotOptions.series.lines.show).to.be(true);
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
expect(ctx.plotOptions.series.lines.steps).to.be(true);
});
});
graphScenario('grid thresholds 100, 200', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 100,
threshold1Color: "#111",
threshold2: 200,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(100);
expect(markings[0].yaxis.to).to.be(200);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(200);
expect(markings[1].yaxis.to).to.be(Infinity);
});
});
graphScenario('inverted grid thresholds 200, 100', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 200,
threshold1Color: "#111",
threshold2: 100,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(200);
expect(markings[0].yaxis.to).to.be(100);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(100);
expect(markings[1].yaxis.to).to.be(-Infinity);
});
});
graphScenario('grid thresholds from zero', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 0,
threshold1Color: "#111",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(0);
});
});
graphScenario('when logBase is log 10', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
leftMax: null,
rightMax: null,
leftMin: null,
rightMin: null,
leftLogBase: 10,
};
});
it('should apply axis transform and ticks', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.transform(100)).to.be(Math.log(100+0.1));
expect(axis.ticks[0]).to.be(0);
expect(axis.ticks[1]).to.be(1);
});
});
graphScenario('should use timeStep for barWidth', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.bars = true;
data[0] = new TimeSeries({
datapoints: [[1,10],[2,20]],
alias: 'series1',
});
});
it('should set barWidth', function() {
expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
});
});
graphScenario('series option overrides, fill & points', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.seriesOverrides = [
{ alias: 'test', fill: 0, points: true }
];
data[1].alias = 'test';
});
it('should match second series and fill zero, and enable points', function() {
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotData[1].lines.fill).to.be(0.001);
expect(ctx.plotData[1].points.show).to.be(true);
});
});
graphScenario('should order series order according to zindex', function(ctx) {
ctx.setup(function(scope) {
scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
});
it('should move zindex 2 last', function() {
expect(ctx.plotData[0].alias).to.be('series2');
expect(ctx.plotData[1].alias).to.be('series1');
});
});
graphScenario('when series is hidden', function(ctx) {
ctx.setup(function(scope) {
scope.hiddenSeries = {'series2': true};
});
it('should remove datapoints and disable stack', function() {
expect(ctx.plotData[0].alias).to.be('series1');
expect(ctx.plotData[1].data.length).to.be(0);
expect(ctx.plotData[1].stack).to.be(false);
});
});
graphScenario('when stack and percent', function(ctx) {
ctx.setup(function(scope) {
scope.panel.percentage = true;
scope.panel.stack = true;
});
it('should show percentage', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.tickFormatter(100, axis)).to.be("100%");
});
});
});

View File

@ -0,0 +1,171 @@
///<reference path="../../../../headers/common.d.ts" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from '../../../../../test/lib/common';
import $ from 'jquery';
import GraphTooltip from '../graph_tooltip';
var scope = {
appEvent: sinon.spy(),
onAppEvent: sinon.spy(),
};
var elem = $('<div></div>');
var dashboard = { };
function describeSharedTooltip(desc, fn) {
var ctx: any = {};
ctx.scope = scope;
ctx.scope.panel = {
tooltip: {
shared: true
},
legend: { },
stack: false
};
ctx.setup = function(setupFn) {
ctx.setupFn = setupFn;
};
describe(desc, function() {
beforeEach(function() {
ctx.setupFn();
var tooltip = new GraphTooltip(elem, dashboard, scope);
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
});
fn(ctx);
});
}
describeSharedTooltip("steppedLine false, stack false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], lines: {} },
{ data: [[10, 2], [12, 3]], lines: {} }
];
ctx.pos = { x: 11 };
});
it('should return 2 series', function() {
expect(ctx.results.length).to.be(2);
});
it('should add time to results array', function() {
expect(ctx.results.time).to.be(10);
});
it('should set value and hoverIndex', function() {
expect(ctx.results[0].value).to.be(15);
expect(ctx.results[1].value).to.be(2);
expect(ctx.results[0].hoverIndex).to.be(0);
});
});
describeSharedTooltip("one series is hidden", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], },
{ data: [] }
];
ctx.pos = { x: 11 };
});
});
describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10,15], [12,20]],
},
stack: true,
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: true
}
];
ctx.scope.panel.stack = true;
ctx.pos = { x: 11 };
});
it('should show stacked value', function() {
expect(ctx.results[1].value).to.be(17);
});
});
describeSharedTooltip("steppedLine false, stack true, individual false, series stack false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 15], [12, 20]],
},
stack: true
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: false
}
];
ctx.scope.panel.stack = true;
ctx.pos = { x: 11 };
});
it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2);
});
});
describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 15], [12, 20]],
},
stack: true
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: false
}
];
ctx.scope.panel.stack = true;
ctx.scope.panel.tooltip.value_type = 'individual';
ctx.pos = { x: 11 };
});
it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2);
});
});

View File

@ -0,0 +1,245 @@
define([
'angular',
'app/app',
'lodash',
'app/core/utils/kbn',
'app/core/time_series',
'app/features/panel/panel_meta',
],
function (angular, app, _, kbn, TimeSeries, PanelMeta) {
'use strict';
/** @ngInject */
function SingleStatCtrl($scope, panelSrv, panelHelper) {
$scope.panelMeta = new PanelMeta({
panelName: 'Singlestat',
editIcon: "fa fa-dashboard",
fullscreen: true,
metricsEditor: true
});
$scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html');
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
// Set and populate defaults
var _d = {
links: [],
datasource: null,
maxDataPoints: 100,
interval: null,
targets: [{}],
cacheTimeout: null,
format: 'none',
prefix: '',
postfix: '',
nullText: null,
valueMaps: [
{ value: 'null', op: '=', text: 'N/A' }
],
nullPointMode: 'connected',
valueName: 'avg',
prefixFontSize: '50%',
valueFontSize: '80%',
postfixFontSize: '50%',
thresholds: '',
colorBackground: false,
colorValue: false,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
sparkline: {
show: false,
full: false,
lineColor: 'rgb(31, 120, 193)',
fillColor: 'rgba(31, 118, 189, 0.18)',
}
};
_.defaults($scope.panel, _d);
$scope.unitFormats = kbn.getUnitFormats();
$scope.setUnitFormat = function(subItem) {
$scope.panel.format = subItem.value;
$scope.render();
};
$scope.init = function() {
panelSrv.init($scope);
};
$scope.refreshData = function(datasource) {
panelHelper.updateTimeRange($scope);
return panelHelper.issueMetricQuery($scope, datasource)
.then($scope.dataHandler, function(err) {
$scope.series = [];
$scope.render();
throw err;
});
};
$scope.loadSnapshot = function(snapshotData) {
panelHelper.updateTimeRange($scope);
$scope.dataHandler(snapshotData);
};
$scope.dataHandler = function(results) {
$scope.series = _.map(results.data, $scope.seriesHandler);
$scope.render();
};
$scope.seriesHandler = function(seriesData) {
var series = new TimeSeries({
datapoints: seriesData.datapoints,
alias: seriesData.target,
});
series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
return series;
};
$scope.setColoring = function(options) {
if (options.background) {
$scope.panel.colorValue = false;
$scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
}
else {
$scope.panel.colorBackground = false;
$scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
}
$scope.render();
};
$scope.invertColorOrder = function() {
var tmp = $scope.panel.colors[0];
$scope.panel.colors[0] = $scope.panel.colors[2];
$scope.panel.colors[2] = tmp;
$scope.render();
};
$scope.getDecimalsForValue = function(value) {
if (_.isNumber($scope.panel.decimals)) {
return { decimals: $scope.panel.decimals, scaledDecimals: null };
}
var delta = value / 2;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
var magn = Math.pow(10, -dec),
norm = delta / magn, // norm is between 1.0 and 10.0
size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
// reduce starting decimals if not needed
if (Math.floor(value) === value) { dec = 0; }
var result = {};
result.decimals = Math.max(0, dec);
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return result;
};
$scope.render = function() {
var data = {};
$scope.setValues(data);
data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
return Number(strVale.trim());
});
data.colorMap = $scope.panel.colors;
$scope.data = data;
$scope.$broadcast('render');
};
$scope.setValues = function(data) {
data.flotpairs = [];
if($scope.series.length > 1) {
$scope.inspector.error = new Error();
$scope.inspector.error.message = 'Multiple Series Error';
$scope.inspector.error.data = 'Metric query returns ' + $scope.series.length +
' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series);
throw $scope.inspector.error;
}
if ($scope.series && $scope.series.length > 0) {
var lastPoint = _.last($scope.series[0].datapoints);
var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
if (_.isString(lastValue)) {
data.value = 0;
data.valueFormated = lastValue;
data.valueRounded = 0;
} else {
data.value = $scope.series[0].stats[$scope.panel.valueName];
data.flotpairs = $scope.series[0].flotpairs;
var decimalInfo = $scope.getDecimalsForValue(data.value);
var formatFunc = kbn.valueFormats[$scope.panel.format];
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
}
}
// check value to text mappings
for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
var map = $scope.panel.valueMaps[i];
// special null case
if (map.value === 'null') {
if (data.value === null || data.value === void 0) {
data.valueFormated = map.text;
return;
}
continue;
}
// value/number to text mapping
var value = parseFloat(map.value);
if (value === data.value) {
data.valueFormated = map.text;
return;
}
}
if (data.value === null || data.value === void 0) {
data.valueFormated = "no value";
}
};
$scope.removeValueMap = function(map) {
var index = _.indexOf($scope.panel.valueMaps, map);
$scope.panel.valueMaps.splice(index, 1);
$scope.render();
};
$scope.addValueMap = function() {
$scope.panel.valueMaps.push({value: '', op: '=', text: '' });
};
$scope.init();
}
return SingleStatCtrl;
});

View File

@ -1,4 +1,4 @@
<grafana-panel>
<div class="singlestat-panel" singlestat-panel></div>
<div class="singlestat-panel"></div>
<div class="clearfix"></div>
</grafana-panel>

View File

@ -1,253 +1,231 @@
define([
'angular',
'app/app',
'./controller',
'lodash',
'app/core/utils/kbn',
'app/core/time_series',
'app/features/panel/panel_meta',
'./singleStatPanel',
'jquery',
'jquery.flot',
],
function (angular, app, _, kbn, TimeSeries, PanelMeta) {
function (SingleStatCtrl, _, $) {
'use strict';
var module = angular.module('grafana.panels.singlestat');
app.useModule(module);
module.directive('grafanaPanelSinglestat', function() {
/** @ngInject */
function singleStatPanel($location, linkSrv, $timeout, templateSrv) {
return {
controller: 'SingleStatCtrl',
controller: SingleStatCtrl,
templateUrl: 'app/plugins/panel/singlestat/module.html',
};
});
link: function(scope, elem) {
var data, panel, linkInfo, $panelContainer;
var firstRender = true;
module.controller('SingleStatCtrl', function($scope, panelSrv, panelHelper) {
scope.$on('render', function() {
if (firstRender) {
var inner = elem.find('.singlestat-panel');
if (inner.length) {
elem = inner;
$panelContainer = elem.parents('.panel-container');
firstRender = false;
}
}
$scope.panelMeta = new PanelMeta({
panelName: 'Singlestat',
editIcon: "fa fa-dashboard",
fullscreen: true,
metricsEditor: true
});
$scope.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%'];
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/singlestat/editor.html');
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
// Set and populate defaults
var _d = {
links: [],
datasource: null,
maxDataPoints: 100,
interval: null,
targets: [{}],
cacheTimeout: null,
format: 'none',
prefix: '',
postfix: '',
nullText: null,
valueMaps: [
{ value: 'null', op: '=', text: 'N/A' }
],
nullPointMode: 'connected',
valueName: 'avg',
prefixFontSize: '50%',
valueFontSize: '80%',
postfixFontSize: '50%',
thresholds: '',
colorBackground: false,
colorValue: false,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
sparkline: {
show: false,
full: false,
lineColor: 'rgb(31, 120, 193)',
fillColor: 'rgba(31, 118, 189, 0.18)',
}
};
_.defaults($scope.panel, _d);
$scope.unitFormats = kbn.getUnitFormats();
$scope.setUnitFormat = function(subItem) {
$scope.panel.format = subItem.value;
$scope.render();
};
$scope.init = function() {
panelSrv.init($scope);
};
$scope.refreshData = function(datasource) {
panelHelper.updateTimeRange($scope);
return panelHelper.issueMetricQuery($scope, datasource)
.then($scope.dataHandler, function(err) {
$scope.series = [];
$scope.render();
throw err;
render();
scope.panelRenderingComplete();
});
};
$scope.loadSnapshot = function(snapshotData) {
panelHelper.updateTimeRange($scope);
$scope.dataHandler(snapshotData);
};
function setElementHeight() {
try {
var height = scope.height || panel.height || scope.row.height;
if (_.isString(height)) {
height = parseInt(height.replace('px', ''), 10);
}
$scope.dataHandler = function(results) {
$scope.series = _.map(results.data, $scope.seriesHandler);
$scope.render();
};
height -= 5; // padding
height -= panel.title ? 24 : 9; // subtract panel title bar
$scope.seriesHandler = function(seriesData) {
var series = new TimeSeries({
datapoints: seriesData.datapoints,
alias: seriesData.target,
});
elem.css('height', height + 'px');
series.flotpairs = series.getFlotPairs($scope.panel.nullPointMode);
return series;
};
$scope.setColoring = function(options) {
if (options.background) {
$scope.panel.colorValue = false;
$scope.panel.colors = ['rgba(71, 212, 59, 0.4)', 'rgba(245, 150, 40, 0.73)', 'rgba(225, 40, 40, 0.59)'];
}
else {
$scope.panel.colorBackground = false;
$scope.panel.colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)'];
}
$scope.render();
};
$scope.invertColorOrder = function() {
var tmp = $scope.panel.colors[0];
$scope.panel.colors[0] = $scope.panel.colors[2];
$scope.panel.colors[2] = tmp;
$scope.render();
};
$scope.getDecimalsForValue = function(value) {
if (_.isNumber($scope.panel.decimals)) {
return { decimals: $scope.panel.decimals, scaledDecimals: null };
}
var delta = value / 2;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
var magn = Math.pow(10, -dec),
norm = delta / magn, // norm is between 1.0 and 10.0
size;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25) {
size = 2.5;
++dec;
return true;
} catch(e) { // IE throws errors sometimes
return false;
}
}
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
function applyColoringThresholds(value, valueString) {
if (!panel.colorValue) {
return valueString;
}
// reduce starting decimals if not needed
if (Math.floor(value) === value) { dec = 0; }
var color = getColorForValue(value);
if (color) {
return '<span style="color:' + color + '">'+ valueString + '</span>';
}
var result = {};
result.decimals = Math.max(0, dec);
result.scaledDecimals = result.decimals - Math.floor(Math.log(size) / Math.LN10) + 2;
return result;
};
$scope.render = function() {
var data = {};
$scope.setValues(data);
data.thresholds = $scope.panel.thresholds.split(',').map(function(strVale) {
return Number(strVale.trim());
});
data.colorMap = $scope.panel.colors;
$scope.data = data;
$scope.$broadcast('render');
};
$scope.setValues = function(data) {
data.flotpairs = [];
if($scope.series.length > 1) {
$scope.inspector.error = new Error();
$scope.inspector.error.message = 'Multiple Series Error';
$scope.inspector.error.data = 'Metric query returns ' + $scope.series.length +
' series. Single Stat Panel expects a single series.\n\nResponse:\n'+JSON.stringify($scope.series);
throw $scope.inspector.error;
}
if ($scope.series && $scope.series.length > 0) {
var lastPoint = _.last($scope.series[0].datapoints);
var lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
if (_.isString(lastValue)) {
data.value = 0;
data.valueFormated = lastValue;
data.valueRounded = 0;
} else {
data.value = $scope.series[0].stats[$scope.panel.valueName];
data.flotpairs = $scope.series[0].flotpairs;
var decimalInfo = $scope.getDecimalsForValue(data.value);
var formatFunc = kbn.valueFormats[$scope.panel.format];
data.valueFormated = formatFunc(data.value, decimalInfo.decimals, decimalInfo.scaledDecimals);
data.valueRounded = kbn.roundValue(data.value, decimalInfo.decimals);
return valueString;
}
}
// check value to text mappings
for(var i = 0; i < $scope.panel.valueMaps.length; i++) {
var map = $scope.panel.valueMaps[i];
// special null case
if (map.value === 'null') {
if (data.value === null || data.value === void 0) {
data.valueFormated = map.text;
function getColorForValue(value) {
for (var i = data.thresholds.length - 1; i >= 0 ; i--) {
if (value >= data.thresholds[i]) {
return data.colorMap[i];
}
}
return null;
}
function getSpan(className, fontSize, value) {
value = templateSrv.replace(value);
return '<span class="' + className + '" style="font-size:' + fontSize + '">' +
value + '</span>';
}
function getBigValueHtml() {
var body = '<div class="singlestat-panel-value-container">';
if (panel.prefix) { body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, scope.panel.prefix); }
var value = applyColoringThresholds(data.valueRounded, data.valueFormated);
body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
if (panel.postfix) { body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.postfix); }
body += '</div>';
return body;
}
function addSparkline() {
var panel = scope.panel;
var width = elem.width() + 20;
var height = elem.height() || 100;
var plotCanvas = $('<div></div>');
var plotCss = {};
plotCss.position = 'absolute';
if (panel.sparkline.full) {
plotCss.bottom = '5px';
plotCss.left = '-5px';
plotCss.width = (width - 10) + 'px';
var dynamicHeightMargin = height <= 100 ? 5 : (Math.round((height/100)) * 15) + 5;
plotCss.height = (height - dynamicHeightMargin) + 'px';
}
else {
plotCss.bottom = "0px";
plotCss.left = "-5px";
plotCss.width = (width - 10) + 'px';
plotCss.height = Math.floor(height * 0.25) + "px";
}
plotCanvas.css(plotCss);
var options = {
legend: { show: false },
series: {
lines: {
show: true,
fill: 1,
lineWidth: 1,
fillColor: panel.sparkline.fillColor,
},
},
yaxes: { show: false },
xaxis: {
show: false,
mode: "time",
min: scope.range.from.valueOf(),
max: scope.range.to.valueOf(),
},
grid: { hoverable: false, show: false },
};
elem.append(plotCanvas);
var plotSeries = {
data: data.flotpairs,
color: panel.sparkline.lineColor
};
$.plot(plotCanvas, [plotSeries], options);
}
function render() {
if (!scope.data) { return; }
data = scope.data;
panel = scope.panel;
setElementHeight();
var body = getBigValueHtml();
if (panel.colorBackground && !isNaN(data.valueRounded)) {
var color = getColorForValue(data.valueRounded);
if (color) {
$panelContainer.css('background-color', color);
if (scope.fullscreen) {
elem.css('background-color', color);
} else {
elem.css('background-color', '');
}
}
} else {
$panelContainer.css('background-color', '');
elem.css('background-color', '');
}
elem.html(body);
if (panel.sparkline.show) {
addSparkline();
}
elem.toggleClass('pointer', panel.links.length > 0);
if (panel.links.length > 0) {
linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], scope.panel.scopedVars);
} else {
linkInfo = null;
}
}
// drilldown link tooltip
var drilldownTooltip = $('<div id="tooltip" class="">hello</div>"');
elem.mouseleave(function() {
if (panel.links.length === 0) { return;}
drilldownTooltip.detach();
});
elem.click(function() {
if (!linkInfo) { return; }
if (linkInfo.target === '_blank') {
var redirectWindow = window.open(linkInfo.href, '_blank');
redirectWindow.location;
return;
}
continue;
}
// value/number to text mapping
var value = parseFloat(map.value);
if (value === data.value) {
data.valueFormated = map.text;
return;
}
}
if (linkInfo.href.indexOf('http') === 0) {
window.location.href = linkInfo.href;
} else {
$timeout(function() {
$location.url(linkInfo.href);
});
}
if (data.value === null || data.value === void 0) {
data.valueFormated = "no value";
drilldownTooltip.detach();
});
elem.mousemove(function(e) {
if (!linkInfo) { return;}
drilldownTooltip.text('click to go to: ' + linkInfo.title);
drilldownTooltip.place_tt(e.pageX+20, e.pageY-15);
});
}
};
}
$scope.removeValueMap = function(map) {
var index = _.indexOf($scope.panel.valueMaps, map);
$scope.panel.valueMaps.splice(index, 1);
$scope.render();
};
$scope.addValueMap = function() {
$scope.panel.valueMaps.push({value: '', op: '=', text: '' });
};
$scope.init();
});
return {
panel: singleStatPanel
};
});

View File

@ -20,7 +20,7 @@ export class TablePanelCtrl {
metricsEditor: true,
});
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panels/table/options.html');
$scope.panelMeta.addEditorTab('Options', 'app/plugins/panel/table/options.html');
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
var panelDefaults = {

View File

@ -118,7 +118,7 @@ export function tablePanelEditor($q, uiSegmentSrv) {
return {
restrict: 'E',
scope: true,
templateUrl: 'app/plugins/panels/table/editor.html',
templateUrl: 'app/plugins/panel/table/editor.html',
controller: TablePanelEditorCtrl,
};
}

View File

@ -10,7 +10,9 @@ import {TablePanelCtrl} from './controller';
import {TableRenderer} from './renderer';
import {tablePanelEditor} from './editor';
export function tablePanel() {
angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);
function tablePanel() {
'use strict';
return {
restrict: 'E',
@ -102,5 +104,4 @@ export function tablePanel() {
};
}
angular.module('grafana.directives').directive('grafanaPanelTable', tablePanel);
angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);
export {tablePanel as panel};

View File

@ -96,13 +96,12 @@ export class TableRenderer {
}
renderCell(columnIndex, value, addWidthHack = false) {
var value = this.formatColumnValue(columnIndex, value);
value = this.formatColumnValue(columnIndex, value);
var style = '';
if (this.colorState.cell) {
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
this.colorState.cell = null;
}
else if (this.colorState.value) {
} else if (this.colorState.value) {
style = ' style="color:' + this.colorState.value + '"';
this.colorState.value = null;
}

View File

@ -52,8 +52,7 @@ transformers['timeseries_to_columns'] = {
if (!points[timeKey]) {
points[timeKey] = {time: dp[1]};
points[timeKey][i] = dp[0];
}
else {
} else {
points[timeKey][i] = dp[0];
}
}

View File

@ -10,17 +10,7 @@ function (angular, app, _, require, PanelMeta) {
var converter;
var module = angular.module('grafana.panels.text', []);
app.useModule(module);
module.directive('grafanaPanelText', function() {
return {
controller: 'TextPanelCtrl',
templateUrl: 'app/plugins/panel/text/module.html',
};
});
module.controller('TextPanelCtrl', function($scope, templateSrv, $sce, panelSrv) {
function TextPanelCtrl($scope, templateSrv, $sce, panelSrv) {
$scope.panelMeta = new PanelMeta({
panelName: 'Text',
@ -107,5 +97,16 @@ function (angular, app, _, require, PanelMeta) {
};
$scope.init();
});
}
function textPanel() {
return {
controller: TextPanelCtrl,
templateUrl: 'app/plugins/panel/text/module.html',
};
}
return {
panel: textPanel,
};
});

View File

@ -1,8 +1,52 @@
# Plugin API
## Changelog
### 3.0 changes to plugin api changes
2.5.1
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: '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

View File

@ -1,51 +0,0 @@
define([
'./helpers',
'app/features/panel/panel_srv',
'app/features/panel/panel_helper',
'app/plugins/panel/graph/module'
], function(helpers) {
'use strict';
describe('GraphCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(module('grafana.services'));
beforeEach(module('grafana.panels.graph'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('GraphCtrl'));
describe('get_data with 2 series', function() {
beforeEach(function() {
ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([]));
ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
data: [
{ target: 'test.cpu1', datapoints: [[1, 10]]},
{ target: 'test.cpu2', datapoints: [[1, 10]]}
]
}));
ctx.scope.render = sinon.spy();
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});
it('should send time series to render', function() {
var data = ctx.scope.render.getCall(0).args[0];
expect(data.length).to.be(2);
});
describe('get_data failure following success', function() {
beforeEach(function() {
ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error'));
ctx.scope.refreshData(ctx.datasource);
ctx.scope.$digest();
});
});
});
});
});

View File

@ -1,231 +0,0 @@
define([
'./helpers',
'angular',
'jquery',
'app/core/time_series',
'app/plugins/panel/graph/graph'
], function(helpers, angular, $, TimeSeries) {
'use strict';
describe('grafanaGraph', function() {
beforeEach(module('grafana.directives'));
function graphScenario(desc, func) {
describe(desc, function() {
var ctx = {};
ctx.setup = function (setupFunc) {
beforeEach(module(function($provide) {
$provide.value("timeSrv", new helpers.TimeSrvStub());
}));
beforeEach(inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
var element = angular.element("<div style='width:500px' grafana-graph><div>");
scope.height = '200px';
scope.panel = {
legend: {},
grid: { },
y_formats: [],
seriesOverrides: [],
tooltip: {
shared: true
}
};
scope.panelRenderingComplete = sinon.spy();
scope.appEvent = sinon.spy();
scope.onAppEvent = sinon.spy();
scope.hiddenSeries = {};
scope.dashboard = { timezone: 'browser' };
scope.range = {
from: new Date('2014-08-09 10:00:00'),
to: new Date('2014-09-09 13:00:00')
};
ctx.data = [];
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
alias: 'series1'
}));
ctx.data.push(new TimeSeries({
datapoints: [[1,1],[2,2]],
alias: 'series2'
}));
setupFunc(scope, ctx.data);
$compile(element)(scope);
scope.$digest();
$.plot = ctx.plotSpy = sinon.spy();
scope.$emit('render', ctx.data);
ctx.plotData = ctx.plotSpy.getCall(0).args[1];
ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
}));
};
func(ctx);
});
}
graphScenario('simple lines options', function(ctx) {
ctx.setup(function(scope) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.linewidth = 3;
scope.panel.steppedLine = true;
});
it('should configure plot with correct options', function() {
expect(ctx.plotOptions.series.lines.show).to.be(true);
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
expect(ctx.plotOptions.series.lines.steps).to.be(true);
});
});
graphScenario('grid thresholds 100, 200', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 100,
threshold1Color: "#111",
threshold2: 200,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(100);
expect(markings[0].yaxis.to).to.be(200);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(200);
expect(markings[1].yaxis.to).to.be(Infinity);
});
});
graphScenario('inverted grid thresholds 200, 100', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 200,
threshold1Color: "#111",
threshold2: 100,
threshold2Color: "#222",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(200);
expect(markings[0].yaxis.to).to.be(100);
expect(markings[0].color).to.be('#111');
expect(markings[1].yaxis.from).to.be(100);
expect(markings[1].yaxis.to).to.be(-Infinity);
});
});
graphScenario('grid thresholds from zero', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
threshold1: 0,
threshold1Color: "#111",
};
});
it('should add grid markings', function() {
var markings = ctx.plotOptions.grid.markings;
expect(markings[0].yaxis.from).to.be(0);
});
});
graphScenario('when logBase is log 10', function(ctx) {
ctx.setup(function(scope) {
scope.panel.grid = {
leftMax: null,
rightMax: null,
leftMin: null,
rightMin: null,
leftLogBase: 10,
};
});
it('should apply axis transform and ticks', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.transform(100)).to.be(Math.log(100+0.1));
expect(axis.ticks[0]).to.be(0);
expect(axis.ticks[1]).to.be(1);
});
});
graphScenario('should use timeStep for barWidth', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.bars = true;
data[0] = new TimeSeries({
datapoints: [[1,10],[2,20]],
alias: 'series1',
});
});
it('should set barWidth', function() {
expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
});
});
graphScenario('series option overrides, fill & points', function(ctx) {
ctx.setup(function(scope, data) {
scope.panel.lines = true;
scope.panel.fill = 5;
scope.panel.seriesOverrides = [
{ alias: 'test', fill: 0, points: true }
];
data[1].alias = 'test';
});
it('should match second series and fill zero, and enable points', function() {
expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
expect(ctx.plotData[1].lines.fill).to.be(0.001);
expect(ctx.plotData[1].points.show).to.be(true);
});
});
graphScenario('should order series order according to zindex', function(ctx) {
ctx.setup(function(scope) {
scope.panel.seriesOverrides = [{ alias: 'series1', zindex: 2 }];
});
it('should move zindex 2 last', function() {
expect(ctx.plotData[0].alias).to.be('series2');
expect(ctx.plotData[1].alias).to.be('series1');
});
});
graphScenario('when series is hidden', function(ctx) {
ctx.setup(function(scope) {
scope.hiddenSeries = {'series2': true};
});
it('should remove datapoints and disable stack', function() {
expect(ctx.plotData[0].alias).to.be('series1');
expect(ctx.plotData[1].data.length).to.be(0);
expect(ctx.plotData[1].stack).to.be(false);
});
});
graphScenario('when stack and percent', function(ctx) {
ctx.setup(function(scope) {
scope.panel.percentage = true;
scope.panel.stack = true;
});
it('should show percentage', function() {
var axis = ctx.plotOptions.yaxes[0];
expect(axis.tickFormatter(100, axis)).to.be("100%");
});
});
});
});

View File

@ -1,171 +0,0 @@
define([
'jquery',
'app/plugins/panel/graph/graph.tooltip'
], function($, GraphTooltip) {
'use strict';
var scope = {
appEvent: sinon.spy(),
onAppEvent: sinon.spy(),
};
var elem = $('<div></div>');
var dashboard = { };
function describeSharedTooltip(desc, fn) {
var ctx = {};
ctx.scope = scope;
ctx.scope.panel = {
tooltip: {
shared: true
},
legend: { },
stack: false
};
ctx.setup = function(setupFn) {
ctx.setupFn = setupFn;
};
describe(desc, function() {
beforeEach(function() {
ctx.setupFn();
var tooltip = new GraphTooltip(elem, dashboard, scope);
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
});
fn(ctx);
});
}
describeSharedTooltip("steppedLine false, stack false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], lines: {} },
{ data: [[10, 2], [12, 3]], lines: {} }
];
ctx.pos = { x: 11 };
});
it('should return 2 series', function() {
expect(ctx.results.length).to.be(2);
});
it('should add time to results array', function() {
expect(ctx.results.time).to.be(10);
});
it('should set value and hoverIndex', function() {
expect(ctx.results[0].value).to.be(15);
expect(ctx.results[1].value).to.be(2);
expect(ctx.results[0].hoverIndex).to.be(0);
});
});
describeSharedTooltip("one series is hidden", function(ctx) {
ctx.setup(function() {
ctx.data = [
{ data: [[10, 15], [12, 20]], },
{ data: [] }
];
ctx.pos = { x: 11 };
});
});
describeSharedTooltip("steppedLine false, stack true, individual false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10,15], [12,20]],
},
stack: true,
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: true
}
];
ctx.scope.panel.stack = true;
ctx.pos = { x: 11 };
});
it('should show stacked value', function() {
expect(ctx.results[1].value).to.be(17);
});
});
describeSharedTooltip("steppedLine false, stack true, individual false, series stack false", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 15], [12, 20]],
},
stack: true
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: false
}
];
ctx.scope.panel.stack = true;
ctx.pos = { x: 11 };
});
it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2);
});
});
describeSharedTooltip("steppedLine false, stack true, individual true", function(ctx) {
ctx.setup(function() {
ctx.data = [
{
data: [[10, 15], [12, 20]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 15], [12, 20]],
},
stack: true
},
{
data: [[10, 2], [12, 3]],
lines: {},
datapoints: {
pointsize: 2,
points: [[10, 2], [12, 3]],
},
stack: false
}
];
ctx.scope.panel.stack = true;
ctx.scope.panel.tooltip.value_type = 'individual';
ctx.pos = { x: 11 };
});
it('should not show stacked value', function() {
expect(ctx.results[1].value).to.be(2);
});
});
});

View File

@ -1,6 +1,4 @@
declare module "test/specs/helpers" {
let helpers: any;
export default helpers;
}
declare let helpers: any;
export default helpers;

View File

@ -1,11 +1,14 @@
define([
'angular',
'./helpers',
'app/plugins/panel/singlestat/controller',
'app/features/panel/panel_srv',
'app/features/panel/panel_helper',
'app/plugins/panel/singlestat/module'
], function(helpers) {
], function(angular, helpers, SingleStatCtrl) {
'use strict';
angular.module('grafana.controllers').controller('SingleStatCtrl', SingleStatCtrl);
describe('SingleStatCtrl', function() {
var ctx = new helpers.ControllerTestContext();
@ -16,7 +19,7 @@ define([
ctx.setup = function (setupFunc) {
beforeEach(module('grafana.services'));
beforeEach(module('grafana.panels.singlestat'));
beforeEach(module('grafana.controllers'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('SingleStatCtrl'));
@ -24,7 +27,7 @@ define([
beforeEach(function() {
setupFunc();
ctx.datasource.query = sinon.stub().returns(ctx.$q.when({
data: [ { target: 'test.cpu1', datapoints: ctx.datapoints } ]
data: [{target: 'test.cpu1', datapoints: ctx.datapoints}]
}));
ctx.scope.refreshData(ctx.datasource);

View File

@ -6,18 +6,19 @@ module.exports = function(config) {
}
},
options: {
configuration: {
rules: {
curly: true,
align: [true, "parameters", "statements"],
indent: [true, "spaces"],
"class-name": true,
"interface-name": true,
"semicolon": true,
"use-strict": [false, "check-module", "check-function"],
"whitespace": [true, "check-branch", "check-decl", "check-type"],
}
}
configuration: 'tslint.json'
// {
// rules: {
// curly: true,
// align: [true, "parameters", "statements"],
// indent: [true, "spaces"],
// "class-name": true,
// "interface-name": true,
// "semicolon": true,
// "use-strict": [false, "check-module", "check-function"],
// "whitespace": [true, "check-branch", "check-decl", "check-type"],
// }
// }
}
};
};

View File

@ -27,15 +27,17 @@ module.exports = function(config, grunt) {
}
if (/(\.ts)$/.test(filepath)) {
newPath = filepath.replace(/^public/, 'public_gen');
grunt.log.writeln('Copying to ' + newPath);
grunt.file.copy(filepath, newPath);
// copy ts file also used by source maps
//changes changed file source to that of the changed file
var option = 'typescript.build.src';
var result = filepath;
grunt.config(option, result);
grunt.task.run('typescript:build');
grunt.task.run('tslint');
// copy ts file also used by source maps
newPath = filepath.replace(/^public/, 'public_gen');
grunt.file.copy(filepath, newPath);
}
});

62
tslint.json Normal file
View File

@ -0,0 +1,62 @@
{
"rules": {
"class-name": true,
"comment-format": [true, "check-space"],
"curly": true,
"eofline": true,
"forin": false,
"indent": [true, "spaces"],
"label-position": true,
"label-undefined": true,
"max-line-length": [true, 140],
"member-access": false,
"no-arg": true,
"no-bitwise": true,
"no-console": [true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-inferrable-types": true,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-comma": true,
"no-trailing-whitespace": true,
"no-unused-expression": false,
"no-unused-variable": false,
"no-unreachable": true,
"no-use-before-declare": true,
"no-var-keyword": false,
"object-literal-sort-keys": false,
"one-line": [true,
"check-open-brace",
"check-catch",
"check-else"
],
"radix": false,
"semicolon": true,
"triple-equals": [true, "allow-null-check"],
"typedef-whitespace": [true, {
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}],
"variable-name": [true, "ban-keywords"],
"whitespace": [true,
"check-branch",
"check-decl",
"check-type"
]
}
}