Merge branch 'master' of https://github.com/grafana/grafana into metadata

This commit is contained in:
utkarshcmu 2016-02-04 13:34:32 -08:00
commit 59a384b453
138 changed files with 4832 additions and 5224 deletions

View File

@ -1,23 +1,26 @@
# 3.0.0 (unrelased master branch)
### New Features
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655)
* **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
* **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
### Breaking changes
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
* **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
### Enhancements
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/issues/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/issues/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/issues/3635)
* **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
### Bug fixes
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
* **InfluxDB**: Fix for InfluxDB and table panel when using Format As Table and having group by time, fixes [#3928](https://github.com/grafana/grafana/issues/3928)
* **Panel Time shift**: Fix for panel time range and using dashboard times liek `Today` and `This Week`, fixes [#3941](https://github.com/grafana/grafana/issues/3941)
* **Row repeat**: Repeated rows will now appear next to each other and not by the bottom of the dashboard, fixes [#3942](https://github.com/grafana/grafana/issues/3942)
# 2.6.1 (unrelased, 2.6.x branch)

View File

@ -77,7 +77,7 @@ The Query Editor exposes capabilities of your Data Source and allows you to quer
Use the Query Editor to build one or more queries (for one or more series) in your time series database. The panel will instantly update allowing you to effectively explore your data in real time and build a perfect query for that particular Panel.
You can utilize [Template variables]((reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
You can utilize [Template variables](/reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
Grafana allows you to reference queries in the Query Editor by the row that theyre on. If you add a second query to graph, you can reference the first query simply by typing in #A. This provides an easy and convenient way to build compounded queries.

View File

@ -3,7 +3,6 @@
import "./directives/annotation_tooltip";
import "./directives/body_class";
import "./directives/config_modal";
import "./directives/confirm_click";
import "./directives/dash_edit_link";
import "./directives/dash_upload";
@ -16,6 +15,8 @@ import "./directives/password_strenght";
import "./directives/spectrum_picker";
import "./directives/tags";
import "./directives/value_select_dropdown";
import "./directives/plugin_component";
import "./directives/rebuild_on_change";
import "./directives/give_focus";
import './jquery_extended';
import './partials';

View File

@ -1,46 +0,0 @@
define([
'lodash',
'jquery',
'../core_module',
],
function (_, $, coreModule) {
'use strict';
coreModule.default.directive('configModal', function($modal, $q, $timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var partial = attrs.configModal;
var id = '#' + partial.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
elem.bind('click',function() {
if ($(id).length) {
elem.attr('data-target', id).attr('data-toggle', 'modal');
scope.$apply(function() { scope.$broadcast('modal-opened'); });
return;
}
var panelModal = $modal({
template: partial,
persist: false,
show: false,
scope: scope.$new(),
keyboard: false
});
$q.when(panelModal).then(function(modalEl) {
elem.attr('data-target', id).attr('data-toggle', 'modal');
$timeout(function () {
if (!modalEl.data('modal').isShown) {
modalEl.modal('show');
}
}, 50);
});
scope.$apply();
});
}
};
});
});

View File

@ -90,7 +90,6 @@ function (angular, coreModule, kbn) {
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {

View File

@ -0,0 +1,199 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import config from 'app/core/config';
import coreModule from 'app/core/core_module';
import {UnknownPanelCtrl} from 'app/plugins/panel/unknown/module';
/** @ngInject */
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
function getTemplate(component) {
if (component.template) {
return $q.when(component.template);
}
var cached = $templateCache.get(component.templateUrl);
if (cached) {
return $q.when(cached);
}
return $http.get(component.templateUrl).then(res => {
return res.data;
});
}
function getPluginComponentDirective(options) {
return function() {
return {
templateUrl: options.Component.templateUrl,
template: options.Component.template,
restrict: 'E',
controller: options.Component,
controllerAs: 'ctrl',
bindToController: true,
scope: options.bindings,
link: (scope, elem, attrs, ctrl) => {
if (ctrl.link) {
ctrl.link(scope, elem, attrs, ctrl);
}
if (ctrl.init) {
ctrl.init();
}
}
};
};
}
function loadPanelComponentInfo(scope, attrs) {
var componentInfo: any = {
name: 'panel-plugin-' + scope.panel.type,
bindings: {dashboard: "=", panel: "=", row: "="},
attrs: {dashboard: "dashboard", panel: "panel", row: "row"},
};
var panelElemName = 'panel-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
if (panelInfo) {
panelCtrlPromise = System.import(panelInfo.module).then(function(panelModule) {
return panelModule.PanelCtrl;
});
}
return panelCtrlPromise.then(function(PanelCtrl: any) {
componentInfo.Component = PanelCtrl;
if (!PanelCtrl || PanelCtrl.registered) {
return componentInfo;
};
if (PanelCtrl.templatePromise) {
return PanelCtrl.templatePromise.then(res => {
return componentInfo;
});
}
PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
PanelCtrl.templateUrl = null;
PanelCtrl.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
return componentInfo;
});
return PanelCtrl.templatePromise;
});
}
function getModule(scope, attrs) {
switch (attrs.type) {
// QueryCtrl
case "query-ctrl": {
let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'query-ctrl-' + ds.meta.id,
bindings: {target: "=", panelCtrl: "=", datasource: "="},
attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
Component: dsModule.QueryCtrl
};
});
});
}
// QueryOptionsCtrl
case "query-options-ctrl": {
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
return System.import(ds.meta.module).then((dsModule): any => {
if (!dsModule.QueryOptionsCtrl) {
return {notFound: true};
}
return {
name: 'query-options-ctrl-' + ds.meta.id,
bindings: {panelCtrl: "="},
attrs: {"panel-ctrl": "ctrl"},
Component: dsModule.QueryOptionsCtrl
};
});
});
}
// Annotations
case "annotations-query-ctrl": {
return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
return {
name: 'annotations-query-ctrl-' + scope.currentDatasource.meta.id,
bindings: {annotation: "=", datasource: "="},
attrs: {"annotation": "currentAnnotation", datasource: "currentDatasource"},
Component: dsModule.AnnotationsQueryCtrl,
};
});
}
// ConfigCtrl
case 'datasource-config-ctrl': {
return System.import(scope.datasourceMeta.module).then(function(dsModule) {
return {
name: 'ds-config-' + scope.datasourceMeta.id,
bindings: {meta: "=", current: "="},
attrs: {meta: "datasourceMeta", current: "current"},
Component: dsModule.ConfigCtrl,
};
});
}
// Panel
case 'panel': {
return loadPanelComponentInfo(scope, attrs);
}
default: {
return $q.reject({message: "Could not find component type: " + attrs.type });
}
}
}
function appendAndCompile(scope, elem, componentInfo) {
var child = angular.element(document.createElement(componentInfo.name));
_.each(componentInfo.attrs, (value, key) => {
child.attr(key, value);
});
$compile(child)(scope);
elem.empty();
elem.append(child);
}
function registerPluginComponent(scope, elem, attrs, componentInfo) {
if (componentInfo.notFound) {
elem.empty();
return;
}
if (!componentInfo.Component) {
throw {message: 'Failed to find exported plugin component for ' + componentInfo.name};
}
if (!componentInfo.Component.registered) {
var directiveName = attrs.$normalize(componentInfo.name);
var directiveFn = getPluginComponentDirective(componentInfo);
coreModule.directive(directiveName, directiveFn);
componentInfo.Component.registered = true;
}
appendAndCompile(scope, elem, componentInfo);
}
return {
restrict: 'E',
link: function(scope, elem, attrs) {
getModule(scope, attrs).then(function (componentInfo) {
registerPluginComponent(scope, elem, attrs, componentInfo);
}).catch(err => {
$rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
console.log('Plugin componnet error', err);
});
}
};
}
coreModule.directive('pluginComponent', pluginDirectiveLoader);

View File

@ -0,0 +1,75 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../core_module';
function getBlockNodes(nodes) {
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes;
for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
blockNodes = $([].slice.call(nodes, 0, i));
}
blockNodes.push(node);
}
}
return blockNodes || nodes;
}
function rebuildOnChange($animate) {
return {
multiElement: true,
terminal: true,
transclude: true,
priority: 600,
restrict: 'E',
link: function(scope, elem, attrs, ctrl, transclude) {
var block, childScope, previousElements;
function cleanUp() {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
}
scope.$watch(attrs.property, function rebuildOnChangeAction(value, oldValue) {
if (childScope && value !== oldValue) {
cleanUp();
}
if (!childScope && (value || attrs.showNull)) {
transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end rebuild on change ');
block = {clone: clone};
$animate.enter(clone, elem.parent(), elem);
});
} else {
cleanUp();
}
});
}
};
}
coreModule.directive('rebuildOnChange', rebuildOnChange);

View File

View File

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

View File

@ -91,8 +91,10 @@
</div>
</div>
<annotations-query-editor datasource="currentDatasource" annotation="currentAnnotation">
</annotations-query-editor>
<rebuild-on-change property="currentAnnotation.datasource">
<plugin-component type="annotations-query-ctrl">
</plugin-component>
</rebuild-on-change>
<br>
<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>

View File

@ -1,25 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function annotationsQueryEditor(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
annotation: "=",
datasource: "="
},
watchPath: "annotation.datasource",
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

