diff --git a/CHANGELOG.md b/CHANGELOG.md index 225e1db96ac..d68dc949c30 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index 9deda2ee97e..b654554fa48 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -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. diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 0b120dd85b7..55c0f4ec049 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -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'; diff --git a/public/app/core/directives/config_modal.js b/public/app/core/directives/config_modal.js deleted file mode 100644 index e37d6797fc4..00000000000 --- a/public/app/core/directives/config_modal.js +++ /dev/null @@ -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(); - }); - } - }; - }); -}); diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index b3d6de2585d..1f96422d49e 100644 --- a/public/app/core/directives/misc.js +++ b/public/app/core/directives/misc.js @@ -90,7 +90,6 @@ function (angular, coreModule, kbn) { var li = '' + '' + (item.text || '') + ''; if (item.submenu && item.submenu.length) { diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts new file mode 100644 index 00000000000..94e50bf5c86 --- /dev/null +++ b/public/app/core/directives/plugin_component.ts @@ -0,0 +1,199 @@ +/// + +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 = `${template}`; + 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); diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts new file mode 100644 index 00000000000..b807d5bc50d --- /dev/null +++ b/public/app/core/directives/rebuild_on_change.ts @@ -0,0 +1,75 @@ +/// + +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); diff --git a/public/app/core/plugins/directive.ts b/public/app/core/plugins/directive.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/annotations/annotations_srv.js b/public/app/features/annotations/annotations_srv.js index 9dce53084fe..b0022135ef5 100644 --- a/public/app/features/annotations/annotations_srv.js +++ b/public/app/features/annotations/annotations_srv.js @@ -2,7 +2,6 @@ define([ 'angular', 'lodash', './editor_ctrl', - './query_editor' ], function (angular, _) { 'use strict'; diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 8894cb4b47f..f5d29e57677 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -91,8 +91,10 @@ - - + + + +
diff --git a/public/app/features/annotations/query_editor.ts b/public/app/features/annotations/query_editor.ts deleted file mode 100644 index 42d3e9047b5..00000000000 --- a/public/app/features/annotations/query_editor.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// - -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); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index e4d74c985ba..2f64fec9be1 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -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; diff --git a/public/app/features/dashboard/dynamicDashboardSrv.js b/public/app/features/dashboard/dynamicDashboardSrv.js index 8e3c24f7202..5c0101be10f 100644 --- a/public/app/features/dashboard/dynamicDashboardSrv.js +++ b/public/app/features/dashboard/dynamicDashboardSrv.js @@ -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; diff --git a/public/app/features/dashboard/rowCtrl.js b/public/app/features/dashboard/rowCtrl.js index e8abb5bbb05..d7ccc22603f 100644 --- a/public/app/features/dashboard/rowCtrl.js +++ b/public/app/features/dashboard/rowCtrl.js @@ -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); diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index abe1ec3279e..b18610d7c1d 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -103,6 +103,11 @@ function (angular, _, $) { if (!panelScope) { return; } + + if (!panelScope.ctrl.editModeInitiated) { + panelScope.ctrl.initEditMode(); + } + this.enterFullscreen(panelScope); return; } diff --git a/public/app/features/datasources/all.js b/public/app/features/datasources/all.js index 8a57bbc5b8a..b181fd475c2 100644 --- a/public/app/features/datasources/all.js +++ b/public/app/features/datasources/all.js @@ -1,5 +1,4 @@ define([ './list_ctrl', './edit_ctrl', - './config_view', ], function () {}); diff --git a/public/app/features/datasources/config_view.ts b/public/app/features/datasources/config_view.ts deleted file mode 100644 index 39f593ba508..00000000000 --- a/public/app/features/datasources/config_view.ts +++ /dev/null @@ -1,25 +0,0 @@ -/// - -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); diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js index 2bd86e5f385..c3242f5b565 100644 --- a/public/app/features/datasources/edit_ctrl.js +++ b/public/app/features/datasources/edit_ctrl.js @@ -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) { diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index 3ebae573ebd..00706cb4de5 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -41,7 +41,10 @@
- + + + +
Testing....
diff --git a/public/app/features/panel/all.js b/public/app/features/panel/all.js index c03089bbf9b..b3ec16e541f 100644 --- a/public/app/features/panel/all.js +++ b/public/app/features/panel/all.js @@ -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 () {}); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 76b6a7688e4..09626131149 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -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}; diff --git a/public/app/features/panel/panel.ts b/public/app/features/panel/panel.ts index 56749f90253..94aeb3878aa 100644 --- a/public/app/features/panel/panel.ts +++ b/public/app/features/panel/panel.ts @@ -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, } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index fa1b8663f9c..96b1a6d9d4d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -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 + }); + } } diff --git a/public/app/features/panel/panel_directive.js b/public/app/features/panel/panel_directive.js deleted file mode 100644 index da920094e97..00000000000 --- a/public/app/features/panel/panel_directive.js +++ /dev/null @@ -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: '', - 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); - }); - } - }; - }); - -}); diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts new file mode 100644 index 00000000000..757f9f8e43d --- /dev/null +++ b/public/app/features/panel/panel_directive.ts @@ -0,0 +1,101 @@ +/// + +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: '', + 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); + }); + } + }; +}); + + diff --git a/public/app/features/panel/panel_loader.ts b/public/app/features/panel/panel_loader.ts deleted file mode 100644 index 58d0fe47c73..00000000000 --- a/public/app/features/panel/panel_loader.ts +++ /dev/null @@ -1,88 +0,0 @@ -/// - -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 = `${template}`; - 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); diff --git a/public/app/features/panel/panel_menu.js b/public/app/features/panel/panel_menu.js index b23da0f2a88..0489489af8e 100644 --- a/public/app/features/panel/panel_menu.js +++ b/public/app/features/panel/panel_menu.js @@ -37,7 +37,7 @@ function (angular, $, _) { template += '
'; template += ''; template += ''; - template += ''; + template += ''; template += '
'; template += '
'; } @@ -53,7 +53,6 @@ function (angular, $, _) { template += ''; }); diff --git a/public/app/features/panel/partials/panel.html b/public/app/features/panel/partials/panel.html index 13c2178e0b4..b5228840687 100644 --- a/public/app/features/panel/partials/panel.html +++ b/public/app/features/panel/partials/panel.html @@ -1,6 +1,6 @@
- + diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html new file mode 100644 index 00000000000..4cf21b32e17 --- /dev/null +++ b/public/app/features/panel/partials/query_editor_row.html @@ -0,0 +1,56 @@ +
+ + +
    +
  • + {{ctrl.target.refId}} +
  • +
  • + + + +
  • +
