mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of https://github.com/grafana/grafana into metadata
This commit is contained in:
commit
59a384b453
13
CHANGELOG.md
13
CHANGELOG.md
@ -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)
|
||||
|
||||
|
@ -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 they’re 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.
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
199
public/app/core/directives/plugin_component.ts
Normal file
199
public/app/core/directives/plugin_component.ts
Normal 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);
|
75
public/app/core/directives/rebuild_on_change.ts
Normal file
75
public/app/core/directives/rebuild_on_change.ts
Normal 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);
|
0
public/app/core/plugins/directive.ts
Normal file
0
public/app/core/plugins/directive.ts
Normal file
@ -2,7 +2,6 @@ define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./editor_ctrl',
|
||||
'./query_editor'
|
||||
], function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -103,6 +103,11 @@ function (angular, _, $) {
|
||||
if (!panelScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panelScope.ctrl.editModeInitiated) {
|
||||
panelScope.ctrl.initEditMode();
|
||||
}
|
||||
|
||||
this.enterFullscreen(panelScope);
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
define([
|
||||
'./list_ctrl',
|
||||
'./edit_ctrl',
|
||||
'./config_view',
|
||||
], function () {});
|
||||
|
@ -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);
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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 () {});
|
||||
|
@ -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};
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
101
public/app/features/panel/panel_directive.ts
Normal file
101
public/app/features/panel/panel_directive.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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);
|
@ -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>';
|
||||
});
|
||||
|
@ -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>
|
||||
|
56
public/app/features/panel/partials/query_editor_row.html
Normal file
56
public/app/features/panel/partials/query_editor_row.html
Normal 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>
|
57
public/app/features/panel/query_ctrl.ts
Normal file
57
public/app/features/panel/query_ctrl.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
18
public/app/features/panel/query_editor_row.ts
Normal file
18
public/app/features/panel/query_editor_row.ts
Normal 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);
|
@ -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">
|
||||
|
@ -61,9 +61,9 @@
|
||||
<div ng-if="editor.index == 2">
|
||||
|
||||
<label>Message:</label>
|
||||
<pre>
|
||||
{{message}}
|
||||
</pre>
|
||||
<pre>
|
||||
{{message}}
|
||||
</pre>
|
||||
|
||||
<label>Stack trace:</label>
|
||||
<pre>
|
||||
|
@ -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">
|
||||
|
@ -1,3 +1,3 @@
|
||||
declare var Datasource: any;
|
||||
export default Datasource;
|
||||
declare var CloudWatchDatasource: any;
|
||||
export {CloudWatchDatasource};
|
||||
|
||||
|
@ -357,5 +357,7 @@ function (angular, _, moment, dateMath) {
|
||||
|
||||
}
|
||||
|
||||
return CloudWatchDatasource;
|
||||
return {
|
||||
CloudWatchDatasource: CloudWatchDatasource
|
||||
};
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
20
public/app/plugins/datasource/cloudwatch/module.ts
Normal file
20
public/app/plugins/datasource/cloudwatch/module.ts
Normal 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,
|
||||
};
|
||||
|
@ -1 +1 @@
|
||||
<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>
|
||||
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
||||
});
|
17
public/app/plugins/datasource/cloudwatch/query_ctrl.ts
Normal file
17
public/app/plugins/datasource/cloudwatch/query_ctrl.ts
Normal 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>}}';
|
||||
}
|
||||
}
|
@ -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: {
|
||||
|
@ -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() {
|
||||
|
34
public/app/plugins/datasource/elasticsearch/config_ctrl.ts
Normal file
34
public/app/plugins/datasource/elasticsearch/config_ctrl.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
declare var Datasource: any;
|
||||
export default Datasource;
|
||||
declare var ElasticDatasource: any;
|
||||
export {ElasticDatasource};
|
||||
|
||||
|
@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
};
|
||||
}
|
||||
|
||||
return ElasticDatasource;
|
||||
return {
|
||||
ElasticDatasource: ElasticDatasource
|
||||
};
|
||||
});
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
||||
|
||||
});
|
19
public/app/plugins/datasource/elasticsearch/module.ts
Normal file
19
public/app/plugins/datasource/elasticsearch/module.ts
Normal 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,
|
||||
};
|
@ -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>
|
||||
|
@ -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">
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
||||
});
|
45
public/app/plugins/datasource/elasticsearch/query_ctrl.ts
Normal file
45
public/app/plugins/datasource/elasticsearch/query_ctrl.ts
Normal 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 [];
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -1,3 +0,0 @@
|
||||
declare var Datasource: any;
|
||||
export default Datasource;
|
||||
|
@ -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;
|
||||
});
|
281
public/app/plugins/datasource/graphite/datasource.ts
Normal file
281
public/app/plugins/datasource/graphite/datasource.ts
Normal 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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
});
|
678
public/app/plugins/datasource/graphite/lexer.ts
Normal file
678
public/app/plugins/datasource/graphite/lexer.ts
Normal 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
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
23
public/app/plugins/datasource/graphite/module.ts
Normal file
23
public/app/plugins/datasource/graphite/module.ts
Normal 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,
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
258
public/app/plugins/datasource/graphite/parser.ts
Normal file
258
public/app/plugins/datasource/graphite/parser.ts
Normal 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));
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
<datasource-http-settings></datasource-http-settings>
|
||||
<datasource-http-settings current="ctrl.current">
|
||||
</datasource-http-settings>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
||||
});
|
276
public/app/plugins/datasource/graphite/query_ctrl.ts
Normal file
276
public/app/plugins/datasource/graphite/query_ctrl.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
118
public/app/plugins/datasource/graphite/specs/lexer_specs.ts
Normal file
118
public/app/plugins/datasource/graphite/specs/lexer_specs.ts
Normal 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');
|
||||
});
|
||||
});
|
183
public/app/plugins/datasource/graphite/specs/parser_specs.ts
Normal file
183
public/app/plugins/datasource/graphite/specs/parser_specs.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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() {
|
||||
|
@ -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;
|
||||
});
|
213
public/app/plugins/datasource/influxdb/datasource.ts
Normal file
213
public/app/plugins/datasource/influxdb/datasource.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
@ -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 ';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
24
public/app/plugins/datasource/influxdb/module.ts
Normal file
24
public/app/plugins/datasource/influxdb/module.ts
Normal 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,
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 & 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>
|
||||
|
@ -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();
|
||||
|
||||
});
|
||||
|
||||
});
|
319
public/app/plugins/datasource/influxdb/query_ctrl.ts
Normal file
319
public/app/plugins/datasource/influxdb/query_ctrl.ts
Normal 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 '=';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
declare var Datasource: any;
|
||||
export default Datasource;
|
||||
declare var OpenTsDatasource: any;
|
||||
export {OpenTsDatasource};
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
13
public/app/plugins/datasource/opentsdb/module.ts
Normal file
13
public/app/plugins/datasource/opentsdb/module.ts
Normal 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
Loading…
Reference in New Issue
Block a user