@ -177,42 +177,6 @@ function (angular, $, _, moment) {
return newPanel;
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p.addDataQueryTo = function(panel, datasource) {
var target = {
refId: this.getNextQueryLetter(panel)
};
if (datasource) {
target.datasource = datasource.name;
}
panel.targets.push(target);
};
p.removeDataQuery = function (panel, query) {
panel.targets = _.without(panel.targets, query);
};
p.duplicateDataQuery = function(panel, query) {
var clone = angular.copy(query);
clone.refId = this.getNextQueryLetter(panel);
panel.targets.push(clone);
};
p.moveDataQuery = function(panel, fromIndex, toIndex) {
_.move(panel.targets, fromIndex, toIndex);
};
p.formatDate = function(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
@ -230,6 +194,16 @@ function (angular, $, _, moment) {
moment.utc(date).fromNow();
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p._updateSchema = function(old) {
var i, j, k;
var oldVersion = this.schemaVersion;

View File

@ -34,7 +34,7 @@ function (angular, _) {
// handle row repeats
if (row.repeat) {
this.repeatRow(row);
this.repeatRow(row, i);
}
// clean up old left overs
else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
@ -58,13 +58,13 @@ function (angular, _) {
};
// returns a new row clone or reuses a clone from previous iteration
this.getRowClone = function(sourceRow, index) {
if (index === 0) {
this.getRowClone = function(sourceRow, repeatIndex, sourceRowIndex) {
if (repeatIndex === 0) {
return sourceRow;
}
var i, panel, row, copy;
var sourceRowId = _.indexOf(this.dashboard.rows, sourceRow) + 1;
var sourceRowId = sourceRowIndex + 1;
// look for row to reuse
for (i = 0; i < this.dashboard.rows.length; i++) {
@ -77,7 +77,7 @@ function (angular, _) {
if (!copy) {
copy = angular.copy(sourceRow);
this.dashboard.rows.push(copy);
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
// set new panel ids
for (i = 0; i < copy.panels.length; i++) {
@ -92,8 +92,8 @@ function (angular, _) {
return copy;
};
// returns a new panel clone or reuses a clone from previous iteration
this.repeatRow = function(row) {
// returns a new row clone or reuses a clone from previous iteration
this.repeatRow = function(row, rowIndex) {
var variables = this.dashboard.templating.list;
var variable = _.findWhere(variables, {name: row.repeat});
if (!variable) {
@ -108,7 +108,7 @@ function (angular, _) {
}
_.each(selected, function(option, index) {
copy = self.getRowClone(row, index);
copy = self.getRowClone(row, index, rowIndex);
copy.scopedVars = {};
copy.scopedVars[variable.name] = option;

View File

@ -61,6 +61,13 @@ function (angular, _, config) {
});
};
$scope.editRow = function() {
$scope.appEvent('show-dash-editor', {
src: 'public/app/partials/roweditor.html',
scope: $scope.$new()
});
};
$scope.moveRow = function(direction) {
var rowsList = $scope.dashboard.rows;
var rowIndex = _.indexOf(rowsList, $scope.row);

View File

@ -103,6 +103,11 @@ function (angular, _, $) {
if (!panelScope) {
return;
}
if (!panelScope.ctrl.editModeInitiated) {
panelScope.ctrl.initEditMode();
}
this.enterFullscreen(panelScope);
return;
}

View File

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

View File

@ -1,25 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function dsConfigView(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
dsMeta: "=",
current: "="
},
watchPath: "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

@ -10,7 +10,10 @@ function (angular, _, config) {
var datasourceTypes = [];
module.directive('datasourceHttpSettings', function() {
return {templateUrl: 'public/app/features/datasources/partials/http_settings.html'};
return {
scope: {current: "="},
templateUrl: 'public/app/features/datasources/partials/http_settings.html'
};
});
module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {

View File

@ -41,7 +41,10 @@
<div class="clearfix"></div>
</div>
<ds-config-view ng-if="datasourceMeta.id" ds-meta="datasourceMeta" current="current"></ds-config-view>
<rebuild-on-change property="datasourceMeta.id">
<plugin-component type="datasource-config-ctrl">
</plugin-component>
</rebuild-on-change>
<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

@ -2,7 +2,7 @@ define([
'./panel_menu',
'./panel_directive',
'./solo_panel_ctrl',
'./panel_loader',
'./query_editor',
'./query_ctrl',
'./panel_editor_tab',
'./query_editor_row',
], function () {});

View File

@ -38,16 +38,10 @@ class MetricsPanelCtrl extends PanelCtrl {
if (!this.panel.targets) {
this.panel.targets = [{}];
}
// hookup initial data fetch
this.$timeout(() => {
if (!this.skipDataOnInit) {
this.refresh();
}
}, 30);;
}
initEditMode() {
super.initEditMode();
this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
this.datasources = this.datasourceSrv.getMetricSources();
@ -140,6 +134,7 @@ class MetricsPanelCtrl extends PanelCtrl {
this.rangeRaw.from = timeFromInfo.from;
this.rangeRaw.to = timeFromInfo.to;
this.range.from = timeFromDate;
this.range.to = dateMath.parse(timeFromInfo.to);
}
}
@ -164,12 +159,12 @@ class MetricsPanelCtrl extends PanelCtrl {
};
issueQueries(datasource) {
this.updateTimeRange();
if (!this.panel.targets || this.panel.targets.length === 0) {
return this.$q.when([]);
}
this.updateTimeRange();
var metricsQuery = {
range: this.range,
rangeRaw: this.rangeRaw,
@ -182,32 +177,19 @@ class MetricsPanelCtrl extends PanelCtrl {
};
this.setTimeQueryStart();
return datasource.query(metricsQuery).then(results => {
this.setTimeQueryEnd();
try {
return datasource.query(metricsQuery).then(results => {
this.setTimeQueryEnd();
if (this.dashboard.snapshot) {
this.panel.snapshotData = results;
}
if (this.dashboard.snapshot) {
this.panel.snapshotData = results;
}
return results;
});
}
addDataQuery(datasource) {
this.dashboard.addDataQueryTo(this.panel, datasource);
}
removeDataQuery(query) {
this.dashboard.removeDataQuery(this.panel, query);
this.refresh();
};
duplicateDataQuery(query) {
this.dashboard.duplicateDataQuery(this.panel, query);
}
moveDataQuery(fromIndex, toIndex) {
this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex);
return results;
});
} catch (err) {
return this.$q.reject(err);
}
}
setDatasource(datasource) {
@ -229,6 +211,13 @@ class MetricsPanelCtrl extends PanelCtrl {
this.datasource = null;
this.refresh();
}
addDataQuery(datasource) {
var target = {
datasource: datasource ? datasource.name : undefined
};
this.panel.targets.push(target);
}
}
export {MetricsPanelCtrl};

View File

@ -4,48 +4,18 @@ import config from 'app/core/config';
import {PanelCtrl} from './panel_ctrl';
import {MetricsPanelCtrl} from './metrics_panel_ctrl';
import {QueryCtrl} from './query_ctrl';
export class DefaultPanelCtrl extends PanelCtrl {
class DefaultPanelCtrl extends PanelCtrl {
/** @ngInject */
constructor($scope, $injector) {
super($scope, $injector);
}
}
class PanelDirective {
template: string;
templateUrl: string;
bindToController: boolean;
scope: any;
controller: any;
controllerAs: string;
getDirective() {
if (!this.controller) {
this.controller = DefaultPanelCtrl;
}
return {
template: this.template,
templateUrl: this.templateUrl,
controller: this.controller,
controllerAs: 'ctrl',
bindToController: true,
scope: {dashboard: "=", panel: "=", row: "="},
link: (scope, elem, attrs, ctrl) => {
ctrl.init();
this.link(scope, elem, attrs, ctrl);
}
};
}
link(scope, elem, attrs, ctrl) {
return null;
}
}
export {
PanelCtrl,
DefaultPanelCtrl,
MetricsPanelCtrl,
PanelDirective,
QueryCtrl,
}

View File

@ -2,6 +2,7 @@
import config from 'app/core/config';
import _ from 'lodash';
import angular from 'angular';
export class PanelCtrl {
panel: any;
@ -63,12 +64,6 @@ export class PanelCtrl {
}
editPanel() {
if (!this.editModeInitiated) {
this.editorTabs = [];
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.initEditMode();
}
this.changeView(true, true);
}
@ -77,7 +72,9 @@ export class PanelCtrl {
}
initEditMode() {
return;
this.editorTabs = [];
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.editModeInitiated = true;
}
addEditorTab(title, directiveFn, index?) {
@ -166,14 +163,26 @@ export class PanelCtrl {
});
}
sharePanel() {
var shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
sharePanel() {
var shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
this.publishAppEvent('show-modal', {
this.publishAppEvent('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: shareScope
});
}
}
openInspector() {
var modalScope = this.$scope.$new();
modalScope.panel = this.panel;
modalScope.dashboard = this.dashboard;
modalScope.inspector = angular.copy(this.inspector);
this.publishAppEvent('show-modal', {
src: 'public/app/partials/inspector.html',
scope: modalScope
});
}
}

View File

@ -1,104 +0,0 @@
define([
'angular',
'jquery',
],
function (angular, $) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/panel.html',
transclude: true,
scope: { ctrl: "=" },
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
});
}
};
});
module.directive('panelResizer', function($rootScope) {
return {
restrict: 'E',
template: '<span class="resize-panel-handle"></span>',
link: function(scope, elem) {
var resizing = false;
var lastPanel = false;
var ctrl = scope.ctrl;
var handleOffset;
var originalHeight;
var originalWidth;
var maxWidth;
function dragStartHandler(e) {
e.preventDefault();
resizing = true;
handleOffset = $(e.target).offset();
originalHeight = parseInt(ctrl.row.height);
originalWidth = ctrl.panel.span;
maxWidth = $(document).width();
lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
$('body').on('mousemove', moveHandler);
$('body').on('mouseup', dragEndHandler);
}
function moveHandler(e) {
ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
// auto adjust other panels
if (Math.floor(rowSpan) < 14) {
// last panel should not push row down
if (lastPanel === ctrl.panel && rowSpan > 12) {
lastPanel.span -= rowSpan - 12;
}
// reduce width of last panel so total in row is 12
else if (lastPanel !== ctrl.panel) {
lastPanel.span = lastPanel.span - (rowSpan - 12);
lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
}
}
scope.$apply(function() {
scope.$broadcast('render');
});
}
function dragEndHandler() {
// if close to 12
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
if (rowSpan < 12 && rowSpan > 11) {
lastPanel.span += 12 - rowSpan;
}
scope.$apply(function() {
$rootScope.$broadcast('render');
});
$('body').off('mousemove', moveHandler);
$('body').off('mouseup', dragEndHandler);
}
elem.on('mousedown', dragStartHandler);
scope.$on("$destroy", function() {
elem.off('mousedown', dragStartHandler);
});
}
};
});
});

View File

@ -0,0 +1,101 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import $ from 'jquery';
var module = angular.module('grafana.directives');
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/panel.html',
transclude: true,
scope: { ctrl: "=" },
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
});
}
};
});
module.directive('panelResizer', function($rootScope) {
return {
restrict: 'E',
template: '<span class="resize-panel-handle"></span>',
link: function(scope, elem) {
var resizing = false;
var lastPanel;
var ctrl = scope.ctrl;
var handleOffset;
var originalHeight;
var originalWidth;
var maxWidth;
function dragStartHandler(e) {
e.preventDefault();
resizing = true;
handleOffset = $(e.target).offset();
originalHeight = parseInt(ctrl.row.height);
originalWidth = ctrl.panel.span;
maxWidth = $(document).width();
lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
$('body').on('mousemove', moveHandler);
$('body').on('mouseup', dragEndHandler);
}
function moveHandler(e) {
ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
// auto adjust other panels
if (Math.floor(rowSpan) < 14) {
// last panel should not push row down
if (lastPanel === ctrl.panel && rowSpan > 12) {
lastPanel.span -= rowSpan - 12;
} else if (lastPanel !== ctrl.panel) {
// reduce width of last panel so total in row is 12
lastPanel.span = lastPanel.span - (rowSpan - 12);
lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
}
}
scope.$apply(function() {
scope.$broadcast('render');
});
}
function dragEndHandler() {
// if close to 12
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
if (rowSpan < 12 && rowSpan > 11) {
lastPanel.span += 12 - rowSpan;
}
scope.$apply(function() {
$rootScope.$broadcast('render');
});
$('body').off('mousemove', moveHandler);
$('body').off('mouseup', dragEndHandler);
}
elem.on('mousedown', dragStartHandler);
scope.$on("$destroy", function() {
elem.off('mousedown', dragStartHandler);
});
}
};
});

View File

@ -1,88 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
import {UnknownPanel} from '../../plugins/panel/unknown/module';
var directiveModule = angular.module('grafana.directives');
/** @ngInject */
function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector, $templateCache) {
return {
restrict: 'E',
scope: {
dashboard: "=",
row: "=",
panel: "="
},
link: function(scope, elem, attrs) {
function getTemplate(directive) {
if (directive.template) {
return $q.when(directive.template);
}
var cached = $templateCache.get(directive.templateUrl);
if (cached) {
return $q.when(cached);
}
return $http.get(directive.templateUrl).then(res => {
return res.data;
});
}
function addPanelAndCompile(name) {
var child = angular.element(document.createElement(name));
child.attr('dashboard', 'dashboard');
child.attr('panel', 'panel');
child.attr('row', 'row');
$compile(child)(scope);
elem.empty();
elem.append(child);
}
function addPanel(name, Panel) {
if (Panel.registered) {
addPanelAndCompile(name);
return;
}
if (Panel.promise) {
Panel.promise.then(() => {
addPanelAndCompile(name);
});
return;
}
var panelInstance = $injector.instantiate(Panel);
var directive = panelInstance.getDirective();
Panel.promise = getTemplate(directive).then(template => {
directive.templateUrl = null;
directive.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
directiveModule.directive(attrs.$normalize(name), function() {
return directive;
});
Panel.registered = true;
addPanelAndCompile(name);
});
}
var panelElemName = 'panel-directive-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
if (!panelInfo) {
addPanel(panelElemName, UnknownPanel);
return;
}
System.import(panelInfo.module).then(function(panelModule) {
addPanel(panelElemName, panelModule.Panel);
}).catch(err => {
console.log('Panel err: ', err);
});
}
};
}
directiveModule.directive('panelLoader', panelLoader);

View File

@ -37,7 +37,7 @@ function (angular, $, _) {
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-remove"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-trash"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
}
@ -53,7 +53,6 @@ function (angular, $, _) {
template += '<a class="panel-menu-link" ';
if (item.click) { template += ' ng-click="' + item.click + '"'; }
if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
template += '>';
template += item.text + '</a>';
});

View File

@ -1,6 +1,6 @@
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
<div class="panel-header">
<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
<span data-placement="top" bs-tooltip="ctrl.error">
<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
</span>

View File

@ -0,0 +1,56 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="ctrl.error" class="tight-form-item">
<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="ctrl.target.datasource">
<em>{{ctrl.target.datasource}}</em>
</li>
<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{ctrl.target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-transclude>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -0,0 +1,57 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class QueryCtrl {
target: any;
datasource: any;
panelCtrl: any;
panel: any;
hasRawMode: boolean;
error: string;
constructor(public $scope, private $injector) {
this.panel = this.panelCtrl.panel;
if (!this.target.refId) {
this.target.refId = this.getNextQueryLetter();
}
}
getNextQueryLetter() {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => {
return _.every(this.panel.targets, function(other) {
return other.refId !== refId;
});
});
}
removeQuery() {
this.panel.targets = _.without(this.panel.targets, this.target);
this.panelCtrl.refresh();
};
duplicateQuery() {
var clone = angular.copy(this.target);
clone.refId = this.getNextQueryLetter();
this.panel.targets.push(clone);
}
moveQuery(direction) {
var index = _.indexOf(this.panel.targets, this.target);
_.move(this.panel.targets, index, index + direction);
}
refresh() {
this.panelCtrl.refresh();
}
toggleHideQuery() {
this.target.hide = !this.target.hide;
this.panelCtrl.refresh();
}
}

View File

@ -1,48 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watchPath: "ctrl.panel.datasource",
directive: scope => {
let datasource = scope.target.datasource || scope.ctrl.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({
watchPath: "ctrl.panel.datasource",
directive: scope => {
return datasourceSrv.get(scope.ctrl.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

@ -0,0 +1,18 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import $ from 'jquery';
var module = angular.module('grafana.directives');
/** @ngInject **/
function queryEditorRowDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/query_editor_row.html',
transclude: true,
scope: {ctrl: "="},
};
}
module.directive('queryEditorRow', queryEditorRowDirective);

View File

@ -65,7 +65,7 @@
</ul>
</li>
<li>
<a dash-editor-link="app/partials/roweditor.html">Row editor</a>
<a ng-click="editRow()">Row editor</a>
</li>
<li>
<a ng-click="deleteRow()">Delete row</a>
@ -81,8 +81,8 @@
<div ng-repeat="panel in row.panels track by panel.id" class="panel" ui-draggable="!dashboard.meta.fullscreen" drag="panel.id"
ui-on-drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
<panel-loader class="panel-margin" dashboard="dashboard" row="row" panel="panel">
</panel-loader>
<plugin-component type="panel" class="panel-margin">
</plugin-component>
</div>
<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">

View File

@ -61,9 +61,9 @@
<div ng-if="editor.index == 2">
<label>Message:</label>
<pre>
{{message}}
</pre>
<pre>
{{message}}
</pre>
<label>Stack trace:</label>
<pre>

View File

@ -1,8 +1,12 @@
<div class="editor-row">
<div class="tight-form-container">
<metrics-query-editor ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
</metrics-query-editor>
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}">
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
<plugin-component type="query-ctrl">
</plugin-component>
</rebuild-on-change>
</div>
</div>
<div style="margin: 20px 0 0 0">
@ -26,7 +30,11 @@
</div>
<metrics-query-options></metrics-query-options>
<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
<plugin-component type="query-options-ctrl">
</plugin-component>
</rebuild-on-change>
</div>
<div class="editor-row" style="margin-top: 30px">

View File

@ -1,3 +1,3 @@
declare var Datasource: any;
export default Datasource;
declare var CloudWatchDatasource: any;
export {CloudWatchDatasource};

View File

@ -357,5 +357,7 @@ function (angular, _, moment, dateMath) {
}
return CloudWatchDatasource;
return {
CloudWatchDatasource: CloudWatchDatasource
};
});

View File

@ -1,27 +0,0 @@
define([
'./datasource',
'./query_parameter_ctrl',
'./query_ctrl',
],
function (CloudWatchDatasource) {
'use strict';
function metricsQueryEditor() {
return {controller: 'CloudWatchQueryCtrl', templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
}
function configView() {
return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/edit_view.html'};
}
return {
Datasource: CloudWatchDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
};
});

View File

@ -0,0 +1,20 @@
import './query_parameter_ctrl';
import {CloudWatchDatasource} from './datasource';
import {CloudWatchQueryCtrl} from './query_ctrl';
class CloudWatchConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/config.html';
}
class CloudWatchAnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html';
}
export {
CloudWatchDatasource as Datasource,
CloudWatchQueryCtrl as QueryCtrl,
CloudWatchConfigCtrl as ConfigCtrl,
CloudWatchAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1 +1 @@
<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>

View File

@ -9,7 +9,7 @@
Credentials profile name<tip>Credentials profile name, as specified in ~/.aws/credentials, leave blank for default</tip>
</li>
<li>
<input type="text" class="tight-form-input input-large last" ng-model='current.database' placeholder="default"></input>
<input type="text" class="tight-form-input input-large last" ng-model='ctrl.current.database' placeholder="default"></input>
</li>
</ul>
<div class="clearfix"></div>
@ -19,12 +19,12 @@
<li class="tight-form-item" style="width: 200px">
Default Region<tip>Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.</tip>
</li>
<!--
<!--
Whenever this list is updated, backend list should also be updated.
Please update the region list in pkg/api/cloudwatch/metric.go
-->
<li>
<select class="tight-form-input input-large last" ng-model="current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
<select class="tight-form-input input-large last" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -1,38 +1,4 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<query-editor-row ctrl="ctrl">
</query-editor-row>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item"
ng-click="target.hide = !target.hide; ctrl.refresh();"
role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<cloudwatch-query-parameter target="target" datasource="ctrl.datasource" on-change="refreshMetricData()"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.refresh()"></cloudwatch-query-parameter>

View File

@ -1,27 +0,0 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('CloudWatchQueryCtrl', function($scope) {
$scope.init = function() {
$scope.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
};
$scope.refreshMetricData = function() {
if (!_.isEqual($scope.oldTarget, $scope.target)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.ctrl.refresh();
}
};
$scope.init();
});
});

View File

@ -0,0 +1,17 @@
///<reference path="../../../headers/common.d.ts" />
import './query_parameter_ctrl';
import _ from 'lodash';
import {QueryCtrl} from 'app/features/panel/panel';
export class CloudWatchQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html';
aliasSyntax: string;
/** @ngInject **/
constructor($scope, $injector) {
super($scope, $injector);
this.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
}
}

View File

@ -9,7 +9,7 @@ function (angular, _) {
module.directive('cloudwatchQueryParameter', function() {
return {
templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.parameter.html',
controller: 'CloudWatchQueryParameterCtrl',
restrict: 'E',
scope: {

View File

@ -3,7 +3,7 @@ import "../datasource";
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {CloudWatchDatasource} from "../datasource";
describe('CloudWatchDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -20,7 +20,7 @@ describe('CloudWatchDatasource', function() {
ctx.$q = $q;
ctx.$httpBackend = $httpBackend;
ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
}));
describe('When performing CloudWatch query', function() {

View File

@ -0,0 +1,34 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class ElasticConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/config.html';
current: any;
/** @ngInject */
constructor($scope) {
this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
}
indexPatternTypes = [
{name: 'No pattern', value: undefined},
{name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'},
{name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'},
{name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'},
{name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'},
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
indexPatternTypeChanged() {
var def = _.findWhere(this.indexPatternTypes, {value: this.current.jsonData.interval});
this.current.database = def.example || 'es-index-name';
}
}

View File

@ -1,3 +1,3 @@
declare var Datasource: any;
export default Datasource;
declare var ElasticDatasource: any;
export {ElasticDatasource};

View File

@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
};
}
return ElasticDatasource;
return {
ElasticDatasource: ElasticDatasource
};
});

View File

@ -1,39 +0,0 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class EditViewCtrl {
/** @ngInject */
constructor($scope) {
$scope.indexPatternTypes = [
{name: 'No pattern', value: undefined},
{name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'},
{name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'},
{name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'},
{name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'},
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
$scope.esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
$scope.indexPatternTypeChanged = function() {
var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
$scope.current.database = def.example || 'es-index-name';
};
}
}
function editViewDirective() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/edit_view.html',
controller: EditViewCtrl,
};
};
export default editViewDirective;

View File

@ -1,30 +0,0 @@
define([
'./datasource',
'./edit_view',
'./bucket_agg',
'./metric_agg',
],
function (ElasticDatasource, editView) {
'use strict';
function metricsQueryEditor() {
return {controller: 'ElasticQueryCtrl', templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html'};
}
function metricsQueryOptions() {
return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.options.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
}
return {
Datasource: ElasticDatasource,
configView: editView.default,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -0,0 +1,19 @@
import {ElasticDatasource} from './datasource';
import {ElasticQueryCtrl} from './query_ctrl';
import {ElasticConfigCtrl} from './config_ctrl';
class ElasticQueryOptionsCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.options.html';
}
class ElasticAnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html';
}
export {
ElasticDatasource as Datasource,
ElasticQueryCtrl as QueryCtrl,
ElasticConfigCtrl as ConfigCtrl,
ElasticQueryOptionsCtrl as QueryOptionsCtrl,
ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1,14 +1,14 @@
<div class="editor-row">
<div class="section" ng-if="annotation.index">
<div class="section" ng-if="ctrl.annotation.index">
<h5>Index name</h5>
<div class="editor-option">
<input type="text" class="span4" ng-model='annotation.index' placeholder="events-*"></input>
<input type="text" class="span4" ng-model='ctrl.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='annotation.query' placeholder="tags:deploy"></input>
<input type="text" class="span6" ng-model='ctrl.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='annotation.timeField' placeholder="@timestamp"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
</div>
<div class="editor-option">
<label class="small">Title</label>
<input type="text" class="input-small" ng-model='annotation.titleField' placeholder="desc"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
</div>
<div class="editor-option">
<label class="small">Tags</label>
<input type="text" class="input-small" ng-model='annotation.tagsField' placeholder="tags"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
</div>
<div class="editor-option">
<label class="small">Text</label>
<input type="text" class="input-small" ng-model='annotation.textField' placeholder=""></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.textField' placeholder=""></input>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
<datasource-http-settings></datasource-http-settings>
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>
<h4>Elasticsearch details</h4>
@ -8,13 +9,13 @@
Index name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.database' placeholder="" required></input>
<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.database' placeholder="" required></input>
</li>
<li class="tight-form-item">
Pattern
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.interval" ng-options="f.value as f.name for f in indexPatternTypes" ng-change="indexPatternTypeChanged()" ></select>
<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.interval" ng-options="f.value as f.name for f in ctrl.indexPatternTypes" ng-change="ctrl.indexPatternTypeChanged()" ></select>
</li>
</ul>
<div class="clearfix"></div>
@ -25,7 +26,7 @@
Time field name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.timeField' placeholder="" required ng-init="current.jsonData.timeField = current.jsonData.timeField || '@timestamp'"></input>
<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.jsonData.timeField' placeholder="" required ng-init=""></input>
</li>
</ul>
<div class="clearfix"></div>
@ -36,7 +37,7 @@
Version
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.esVersion" ng-options="f.value as f.name for f in esVersions"></select>
<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions"></select>
</li>
</ul>
<div class="clearfix"></div>
@ -52,7 +53,7 @@
Group by time interval
</li>
<li>
<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval"
<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="ctrl.current.jsonData.timeInterval"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="tight-form-item">

View File

@ -1,77 +1,32 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<query-editor-row ctrl="ctrl">
<li class="tight-form-item query-keyword" style="width: 75px">
Query
</li>
<li>
<input type="text" class="tight-form-input" style="width: 345px;" ng-model="ctrl.target.query" spellcheck='false' placeholder="Lucene query" ng-blur="ctrl.refresh()">
</li>
<li class="tight-form-item query-keyword">
Alias
</li>
<li>
<input type="text" class="tight-form-input" style="width: 200px;" ng-model="ctrl.target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="ctrl.refresh()">
</li>
</query-editor-row>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="panelCtrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item query-keyword" style="width: 75px">
Query
</li>
<li>
<input type="text" class="tight-form-input" style="width: 345px;" ng-model="target.query" spellcheck='false' placeholder="Lucene query" ng-blur="panelCtrl.refresh()">
</li>
<li class="tight-form-item query-keyword">
Alias
</li>
<li>
<input type="text" class="tight-form-input" style="width: 200px;" ng-model="target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="panelCtrl.refresh()">
</li>
</ul>
<div class="clearfix"></div>
<div ng-repeat="agg in ctrl.target.metrics">
<elastic-metric-agg
target="ctrl.target" index="$index"
get-fields="ctrl.getFields($fieldType)"
on-change="ctrl.queryUpdated()"
es-version="ctrl.esVersion">
</elastic-metric-agg>
</div>
<div ng-hide="target.rawQuery">
<div ng-repeat="agg in target.metrics">
<elastic-metric-agg
target="target" index="$index"
get-fields="getFields($fieldType)"
on-change="queryUpdated()"
es-version="esVersion">
</elastic-metric-agg>
</div>
<div ng-repeat="agg in target.bucketAggs">
<elastic-bucket-agg
target="target" index="$index"
get-fields="getFields($fieldType)"
on-change="queryUpdated()">
</elastic-bucket-agg>
</div>
<div ng-repeat="agg in ctrl.target.bucketAggs">
<elastic-bucket-agg
target="ctrl.target" index="$index"
get-fields="ctrl.getFields($fieldType)"
on-change="ctrl.queryUpdated()">
</elastic-bucket-agg>
</div>

View File

@ -8,7 +8,7 @@
Group by time interval
</li>
<li>
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panel.interval" ng-blur="ctrl.refresh();"
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="tight-form-item">
@ -23,7 +23,7 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
@ -34,7 +34,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 1">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
<h5>Alias patterns</h5>
<ul ng-non-bindable>
<li>{{term fieldname}} = replaced with value of term group by</li>

View File

@ -1,46 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $rootScope, $timeout, uiSegmentSrv) {
$scope.esVersion = $scope.datasource.esVersion;
$scope.panelCtrl = $scope.ctrl;
$scope.init = function() {
var target = $scope.target;
if (!target) { return; }
$scope.queryUpdated();
};
$scope.getFields = function(type) {
var jsonStr = angular.toJson({find: 'fields', type: type});
return $scope.datasource.metricFindQuery(jsonStr)
.then(uiSegmentSrv.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
$scope.queryUpdated = function() {
var newJson = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
if (newJson !== $scope.oldQueryRaw) {
$scope.rawQueryOld = newJson;
$scope.panelCtrl.refresh();
}
$rootScope.appEvent('elastic-query-updated');
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
};
$scope.init();
});
});

View File

@ -0,0 +1,45 @@
///<reference path="../../../headers/common.d.ts" />
import './bucket_agg';
import './metric_agg';
import angular from 'angular';
import _ from 'lodash';
import {QueryCtrl} from 'app/features/panel/panel';
export class ElasticQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html';
esVersion: any;
rawQueryOld: string;
/** @ngInject **/
constructor($scope, $injector, private $rootScope, private $timeout, private uiSegmentSrv) {
super($scope, $injector);
this.esVersion = this.datasource.esVersion;
this.queryUpdated();
}
getFields(type) {
var jsonStr = angular.toJson({find: 'fields', type: type});
return this.datasource.metricFindQuery(jsonStr)
.then(this.uiSegmentSrv.transformToSegments(false))
.catch(this.handleQueryError.bind(this));
}
queryUpdated() {
var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
if (newJson !== this.rawQueryOld) {
this.rawQueryOld = newJson;
this.refresh();
}
this.$rootScope.appEvent('elastic-query-updated');
}
handleQueryError(err) {
this.error = err.message || 'Failed to issue metric query';
return [];
}
}

View File

@ -3,7 +3,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
import moment from 'moment';
import angular from 'angular';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {ElasticDatasource} from "../datasource";
describe('ElasticDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -21,7 +21,7 @@ describe('ElasticDatasource', function() {
function createDatasource(instanceSettings) {
instanceSettings.jsonData = instanceSettings.jsonData || {};
ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = ctx.$injector.instantiate(ElasticDatasource, {instanceSettings: instanceSettings});
}
describe('When testing datasource with index pattern', function() {

View File

@ -1,29 +0,0 @@
///<amd-dependency path="../query_ctrl" />
///<amd-dependency path="app/core/services/segment_srv" />
///<amd-dependency path="test/specs/helpers" name="helpers" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
describe('ElasticQueryCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('ElasticQueryCtrl'));
beforeEach(function() {
ctx.scope.target = {};
ctx.scope.$parent = { get_data: sinon.spy() };
ctx.scope.datasource = ctx.datasource;
ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
});
describe('init', function() {
beforeEach(function() {
ctx.scope.init();
});
});
});

View File

@ -2,17 +2,15 @@
import angular from 'angular';
import {GrafanaDatasource} from './datasource';
import {QueryCtrl} from 'app/features/panel/panel';
var module = angular.module('grafana.directives');
function grafanaMetricsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/grafana/partials/query.editor.html'};
class GrafanaQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/grafana/partials/query.editor.html';
}
export {
GrafanaDatasource,
GrafanaDatasource as Datasource,
grafanaMetricsQueryEditor as metricsQueryEditor
GrafanaQueryCtrl as QueryCtrl,
};

View File

@ -1,56 +1,5 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1"
ng-click="duplicate()">
Duplicate
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index-1)">
Move up
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index+1)">
Move down
</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
<li class="tight-form-item">
Test metric (fake data source)
</li>
</ul>
<div class="clearfix"></div>
</div>
<query-editor-row ctrl="ctrl">
<li class="tight-form-item">
Test metric (fake data source)
</li>
</query-editor-row>

View File

@ -22,6 +22,7 @@ function (angular, _, $, gfunc) {
link: function($scope, elem) {
var categories = gfunc.getCategories();
var allFunctions = getAllFunctionNames(categories);
var ctrl = $scope.ctrl;
$scope.functionMenu = createFunctionDropDownMenu(categories);
@ -48,7 +49,7 @@ function (angular, _, $, gfunc) {
}
$scope.$apply(function() {
$scope.addFunction(funcDef);
ctrl.addFunction(funcDef);
});
$input.trigger('blur');

View File

@ -1,3 +0,0 @@
declare var Datasource: any;
export default Datasource;

View File

@ -1,296 +0,0 @@
define([
'angular',
'lodash',
'jquery',
'app/core/config',
'app/core/utils/datemath',
'./query_ctrl',
'./func_editor',
'./add_graphite_func',
],
function (angular, _, $, config, dateMath) {
'use strict';
/** @ngInject */
function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
this.query = function(options) {
try {
var graphOptions = {
from: this.translateTime(options.rangeRaw.from, false),
until: this.translateTime(options.rangeRaw.to, true),
targets: options.targets,
format: options.format,
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return $q.when([]);
}
if (options.format === 'png') {
return $q.when(this.url + '/render' + '?' + params.join('&'));
}
var httpOptions = { method: this.render_method, url: '/render' };
if (httpOptions.method === 'GET') {
httpOptions.url = httpOptions.url + '?' + params.join('&');
}
else {
httpOptions.data = params.join('&');
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
}
catch(err) {
return $q.reject(err);
}
};
this.convertDataPointsToMs = function(result) {
if (!result || !result.data) { return []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
for (var y = 0; y < series.datapoints.length; y++) {
series.datapoints[y][1] *= 1000;
}
}
return result;
};
this.annotationQuery = function(options) {
// Graphite metric as annotation
if (options.annotation.target) {
var target = templateSrv.replace(options.annotation.target);
var graphiteQuery = {
rangeRaw: options.rangeRaw,
targets: [{ target: target }],
format: 'json',
maxDataPoints: 100
};
return this.query(graphiteQuery)
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
}
return list;
});
}
// Graphite event as annotation
else {
var tags = templateSrv.replace(options.annotation.tags);
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
}
};
this.events = function(options) {
try {
var tags = '';
if (options.tags) {
tags = '&tags=' + options.tags;
}
return this.doGraphiteRequest({
method: 'GET',
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
'&until=' + this.translateTime(options.range.to, true) + tags,
});
}
catch(err) {
return $q.reject(err);
}
};
this.translateTime = function(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
}
else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
return date;
}
date = dateMath.parse(date, roundUp);
}
// graphite' s from filter is exclusive
// here we step back one minute in order
// to guarantee that we get all the data that
// exists for the specified range
if (roundUp) {
if (date.get('s')) {
date.add(1, 'm');
}
}
else if (roundUp === false) {
if (date.get('s')) {
date.subtract(1, 'm');
}
}
return date.unix();
};
this.metricFindQuery = function(query) {
var interpolated;
try {
interpolated = encodeURIComponent(templateSrv.replace(query));
}
catch(err) {
return $q.reject(err);
}
return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
.then(function(results) {
return _.map(results.data, function(metric) {
return {
text: metric.text,
expandable: metric.expandable ? true : false
};
});
});
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this.listDashboards = function(query) {
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
.then(function(results) {
return results.data.dashboards;
});
};
this.loadDashboard = function(dashName) {
return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
};
this.doGraphiteRequest = function(options) {
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return backendSrv.datasourceRequest(options);
};
this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.buildGraphiteParams = function(options, scopedVars) {
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var clean_options = [], targets = {};
var target, targetValue, i;
var regex = /\#([A-Z])/g;
var intervalFormatFixRegex = /'(\d+)m'/gi;
var hasTargets = false;
if (options.format !== 'png') {
options['format'] = 'json';
}
function fixIntervalFormat(match) {
return match.replace('m', 'min').replace('M', 'mon');
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
if (!target.refId) {
target.refId = this._seriesRefLetters[i];
}
targetValue = templateSrv.replace(target.target, scopedVars);
targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
targets[target.refId] = targetValue;
}
function nestedSeriesRegexReplacer(match, g1) {
return targets[g1];
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
targetValue = targets[target.refId];
targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
targets[target.refId] = targetValue;
if (!target.hide) {
hasTargets = true;
clean_options.push("target=" + encodeURIComponent(targetValue));
}
}
_.each(options, function (value, key) {
if ($.inArray(key, graphite_options) === -1) { return; }
if (value) {
clean_options.push(key + "=" + encodeURIComponent(value));
}
});
if (!hasTargets) {
return [];
}
return clean_options;
};
}
return GraphiteDatasource;
});

View File

@ -0,0 +1,281 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
/** @ngInject */
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
this.query = function(options) {
try {
var graphOptions = {
from: this.translateTime(options.rangeRaw.from, false),
until: this.translateTime(options.rangeRaw.to, true),
targets: options.targets,
format: options.format,
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return $q.when([]);
}
if (options.format === 'png') {
return $q.when(this.url + '/render' + '?' + params.join('&'));
}
var httpOptions: any = {method: this.render_method, url: '/render'};
if (httpOptions.method === 'GET') {
httpOptions.url = httpOptions.url + '?' + params.join('&');
} else {
httpOptions.data = params.join('&');
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
} catch (err) {
return $q.reject(err);
}
};
this.convertDataPointsToMs = function(result) {
if (!result || !result.data) { return []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
for (var y = 0; y < series.datapoints.length; y++) {
series.datapoints[y][1] *= 1000;
}
}
return result;
};
this.annotationQuery = function(options) {
// Graphite metric as annotation
if (options.annotation.target) {
var target = templateSrv.replace(options.annotation.target);
var graphiteQuery = {
rangeRaw: options.rangeRaw,
targets: [{ target: target }],
format: 'json',
maxDataPoints: 100
};
return this.query(graphiteQuery)
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
}
return list;
});
} else {
// Graphite event as annotation
var tags = templateSrv.replace(options.annotation.tags);
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
}
};
this.events = function(options) {
try {
var tags = '';
if (options.tags) {
tags = '&tags=' + options.tags;
}
return this.doGraphiteRequest({
method: 'GET',
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
'&until=' + this.translateTime(options.range.to, true) + tags,
});
} catch (err) {
return $q.reject(err);
}
};
this.translateTime = function(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
} else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
return date;
}
date = dateMath.parse(date, roundUp);
}
// graphite' s from filter is exclusive
// here we step back one minute in order
// to guarantee that we get all the data that
// exists for the specified range
if (roundUp) {
if (date.get('s')) {
date.add(1, 'm');
}
} else if (roundUp === false) {
if (date.get('s')) {
date.subtract(1, 'm');
}
}
return date.unix();
};
this.metricFindQuery = function(query) {
var interpolated;
try {
interpolated = encodeURIComponent(templateSrv.replace(query));
} catch (err) {
return $q.reject(err);
}
return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
.then(function(results) {
return _.map(results.data, function(metric) {
return {
text: metric.text,
expandable: metric.expandable ? true : false
};
});
});
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this.listDashboards = function(query) {
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
.then(function(results) {
return results.data.dashboards;
});
};
this.loadDashboard = function(dashName) {
return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
};
this.doGraphiteRequest = function(options) {
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return backendSrv.datasourceRequest(options);
};
this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.buildGraphiteParams = function(options, scopedVars) {
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var clean_options = [], targets = {};
var target, targetValue, i;
var regex = /\#([A-Z])/g;
var intervalFormatFixRegex = /'(\d+)m'/gi;
var hasTargets = false;
if (options.format !== 'png') {
options['format'] = 'json';
}
function fixIntervalFormat(match) {
return match.replace('m', 'min').replace('M', 'mon');
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
if (!target.refId) {
target.refId = this._seriesRefLetters[i];
}
targetValue = templateSrv.replace(target.target, scopedVars);
targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
targets[target.refId] = targetValue;
}
function nestedSeriesRegexReplacer(match, g1) {
return targets[g1];
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
targetValue = targets[target.refId];
targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
targets[target.refId] = targetValue;
if (!target.hide) {
hasTargets = true;
clean_options.push("target=" + encodeURIComponent(targetValue));
}
}
_.each(options, function (value, key) {
if (_.indexOf(graphite_options, key) === -1) { return; }
if (value) {
clean_options.push(key + "=" + encodeURIComponent(value));
}
});
if (!hasTargets) {
return [];
}
return clean_options;
};
}