+ +
    +
+ +
+
diff --git a/public/app/features/panel/query_ctrl.ts b/public/app/features/panel/query_ctrl.ts new file mode 100644 index 00000000000..66370595252 --- /dev/null +++ b/public/app/features/panel/query_ctrl.ts @@ -0,0 +1,57 @@ +/// + +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(); + } +} + diff --git a/public/app/features/panel/query_editor.ts b/public/app/features/panel/query_editor.ts deleted file mode 100644 index 7a5c6126740..00000000000 --- a/public/app/features/panel/query_editor.ts +++ /dev/null @@ -1,48 +0,0 @@ -/// - -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); diff --git a/public/app/features/panel/query_editor_row.ts b/public/app/features/panel/query_editor_row.ts new file mode 100644 index 00000000000..a4cb6155139 --- /dev/null +++ b/public/app/features/panel/query_editor_row.ts @@ -0,0 +1,18 @@ +/// + +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); diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 97db5617a1c..d63c0d01e29 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -65,7 +65,7 @@
  • - Row editor + Row editor
  • Delete row @@ -81,8 +81,8 @@
    - - + +
    diff --git a/public/app/partials/inspector.html b/public/app/partials/inspector.html index 5a83afe9a3b..9caa38bb39f 100644 --- a/public/app/partials/inspector.html +++ b/public/app/partials/inspector.html @@ -61,9 +61,9 @@
    -
    -			{{message}}
    -		
    +
    +{{message}}
    +
    diff --git a/public/app/partials/metrics.html b/public/app/partials/metrics.html
    index 8a7eb1bfd71..f7b1672cd97 100644
    --- a/public/app/partials/metrics.html
    +++ b/public/app/partials/metrics.html
    @@ -1,8 +1,12 @@
     
    - - +
    + + + + +
    @@ -26,7 +30,11 @@
    - + + + + +
    diff --git a/public/app/plugins/datasource/cloudwatch/datasource.d.ts b/public/app/plugins/datasource/cloudwatch/datasource.d.ts index a50d7ca49cc..afc49d07dea 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.d.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.d.ts @@ -1,3 +1,3 @@ -declare var Datasource: any; -export default Datasource; +declare var CloudWatchDatasource: any; +export {CloudWatchDatasource}; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 6f9f24ff867..119c4a83167 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -357,5 +357,7 @@ function (angular, _, moment, dateMath) { } - return CloudWatchDatasource; + return { + CloudWatchDatasource: CloudWatchDatasource + }; }); diff --git a/public/app/plugins/datasource/cloudwatch/module.js b/public/app/plugins/datasource/cloudwatch/module.js deleted file mode 100644 index 6f15457a43e..00000000000 --- a/public/app/plugins/datasource/cloudwatch/module.js +++ /dev/null @@ -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, - }; -}); diff --git a/public/app/plugins/datasource/cloudwatch/module.ts b/public/app/plugins/datasource/cloudwatch/module.ts new file mode 100644 index 00000000000..674506310ab --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/module.ts @@ -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, +}; + diff --git a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html index 583a578134d..050698f3ff2 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html @@ -1 +1 @@ - + diff --git a/public/app/plugins/datasource/cloudwatch/partials/edit_view.html b/public/app/plugins/datasource/cloudwatch/partials/config.html similarity index 76% rename from public/app/plugins/datasource/cloudwatch/partials/edit_view.html rename to public/app/plugins/datasource/cloudwatch/partials/config.html index fbb641633ac..92a963da7c6 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/edit_view.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -9,7 +9,7 @@ Credentials profile nameCredentials profile name, as specified in ~/.aws/credentials, leave blank for default
  • - +
  • @@ -19,12 +19,12 @@
  • Default RegionSpecify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
  • -
  • - +
  • diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.editor.html b/public/app/plugins/datasource/cloudwatch/partials/query.editor.html index fa5e74c33c2..a64635dc397 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/query.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/query.editor.html @@ -1,38 +1,4 @@ -
    - + + -
      -
    • - {{target.refId}} -
    • -
    • - - - -
    • -
    - -
    -
    - - + diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.js b/public/app/plugins/datasource/cloudwatch/query_ctrl.js deleted file mode 100644 index 927c08b3f2d..00000000000 --- a/public/app/plugins/datasource/cloudwatch/query_ctrl.js +++ /dev/null @@ -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}} {{}}'; - }; - - $scope.refreshMetricData = function() { - if (!_.isEqual($scope.oldTarget, $scope.target)) { - $scope.oldTarget = angular.copy($scope.target); - $scope.ctrl.refresh(); - } - }; - - $scope.init(); - - }); - -}); diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts new file mode 100644 index 00000000000..daacd7fe198 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/query_ctrl.ts @@ -0,0 +1,17 @@ +/// + +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}} {{}}'; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js index 3be1f0d5a01..dc027f60aef 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js +++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js @@ -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: { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index ed5f9418f8f..cec32420aea 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -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() { diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts new file mode 100644 index 00000000000..4c14e1133bf --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -0,0 +1,34 @@ +/// + +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'; + } +} + diff --git a/public/app/plugins/datasource/elasticsearch/datasource.d.ts b/public/app/plugins/datasource/elasticsearch/datasource.d.ts index a50d7ca49cc..3682abfb614 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.d.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.d.ts @@ -1,3 +1,3 @@ -declare var Datasource: any; -export default Datasource; +declare var ElasticDatasource: any; +export {ElasticDatasource}; diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 0d2fd174c4d..294f666abc7 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes }; } - return ElasticDatasource; + return { + ElasticDatasource: ElasticDatasource + }; }); diff --git a/public/app/plugins/datasource/elasticsearch/edit_view.ts b/public/app/plugins/datasource/elasticsearch/edit_view.ts deleted file mode 100644 index c7e17d4fbea..00000000000 --- a/public/app/plugins/datasource/elasticsearch/edit_view.ts +++ /dev/null @@ -1,39 +0,0 @@ -/// - -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; diff --git a/public/app/plugins/datasource/elasticsearch/module.js b/public/app/plugins/datasource/elasticsearch/module.js deleted file mode 100644 index d38afe8e936..00000000000 --- a/public/app/plugins/datasource/elasticsearch/module.js +++ /dev/null @@ -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, - }; - -}); diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts new file mode 100644 index 00000000000..438649017e5 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/module.ts @@ -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, +}; diff --git a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html index 0b7070904de..8f761b67865 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html @@ -1,14 +1,14 @@
    -
    +
    Index name
    - +
    Search query (lucene) Use [[filterName]] in query to replace part of the query with a filter value
    - +
    @@ -18,22 +18,22 @@
    Field mappings
    - +
    - +
    - +
    - +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/edit_view.html b/public/app/plugins/datasource/elasticsearch/partials/config.html similarity index 67% rename from public/app/plugins/datasource/elasticsearch/partials/edit_view.html rename to public/app/plugins/datasource/elasticsearch/partials/config.html index bf43012c72e..05df80143e3 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/edit_view.html +++ b/public/app/plugins/datasource/elasticsearch/partials/config.html @@ -1,4 +1,5 @@ - + +

    Elasticsearch details

    @@ -8,13 +9,13 @@ Index name
  • - +
  • Pattern
  • - +
  • @@ -25,7 +26,7 @@ Time field name
  • - +
  • @@ -36,7 +37,7 @@ Version
  • - +
  • @@ -52,7 +53,7 @@ Group by time interval
  • -
  • diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html index bbf5964b220..017f5cf1a42 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html @@ -1,77 +1,32 @@ -
    - - -
      -
    • - {{target.refId}} -
    • -
    • - - - -
    • -
    - -
      -
    • - Query -
    • -
    • - -
    • -
    • - Alias -
    • -
    • - -
    • -
    -
    +
    + +
    -
    -
    - - -
    - -
    - - -
    - +
    + +
    + diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.options.html b/public/app/plugins/datasource/elasticsearch/partials/query.options.html index 628a0a0bf3d..bac22df7136 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.options.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.options.html @@ -8,7 +8,7 @@ Group by time interval
  • -
  • @@ -23,7 +23,7 @@
  • - + alias patterns
  • @@ -34,7 +34,7 @@
    -
    +
    Alias patterns
    • {{term fieldname}} = replaced with value of term group by
    • diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.js b/public/app/plugins/datasource/elasticsearch/query_ctrl.js deleted file mode 100644 index 90214dff17c..00000000000 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.js +++ /dev/null @@ -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(); - - }); - -}); diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts new file mode 100644 index 00000000000..fb04f39b8ac --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -0,0 +1,45 @@ +/// + +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 []; + } +} diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index a7e6a642550..133d138be82 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -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() { diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts deleted file mode 100644 index aceef08a3d9..00000000000 --- a/public/app/plugins/datasource/elasticsearch/specs/query_ctrl_specs.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// -/// -/// - -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(); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana/module.ts b/public/app/plugins/datasource/grafana/module.ts index 7eba430ee43..19233c4f975 100644 --- a/public/app/plugins/datasource/grafana/module.ts +++ b/public/app/plugins/datasource/grafana/module.ts @@ -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, }; diff --git a/public/app/plugins/datasource/grafana/partials/query.editor.html b/public/app/plugins/datasource/grafana/partials/query.editor.html index 15297d5c3f0..3131b4cc346 100644 --- a/public/app/plugins/datasource/grafana/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana/partials/query.editor.html @@ -1,56 +1,5 @@ -
      - - -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      • - Test metric (fake data source) -
      • -
      -
      -
      + +
    • + Test metric (fake data source) +
    • +
      diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 46d93f6481f..6c218513d57 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -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'); diff --git a/public/app/plugins/datasource/graphite/datasource.d.ts b/public/app/plugins/datasource/graphite/datasource.d.ts deleted file mode 100644 index a50d7ca49cc..00000000000 --- a/public/app/plugins/datasource/graphite/datasource.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare var Datasource: any; -export default Datasource; - diff --git a/public/app/plugins/datasource/graphite/datasource.js b/public/app/plugins/datasource/graphite/datasource.js deleted file mode 100644 index 3a169eaae4e..00000000000 --- a/public/app/plugins/datasource/graphite/datasource.js +++ /dev/null @@ -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; -}); diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts new file mode 100644 index 00000000000..c57e5fcfd26 --- /dev/null +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -0,0 +1,281 @@ +/// + +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; + }; +} diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js index 63ee4b3f7cf..cd1f747bc4d 100644 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ b/public/app/plugins/datasource/graphite/func_editor.js @@ -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; } diff --git a/public/app/plugins/datasource/graphite/lexer.js b/public/app/plugins/datasource/graphite/lexer.js deleted file mode 100644 index 2b8affd04e2..00000000000 --- a/public/app/plugins/datasource/graphite/lexer.js +++ /dev/null @@ -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; - -}); diff --git a/public/app/plugins/datasource/graphite/lexer.ts b/public/app/plugins/datasource/graphite/lexer.ts new file mode 100644 index 00000000000..1835921d40b --- /dev/null +++ b/public/app/plugins/datasource/graphite/lexer.ts @@ -0,0 +1,678 @@ +/// + +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 + }; + }, + + }; + diff --git a/public/app/plugins/datasource/graphite/module.js b/public/app/plugins/datasource/graphite/module.js deleted file mode 100644 index 64f0945aef3..00000000000 --- a/public/app/plugins/datasource/graphite/module.js +++ /dev/null @@ -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, - }; -}); diff --git a/public/app/plugins/datasource/graphite/module.ts b/public/app/plugins/datasource/graphite/module.ts new file mode 100644 index 00000000000..63c04833fc2 --- /dev/null +++ b/public/app/plugins/datasource/graphite/module.ts @@ -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, +}; + diff --git a/public/app/plugins/datasource/graphite/parser.js b/public/app/plugins/datasource/graphite/parser.js deleted file mode 100644 index 43fd148f1ea..00000000000 --- a/public/app/plugins/datasource/graphite/parser.js +++ /dev/null @@ -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; -}); diff --git a/public/app/plugins/datasource/graphite/parser.ts b/public/app/plugins/datasource/graphite/parser.ts new file mode 100644 index 00000000000..bfafdda7815 --- /dev/null +++ b/public/app/plugins/datasource/graphite/parser.ts @@ -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)); + }, +}; + diff --git a/public/app/plugins/datasource/graphite/partials/annotations.editor.html b/public/app/plugins/datasource/graphite/partials/annotations.editor.html index 9253bf614a6..ea5c2f7f50f 100644 --- a/public/app/plugins/datasource/graphite/partials/annotations.editor.html +++ b/public/app/plugins/datasource/graphite/partials/annotations.editor.html @@ -1,14 +1,14 @@
      - +
      - +
      diff --git a/public/app/plugins/datasource/graphite/partials/config.html b/public/app/plugins/datasource/graphite/partials/config.html index 9f5259cb2ea..d7a0f739ccb 100644 --- a/public/app/plugins/datasource/graphite/partials/config.html +++ b/public/app/plugins/datasource/graphite/partials/config.html @@ -1,2 +1,3 @@ - + + diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 9cb2b454132..90dfe0794c0 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -1,73 +1,21 @@ -
      - + -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      +
    • + +
    • - - - +
    • - -
      -
      +
    • + +
    • +
    • + + +
    • + + +
    • + + diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html index f20b55b1348..35c95d03e88 100644 --- a/public/app/plugins/datasource/graphite/partials/query.options.html +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -1,5 +1,4 @@
      -
      • @@ -11,7 +10,7 @@
      • @@ -39,27 +38,27 @@
      • - + shorter legend names
      • - + series as parameters
      • - + stacking
      • - + templating
      • - + max data points
      • @@ -71,7 +70,7 @@
        -
        +
        Shorter legend names
        • alias() function to specify a custom series name
        • @@ -81,7 +80,7 @@
        -
        +
        Series as parameter
        • Some graphite functions allow you to have many series arguments
        • @@ -99,7 +98,7 @@
        -
        +
        Stacking
        • You find the stacking option under Display Styles tab
        • @@ -107,7 +106,7 @@
        -
        +
        Templating
        • You can use a template variable in place of metric names
        • @@ -116,7 +115,7 @@
        -
        +
        Max data points
        • Every graphite request is issued with a maxDataPoints parameter
        • diff --git a/public/app/plugins/datasource/graphite/query_ctrl.js b/public/app/plugins/datasource/graphite/query_ctrl.js deleted file mode 100644 index 55083aefaa2..00000000000 --- a/public/app/plugins/datasource/graphite/query_ctrl.js +++ /dev/null @@ -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(); - - }); - -}); diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts new file mode 100644 index 00000000000..23c6c2aa6d6 --- /dev/null +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -0,0 +1,276 @@ +/// + +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; + } + } + } +} diff --git a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts index 810e1be3516..2125922b350 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts @@ -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() { diff --git a/public/app/plugins/datasource/graphite/specs/lexer_specs.ts b/public/app/plugins/datasource/graphite/specs/lexer_specs.ts new file mode 100644 index 00000000000..1f4be736536 --- /dev/null +++ b/public/app/plugins/datasource/graphite/specs/lexer_specs.ts @@ -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'); + }); +}); diff --git a/public/app/plugins/datasource/graphite/specs/parser_specs.ts b/public/app/plugins/datasource/graphite/specs/parser_specs.ts new file mode 100644 index 00000000000..a69dcb56dfb --- /dev/null +++ b/public/app/plugins/datasource/graphite/specs/parser_specs.ts @@ -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'); + }); +}); diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts index 195e872efc7..8c191bba528 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts @@ -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() { diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js deleted file mode 100644 index 8d171deb7f9..00000000000 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ /dev/null @@ -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; -}); diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts new file mode 100644 index 00000000000..2ef73dc8f2d --- /dev/null +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -0,0 +1,213 @@ +/// + +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'; + } +} + diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index d932e7dd8d7..6eb2d84aa49 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -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 '; diff --git a/public/app/plugins/datasource/influxdb/influx_series.js b/public/app/plugins/datasource/influxdb/influx_series.js index 86f018ee11d..19e20390fd2 100644 --- a/public/app/plugins/datasource/influxdb/influx_series.js +++ b/public/app/plugins/datasource/influxdb/influx_series.js @@ -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); } } }); diff --git a/public/app/plugins/datasource/influxdb/module.js b/public/app/plugins/datasource/influxdb/module.js deleted file mode 100644 index b7f266f9f89..00000000000 --- a/public/app/plugins/datasource/influxdb/module.js +++ /dev/null @@ -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, - }; -}); diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts new file mode 100644 index 00000000000..fc19413f18d --- /dev/null +++ b/public/app/plugins/datasource/influxdb/module.ts @@ -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, +}; + + diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 9d1c967c668..a371fc9a7c8 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -1,4 +1,5 @@ - + +

          InfluxDB Details

          @@ -8,7 +9,7 @@ Database
        • - +
        @@ -19,13 +20,13 @@ User
      • - +
      • Password
      • - +
      diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 3322be6c423..140f298f458 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,119 +1,73 @@ -
      -
      - - -
        -
      • - {{target.refId}} -
      • -
      • - - - -
      • -
      - -