View File

@ -27,6 +27,7 @@ function (angular, _, $) {
link: function postLink($scope, elem) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(funcControlsTemplate);
var ctrl = $scope.ctrl;
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
@ -79,11 +80,13 @@ function (angular, _, $) {
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
$scope.$apply($scope.targetChanged);
}
$scope.$apply(function() {
ctrl.targetChanged();
});
$input.hide();
$link.show();
$input.hide();
$link.show();
}
}
function inputKeyPress(paramIndex, e) {
@ -198,7 +201,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
$scope.removeFunction($scope.func);
ctrl.removeFunction($scope.func);
});
return;
}
@ -206,7 +209,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index - 1);
$scope.targetChanged();
ctrl.targetChanged();
});
return;
}
@ -214,7 +217,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index + 1);
$scope.targetChanged();
ctrl.targetChanged();
});
return;
}

View File

@ -1,682 +0,0 @@
define([
'lodash'
], function(_) {
'use strict';
// This is auto generated from the unicode tables.
// The tables are at:
// http://www.fileformat.info/info/unicode/category/Lu/list.htm
// http://www.fileformat.info/info/unicode/category/Ll/list.htm
// http://www.fileformat.info/info/unicode/category/Lt/list.htm
// http://www.fileformat.info/info/unicode/category/Lm/list.htm
// http://www.fileformat.info/info/unicode/category/Lo/list.htm
// http://www.fileformat.info/info/unicode/category/Nl/list.htm
var unicodeLetterTable = [
170, 170, 181, 181, 186, 186, 192, 214,
216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
177984, 177984, 178205, 178205, 194560, 195101
];
var identifierStartTable = [];
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i === 126 || // ~
i === 124 || // |
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
i === 45 || // -
i === 42 || // *
i === 58 || // :
i === 91 || // templateStart [
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i === 61 || // =
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
function Lexer(expression) {
this.input = expression;
this.char = 1;
this.from = 1;
}
Lexer.prototype = {
peek: function (i) {
return this.input.charAt(i || 0);
},
skip: function (i) {
i = i || 1;
this.char += i;
this.input = this.input.slice(i);
},
tokenize: function() {
var list = [];
var token;
while (token = this.next()) {
list.push(token);
}
return list;
},
next: function() {
this.from = this.char;
// Move to the next non-space character.
var start;
if (/\s/.test(this.peek())) {
start = this.char;
while (/\s/.test(this.peek())) {
this.from += 1;
this.skip();
}
if (this.peek() === "") { // EOL
return null;
}
}
var match = this.scanStringLiteral();
if (match) {
return match;
}
match =
this.scanPunctuator() ||
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
return match;
}
// No token could be matched, give up.
return null;
},
scanTemplateSequence: function() {
if (this.peek() === '[' && this.peek(1) === '[') {
return {
type: 'templateStart',
value: '[[',
pos: this.char
};
}
if (this.peek() === ']' && this.peek(1) === ']') {
return {
type: 'templateEnd',
value: '[[',
pos: this.char
};
}
return null;
},
/*
* Extract a JavaScript identifier out of the next sequence of
* characters or return 'null' if its not possible. In addition,
* to Identifier this method can also produce BooleanLiteral
* (true/false) and NullLiteral (null).
*/
scanIdentifier: function() {
var id = "";
var index = 0;
var type, char;
// Detects any character in the Unicode categories "Uppercase
// letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
// (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
// "Letter number (Nl)".
//
// Both approach and unicodeLetterTable were borrowed from
// Google's Traceur.
function isUnicodeLetter(code) {
for (var i = 0; i < unicodeLetterTable.length;) {
if (code < unicodeLetterTable[i++]) {
return false;
}
if (code <= unicodeLetterTable[i++]) {
return true;
}
}
return false;
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
var readUnicodeEscapeSequence = _.bind(function () {
/*jshint validthis:true */
index += 1;
if (this.peek(index) !== "u") {
return null;
}
var ch1 = this.peek(index + 1);
var ch2 = this.peek(index + 2);
var ch3 = this.peek(index + 3);
var ch4 = this.peek(index + 4);
var code;
if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
if (isUnicodeLetter(code)) {
index += 5;
return "\\u" + ch1 + ch2 + ch3 + ch4;
}
return null;
}
return null;
}, this);
var getIdentifierStart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (chr === '*') {
index += 1;
return chr;
}
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierStartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
var getIdentifierPart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierPartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
char = getIdentifierStart();
if (char === null) {
return null;
}
id = char;
for (;;) {
char = getIdentifierPart();
if (char === null) {
break;
}
id += char;
}
switch (id) {
case 'true': {
type = 'bool';
break;
}
case 'false': {
type = 'bool';
break;
}
default:
type = "identifier";
}
return {
type: type,
value: id,
pos: this.char
};
},
/*
* Extract a numeric literal out of the next sequence of
* characters or return 'null' if its not possible. This method
* supports all numeric literals described in section 7.8.3
* of the EcmaScript 5 specification.
*
* This method's implementation was heavily influenced by the
* scanNumericLiteral function in the Esprima parser's source code.
*/
scanNumericLiteral: function () {
var index = 0;
var value = "";
var length = this.input.length;
var char = this.peek(index);
var bad;
function isDecimalDigit(str) {
return (/^[0-9]$/).test(str);
}
function isOctalDigit(str) {
return (/^[0-7]$/).test(str);
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
function isIdentifierStart(ch) {
return (ch === "$") || (ch === "_") || (ch === "\\") ||
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// handle negative num literals
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};
return Lexer;
});

View File

@ -0,0 +1,678 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
// This is auto generated from the unicode tables.
// The tables are at:
// http://www.fileformat.info/info/unicode/category/Lu/list.htm
// http://www.fileformat.info/info/unicode/category/Ll/list.htm
// http://www.fileformat.info/info/unicode/category/Lt/list.htm
// http://www.fileformat.info/info/unicode/category/Lm/list.htm
// http://www.fileformat.info/info/unicode/category/Lo/list.htm
// http://www.fileformat.info/info/unicode/category/Nl/list.htm
var unicodeLetterTable = [
170, 170, 181, 181, 186, 186, 192, 214,
216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
177984, 177984, 178205, 178205, 194560, 195101
];
var identifierStartTable = [];
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i === 126 || // ~
i === 124 || // |
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
i === 45 || // -
i === 42 || // *
i === 58 || // :
i === 91 || // templateStart [
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i === 61 || // =
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
export function Lexer(expression) {
this.input = expression;
this.char = 1;
this.from = 1;
}
Lexer.prototype = {
peek: function (i) {
return this.input.charAt(i || 0);
},
skip: function (i) {
i = i || 1;
this.char += i;
this.input = this.input.slice(i);
},
tokenize: function() {
var list = [];
var token;
while (token = this.next()) {
list.push(token);
}
return list;
},
next: function() {
this.from = this.char;
// Move to the next non-space character.
var start;
if (/\s/.test(this.peek())) {
start = this.char;
while (/\s/.test(this.peek())) {
this.from += 1;
this.skip();
}
if (this.peek() === "") { // EOL
return null;
}
}
var match = this.scanStringLiteral();
if (match) {
return match;
}
match =
this.scanPunctuator() ||
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
return match;
}
// No token could be matched, give up.
return null;
},
scanTemplateSequence: function() {
if (this.peek() === '[' && this.peek(1) === '[') {
return {
type: 'templateStart',
value: '[[',
pos: this.char
};
}
if (this.peek() === ']' && this.peek(1) === ']') {
return {
type: 'templateEnd',
value: '[[',
pos: this.char
};
}
return null;
},
/*
* Extract a JavaScript identifier out of the next sequence of
* characters or return 'null' if its not possible. In addition,
* to Identifier this method can also produce BooleanLiteral
* (true/false) and NullLiteral (null).
*/
scanIdentifier: function() {
var id = "";
var index = 0;
var type, char;
// Detects any character in the Unicode categories "Uppercase
// letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
// (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
// "Letter number (Nl)".
//
// Both approach and unicodeLetterTable were borrowed from
// Google's Traceur.
function isUnicodeLetter(code) {
for (var i = 0; i < unicodeLetterTable.length;) {
if (code < unicodeLetterTable[i++]) {
return false;
}
if (code <= unicodeLetterTable[i++]) {
return true;
}
}
return false;
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
var readUnicodeEscapeSequence = _.bind(function () {
/*jshint validthis:true */
index += 1;
if (this.peek(index) !== "u") {
return null;
}
var ch1 = this.peek(index + 1);
var ch2 = this.peek(index + 2);
var ch3 = this.peek(index + 3);
var ch4 = this.peek(index + 4);
var code;
if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
if (isUnicodeLetter(code)) {
index += 5;
return "\\u" + ch1 + ch2 + ch3 + ch4;
}
return null;
}
return null;
}, this);
var getIdentifierStart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (chr === '*') {
index += 1;
return chr;
}
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierStartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
var getIdentifierPart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierPartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
char = getIdentifierStart();
if (char === null) {
return null;
}
id = char;
for (;;) {
char = getIdentifierPart();
if (char === null) {
break;
}
id += char;
}
switch (id) {
case 'true': {
type = 'bool';
break;
}
case 'false': {
type = 'bool';
break;
}
default:
type = "identifier";
}
return {
type: type,
value: id,
pos: this.char
};
},
/*
* Extract a numeric literal out of the next sequence of
* characters or return 'null' if its not possible. This method
* supports all numeric literals described in section 7.8.3
* of the EcmaScript 5 specification.
*
* This method's implementation was heavily influenced by the
* scanNumericLiteral function in the Esprima parser's source code.
*/
scanNumericLiteral: function (): any {
var index = 0;
var value = "";
var length = this.input.length;
var char = this.peek(index);
var bad;
function isDecimalDigit(str) {
return (/^[0-9]$/).test(str);
}
function isOctalDigit(str) {
return (/^[0-7]$/).test(str);
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
function isIdentifierStart(ch) {
return (ch === "$") || (ch === "_") || (ch === "\\") ||
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// handle negative num literals
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};

View File

@ -1,33 +0,0 @@
define([
'./datasource',
],
function (GraphiteDatasource) {
'use strict';
function metricsQueryEditor() {
return {
controller: 'GraphiteQueryCtrl',
templateUrl: 'public/app/plugins/datasource/graphite/partials/query.editor.html'
};
}
function metricsQueryOptions() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/query.options.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/annotations.editor.html'};
}
function configView() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/config.html'};
}
return {
Datasource: GraphiteDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -0,0 +1,23 @@
import {GraphiteDatasource} from './datasource';
import {GraphiteQueryCtrl} from './query_ctrl';
class GraphiteConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
}
class GraphiteQueryOptionsCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.options.html';
}
class AnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/annotations.editor.html';
}
export {
GraphiteDatasource as Datasource,
GraphiteQueryCtrl as QueryCtrl,
GraphiteConfigCtrl as ConfigCtrl,
GraphiteQueryOptionsCtrl as QueryOptionsCtrl,
AnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1,265 +0,0 @@
define([
'./lexer'
], function (Lexer) {
'use strict';
function Parser(expression) {
this.expression = expression;
this.lexer = new Lexer(expression);
this.tokens = this.lexer.tokenize();
this.index = 0;
}
Parser.prototype = {
getAst: function () {
return this.start();
},
start: function () {
try {
return this.functionCall() || this.metricExpression();
}
catch (e) {
return {
type: 'error',
message: e.message,
pos: e.pos
};
}
},
curlyBraceSegment: function() {
if (this.match('identifier', '{') || this.match('{')) {
var curlySegment = "";
while (!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: curlySegment
};
}
else {
return null;
}
},
metricSegment: function() {
var curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number')) {
// hack to handle float numbers in metric segments
var parts = this.consumeToken().value.split('.');
if (parts.length === 2) {
this.tokens.splice(this.index, 0, { type: '.' });
this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
}
return {
type: 'segment',
value: parts[0]
};
}
if (!this.match('templateStart')) {
this.errorMark('Expected metric identifier');
}
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
}
var node = {
type: 'template',
value: this.consumeToken().value
};
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.consumeToken();
return node;
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
return null;
}
var node = {
type: 'metric',
segments: []
};
node.segments.push(this.metricSegment());
while (this.match('.')) {
this.consumeToken();
var segment = this.metricSegment();
if (!segment) {
this.errorMark('Expected metric identifier');
}
node.segments.push(segment);
}
return node;
},
functionCall: function() {
if (!this.match('identifier', '(')) {
return null;
}
var node = {
type: 'function',
name: this.consumeToken().value,
};
// consume left parenthesis
this.consumeToken();
node.params = this.functionParameters();
if (!this.match(')')) {
this.errorMark('Expected closing parenthesis');
}
this.consumeToken();
return node;
},
boolExpression: function() {
if (!this.match('bool')) {
return null;
}
return {
type: 'bool',
value: this.consumeToken().value === 'true',
};
},
functionParameters: function () {
if (this.match(')') || this.match('')) {
return [];
}
var param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.boolExpression() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {
return [param];
}
this.consumeToken();
return [param].concat(this.functionParameters());
},
seriesRefExpression: function() {
if (!this.match('identifier')) {
return null;
}
var value = this.tokens[this.index].value;
if (!value.match(/\#[A-Z]/)) {
return null;
}
var token = this.consumeToken();
return {
type: 'series-ref',
value: token.value
};
},
numericLiteral: function () {
if (!this.match('number')) {
return null;
}
return {
type: 'number',
value: parseFloat(this.consumeToken().value)
};
},
stringLiteral: function () {
if (!this.match('string')) {
return null;
}
var token = this.consumeToken();
if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos };
}
return {
type: 'string',
value: token.value
};
},
errorMark: function(text) {
var currentToken = this.tokens[this.index];
var type = currentToken ? currentToken.type : 'end of string';
throw {
message: text + " instead found " + type,
pos: currentToken ? currentToken.pos : this.lexer.char
};
},
// returns token value and incre
consumeToken: function() {
this.index++;
return this.tokens[this.index - 1];
},
matchToken: function(type, index) {
var token = this.tokens[this.index + index];
return (token === undefined && type === '') ||
token && token.type === type;
},
match: function(token1, token2) {
return this.matchToken(token1, 0) &&
(!token2 || this.matchToken(token2, 1));
},
};
return Parser;
});

View File

@ -0,0 +1,258 @@
import {Lexer} from './lexer';
export function Parser(expression) {
this.expression = expression;
this.lexer = new Lexer(expression);
this.tokens = this.lexer.tokenize();
this.index = 0;
}
Parser.prototype = {
getAst: function () {
return this.start();
},
start: function () {
try {
return this.functionCall() || this.metricExpression();
} catch (e) {
return {
type: 'error',
message: e.message,
pos: e.pos
};
}
},
curlyBraceSegment: function() {
if (this.match('identifier', '{') || this.match('{')) {
var curlySegment = "";
while (!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: curlySegment
};
} else {
return null;
}
},
metricSegment: function() {
var curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number')) {
// hack to handle float numbers in metric segments
var parts = this.consumeToken().value.split('.');
if (parts.length === 2) {
this.tokens.splice(this.index, 0, { type: '.' });
this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
}
return {
type: 'segment',
value: parts[0]
};
}
if (!this.match('templateStart')) {
this.errorMark('Expected metric identifier');
}
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
}
var node = {
type: 'template',
value: this.consumeToken().value
};
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.consumeToken();
return node;
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
return null;
}
var node = {
type: 'metric',
segments: []
};
node.segments.push(this.metricSegment());
while (this.match('.')) {
this.consumeToken();
var segment = this.metricSegment();
if (!segment) {
this.errorMark('Expected metric identifier');
}
node.segments.push(segment);
}
return node;
},
functionCall: function() {
if (!this.match('identifier', '(')) {
return null;
}
var node: any = {
type: 'function',
name: this.consumeToken().value,
};
// consume left parenthesis
this.consumeToken();
node.params = this.functionParameters();
if (!this.match(')')) {
this.errorMark('Expected closing parenthesis');
}
this.consumeToken();
return node;
},
boolExpression: function() {
if (!this.match('bool')) {
return null;
}
return {
type: 'bool',
value: this.consumeToken().value === 'true',
};
},
functionParameters: function () {
if (this.match(')') || this.match('')) {
return [];
}
var param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.boolExpression() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {
return [param];
}
this.consumeToken();
return [param].concat(this.functionParameters());
},
seriesRefExpression: function() {
if (!this.match('identifier')) {
return null;
}
var value = this.tokens[this.index].value;
if (!value.match(/\#[A-Z]/)) {
return null;
}
var token = this.consumeToken();
return {
type: 'series-ref',
value: token.value
};
},
numericLiteral: function () {
if (!this.match('number')) {
return null;
}
return {
type: 'number',
value: parseFloat(this.consumeToken().value)
};
},
stringLiteral: function () {
if (!this.match('string')) {
return null;
}
var token = this.consumeToken();
if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos };
}
return {
type: 'string',
value: token.value
};
},
errorMark: function(text) {
var currentToken = this.tokens[this.index];
var type = currentToken ? currentToken.type : 'end of string';
throw {
message: text + " instead found " + type,
pos: currentToken ? currentToken.pos : this.lexer.char
};
},
// returns token value and incre
consumeToken: function() {
this.index++;
return this.tokens[this.index - 1];
},
matchToken: function(type, index) {
var token = this.tokens[this.index + index];
return (token === undefined && type === '') ||
token && token.type === type;
},
match: function(token1, token2) {
return this.matchToken(token1, 0) &&
(!token2 || this.matchToken(token2, 1));
},
};

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='annotation.target' placeholder=""></input>
<input type="text" class="span10" ng-model='ctrl.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='annotation.tags' placeholder=""></input>
<input type="text" ng-model='ctrl.annotation.tags' placeholder=""></input>
</div>
</div>

View File

@ -1,2 +1,3 @@
<datasource-http-settings></datasource-http-settings>
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>

View File

@ -1,73 +1,21 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<a class="pointer" tabindex="1" ng-click="toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="toggleEditorMode()">
Switch editor mode
</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<query-editor-row ctrl="ctrl">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<li class="tight-form-flex-wrapper" ng-show="ctrl.target.textEditor">
<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="ctrl.target.target" give-focus="ctrl.target.textEditor" spellcheck='false' ng-model-onblur ng-change="ctrl.targetTextChanged()"></input>
</li>
<span style="display: block; overflow: hidden;">
<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="target.target" give-focus="target.textEditor" spellcheck='false' ng-model-onblur ng-change="panelCtrl.getData()" ng-show="target.textEditor"></input>
</span>
<li ng-hide-start="ctrl.target.textEditor"></li>
<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
<li ng-repeat="segment in segments" role="menuitem">
<metric-segment segment="segment" get-options="getAltSegments($index)" on-change="segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
</ul>
<div class="clearfix"></div>
</div>
<li ng-repeat="segment in ctrl.segments" role="menuitem">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in ctrl.functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
<li ng-hide-end></li>
</query-editor-row>

View File

@ -1,5 +1,4 @@
<section class="grafana-metric-options">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item tight-form-item-icon">
@ -11,7 +10,7 @@
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="ctrl.panel.cacheTimeout"
ng-model="ctrl.panelCtrl.panel.cacheTimeout"
bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
@ -23,10 +22,10 @@
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="ctrl.panel.maxDataPoints"
ng-model="ctrl.panelCtrl.panel.maxDataPoints"
bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
data-placement="right"
ng-model-onblur ng-change="ctrl.refresh()"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
spellcheck='false'
placeholder="auto"></input>
</li>
@ -39,27 +38,27 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
shorter legend names
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
series as parameters
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
templating
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
max data points
</a>
</li>
@ -71,7 +70,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 1">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
<h5>Shorter legend names</h5>
<ul>
<li>alias() function to specify a custom series name</li>
@ -81,7 +80,7 @@
</ul>
</div>
<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 2">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
<h5>Series as parameter</h5>
<ul>
<li>Some graphite functions allow you to have many series arguments</li>
@ -99,7 +98,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 3">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
<h5>Stacking</h5>
<ul>
<li>You find the stacking option under Display Styles tab</li>
@ -107,7 +106,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 4">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 4">
<h5>Templating</h5>
<ul>
<li>You can use a template variable in place of metric names</li>
@ -116,7 +115,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 5">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 5">
<h5>Max data points</h5>
<ul>
<li>Every graphite request is issued with a maxDataPoints parameter</li>

View File

@ -1,292 +0,0 @@
define([
'angular',
'lodash',
'app/core/config',
'./gfunc',
'./parser'
],
function (angular, _, config, gfunc, Parser) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('GraphiteQueryCtrl', function($scope, uiSegmentSrv, templateSrv) {
var panelCtrl = $scope.panelCtrl = $scope.ctrl;
var datasource = $scope.datasource;
$scope.init = function() {
if ($scope.target) {
$scope.target.target = $scope.target.target || '';
parseTarget();
}
};
$scope.toggleEditorMode = function() {
$scope.target.textEditor = !$scope.target.textEditor;
parseTarget();
};
// The way parsing and the target editor works needs
// to be rewritten to handle functions that take multiple series
function parseTarget() {
$scope.functions = [];
$scope.segments = [];
delete $scope.parserError;
if ($scope.target.textEditor) {
return;
}
var parser = new Parser($scope.target.target);
var astNode = parser.getAst();
if (astNode === null) {
checkOtherSegments(0);
return;
}
if (astNode.type === 'error') {
$scope.parserError = astNode.message + " at position: " + astNode.pos;
$scope.target.textEditor = true;
return;
}
try {
parseTargeRecursive(astNode);
}
catch (err) {
console.log('error parsing target:', err.message);
$scope.parserError = err.message;
$scope.target.textEditor = true;
}
checkOtherSegments($scope.segments.length - 1);
}
function addFunctionParameter(func, value, index, shiftBack) {
if (shiftBack) {
index = Math.max(index - 1, 0);
}
func.params[index] = value;
}
function parseTargeRecursive(astNode, func, index) {
if (astNode === null) {
return null;
}
switch(astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, function(param, index) {
parseTargeRecursive(param, innerFunc, index);
});
innerFunc.updateText();
$scope.functions.push(innerFunc);
break;
case 'series-ref':
addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
break;
case 'bool':
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
addFunctionParameter(func, astNode.value, index, true);
break;
case 'metric':
if ($scope.segments.length > 0) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
addFunctionParameter(func, astNode.segments[0].value, index, true);
break;
}
$scope.segments = _.map(astNode.segments, function(segment) {
return uiSegmentSrv.newSegment(segment);
});
}
}
function getSegmentPathUpTo(index) {
var arr = $scope.segments.slice(0, index);
return _.reduce(arr, function(result, segment) {
return result ? (result + "." + segment.value) : segment.value;
}, "");
}
function checkOtherSegments(fromIndex) {
if (fromIndex === 0) {
$scope.segments.push(uiSegmentSrv.newSelectMetric());
return;
}
var path = getSegmentPathUpTo(fromIndex + 1);
return datasource.metricFindQuery(path)
.then(function(segments) {
if (segments.length === 0) {
if (path !== '') {
$scope.segments = $scope.segments.splice(0, fromIndex);
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
} else if (segments[0].expandable) {
if ($scope.segments.length === fromIndex) {
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
else {
return checkOtherSegments(fromIndex + 1);
}
}
})
.then(null, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
});
}
function setSegmentFocus(segmentIndex) {
_.each($scope.segments, function(segment, index) {
segment.focus = segmentIndex === index;
});
}
function wrapFunction(target, func) {
return func.render(target);
}
$scope.getAltSegments = function (index) {
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
return datasource.metricFindQuery(query).then(function(segments) {
var altSegments = _.map(segments, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(templateSrv.variables, function(variable) {
altSegments.unshift(uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
}));
});
// add wildcard option
altSegments.unshift(uiSegmentSrv.newSegment('*'));
return altSegments;
})
.then(null, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
});
};
$scope.segmentValueChanged = function (segment, segmentIndex) {
delete $scope.parserError;
if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
$scope.functions = [];
}
if (segment.expandable) {
return checkOtherSegments(segmentIndex + 1).then(function() {
setSegmentFocus(segmentIndex + 1);
$scope.targetChanged();
});
}
else {
$scope.segments = $scope.segments.splice(0, segmentIndex + 1);
}
setSegmentFocus(segmentIndex + 1);
$scope.targetChanged();
};
$scope.targetTextChanged = function() {
parseTarget();
panelCtrl.refresh();
};
$scope.targetChanged = function() {
if ($scope.parserError) {
return;
}
var oldTarget = $scope.target.target;
var target = getSegmentPathUpTo($scope.segments.length);
$scope.target.target = _.reduce($scope.functions, wrapFunction, target);
if ($scope.target.target !== oldTarget) {
if ($scope.segments[$scope.segments.length - 1].value !== 'select metric') {
panelCtrl.refresh();
}
}
};
$scope.removeFunction = function(func) {
$scope.functions = _.without($scope.functions, func);
$scope.targetChanged();
};
$scope.addFunction = function(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
$scope.functions.push(newFunc);
$scope.moveAliasFuncLast();
$scope.smartlyHandleNewAliasByNode(newFunc);
if ($scope.segments.length === 1 && $scope.segments[0].fake) {
$scope.segments = [];
}
if (!newFunc.params.length && newFunc.added) {
$scope.targetChanged();
}
};
$scope.moveAliasFuncLast = function() {
var aliasFunc = _.find($scope.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
$scope.functions = _.without($scope.functions, aliasFunc);
$scope.functions.push(aliasFunc);
}
};
$scope.smartlyHandleNewAliasByNode = function(func) {
if (func.def.name !== 'aliasByNode') {
return;
}
for(var i = 0; i < $scope.segments.length; i++) {
if ($scope.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
$scope.targetChanged();
return;
}
}
};
$scope.toggleMetricOptions = function() {
$scope.panel.metricOptionsEnabled = !$scope.panel.metricOptionsEnabled;
if (!$scope.panel.metricOptionsEnabled) {
delete $scope.panel.cacheTimeout;
}
};
$scope.init();
});
});

View File

@ -0,0 +1,276 @@
///<reference path="../../../headers/common.d.ts" />
import './add_graphite_func';
import './func_editor';
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import gfunc from './gfunc';
import {Parser} from './parser';
import {QueryCtrl} from 'app/features/panel/panel';
export class GraphiteQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.editor.html';
functions: any[];
segments: any[];
/** @ngInject **/
constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
super($scope, $injector);
if (this.target) {
this.target.target = this.target.target || '';
this.parseTarget();
}
}
toggleEditorMode() {
this.target.textEditor = !this.target.textEditor;
this.parseTarget();
}
parseTarget() {
this.functions = [];
this.segments = [];
this.error = null;
if (this.target.textEditor) {
return;
}
var parser = new Parser(this.target.target);
var astNode = parser.getAst();
if (astNode === null) {
this.checkOtherSegments(0);
return;
}
if (astNode.type === 'error') {
this.error = astNode.message + " at position: " + astNode.pos;
this.target.textEditor = true;
return;
}
try {
this.parseTargeRecursive(astNode, null, 0);
} catch (err) {
console.log('error parsing target:', err.message);
this.error = err.message;
this.target.textEditor = true;
}
this.checkOtherSegments(this.segments.length - 1);
}
addFunctionParameter(func, value, index, shiftBack) {
if (shiftBack) {
index = Math.max(index - 1, 0);
}
func.params[index] = value;
}
parseTargeRecursive(astNode, func, index) {
if (astNode === null) {
return null;
}
switch (astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, (param, index) => {
this.parseTargeRecursive(param, innerFunc, index);
});
innerFunc.updateText();
this.functions.push(innerFunc);
break;
case 'series-ref':
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
break;
case 'bool':
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
this.addFunctionParameter(func, astNode.value, index, true);
break;
case 'metric':
if (this.segments.length > 0) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
break;
}
this.segments = _.map(astNode.segments, segment => {
return this.uiSegmentSrv.newSegment(segment);
});
}
}
getSegmentPathUpTo(index) {
var arr = this.segments.slice(0, index);
return _.reduce(arr, function(result, segment) {
return result ? (result + "." + segment.value) : segment.value;
}, "");
}
checkOtherSegments(fromIndex) {
if (fromIndex === 0) {
this.segments.push(this.uiSegmentSrv.newSelectMetric());
return;
}
var path = this.getSegmentPathUpTo(fromIndex + 1);
return this.datasource.metricFindQuery(path).then(segments => {
if (segments.length === 0) {
if (path !== '') {
this.segments = this.segments.splice(0, fromIndex);
this.segments.push(this.uiSegmentSrv.newSelectMetric());
}
} else if (segments[0].expandable) {
if (this.segments.length === fromIndex) {
this.segments.push(this.uiSegmentSrv.newSelectMetric());
} else {
return this.checkOtherSegments(fromIndex + 1);
}
}
}).catch(err => {
this.error = err.message || 'Failed to issue metric query';
});
}
setSegmentFocus(segmentIndex) {
_.each(this.segments, (segment, index) => {
segment.focus = segmentIndex === index;
});
}
wrapFunction(target, func) {
return func.render(target);
}
getAltSegments(index) {
var query = index === 0 ? '*' : this.getSegmentPathUpTo(index) + '.*';
return this.datasource.metricFindQuery(query).then(segments => {
var altSegments = _.map(segments, segment => {
return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(this.templateSrv.variables, variable => {
altSegments.unshift(this.uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
}));
});
// add wildcard option
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
return altSegments;
}).catch(err => {
this.error = err.message || 'Failed to issue metric query';
return [];
});
}
segmentValueChanged(segment, segmentIndex) {
this.error = null;
if (this.functions.length > 0 && this.functions[0].def.fake) {
this.functions = [];
}
if (segment.expandable) {
return this.checkOtherSegments(segmentIndex + 1).then(() => {
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
});
} else {
this.segments = this.segments.splice(0, segmentIndex + 1);
}
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
}
targetTextChanged() {
this.parseTarget();
this.panelCtrl.refresh();
}
targetChanged() {
if (this.error) {
return;
}
var oldTarget = this.target.target;
var target = this.getSegmentPathUpTo(this.segments.length);
this.target.target = _.reduce(this.functions, this.wrapFunction, target);
if (this.target.target !== oldTarget) {
if (this.segments[this.segments.length - 1].value !== 'select metric') {
this.panelCtrl.refresh();
}
}
}
removeFunction(func) {
this.functions = _.without(this.functions, func);
this.targetChanged();
}
addFunction(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
this.functions.push(newFunc);
this.moveAliasFuncLast();
this.smartlyHandleNewAliasByNode(newFunc);
if (this.segments.length === 1 && this.segments[0].fake) {
this.segments = [];
}
if (!newFunc.params.length && newFunc.added) {
this.targetChanged();
}
}
moveAliasFuncLast() {
var aliasFunc = _.find(this.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
this.functions = _.without(this.functions, aliasFunc);
this.functions.push(aliasFunc);
}
}
smartlyHandleNewAliasByNode(func) {
if (func.def.name !== 'aliasByNode') {
return;
}
for (var i = 0; i < this.segments.length; i++) {
if (this.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
this.targetChanged();
return;
}
}
}
}

View File

@ -1,7 +1,7 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {GraphiteDatasource} from "../datasource";
describe('graphiteDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -18,7 +18,7 @@ describe('graphiteDatasource', function() {
}));
beforeEach(function() {
ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
});
describe('When querying influxdb with one target using query editor target spec', function() {

View File

@ -0,0 +1,118 @@
import {describe, it, expect} from 'test/lib/common';
import {Lexer} from '../lexer';
describe('when lexing graphite expression', function() {
it('should tokenize metric expression', function() {
var lexer = new Lexer('metric.test.*.asd.count');
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('metric');
expect(tokens[1].value).to.be('.');
expect(tokens[2].type).to.be('identifier');
expect(tokens[4].type).to.be('identifier');
expect(tokens[4].pos).to.be(13);
});
it('should tokenize metric expression with dash', function() {
var lexer = new Lexer('metric.test.se1-server-*.asd.count');
var tokens = lexer.tokenize();
expect(tokens[4].type).to.be('identifier');
expect(tokens[4].value).to.be('se1-server-*');
});
it('should tokenize metric expression with dash2', function() {
var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('net');
expect(tokens[2].value).to.be('192-168-1-1');
});
it('should tokenize metric expression with equal sign', function() {
var lexer = new Lexer('apps=test');
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('apps=test');
});
it('simple function2', function() {
var lexer = new Lexer('offset(test.metric, -100)');
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('identifier');
expect(tokens[4].type).to.be('identifier');
expect(tokens[6].type).to.be('number');
});
it('should tokenize metric expression with curly braces', function() {
var lexer = new Lexer('metric.se1-{first, second}.count');
var tokens = lexer.tokenize();
expect(tokens.length).to.be(10);
expect(tokens[3].type).to.be('{');
expect(tokens[4].value).to.be('first');
expect(tokens[5].value).to.be(',');
expect(tokens[6].value).to.be('second');
});
it('should tokenize metric expression with number segments', function() {
var lexer = new Lexer("metric.10.12_10.test");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('10');
expect(tokens[4].value).to.be('12_10');
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize func call with numbered metric and number arg', function() {
var lexer = new Lexer("scale(metric.10, 15)");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('metric');
expect(tokens[4].value).to.be('10');
expect(tokens[4].type).to.be('number');
expect(tokens[6].type).to.be('number');
});
it('should tokenize metric with template parameter', function() {
var lexer = new Lexer("metric.[[server]].test");
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('[[server]]');
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize metric with question mark', function() {
var lexer = new Lexer("metric.server_??.test");
var tokens = lexer.tokenize();
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('server_??');
expect(tokens[4].type).to.be('identifier');
});
it('should handle error with unterminated string', function() {
var lexer = new Lexer("alias(metric, 'asd)");
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('alias');
expect(tokens[1].value).to.be('(');
expect(tokens[2].value).to.be('metric');
expect(tokens[3].value).to.be(',');
expect(tokens[4].type).to.be('string');
expect(tokens[4].isUnclosed).to.be(true);
expect(tokens[4].pos).to.be(20);
});
it('should handle float parameters', function() {
var lexer = new Lexer("alias(metric, 0.002)");
var tokens = lexer.tokenize();
expect(tokens[4].type).to.be('number');
expect(tokens[4].value).to.be('0.002');
});
it('should handle bool parameters', function() {
var lexer = new Lexer("alias(metric, true, false)");
var tokens = lexer.tokenize();
expect(tokens[4].type).to.be('bool');
expect(tokens[4].value).to.be('true');
expect(tokens[6].type).to.be('bool');
});
});

View File

@ -0,0 +1,183 @@
import {describe, it, expect} from 'test/lib/common';
import {Parser} from '../parser';
describe('when parsing', function() {
it('simple metric expression', function() {
var parser = new Parser('metric.test.*.asd.count');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(5);
expect(rootNode.segments[0].value).to.be('metric');
});
it('simple metric expression with numbers in segments', function() {
var parser = new Parser('metric.10.15_20.5');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(4);
expect(rootNode.segments[1].value).to.be('10');
expect(rootNode.segments[2].value).to.be('15_20');
expect(rootNode.segments[3].value).to.be('5');
});
it('simple metric expression with curly braces', function() {
var parser = new Parser('metric.se1-{count, max}');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(2);
expect(rootNode.segments[1].value).to.be('se1-{count,max}');
});
it('simple metric expression with curly braces at start of segment and with post chars', function() {
var parser = new Parser('metric.{count, max}-something.count');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(3);
expect(rootNode.segments[1].value).to.be('{count,max}-something');
});
it('simple function', function() {
var parser = new Parser('sum(test)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(1);
});
it('simple function2', function() {
var parser = new Parser('offset(test.metric, -100)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[1].type).to.be('number');
});
it('simple function with string arg', function() {
var parser = new Parser("randomWalk('test')");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(1);
expect(rootNode.params[0].type).to.be('string');
});
it('function with multiple args', function() {
var parser = new Parser("sum(test, 1, 'test')");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(3);
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[1].type).to.be('number');
expect(rootNode.params[2].type).to.be('string');
});
it('function with nested function', function() {
var parser = new Parser("sum(scaleToSeconds(test, 1))");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(1);
expect(rootNode.params[0].type).to.be('function');
expect(rootNode.params[0].name).to.be('scaleToSeconds');
expect(rootNode.params[0].params.length).to.be(2);
expect(rootNode.params[0].params[0].type).to.be('metric');
expect(rootNode.params[0].params[1].type).to.be('number');
});
it('function with multiple series', function() {
var parser = new Parser("sum(test.test.*.count, test.timers.*.count)");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params.length).to.be(2);
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[1].type).to.be('metric');
});
it('function with templated series', function() {
var parser = new Parser("sum(test.[[server]].count)");
var rootNode = parser.getAst();
expect(rootNode.message).to.be(undefined);
expect(rootNode.params[0].type).to.be('metric');
expect(rootNode.params[0].segments[1].type).to.be('segment');
expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
});
it('invalid metric expression', function() {
var parser = new Parser('metric.test.*.asd.');
var rootNode = parser.getAst();
expect(rootNode.message).to.be('Expected metric identifier instead found end of string');
expect(rootNode.pos).to.be(19);
});
it('invalid function expression missing closing parenthesis', function() {
var parser = new Parser('sum(test');
var rootNode = parser.getAst();
expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string');
expect(rootNode.pos).to.be(9);
});
it('unclosed string in function', function() {
var parser = new Parser("sum('test)");
var rootNode = parser.getAst();
expect(rootNode.message).to.be('Unclosed string parameter');
expect(rootNode.pos).to.be(11);
});
it('handle issue #69', function() {
var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
});
it('handle float function arguments', function() {
var parser = new Parser('scale(test, 0.002)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[1].type).to.be('number');
expect(rootNode.params[1].value).to.be(0.002);
});
it('handle curly brace pattern at start', function() {
var parser = new Parser('{apps}.test');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments[0].value).to.be('{apps}');
expect(rootNode.segments[1].value).to.be('test');
});
it('series parameters', function() {
var parser = new Parser('asPercent(#A, #B)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[0].type).to.be('series-ref');
expect(rootNode.params[0].value).to.be('#A');
expect(rootNode.params[1].value).to.be('#B');
});
it('series parameters, issue 2788', function() {
var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[0].type).to.be('function');
expect(rootNode.params[1].value).to.be('10m');
expect(rootNode.params[3].type).to.be('bool');
});
it('should parse metric expression with ip number segments', function() {
var parser = new Parser('5.10.123.5');
var rootNode = parser.getAst();
expect(rootNode.segments[0].value).to.be('5');
expect(rootNode.segments[1].value).to.be('10');
expect(rootNode.segments[2].value).to.be('123');
expect(rootNode.segments[3].value).to.be('5');
});
});

View File

@ -5,6 +5,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
import gfunc from '../gfunc';
import helpers from 'test/specs/helpers';
import {GraphiteQueryCtrl} from '../query_ctrl';
describe('GraphiteQueryCtrl', function() {
var ctx = new helpers.ControllerTestContext();
@ -17,53 +18,47 @@ describe('GraphiteQueryCtrl', function() {
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.scope.ctrl = {panel: ctx.panel};
ctx.panelCtrl = ctx.scope.ctrl;
ctx.scope.datasource = ctx.datasource;
ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.controller = $controller('GraphiteQueryCtrl', {$scope: ctx.scope});
ctx.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'};
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.panelCtrl = {panel: {}};
ctx.panelCtrl.refresh = sinon.spy();
ctx.ctrl = $controller(GraphiteQueryCtrl, {$scope: ctx.scope}, {
panelCtrl: ctx.panelCtrl,
datasource: ctx.datasource,
target: ctx.target
});
ctx.scope.$digest();
}));
beforeEach(function() {
ctx.scope.target = {target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)'};
});
describe('init', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.$digest();
});
it('should validate metric key exists', function() {
expect(ctx.scope.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*');
expect(ctx.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*');
});
it('should delete last segment if no metrics are found', function() {
expect(ctx.scope.segments[2].value).to.be('select metric');
expect(ctx.ctrl.segments[2].value).to.be('select metric');
});
it('should parse expression and build function model', function() {
expect(ctx.scope.functions.length).to.be(2);
expect(ctx.ctrl.functions.length).to.be(2);
});
});
describe('when adding function', function() {
beforeEach(function() {
ctx.scope.target.target = 'test.prod.*.count';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));
ctx.scope.init();
ctx.scope.$digest();
ctx.panelCtrl.refresh = sinon.spy();
ctx.scope.addFunction(gfunc.getFuncDef('aliasByNode'));
ctx.ctrl.target.target = 'test.prod.*.count';
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
});
it('should add function with correct node number', function() {
expect(ctx.scope.functions[0].params[0]).to.be(2);
expect(ctx.ctrl.functions[0].params[0]).to.be(2);
});
it('should update target', function() {
expect(ctx.scope.target.target).to.be('aliasByNode(test.prod.*.count, 2)');
expect(ctx.ctrl.target.target).to.be('aliasByNode(test.prod.*.count, 2)');
});
it('should call refresh', function() {
@ -73,78 +68,72 @@ describe('GraphiteQueryCtrl', function() {
describe('when adding function before any metric segment', function() {
beforeEach(function() {
ctx.scope.target.target = '';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.addFunction(gfunc.getFuncDef('asPercent'));
ctx.ctrl.target.target = '';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
ctx.ctrl.parseTarget();
ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
});
it('should add function and remove select metric link', function() {
expect(ctx.scope.segments.length).to.be(0);
expect(ctx.ctrl.segments.length).to.be(0);
});
});
describe('when initalizing target without metric expression and only function', function() {
beforeEach(function() {
ctx.scope.target.target = 'asPercent(#A, #B)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.ctrl.target.target = 'asPercent(#A, #B)';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.ctrl.parseTarget();
ctx.scope.$digest();
});
it('should not add select metric segment', function() {
expect(ctx.scope.segments.length).to.be(0);
expect(ctx.ctrl.segments.length).to.be(0);
});
it('should add both series refs as params', function() {
expect(ctx.scope.functions[0].params.length).to.be(2);
expect(ctx.ctrl.functions[0].params.length).to.be(2);
});
});
describe('when initializing a target with single param func using variable', function() {
beforeEach(function() {
ctx.scope.target.target = 'movingAverage(prod.count, $var)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.$digest();
ctx.ctrl.target.target = 'movingAverage(prod.count, $var)';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.ctrl.parseTarget();
});
it('should add 2 segments', function() {
expect(ctx.scope.segments.length).to.be(2);
expect(ctx.ctrl.segments.length).to.be(2);
});
it('should add function param', function() {
expect(ctx.scope.functions[0].params.length).to.be(1);
expect(ctx.ctrl.functions[0].params.length).to.be(1);
});
});
describe('when initalizing target without metric expression and function with series-ref', function() {
beforeEach(function() {
ctx.scope.target.target = 'asPercent(metric.node.count, #A)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.$parent = { get_data: sinon.spy() };
ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.ctrl.parseTarget();
});
it('should add segments', function() {
expect(ctx.scope.segments.length).to.be(3);
expect(ctx.ctrl.segments.length).to.be(3);
});
it('should have correct func params', function() {
expect(ctx.scope.functions[0].params.length).to.be(1);
expect(ctx.ctrl.functions[0].params.length).to.be(1);
});
});
describe('when getting altSegments and metricFindQuery retuns empty array', function() {
beforeEach(function() {
ctx.scope.target.target = 'test.count';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.getAltSegments(1).then(function(results) {
ctx.ctrl.target.target = 'test.count';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.ctrl.parseTarget();
ctx.ctrl.getAltSegments(1).then(function(results) {
ctx.altSegments = results;
});
ctx.scope.$digest();
@ -153,22 +142,18 @@ describe('GraphiteQueryCtrl', function() {
it('should have no segments', function() {
expect(ctx.altSegments.length).to.be(0);
});
});
describe('targetChanged', function() {
beforeEach(function() {
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));
ctx.scope.init();
ctx.scope.$digest();
ctx.panelCtrl.refresh = sinon.spy();
ctx.scope.target.target = '';
ctx.scope.targetChanged();
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
ctx.ctrl.parseTarget();
ctx.ctrl.target.target = '';
ctx.ctrl.targetChanged();
});
it('should rebuld target after expression model', function() {
expect(ctx.scope.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
expect(ctx.ctrl.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
});
it('should call panelCtrl.refresh', function() {

View File

@ -1,221 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/datemath',
'./influx_series',
'./influx_query',
'./query_ctrl',
],
function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
'use strict';
InfluxQuery = InfluxQuery.default;
/** @ngInject */
function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.type = 'influxdb';
this.urls = _.map(instanceSettings.url.split(','), function(url) {
return url.trim();
});
this.username = instanceSettings.username;
this.password = instanceSettings.password;
this.name = instanceSettings.name;
this.database = instanceSettings.database;
this.basicAuth = instanceSettings.basicAuth;
this.supportAnnotations = true;
this.supportMetrics = true;
this.query = function(options) {
var timeFilter = getTimeFilter(options);
var queryTargets = [];
var i, y;
var allQueries = _.map(options.targets, function(target) {
if (target.hide) { return []; }
queryTargets.push(target);
// build query
var queryModel = new InfluxQuery(target);
var query = queryModel.render();
query = query.replace(/\$interval/g, (target.interval || options.interval));
return query;
}).join("\n");
// replace grafana variables
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
// replace templated variables
allQueries = templateSrv.replace(allQueries, options.scopedVars);
return this._seriesQuery(allQueries).then(function(data) {
if (!data || !data.results) {
return [];
}
var seriesList = [];
for (i = 0; i < data.results.length; i++) {
var result = data.results[i];
if (!result || !result.series) { continue; }
var target = queryTargets[i];
var alias = target.alias;
if (alias) {
alias = templateSrv.replace(target.alias, options.scopedVars);
}
var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
switch(target.resultFormat) {
case 'table': {
seriesList.push(influxSeries.getTable());
break;
}
default: {
var timeSeries = influxSeries.getTimeSeries();
for (y = 0; y < timeSeries.length; y++) {
seriesList.push(timeSeries[y]);
}
break;
}
}
}
return { data: seriesList };
});
};
this.annotationQuery = function(options) {
var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw});
var query = options.annotation.query.replace('$timeFilter', timeFilter);
query = templateSrv.replace(query);
return this._seriesQuery(query).then(function(data) {
if (!data || !data.results || !data.results[0]) {
throw { message: 'No results in response from InfluxDB' };
}
return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations();
});
};
this.metricFindQuery = function (query) {
var interpolated;
try {
interpolated = templateSrv.replace(query);
}
catch (err) {
return $q.reject(err);
}
return this._seriesQuery(interpolated).then(function (results) {
if (!results || results.results.length === 0) { return []; }
var influxResults = results.results[0];
if (!influxResults.series) {
return [];
}
var series = influxResults.series[0];
return _.map(series.values, function(value) {
if (_.isArray(value)) {
return { text: value[0] };
} else {
return { text: value };
}
});
});
};
this._seriesQuery = function(query) {
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
};
this.testDatasource = function() {
return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this._influxRequest = function(method, url, data) {
var self = this;
var currentUrl = self.urls.shift();
self.urls.push(currentUrl);
var params = {
u: self.username,
p: self.password,
};
if (self.database) {
params.db = self.database;
}
if (method === 'GET') {
_.extend(params, data);
data = null;
}
var options = {
method: method,
url: currentUrl + url,
params: params,
data: data,
precision: "ms",
inspect: { type: 'influxdb' },
};
options.headers = options.headers || {};
if (self.basicAuth) {
options.headers.Authorization = self.basicAuth;
}
return backendSrv.datasourceRequest(options).then(function(result) {
return result.data;
}, function(err) {
if (err.status !== 0 || err.status >= 300) {
if (err.data && err.data.error) {
throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config };
}
else {
throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config };
}
}
});
};
function getTimeFilter(options) {
var from = getInfluxTime(options.rangeRaw.from, false);
var until = getInfluxTime(options.rangeRaw.to, true);
var fromIsAbsolute = from[from.length-1] === 's';
if (until === 'now()' && !fromIsAbsolute) {
return 'time > ' + from;
}
return 'time > ' + from + ' and time < ' + until;
}
function getInfluxTime(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now()';
}
var parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
if (parts) {
var amount = parseInt(parts[1]);
var unit = parts[2];
return 'now() - ' + amount + unit;
}
date = dateMath.parse(date, roundUp);
}
return (date.valueOf() / 1000).toFixed(0) + 's';
}
}
return InfluxDatasource;
});

View File

@ -0,0 +1,213 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import InfluxSeries from './influx_series';
import InfluxQuery from './influx_query';
/** @ngInject */
export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.type = 'influxdb';
this.urls = _.map(instanceSettings.url.split(','), function(url) {
return url.trim();
});
this.username = instanceSettings.username;
this.password = instanceSettings.password;
this.name = instanceSettings.name;
this.database = instanceSettings.database;
this.basicAuth = instanceSettings.basicAuth;
this.supportAnnotations = true;
this.supportMetrics = true;
this.query = function(options) {
var timeFilter = getTimeFilter(options);
var queryTargets = [];
var i, y;
var allQueries = _.map(options.targets, function(target) {
if (target.hide) { return []; }
queryTargets.push(target);
// build query
var queryModel = new InfluxQuery(target);
var query = queryModel.render();
query = query.replace(/\$interval/g, (target.interval || options.interval));
return query;
}).join("\n");
// replace grafana variables
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
// replace templated variables
allQueries = templateSrv.replace(allQueries, options.scopedVars);
return this._seriesQuery(allQueries).then(function(data): any {
if (!data || !data.results) {
return [];
}
var seriesList = [];
for (i = 0; i < data.results.length; i++) {
var result = data.results[i];
if (!result || !result.series) { continue; }
var target = queryTargets[i];
var alias = target.alias;
if (alias) {
alias = templateSrv.replace(target.alias, options.scopedVars);
}
var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
switch (target.resultFormat) {
case 'table': {
seriesList.push(influxSeries.getTable());
break;
}
default: {
var timeSeries = influxSeries.getTimeSeries();
for (y = 0; y < timeSeries.length; y++) {
seriesList.push(timeSeries[y]);
}
break;
}
}
}
return { data: seriesList };
});
};
this.annotationQuery = function(options) {
var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw});
var query = options.annotation.query.replace('$timeFilter', timeFilter);
query = templateSrv.replace(query);
return this._seriesQuery(query).then(function(data) {
if (!data || !data.results || !data.results[0]) {
throw { message: 'No results in response from InfluxDB' };
}
return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations();
});
};
this.metricFindQuery = function (query) {
var interpolated;
try {
interpolated = templateSrv.replace(query);
} catch (err) {
return $q.reject(err);
}
return this._seriesQuery(interpolated).then(function (results) {
if (!results || results.results.length === 0) { return []; }
var influxResults = results.results[0];
if (!influxResults.series) {
return [];
}
var series = influxResults.series[0];
return _.map(series.values, function(value) {
if (_.isArray(value)) {
return { text: value[0] };
} else {
return { text: value };
}
});
});
};
this._seriesQuery = function(query) {
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
};
this.testDatasource = function() {
return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this._influxRequest = function(method, url, data) {
var self = this;
var currentUrl = self.urls.shift();
self.urls.push(currentUrl);
var params: any = {
u: self.username,
p: self.password,
};
if (self.database) {
params.db = self.database;
}
if (method === 'GET') {
_.extend(params, data);
data = null;
}
var options: any = {
method: method,
url: currentUrl + url,
params: params,
data: data,
precision: "ms",
inspect: { type: 'influxdb' },
};
options.headers = options.headers || {};
if (self.basicAuth) {
options.headers.Authorization = self.basicAuth;
}
return backendSrv.datasourceRequest(options).then(function(result) {
return result.data;
}, function(err) {
if (err.status !== 0 || err.status >= 300) {
if (err.data && err.data.error) {
throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config };
} else {
throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config };
}
}
});
};
function getTimeFilter(options) {
var from = getInfluxTime(options.rangeRaw.from, false);
var until = getInfluxTime(options.rangeRaw.to, true);
var fromIsAbsolute = from[from.length-1] === 's';
if (until === 'now()' && !fromIsAbsolute) {
return 'time > ' + from;
}
return 'time > ' + from + ' and time < ' + until;
}
function getInfluxTime(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now()';
}
var parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
if (parts) {
var amount = parseInt(parts[1]);
var unit = parts[2];
return 'now() - ' + amount + unit;
}
date = dateMath.parse(date, roundUp);
}
return (date.valueOf() / 1000).toFixed(0) + 's';
}
}

View File

@ -175,7 +175,7 @@ export default class InfluxQuery {
}
if (!target.measurement) {
throw "Metric measurement is missing";
throw {message: "Metric measurement is missing"};
}
var query = 'SELECT ';

View File

@ -133,14 +133,18 @@ function (_, TableModel) {
if (series.values) {
for (i = 0; i < series.values.length; i++) {
var values = series.values[i];
var reordered = [values[0]];
if (series.tags) {
for (var key in series.tags) {
if (series.tags.hasOwnProperty(key)) {
values.splice(1, 0, series.tags[key]);
reordered.push(series.tags[key]);
}
}
}
table.rows.push(values);
for (j = 1; j < values.length; j++) {
reordered.push(values[j]);
}
table.rows.push(reordered);
}
}
});

View File

@ -1,30 +0,0 @@
define([
'./datasource',
],
function (InfluxDatasource) {
'use strict';
function influxMetricsQueryEditor() {
return {controller: 'InfluxQueryCtrl', templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.editor.html'};
}
function influxMetricsQueryOptions() {
return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.options.html'};
}
function influxAnnotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html'};
}
function influxConfigView() {
return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/config.html'};
}
return {
Datasource: InfluxDatasource,
metricsQueryEditor: influxMetricsQueryEditor,
metricsQueryOptions: influxMetricsQueryOptions,
annotationsQueryEditor: influxAnnotationsQueryEditor,
configView: influxConfigView,
};
});

View File

@ -0,0 +1,24 @@
import {InfluxDatasource} from './datasource';
import {InfluxQueryCtrl} from './query_ctrl';
class InfluxConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/influxdb/partials/config.html';
}
class InfluxQueryOptionsCtrl {
static templateUrl = 'public/app/plugins/datasource/influxdb/partials/query.options.html';
}
class InfluxAnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html';
}
export {
InfluxDatasource as Datasource,
InfluxQueryCtrl as QueryCtrl,
InfluxConfigCtrl as ConfigCtrl,
InfluxQueryOptionsCtrl as QueryOptionsCtrl,
InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1,4 +1,5 @@
<datasource-http-settings></datasource-http-settings>
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>
<h4>InfluxDB Details</h4>
@ -8,7 +9,7 @@
Database
</li>
<li>
<input type="text" class="tight-form-input input-large" ng-model='current.database' placeholder="" required></input>
<input type="text" class="tight-form-input input-large" ng-model='ctrl.current.database' placeholder="" required></input>
</li>
</ul>
<div class="clearfix"></div>
@ -19,13 +20,13 @@
User
</li>
<li>
<input type="text" class="tight-form-input input-large" ng-model='current.user' placeholder="" required></input>
<input type="text" class="tight-form-input input-large" ng-model='ctrl.current.user' placeholder="" required></input>
</li>
<li class="tight-form-item">
Password
</li>
<li>
<input type="password" class="tight-form-input input-large" ng-model='current.password' placeholder="" required></input>
<input type="password" class="tight-form-input input-large" ng-model='ctrl.current.password' placeholder="" required></input>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -1,119 +1,73 @@
<div class="">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="panelCtrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-hide="target.rawQuery">
<query-editor-row ctrl="ctrl">
<ul class="tight-form-list" ng-hide="ctrl.target.rawQuery">
<li class="tight-form-item query-keyword" style="width: 75px">
FROM
</li>
<li>
<metric-segment segment="policySegment" get-options="getPolicySegments()" on-change="policyChanged()"></metric-segment>
<metric-segment segment="ctrl.policySegment" get-options="ctrl.getPolicySegments()" on-change="ctrl.policyChanged()"></metric-segment>
</li>
<li>
<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
<metric-segment segment="ctrl.measurementSegment" get-options="ctrl.getMeasurements()" on-change="ctrl.measurementChanged()"></metric-segment>
</li>
<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
WHERE
</li>
<li ng-repeat="segment in tagSegments">
<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
<li ng-repeat="segment in ctrl.tagSegments">
<metric-segment segment="segment" get-options="ctrl.getTagsOrValues(segment, $index)" on-change="ctrl.tagSegmentUpdated(segment, $index)"></metric-segment>
</li>
</ul>
<div class="tight-form-flex-wrapper" ng-show="target.rawQuery">
<input type="text" class="tight-form-clear-input" ng-model="target.query" spellcheck="false" style="width: 100%;" ng-blur="panelCtrl.refresh()"></input>
<div class="tight-form-flex-wrapper" ng-show="ctrl.target.rawQuery">
<input type="text" class="tight-form-clear-input" ng-model="ctrl.target.query" spellcheck="false" style="width: 100%;" ng-blur="ctrl.refresh()"></input>
</div>
</query-editor-row>
<div ng-hide="ctrl.target.rawQuery">
<div class="tight-form" ng-repeat="selectParts in ctrl.queryModel.selectModels">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">SELECT</span>
</li>
<li ng-repeat="part in selectParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeSelectPart(selectParts, part)" part-updated="ctrl.selectPartUpdated(selectParts, part)" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
</li>
<li class="dropdown" dropdown-typeahead="ctrl.selectMenu" dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div ng-hide="target.rawQuery">
<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span ng-show="$index === 0">SELECT</span>
</li>
<li ng-repeat="part in selectParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeSelectPart(selectParts, part)" part-updated="selectPartUpdated(selectParts, part)" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
<span>GROUP BY</span>
</li>
<li ng-repeat="part in queryModel.groupByParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="panelCtrl.refresh();" get-options="getPartOptions(part)"></influx-query-part-editor>
</li>
<li>
<metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
ALIAS BY
<span>GROUP BY</span>
</li>
<li ng-repeat="part in ctrl.queryModel.groupByParts">
<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
</li>
<li>
<input type="text" class="tight-form-clear-input input-xlarge" ng-model="target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="panelCtrl.refresh()">
</li>
<li class="tight-form-item">
Format as
</li>
<li>
<select class="input-small tight-form-input" style="width: 104px" ng-model="target.resultFormat" ng-options="f.value as f.text for f in resultFormats" ng-change="panelCtrl.refresh()"></select>
<metric-segment segment="ctrl.groupBySegment" get-options="ctrl.getGroupByOptions()" on-change="ctrl.groupByAction(part, $index)"></metric-segment>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
ALIAS BY
</li>
<li>
<input type="text" class="tight-form-clear-input input-xlarge" ng-model="ctrl.target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="ctrl.refresh()">
</li>
<li class="tight-form-item">
Format as
</li>
<li>
<select class="input-small tight-form-input" style="width: 104px" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -8,7 +8,7 @@
Group by time interval
</li>
<li>
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panel.interval" ng-blur="ctrl.refresh();"
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="tight-form-item">
@ -24,17 +24,17 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking &amp; and fill
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
group by time
</a>
</li>
@ -46,7 +46,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 1">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
<h5>Alias patterns</h5>
<ul>
<li>$m = replaced with measurement name</li>
@ -58,7 +58,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 2">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
<h5>Stacking and fill</h5>
<ul>
<li>When stacking is enabled it important that points align</li>
@ -69,7 +69,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 3">
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
<h5>Group by time</h5>
<ul>
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>

View File

@ -1,322 +0,0 @@
define([
'angular',
'lodash',
'./query_builder',
'./influx_query',
'./query_part',
'./query_part_editor',
],
function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
'use strict';
var module = angular.module('grafana.controllers');
InfluxQuery = InfluxQuery.default;
queryPart = queryPart.default;
module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
var panelCtrl = $scope.ctrl;
var datasource = $scope.datasource;
$scope.panelCtrl = panelCtrl;
$scope.init = function() {
if (!$scope.target) { return; }
$scope.target = $scope.target;
$scope.queryModel = new InfluxQuery($scope.target);
$scope.queryBuilder = new InfluxQueryBuilder($scope.target, datasource.database);
$scope.groupBySegment = uiSegmentSrv.newPlusButton();
$scope.resultFormats = [
{text: 'Time series', value: 'time_series'},
{text: 'Table', value: 'table'},
];
$scope.policySegment = uiSegmentSrv.newSegment($scope.target.policy);
if (!$scope.target.measurement) {
$scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
$scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
}
$scope.tagSegments = [];
_.each($scope.target.tags, function(tag) {
if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~";
} else {
tag.operator = '=';
}
}
if (tag.condition) {
$scope.tagSegments.push(uiSegmentSrv.newCondition(tag.condition));
}
$scope.tagSegments.push(uiSegmentSrv.newKey(tag.key));
$scope.tagSegments.push(uiSegmentSrv.newOperator(tag.operator));
$scope.tagSegments.push(uiSegmentSrv.newKeyValue(tag.value));
});
$scope.fixTagSegments();
$scope.buildSelectMenu();
$scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
};
$scope.buildSelectMenu = function() {
var categories = queryPart.getCategories();
$scope.selectMenu = _.reduce(categories, function(memo, cat, key) {
var menu = {text: key};
menu.submenu = _.map(cat, function(item) {
return {text: item.type, value: item.type};
});
memo.push(menu);
return memo;
}, []);
};
$scope.getGroupByOptions = function() {
var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return datasource.metricFindQuery(query)
.then(function(tags) {
var options = [];
if (!$scope.queryModel.hasFill()) {
options.push(uiSegmentSrv.newSegment({value: 'fill(null)'}));
}
if (!$scope.queryModel.hasGroupByTime()) {
options.push(uiSegmentSrv.newSegment({value: 'time($interval)'}));
}
_.each(tags, function(tag) {
options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
});
return options;
})
.then(null, $scope.handleQueryError);
};
$scope.groupByAction = function() {
$scope.queryModel.addGroupBy($scope.groupBySegment.value);
var plusButton = uiSegmentSrv.newPlusButton();
$scope.groupBySegment.value = plusButton.value;
$scope.groupBySegment.html = plusButton.html;
panelCtrl.refresh();
};
$scope.removeGroupByPart = function(part, index) {
$scope.queryModel.removeGroupByPart(part, index);
panelCtrl.refresh();
};
$scope.addSelectPart = function(selectParts, cat, subitem) {
$scope.queryModel.addSelectPart(selectParts, subitem.value);
panelCtrl.refresh();
};
$scope.removeSelectPart = function(selectParts, part) {
$scope.queryModel.removeSelectPart(selectParts, part);
panelCtrl.refresh();
};
$scope.selectPartUpdated = function() {
panelCtrl.refresh();
};
$scope.fixTagSegments = function() {
var count = $scope.tagSegments.length;
var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
if (!lastSegment || lastSegment.type !== 'plus-button') {
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
};
$scope.measurementChanged = function() {
$scope.target.measurement = $scope.measurementSegment.value;
panelCtrl.refresh();
};
$scope.getPolicySegments = function() {
var policiesQuery = $scope.queryBuilder.buildExploreQuery('RETENTION POLICIES');
return datasource.metricFindQuery(policiesQuery)
.then($scope.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
$scope.policyChanged = function() {
$scope.target.policy = $scope.policySegment.value;
panelCtrl.refresh();
};
$scope.toggleQueryMode = function () {
$scope.target.rawQuery = !$scope.target.rawQuery;
};
$scope.getMeasurements = function () {
var query = $scope.queryBuilder.buildExploreQuery('MEASUREMENTS');
return datasource.metricFindQuery(query)
.then($scope.transformToSegments(true), $scope.handleQueryError);
};
$scope.getPartOptions = function(part) {
if (part.def.type === 'field') {
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
return datasource.metricFindQuery(fieldsQuery)
.then($scope.transformToSegments(true), $scope.handleQueryError);
}
if (part.def.type === 'tag') {
var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
return datasource.metricFindQuery(tagsQuery)
.then($scope.transformToSegments(true), $scope.handleQueryError);
}
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
};
$scope.transformToSegments = function(addTemplateVars) {
return function(results) {
var segments = _.map(results, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
});
}
return segments;
};
};
$scope.getTagsOrValues = function(segment, index) {
if (segment.type === 'condition') {
return $q.when([uiSegmentSrv.newSegment('AND'), uiSegmentSrv.newSegment('OR')]);
}
if (segment.type === 'operator') {
var nextValue = $scope.tagSegments[index+1].value;
if (/^\/.*\/$/.test(nextValue)) {
return $q.when(uiSegmentSrv.newOperators(['=~', '!~']));
} else {
return $q.when(uiSegmentSrv.newOperators(['=', '<>', '<', '>']));
}
}
var query, addTemplateVars;
if (segment.type === 'key' || segment.type === 'plus-button') {
query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
addTemplateVars = false;
} else if (segment.type === 'value') {
query = $scope.queryBuilder.buildExploreQuery('TAG_VALUES', $scope.tagSegments[index-2].value);
addTemplateVars = true;
}
return datasource.metricFindQuery(query)
.then($scope.transformToSegments(addTemplateVars))
.then(function(results) {
if (segment.type === 'key') {
results.splice(0, 0, angular.copy($scope.removeTagFilterSegment));
}
return results;
})
.then(null, $scope.handleQueryError);
};
$scope.getFieldSegments = function() {
var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
return datasource.metricFindQuery(fieldsQuery)
.then($scope.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
$scope.getTagOptions = function() {
};
$scope.setFill = function(fill) {
$scope.target.fill = fill;
panelCtrl.refresh();
};
$scope.tagSegmentUpdated = function(segment, index) {
$scope.tagSegments[index] = segment;
// handle remove tag condition
if (segment.value === $scope.removeTagFilterSegment.value) {
$scope.tagSegments.splice(index, 3);
if ($scope.tagSegments.length === 0) {
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
} else if ($scope.tagSegments.length > 2) {
$scope.tagSegments.splice(Math.max(index-1, 0), 1);
if ($scope.tagSegments[$scope.tagSegments.length-1].type !== 'plus-button') {
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
}
}
else {
if (segment.type === 'plus-button') {
if (index > 2) {
$scope.tagSegments.splice(index, 0, uiSegmentSrv.newCondition('AND'));
}
$scope.tagSegments.push(uiSegmentSrv.newOperator('='));
$scope.tagSegments.push(uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if ((index+1) === $scope.tagSegments.length) {
$scope.tagSegments.push(uiSegmentSrv.newPlusButton());
}
}
$scope.rebuildTargetTagConditions();
};
$scope.rebuildTargetTagConditions = function() {
var tags = [];
var tagIndex = 0;
var tagOperator = "";
_.each($scope.tagSegments, function(segment2, index) {
if (segment2.type === 'key') {
if (tags.length === 0) {
tags.push({});
}
tags[tagIndex].key = segment2.value;
}
else if (segment2.type === 'value') {
tagOperator = $scope.getTagValueOperator(segment2.value, tags[tagIndex].operator);
if (tagOperator) {
$scope.tagSegments[index-1] = uiSegmentSrv.newOperator(tagOperator);
tags[tagIndex].operator = tagOperator;
}
tags[tagIndex].value = segment2.value;
}
else if (segment2.type === 'condition') {
tags.push({ condition: segment2.value });
tagIndex += 1;
}
else if (segment2.type === 'operator') {
tags[tagIndex].operator = segment2.value;
}
});
$scope.target.tags = tags;
panelCtrl.refresh();
};
$scope.getTagValueOperator = function(tagValue, tagOperator) {
if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) {
return '=~';
}
else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) {
return '=';
}
};
$scope.init();
});
});

View File

@ -0,0 +1,319 @@
///<reference path="../../../headers/common.d.ts" />
import './query_part_editor';
import './query_part_editor';
import angular from 'angular';
import _ from 'lodash';
import InfluxQueryBuilder from './query_builder';
import InfluxQuery from './influx_query';
import queryPart from './query_part';
import {QueryCtrl} from 'app/features/panel/panel';
export class InfluxQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/influxdb/partials/query.editor.html';
queryModel: InfluxQuery;
queryBuilder: any;
groupBySegment: any;
resultFormats: any[];
policySegment: any;
tagSegments: any[];
selectMenu: any;
measurementSegment: any;
removeTagFilterSegment: any;
/** @ngInject **/
constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
super($scope, $injector);
this.target = this.target;
this.queryModel = new InfluxQuery(this.target);
this.queryBuilder = new InfluxQueryBuilder(this.target, this.datasource.database);
this.groupBySegment = this.uiSegmentSrv.newPlusButton();
this.resultFormats = [
{text: 'Time series', value: 'time_series'},
{text: 'Table', value: 'table'},
];
this.policySegment = uiSegmentSrv.newSegment(this.target.policy);
if (!this.target.measurement) {
this.measurementSegment = uiSegmentSrv.newSelectMeasurement();
} else {
this.measurementSegment = uiSegmentSrv.newSegment(this.target.measurement);
}
this.tagSegments = [];
for (let tag of this.target.tags) {
if (!tag.operator) {
if (/^\/.*\/$/.test(tag.value)) {
tag.operator = "=~";
} else {
tag.operator = '=';
}
}
if (tag.condition) {
this.tagSegments.push(uiSegmentSrv.newCondition(tag.condition));
}
this.tagSegments.push(uiSegmentSrv.newKey(tag.key));
this.tagSegments.push(uiSegmentSrv.newOperator(tag.operator));
this.tagSegments.push(uiSegmentSrv.newKeyValue(tag.value));
}
this.fixTagSegments();
this.buildSelectMenu();
this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
}
buildSelectMenu() {
var categories = queryPart.getCategories();
this.selectMenu = _.reduce(categories, function(memo, cat, key) {
var menu = {
text: key,
submenu: cat.map(item => {
return {text: item.type, value: item.type};
}),
};
memo.push(menu);
return memo;
}, []);
}
getGroupByOptions() {
var query = this.queryBuilder.buildExploreQuery('TAG_KEYS');
return this.datasource.metricFindQuery(query).then(tags => {
var options = [];
if (!this.queryModel.hasFill()) {
options.push(this.uiSegmentSrv.newSegment({value: 'fill(null)'}));
}
if (!this.queryModel.hasGroupByTime()) {
options.push(this.uiSegmentSrv.newSegment({value: 'time($interval)'}));
}
for (let tag of tags) {
options.push(this.uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
}
return options;
}).catch(this.handleQueryError.bind(this));
}
groupByAction() {
this.queryModel.addGroupBy(this.groupBySegment.value);
var plusButton = this.uiSegmentSrv.newPlusButton();
this.groupBySegment.value = plusButton.value;
this.groupBySegment.html = plusButton.html;
this.panelCtrl.refresh();
}
removeGroupByPart(part, index) {
this.queryModel.removeGroupByPart(part, index);
this.panelCtrl.refresh();
}
addSelectPart(selectParts, cat, subitem) {
this.queryModel.addSelectPart(selectParts, subitem.value);
this.panelCtrl.refresh();
}
removeSelectPart(selectParts, part) {
this.queryModel.removeSelectPart(selectParts, part);
this.panelCtrl.refresh();
}
selectPartUpdated() {
this.panelCtrl.refresh();
}
fixTagSegments() {
var count = this.tagSegments.length;
var lastSegment = this.tagSegments[Math.max(count-1, 0)];
if (!lastSegment || lastSegment.type !== 'plus-button') {
this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
}
}
measurementChanged() {
this.target.measurement = this.measurementSegment.value;
this.panelCtrl.refresh();
}
getPolicySegments() {
var policiesQuery = this.queryBuilder.buildExploreQuery('RETENTION POLICIES');
return this.datasource.metricFindQuery(policiesQuery)
.then(this.transformToSegments(false))
.catch(this.handleQueryError.bind(this));
}
policyChanged() {
this.target.policy = this.policySegment.value;
this.panelCtrl.refresh();
}
toggleEditorMode() {
this.target.rawQuery = !this.target.rawQuery;
}
getMeasurements() {
var query = this.queryBuilder.buildExploreQuery('MEASUREMENTS');
return this.datasource.metricFindQuery(query)
.then(this.transformToSegments(true))
.catch(this.handleQueryError.bind(this));
}
getPartOptions(part) {
if (part.def.type === 'field') {
var fieldsQuery = this.queryBuilder.buildExploreQuery('FIELDS');
return this.datasource.metricFindQuery(fieldsQuery)
.then(this.transformToSegments(true))
.catch(this.handleQueryError.bind(this));
}
if (part.def.type === 'tag') {
var tagsQuery = this.queryBuilder.buildExploreQuery('TAG_KEYS');
return this.datasource.metricFindQuery(tagsQuery)
.then(this.transformToSegments(true))
.catch(this.handleQueryError.bind(true));
}
}
handleQueryError(err) {
this.error = err.message || 'Failed to issue metric query';
return [];
}
transformToSegments(addTemplateVars) {
return (results) => {
var segments = _.map(results, segment => {
return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
for (let variable of this.templateSrv.variables) {
segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
}
}
return segments;
};
}
getTagsOrValues(segment, index) {
if (segment.type === 'condition') {
return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]);
}
if (segment.type === 'operator') {
var nextValue = this.tagSegments[index+1].value;
if (/^\/.*\/$/.test(nextValue)) {
return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~']));
} else {
return this.$q.when(this.uiSegmentSrv.newOperators(['=', '<>', '<', '>']));
}
}
var query, addTemplateVars;
if (segment.type === 'key' || segment.type === 'plus-button') {
query = this.queryBuilder.buildExploreQuery('TAG_KEYS');
addTemplateVars = false;
} else if (segment.type === 'value') {
query = this.queryBuilder.buildExploreQuery('TAG_VALUES', this.tagSegments[index-2].value);
addTemplateVars = true;
}
return this.datasource.metricFindQuery(query)
.then(this.transformToSegments(addTemplateVars))
.then(results => {
if (segment.type === 'key') {
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
}
return results;
})
.catch(this.handleQueryError.bind(this));
}
getFieldSegments() {
var fieldsQuery = this.queryBuilder.buildExploreQuery('FIELDS');
return this.datasource.metricFindQuery(fieldsQuery)
.then(this.transformToSegments(false))
.catch(this.handleQueryError);
}
setFill(fill) {
this.target.fill = fill;
this.panelCtrl.refresh();
}
tagSegmentUpdated(segment, index) {
this.tagSegments[index] = segment;
// handle remove tag condition
if (segment.value === this.removeTagFilterSegment.value) {
this.tagSegments.splice(index, 3);
if (this.tagSegments.length === 0) {
this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
} else if (this.tagSegments.length > 2) {
this.tagSegments.splice(Math.max(index-1, 0), 1);
if (this.tagSegments[this.tagSegments.length-1].type !== 'plus-button') {
this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
}
}
} else {
if (segment.type === 'plus-button') {
if (index > 2) {
this.tagSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
}
this.tagSegments.push(this.uiSegmentSrv.newOperator('='));
this.tagSegments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
segment.type = 'key';
segment.cssClass = 'query-segment-key';
}
if ((index+1) === this.tagSegments.length) {
this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
}
}
this.rebuildTargetTagConditions();
}
rebuildTargetTagConditions() {
var tags = [];
var tagIndex = 0;
var tagOperator = "";
_.each(this.tagSegments, (segment2, index) => {
if (segment2.type === 'key') {
if (tags.length === 0) {
tags.push({});
}
tags[tagIndex].key = segment2.value;
} else if (segment2.type === 'value') {
tagOperator = this.getTagValueOperator(segment2.value, tags[tagIndex].operator);
if (tagOperator) {
this.tagSegments[index-1] = this.uiSegmentSrv.newOperator(tagOperator);
tags[tagIndex].operator = tagOperator;
}
tags[tagIndex].value = segment2.value;
} else if (segment2.type === 'condition') {
tags.push({ condition: segment2.value });
tagIndex += 1;
} else if (segment2.type === 'operator') {
tags[tagIndex].operator = segment2.value;
}
});
this.target.tags = tags;
this.panelCtrl.refresh();
}
getTagValueOperator(tagValue, tagOperator) {
if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) {
return '=~';
} else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) {
return '=';
}
}
}

View File

@ -189,9 +189,9 @@ describe('when generating timeseries from influxdb response', function() {
series: [
{
name: 'app.prod.server1.count',
tags: {},
columns: ['time', 'datacenter', 'value'],
values: [[1431946625000, 'America', 10], [1431946626000, 'EU', 12]]
tags: {datacenter: 'Africa', server: 'server2'},
columns: ['time', 'value2', 'value'],
values: [[1431946625000, 23, 10], [1431946626000, 25, 12]]
}
]
};
@ -201,8 +201,8 @@ describe('when generating timeseries from influxdb response', function() {
var table = series.getTable();
expect(table.type).to.be('table');
expect(table.columns.length).to.be(3);
expect(table.rows[0]).to.eql([1431946625000, 'America', 10]);
expect(table.columns.length).to.be(5);
expect(table.rows[0]).to.eql([1431946625000, 'Africa', 'server2', 23, 10]);
});
});

View File

@ -2,6 +2,7 @@ import '../query_ctrl';
import 'app/core/services/segment_srv';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
import {InfluxQueryCtrl} from '../query_ctrl';
describe('InfluxDBQueryCtrl', function() {
var ctx = new helpers.ControllerTestContext();
@ -14,179 +15,164 @@ describe('InfluxDBQueryCtrl', function() {
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.scope.ctrl = {panel: ctx.panel};
ctx.scope.datasource = ctx.datasource;
ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.panelCtrl = ctx.scope.ctrl;
ctx.controller = $controller('InfluxQueryCtrl', {$scope: ctx.scope});
ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
ctx.panelCtrl = {panel: {}};
ctx.panelCtrl.refresh = sinon.spy();
ctx.target = {target: {}};
ctx.ctrl = $controller(InfluxQueryCtrl, {$scope: ctx.scope}, {
panelCtrl: ctx.panelCtrl,
target: ctx.target,
datasource: ctx.datasource
});
}));
beforeEach(function() {
ctx.scope.target = {};
ctx.panelCtrl.refresh = sinon.spy();
});
describe('init', function() {
beforeEach(function() {
ctx.scope.init();
});
it('should init tagSegments', function() {
expect(ctx.scope.tagSegments.length).to.be(1);
expect(ctx.ctrl.tagSegments.length).to.be(1);
});
it('should init measurementSegment', function() {
expect(ctx.scope.measurementSegment.value).to.be('select measurement');
expect(ctx.ctrl.measurementSegment.value).to.be('select measurement');
});
});
describe('when first tag segment is updated', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
});
it('should update tag key', function() {
expect(ctx.scope.target.tags[0].key).to.be('asd');
expect(ctx.scope.tagSegments[0].type).to.be('key');
expect(ctx.ctrl.target.tags[0].key).to.be('asd');
expect(ctx.ctrl.tagSegments[0].type).to.be('key');
});
it('should add tagSegments', function() {
expect(ctx.scope.tagSegments.length).to.be(3);
expect(ctx.ctrl.tagSegments.length).to.be(3);
});
});
describe('when last tag value segment is updated', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
});
it('should update tag value', function() {
expect(ctx.scope.target.tags[0].value).to.be('server1');
expect(ctx.ctrl.target.tags[0].value).to.be('server1');
});
it('should set tag operator', function() {
expect(ctx.scope.target.tags[0].operator).to.be('=');
expect(ctx.ctrl.target.tags[0].operator).to.be('=');
});
it('should add plus button for another filter', function() {
expect(ctx.scope.tagSegments[3].fake).to.be(true);
expect(ctx.ctrl.tagSegments[3].fake).to.be(true);
});
});
describe('when last tag value segment is updated to regex', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.scope.tagSegmentUpdated({value: '/server.*/', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
ctx.ctrl.tagSegmentUpdated({value: '/server.*/', type: 'value'}, 2);
});
it('should update operator', function() {
expect(ctx.scope.tagSegments[1].value).to.be('=~');
expect(ctx.scope.target.tags[0].operator).to.be('=~');
expect(ctx.ctrl.tagSegments[1].value).to.be('=~');
expect(ctx.ctrl.target.tags[0].operator).to.be('=~');
});
});
describe('when second tag key is added', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
});
it('should update tag key', function() {
expect(ctx.scope.target.tags[1].key).to.be('key2');
expect(ctx.ctrl.target.tags[1].key).to.be('key2');
});
it('should add AND segment', function() {
expect(ctx.scope.tagSegments[3].value).to.be('AND');
expect(ctx.ctrl.tagSegments[3].value).to.be('AND');
});
});
describe('when condition is changed', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.ctrl.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
});
it('should update tag condition', function() {
expect(ctx.scope.target.tags[1].condition).to.be('OR');
expect(ctx.ctrl.target.tags[1].condition).to.be('OR');
});
it('should update AND segment', function() {
expect(ctx.scope.tagSegments[3].value).to.be('OR');
expect(ctx.scope.tagSegments.length).to.be(7);
expect(ctx.ctrl.tagSegments[3].value).to.be('OR');
expect(ctx.ctrl.tagSegments.length).to.be(7);
});
});
describe('when deleting first tag filter after value is selected', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 0);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 0);
});
it('should remove tags', function() {
expect(ctx.scope.target.tags.length).to.be(0);
expect(ctx.ctrl.target.tags.length).to.be(0);
});
it('should remove all segment after 2 and replace with plus button', function() {
expect(ctx.scope.tagSegments.length).to.be(1);
expect(ctx.scope.tagSegments[0].type).to.be('plus-button');
expect(ctx.ctrl.tagSegments.length).to.be(1);
expect(ctx.ctrl.tagSegments[0].type).to.be('plus-button');
});
});
describe('when deleting second tag value before second tag value is complete', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
});
it('should remove all segment after 2 and replace with plus button', function() {
expect(ctx.scope.tagSegments.length).to.be(4);
expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
expect(ctx.ctrl.tagSegments.length).to.be(4);
expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
});
});
describe('when deleting second tag value before second tag value is complete', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
});
it('should remove all segment after 2 and replace with plus button', function() {
expect(ctx.scope.tagSegments.length).to.be(4);
expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
expect(ctx.ctrl.tagSegments.length).to.be(4);
expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
});
});
describe('when deleting second tag value after second tag filter is complete', function() {
beforeEach(function() {
ctx.scope.init();
ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.scope.tagSegmentUpdated({value: 'value', type: 'value'}, 6);
ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
ctx.ctrl.tagSegmentUpdated({value: 'value', type: 'value'}, 6);
ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
});
it('should remove all segment after 2 and replace with plus button', function() {
expect(ctx.scope.tagSegments.length).to.be(4);
expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
expect(ctx.ctrl.tagSegments.length).to.be(4);
expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
});
});
});

View File

@ -1,3 +1,3 @@
declare var Datasource: any;
export default Datasource;
declare var OpenTsDatasource: any;
export {OpenTsDatasource};

View File

@ -3,19 +3,19 @@ define([
'lodash',
'app/core/utils/datemath',
'moment',
'./queryCtrl',
],
function (angular, _, dateMath) {
'use strict';
/** @ngInject */
function OpenTSDBDatasource(instanceSettings, $q, backendSrv, templateSrv) {
function OpenTsDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.type = 'opentsdb';
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.withCredentials = instanceSettings.withCredentials;
this.basicAuth = instanceSettings.basicAuth;
this.supportMetrics = true;
this.tagKeys = {};
// Called once per panel (graph)
this.query = function(options) {
@ -51,10 +51,13 @@ function (angular, _, dateMath) {
if (index === -1) {
index = 0;
}
this._saveTagKeys(metricData);
return transformMetricData(metricData, groupByTags, options.targets[index], options);
});
}.bind(this));
return { data: result };
});
}.bind(this));
};
this.performTimeSeriesQuery = function(queries, start, end) {
@ -73,13 +76,13 @@ function (angular, _, dateMath) {
url: this.url + '/api/query',
data: reqBody
};
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = {
"Authorization": this.basicAuth
};
options.headers = {"Authorization": this.basicAuth};
}
// In case the backend is 3rd-party hosted and does not suport OPTIONS, urlencoded requests
@ -88,6 +91,19 @@ function (angular, _, dateMath) {
return backendSrv.datasourceRequest(options);
};
this.suggestTagKeys = function(metric) {
return $q.when(this.tagKeys[metric] || []);
};
this._saveTagKeys = function(metricData) {
var tagKeys = Object.keys(metricData.tags);
_.each(metricData.aggregateTags, function(tag) {
tagKeys.push(tag);
});
this.tagKeys[metricData.metric] = tagKeys;
};
this._performSuggestQuery = function(query, type) {
return this._get('/api/suggest', {type: type, q: query, max: 1000}).then(function(result) {
return result.data;
@ -325,5 +341,7 @@ function (angular, _, dateMath) {
}
return OpenTSDBDatasource;
return {
OpenTsDatasource: OpenTsDatasource
};
});

View File

@ -1,23 +0,0 @@
define([
'./datasource',
],
function (OpenTsDatasource) {
'use strict';
function metricsQueryEditor() {
return {
controller: 'OpenTSDBQueryCtrl',
templateUrl: 'public/app/plugins/datasource/opentsdb/partials/query.editor.html',
};
}
function configView() {
return {templateUrl: 'public/app/plugins/datasource/opentsdb/partials/config.html'};
}
return {
Datasource: OpenTsDatasource,
metricsQueryEditor: metricsQueryEditor,
configView: configView,
};
});

View File

@ -0,0 +1,13 @@
import {OpenTsDatasource} from './datasource';
import {OpenTsQueryCtrl} from './query_ctrl';
class OpenTsConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/opentsdb/partials/config.html';
}
export {
OpenTsDatasource as Datasource,
OpenTsQueryCtrl as QueryCtrl,
OpenTsConfigCtrl as ConfigCtrl,
};

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