From b65642564a311f07b572ce2e1d63e4b83cb20903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 8 Oct 2016 10:06:47 +0200 Subject: [PATCH 01/12] poc for new metric segment --- .../form_dropdown/form_dropdown.html | 13 ++ .../components/form_dropdown/form_dropdown.ts | 182 ++++++++++++++++++ public/app/core/core.ts | 2 + public/app/core/directives/metric_segment.js | 2 - .../elasticsearch/partials/bucket_agg.html | 3 +- .../elasticsearch/partials/metric_agg.html | 6 +- 6 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 public/app/core/components/form_dropdown/form_dropdown.html create mode 100644 public/app/core/components/form_dropdown/form_dropdown.ts diff --git a/public/app/core/components/form_dropdown/form_dropdown.html b/public/app/core/components/form_dropdown/form_dropdown.html new file mode 100644 index 00000000000..34737eab27e --- /dev/null +++ b/public/app/core/components/form_dropdown/form_dropdown.html @@ -0,0 +1,13 @@ + + + + diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts new file mode 100644 index 00000000000..fd363d3b75e --- /dev/null +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -0,0 +1,182 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from '../../core_module'; + +function typeaheadMatcher(item) { + var str = this.query; + if (str[0] === '/') { str = str.substring(1); } + if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); } + return item.toLowerCase().match(str.toLowerCase()); +} + +export class FormDropdownCtrl { + inputElement: any; + linkElement: any; + value: any; + text: any; + display: any; + options: any; + cssClass: any; + allowCustom: any; + linkMode: boolean; + cancelBlur: any; + onChange: any; + + constructor(private $scope, $element, private $sce, private templateSrv) { + this.inputElement = $element.find('input').first(); + this.linkElement = $element.find('a').first(); + this.linkMode = true; + this.cancelBlur = null; + + if (this.options) { + var item = _.find(this.options, {value: this.value}); + this.updateDisplay(item ? item.text : this.value); + } + + this.inputElement.attr('data-provide', 'typeahead'); + this.inputElement.typeahead({ + source: this.typeaheadSource.bind(this), + minLength: 0, + items: 10000, + updater: this.typeaheadUpdater.bind(this), + matcher: typeaheadMatcher, + }); + + // modify typeahead lookup + // this = typeahead + var typeahead = this.inputElement.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + var items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + + this.linkElement.keydown(evt => { + // trigger typeahead on down arrow or enter key + if (evt.keyCode === 40 || evt.keyCode === 13) { + this.linkElement.click(); + } + }); + + this.inputElement.blur(this.inputBlur.bind(this)); + } + + typeaheadSource(query, callback) { + if (this.options) { + var typeaheadOptions = _.map(this.options, 'text'); + + // add current custom value + if (this.allowCustom) { + if (_.indexOf(typeaheadOptions, this.text) === -1) { + typeaheadOptions.unshift(this.text); + } + } + + callback(typeaheadOptions); + } + } + + typeaheadUpdater(text) { + if (text === this.text) { + clearTimeout(this.cancelBlur); + this.inputElement.focus(); + return text; + } + + this.inputElement.val(text); + this.switchToLink(true); + return text; + } + + switchToLink(fromClick) { + if (this.linkMode && !fromClick) { return; } + + clearTimeout(this.cancelBlur); + this.cancelBlur = null; + this.linkMode = true; + this.inputElement.hide(); + this.linkElement.show(); + this.updateValue(this.inputElement.val()); + } + + inputBlur() { + // happens long before the click event on the typeahead options + // need to have long delay because the blur + this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200); + } + + updateValue(text) { + if (text === '' || this.text === text) { + return; + } + + this.$scope.$apply(() => { + var option = _.find(this.options, {text: text}); + + if (option) { + this.value = option.value; + this.updateDisplay(option.text); + } else if (this.allowCustom) { + this.value = text; + this.updateDisplay(text); + } + + // needs to call this after digest so + // property is synced with outerscope + this.$scope.$$postDigest(() => { + this.$scope.$apply(() => { + this.onChange(); + }); + }); + + }); + } + + updateDisplay(text) { + this.text = text; + this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); + } + + open() { + this.inputElement.show(); + + this.inputElement.css('width', (Math.max(this.linkElement.width(), 80) + 16) + 'px'); + this.inputElement.focus(); + + this.linkElement.hide(); + this.linkMode = false; + + var typeahead = this.inputElement.data('typeahead'); + if (typeahead) { + this.inputElement.val(''); + typeahead.lookup(); + } + } +} + + + +export function formDropdownDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/form_dropdown/form_dropdown.html', + controller: FormDropdownCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + value: "=", + options: "=", + getOptions: "&", + onChange: "&", + cssClass: "@", + allowCustom: "@", + }, + link: function() { + } + }; +} + +coreModule.directive('gfFormDropdown', formDropdownDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index d44cbf4dbfb..ea55c22be8f 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -35,6 +35,7 @@ import {switchDirective} from './components/switch'; import {dashboardSelector} from './components/dashboard_selector'; import {queryPartEditorDirective} from './components/query_part/query_part_editor'; import {WizardFlow} from './components/wizard/wizard'; +import {formDropdownDirective} from './components/form_dropdown/form_dropdown'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -62,4 +63,5 @@ export { queryPartEditorDirective, WizardFlow, colors, + formDropdownDirective, }; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 2001073ed80..62805161155 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -143,7 +143,6 @@ function (_, $, coreModule) { $input.focus(); linkMode = false; - var typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); @@ -152,7 +151,6 @@ function (_, $, coreModule) { }); $input.blur($scope.inputBlur); - $compile(elem.contents())($scope); } }; diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 36e914d06e0..2b674a9fdf9 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -5,7 +5,8 @@ Then by - + + diff --git a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html index faa12b5693d..472cfca37fa 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html @@ -11,9 +11,9 @@
- - - + + +
From 6a95df403ac967f1b24133209e36ebe313ce2d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Jun 2017 19:42:45 -0400 Subject: [PATCH 02/12] refacoring: more work on metric segment replacement --- .../form_dropdown/form_dropdown.html | 13 --- .../components/form_dropdown/form_dropdown.ts | 90 ++++++++++++++----- public/app/core/directives/dash_edit_link.js | 2 +- public/app/core/directives/metric_segment.js | 2 + public/app/features/panel/metrics_tab.ts | 51 +++++------ .../features/panel/partials/metrics_tab.html | 15 ++-- 6 files changed, 102 insertions(+), 71 deletions(-) delete mode 100644 public/app/core/components/form_dropdown/form_dropdown.html diff --git a/public/app/core/components/form_dropdown/form_dropdown.html b/public/app/core/components/form_dropdown/form_dropdown.html deleted file mode 100644 index 34737eab27e..00000000000 --- a/public/app/core/components/form_dropdown/form_dropdown.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index fd363d3b75e..eb8be1e0945 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -15,15 +15,17 @@ function typeaheadMatcher(item) { export class FormDropdownCtrl { inputElement: any; linkElement: any; - value: any; - text: any; + model: any; display: any; + text: any; options: any; cssClass: any; allowCustom: any; linkMode: boolean; cancelBlur: any; onChange: any; + getOptions: any; + optionCache: any; constructor(private $scope, $element, private $sce, private templateSrv) { this.inputElement = $element.find('input').first(); @@ -31,11 +33,15 @@ export class FormDropdownCtrl { this.linkMode = true; this.cancelBlur = null; - if (this.options) { - var item = _.find(this.options, {value: this.value}); - this.updateDisplay(item ? item.text : this.value); + if (!this.getOptions) { + this.getOptions = () => { + return Promise.resolve(this.options); + }; } + // listen to model changes + $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.typeahead({ source: this.typeaheadSource.bind(this), @@ -64,19 +70,40 @@ export class FormDropdownCtrl { this.inputElement.blur(this.inputBlur.bind(this)); } - typeaheadSource(query, callback) { - if (this.options) { - var typeaheadOptions = _.map(this.options, 'text'); + modelChanged(newVal) { + if (_.isObject(this.model)) { + this.updateDisplay(this.model.text); + } else { - // add current custom value + // if we have text use it + if (this.text) { + this.updateDisplay(this.text); + } else { + // otherwise we need to do initial lookup, usually happens first time + this.getOptions().then(options => { + var item = _.find(options, {value: this.model}); + this.updateDisplay(item ? item.text : this.model); + }); + } + } + } + + typeaheadSource(query, callback) { + this.getOptions({$query: query}).then(options => { + this.optionCache = options; + + // extract texts + let optionTexts = _.map(options, 'text'); + + // add custom values if (this.allowCustom) { - if (_.indexOf(typeaheadOptions, this.text) === -1) { - typeaheadOptions.unshift(this.text); + if (_.indexOf(optionTexts, this.text) === -1) { + options.unshift(this.text); } } - callback(typeaheadOptions); - } + callback(optionTexts); + }); } typeaheadUpdater(text) { @@ -114,21 +141,29 @@ export class FormDropdownCtrl { } this.$scope.$apply(() => { - var option = _.find(this.options, {text: text}); + var option = _.find(this.optionCache, {text: text}); if (option) { - this.value = option.value; - this.updateDisplay(option.text); + if (_.isObject(this.model)) { + this.model = option; + } else { + this.model = option.value; + } + this.text = option.text; } else if (this.allowCustom) { - this.value = text; - this.updateDisplay(text); + if (_.isObject(this.model)) { + this.model.text = this.model.value = text; + } else { + this.model = text; + } + this.text = text; } // needs to call this after digest so // property is synced with outerscope this.$scope.$$postDigest(() => { this.$scope.$apply(() => { - this.onChange(); + this.onChange({$option: option}); }); }); @@ -157,17 +192,30 @@ export class FormDropdownCtrl { } } +const template = ` + + +`; export function formDropdownDirective() { return { restrict: 'E', - templateUrl: 'public/app/core/components/form_dropdown/form_dropdown.html', + template: template, controller: FormDropdownCtrl, bindToController: true, controllerAs: 'ctrl', scope: { - value: "=", + model: "=", options: "=", getOptions: "&", onChange: "&", diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index 3e4bdd4c5c7..a4c1ad53b3c 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -35,7 +35,7 @@ function ($, angular, coreModule) { options.html = editViewMap[options.editview].html; } - if (lastEditView === options.editview) { + if (lastEditView && lastEditView === options.editview) { hideEditorPane(false); return; } diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 0605d54a815..2e9442c15a0 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -143,6 +143,7 @@ function (_, $, coreModule) { $input.focus(); linkMode = false; + var typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); @@ -151,6 +152,7 @@ function (_, $, coreModule) { }); $input.blur($scope.inputBlur); + $compile(elem.contents())($scope); } }; diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index f2d99738040..c03c4d83bc2 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -5,8 +5,6 @@ import _ from 'lodash'; import {DashboardModel} from '../dashboard/model'; export class MetricsTabCtrl { - dsSegment: any; - mixedDsSegment: any; dsName: string; panel: any; panelCtrl: any; @@ -14,30 +12,26 @@ export class MetricsTabCtrl { current: any; nextRefId: string; dashboard: DashboardModel; + panelDsValue: any; + addQueryDropdown: any; /** @ngInject */ - constructor($scope, private uiSegmentSrv, datasourceSrv) { + constructor($scope, private uiSegmentSrv, private datasourceSrv) { this.panelCtrl = $scope.ctrl; $scope.ctrl = this; this.panel = this.panelCtrl.panel; this.dashboard = this.panelCtrl.dashboard; this.datasources = datasourceSrv.getMetricSources(); - - var dsValue = this.panelCtrl.panel.datasource || null; + this.panelDsValue = this.panelCtrl.panel.datasource || null; for (let ds of this.datasources) { - if (ds.value === dsValue) { + if (ds.value === this.panelDsValue) { this.current = ds; } } - if (!this.current) { - this.current = {name: dsValue + ' not found', value: null}; - } - - this.dsSegment = uiSegmentSrv.newSegment({value: this.current.name, selectMode: true}); - this.mixedDsSegment = uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); + this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; // update next ref id this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel); @@ -46,33 +40,28 @@ export class MetricsTabCtrl { getOptions(includeBuiltin) { return Promise.resolve(this.datasources.filter(value => { return includeBuiltin || !value.meta.builtIn; - }).map(value => { - return this.uiSegmentSrv.newSegment(value.name); + }).map(ds => { + return {value: ds.value, text: ds.name, datasource: ds}; })); } - datasourceChanged() { - var ds = _.find(this.datasources, {name: this.dsSegment.value}); - if (ds) { - this.current = ds; - this.panelCtrl.setDatasource(ds); + datasourceChanged(option) { + if (!option) { + return; } + + this.current = option.datasource; + this.panelCtrl.setDatasource(option.datasource); } - mixedDatasourceChanged() { - var target: any = {isNew: true}; - var ds = _.find(this.datasources, {name: this.mixedDsSegment.value}); - - if (ds) { - target.datasource = ds.name; - this.panelCtrl.addQuery(target); + addMixedQuery(option) { + if (!option) { + return; } - // metric segments are really bad, requires hacks to update - const segment = this.uiSegmentSrv.newSegment({value: 'Add Query', selectMode: true, fake: true}); - this.mixedDsSegment.value = segment.value; - this.mixedDsSegment.html = segment.html; - this.mixedDsSegment.text = segment.text; + var target: any = {isNew: true}; + this.panelCtrl.addQuery({isNew: true, datasource: option.datasource.name}); + this.addQueryDropdown = {text: 'Add Query', value: null, fake: true}; } addQuery() { diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index a020a2edbd1..32f43f1d639 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -19,7 +19,10 @@
@@ -30,10 +33,12 @@
- - + + +
From 5f3b5fdcb25c8d3c35b098d990520b1f1dc93d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 12:21:12 -0400 Subject: [PATCH 03/12] updated --- .../core/components/form_dropdown/form_dropdown.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index eb8be1e0945..cb7390ead9a 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -194,17 +194,19 @@ export class FormDropdownCtrl { const template = ` + data-provide="typeahead" + class="gf-form-input" + spellcheck="false" + style="display:none"> + + ng-bind-html="ctrl.display"> + `; export function formDropdownDirective() { @@ -221,6 +223,7 @@ export function formDropdownDirective() { onChange: "&", cssClass: "@", allowCustom: "@", + selectMode: "@", }, link: function() { } From 76c4bfe2682a926ee479783abfe3ba4571e973ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 14:03:26 -0400 Subject: [PATCH 04/12] ux: new metric segment is starting to work --- .../components/form_dropdown/form_dropdown.ts | 21 +++++++++---------- .../features/panel/partials/metrics_tab.html | 3 +-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index cb7390ead9a..50017225720 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -20,7 +20,9 @@ export class FormDropdownCtrl { text: any; options: any; cssClass: any; + cssClasses: any; allowCustom: any; + labelMode: boolean; linkMode: boolean; cancelBlur: any; onChange: any; @@ -33,15 +35,15 @@ export class FormDropdownCtrl { this.linkMode = true; this.cancelBlur = null; - if (!this.getOptions) { - this.getOptions = () => { - return Promise.resolve(this.options); - }; - } - // listen to model changes $scope.$watch("ctrl.model", this.modelChanged.bind(this)); + if (this.labelMode) { + this.cssClasses = 'gf-form-label ' + this.cssClass; + } else { + this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass; + } + this.inputElement.attr('data-provide', 'typeahead'); this.inputElement.typeahead({ source: this.typeaheadSource.bind(this), @@ -199,9 +201,7 @@ const template = ` spellcheck="false" style="display:none"> - -Panel Data Source + on-change="ctrl.datasourceChanged($option)"> From 840099bec0b58f464f3605c30c2f072a96f4d994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Jun 2017 15:56:24 -0400 Subject: [PATCH 05/12] refactor: metric segment remake --- .../components/form_dropdown/form_dropdown.ts | 38 ++++++++++---- .../features/panel/partials/metrics_tab.html | 1 + .../datasource/elasticsearch/bucket_agg.js | 21 +++++--- .../elasticsearch/partials/bucket_agg.html | 51 ++++++++++++++++--- .../elasticsearch/partials/metric_agg.html | 6 +-- .../datasource/elasticsearch/query_ctrl.ts | 4 +- 6 files changed, 91 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 50017225720..72a101388b7 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -28,8 +28,9 @@ export class FormDropdownCtrl { onChange: any; getOptions: any; optionCache: any; + lookupText: boolean; - constructor(private $scope, $element, private $sce, private templateSrv) { + constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); this.linkMode = true; @@ -69,29 +70,45 @@ export class FormDropdownCtrl { } }); + this.inputElement.keydown(evt => { + if (evt.keyCode === 13) { + this.inputElement.blur(); + } + }); + this.inputElement.blur(this.inputBlur.bind(this)); } - modelChanged(newVal) { + getOptionsInternal(query) { + var result = this.getOptions({$query: query}); + if (this.isPromiseLike(result)) { + return result; + } + return this.$q.when(result); + } + + isPromiseLike(obj) { + return obj && (typeof obj.then === 'function'); + } + + modelChanged() { if (_.isObject(this.model)) { this.updateDisplay(this.model.text); } else { - // if we have text use it - if (this.text) { - this.updateDisplay(this.text); - } else { - // otherwise we need to do initial lookup, usually happens first time - this.getOptions().then(options => { + if (this.lookupText) { + this.getOptionsInternal("").then(options => { var item = _.find(options, {value: this.model}); this.updateDisplay(item ? item.text : this.model); }); + } else { + this.updateDisplay(this.model); } } } typeaheadSource(query, callback) { - this.getOptions({$query: query}).then(options => { + this.getOptionsInternal(query).then(options => { this.optionCache = options; // extract texts @@ -223,9 +240,8 @@ export function formDropdownDirective() { cssClass: "@", allowCustom: "@", labelMode: "@", + lookupText: "@", }, - link: function() { - } }; } diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index d8d8816d63b..bc0bcf7c6b2 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -35,6 +35,7 @@
diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index b2cfc819579..5adaed173af 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -26,13 +26,21 @@ function (angular, _, queryDef) { var bucketAggs = $scope.target.bucketAggs; $scope.orderByOptions = []; - $scope.bucketAggTypes = queryDef.bucketAggTypes; - $scope.orderOptions = queryDef.orderOptions; - $scope.sizeOptions = queryDef.sizeOptions; + + $scope.getBucketAggTypes = function() { + return queryDef.bucketAggTypes; + }; + + $scope.getOrderOptions = function() { + return queryDef.orderOptions; + }; + + $scope.getSizeOptions = function() { + return queryDef.sizeOptions; + }; $rootScope.onAppEvent('elastic-query-updated', function() { $scope.validateModel(); - $scope.updateOrderByOptions(); }, $scope); $scope.init = function() { @@ -166,11 +174,10 @@ function (angular, _, queryDef) { $scope.toggleOptions = function() { $scope.showOptions = !$scope.showOptions; - $scope.updateOrderByOptions(); }; - $scope.updateOrderByOptions = function() { - $scope.orderByOptions = queryDef.getOrderByOptions($scope.target); + $scope.getOrderByOptions = function() { + return queryDef.getOrderByOptions($scope.target); }; $scope.getFieldsInternal = function() { diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index f6c86db0d1e..a180f97c994 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -5,9 +5,22 @@ Then by - - - + + + +
@@ -34,7 +47,13 @@
- + +
@@ -67,11 +86,23 @@
- + +
- + +
@@ -79,7 +110,13 @@
- + +
- - - + + +
diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index 009befb37a4..ef6f1c5cdc7 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -31,11 +31,11 @@ export class ElasticQueryCtrl extends QueryCtrl { queryUpdated() { var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true); - if (newJson !== this.rawQueryOld) { - this.rawQueryOld = newJson; + if (this.rawQueryOld && newJson !== this.rawQueryOld) { this.refresh(); } + this.rawQueryOld = newJson; this.$rootScope.appEvent('elastic-query-updated'); } From a49e82e4470490f62445cdadd0397523bd9315d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Jun 2017 09:47:21 -0400 Subject: [PATCH 06/12] ux: revert dashboard search nav change, this is a tempoary change until we redesign the sidenav --- .../features/dashboard/dashnav/dashnav.html | 138 +++++++++++------- .../app/features/dashboard/dashnav/dashnav.ts | 19 ++- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index f1615a0bd1c..bab27289aca 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -1,63 +1,95 @@ - - - - -
+
+ + diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index e77c2bb3e09..98cefa46c7f 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -22,8 +22,8 @@ export class DashNavCtrl { private backendSrv, private $timeout, private datasourceSrv, - private navModelSrv) { - + private navModelSrv, + private contextSrv) { this.navModel = navModelSrv.getDashboardNav(this.dashboard, this); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope); @@ -38,6 +38,10 @@ export class DashNavCtrl { } } + toggleSideMenu() { + this.contextSrv.toggleSideMenu(); + } + openEditView(editview) { var search = _.extend(this.$location.search(), {editview: editview}); this.$location.search(search); @@ -135,6 +139,17 @@ export class DashNavCtrl { var uri = "data:application/json;charset=utf-8," + encodeURIComponent(html); var newWindow = window.open(uri); } + + showSearch() { + this.$rootScope.appEvent('show-dash-search'); + } + + navItemClicked(navItem, evt) { + if (navItem.clickHandler) { + navItem.clickHandler(); + evt.preventDefault(); + } + } } export function dashNavDirective() { From 5d63ad21c1b8e2ffcac7cc28a0bad545925238aa Mon Sep 17 00:00:00 2001 From: Brandon Arp Date: Fri, 16 Jun 2017 07:45:52 -0700 Subject: [PATCH 07/12] allow heatmap parsing of scaled datapoints (#8632) --- .../app/plugins/panel/heatmap/heatmap_data_converter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts index f7e32f3df7c..07057c53985 100644 --- a/public/app/plugins/panel/heatmap/heatmap_data_converter.ts +++ b/public/app/plugins/panel/heatmap/heatmap_data_converter.ts @@ -207,15 +207,20 @@ function pushToXBuckets(buckets, point, bucketNum, seriesName) { } function pushToYBuckets(buckets, bucketNum, value, point, bounds) { + var count = 1; + // Use the 3rd argument as scale/count + if (point.length > 2) { + count = parseInt(point[2]); + } if (buckets[bucketNum]) { buckets[bucketNum].values.push(value); - buckets[bucketNum].count += 1; + buckets[bucketNum].count += count; } else { buckets[bucketNum] = { y: bucketNum, bounds: bounds, values: [value], - count: 1, + count: count, }; } } From 056c57d55197bac83f3af6c6a5fc40ae04e87262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Jun 2017 10:47:17 -0400 Subject: [PATCH 08/12] ux: temporary remove search --- public/app/core/components/navbar/navbar.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index ea8eb21d6d6..085e2eef920 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -8,9 +8,9 @@ - - - + + +
From 724368d0cdf0f95922b14601d6ce65b5d90da687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Jun 2017 11:43:37 -0400 Subject: [PATCH 09/12] fix: data source dropdown select --- public/app/core/components/form_dropdown/form_dropdown.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 72a101388b7..d8212e2f55c 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -72,7 +72,9 @@ export class FormDropdownCtrl { this.inputElement.keydown(evt => { if (evt.keyCode === 13) { - this.inputElement.blur(); + setTimeout(() => { + this.inputElement.blur(); + }, 100); } }); From 49fdbb3843753128eba5877c3e016e6af286455d Mon Sep 17 00:00:00 2001 From: Salman Jalali Date: Fri, 16 Jun 2017 16:10:45 -0700 Subject: [PATCH 10/12] Update kbn.js --- public/app/core/utils/kbn.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index d619d9c4977..6f8340d5940 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -841,7 +841,7 @@ function($, _) { { text: 'temperature', submenu: [ - {text: 'Celcius (°C)', value: 'celsius' }, + {text: 'Celsius (°C)', value: 'celsius' }, {text: 'Farenheit (°F)', value: 'farenheit' }, {text: 'Kelvin (K)', value: 'kelvin' }, ] From ad080af38f860e168e40e9357d66a2e1b619ff11 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sat, 17 Jun 2017 20:59:19 +0200 Subject: [PATCH 11/12] docs: add tutorial for using API --- docs/sources/tutorials/api_org_token_howto.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/sources/tutorials/api_org_token_howto.md diff --git a/docs/sources/tutorials/api_org_token_howto.md b/docs/sources/tutorials/api_org_token_howto.md new file mode 100644 index 00000000000..e985b499dbe --- /dev/null +++ b/docs/sources/tutorials/api_org_token_howto.md @@ -0,0 +1,74 @@ ++++ +title = "API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization" +type = "docs" +keywords = ["grafana", "tutorials", "API", "Token", "Org", "Organization"] +[menu.docs] +parent = "tutorials" +weight = 10 ++++ + +# API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization + +A common scenario is to want to via the Grafana API setup new Grafana organizations or to add dynamically generated dashboards to an existing organization. + +## Authentication + +There are two ways to authenticate against the API: basic authentication and API Tokens. + +Some parts of the API are only available through basic authentication and these parts of the API usually require that the user is a Grafana Admin. But all organization actions are accessed via an API Token. An API Token is tied to an organization and can be used to create dashboards etc but only for that organization. + +## How To Create A New Organization and an API Token + +The task is to create a new organization and then add a Token that can be used by other users. In the examples below which use basic auth, the user is `admin` and the password is `admin`. + +1. [Create the org](http://docs.grafana.org/http_api/org/#create-organisation). Here is an example using curl: + ``` + curl -X POST -H "Content-Type: application/json" -d '{"name":"apiorg"}' http://admin:admin@localhost:3000/api/orgs + ``` + + This should return a response: `{"message":"Organization created","orgId":6}`. Use the orgId for the next steps. + +2. Optional step. If the org was created previously and/or step 3 fails then first [add your Admin user to the org](http://docs.grafana.org/http_api/org/#add-user-in-organisation): + ``` + curl -X POST -H "Content-Type: application/json" -d '{"loginOrEmail":"admin", "role": "Admin"}' http://admin:admin@localhost:3000/api/orgs//users + ``` + +3. [Switch the org context for the Admin user to the new org](http://docs.grafana.org/http_api/user/#switch-user-context): + ``` + curl -X POST http://admin:admin@localhost:3000/api/user/using/ + ``` + +4. [Create the API token](http://docs.grafana.org/http_api/auth/#create-api-key): + ``` + curl -X POST -H "Content-Type: application/json" -d '{"name":"apikeycurl", "role": "Admin"}' http://admin:admin@localhost:3000/api/auth/keys + ``` + + This should return a response: `{"name":"apikeycurl","key":"eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ=="}`. + + Save the key returned here in your password manager as it is not possible to fetch again it in the future. + +## How To Add A Dashboard + +Using the Token that was created in the previous step, you can create a dashboard or carry out other actions without having to switch organizations. + +1. [Add a dashboard](http://docs.grafana.org/http_api/dashboard/#create-update-dashboard) using the key (or bearer token as it is also called): + + ``` + curl -X POST --insecure -H "Authorization: Bearer eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ==" -H "Content-Type: application/json" -d '{ + "dashboard": { + "id": null, + "title": "Production Overview", + "tags": [ "templated" ], + "timezone": "browser", + "rows": [ + { + } + ], + "schemaVersion": 6, + "version": 0 + }, + "overwrite": false + }' http://localhost:3000/api/dashboards/db + ``` + + This import will not work if you exported the dashboard via the Share -> Export menu in the Grafana UI (it strips out data source names etc.). View the JSON and save it to a file instead or fetch the dashboard JSON via the API. From 0eb297822c44edea7d4e981b654b658fccd02d9a Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Sat, 17 Jun 2017 23:10:12 +0200 Subject: [PATCH 12/12] httpserver: fixes #8641 Changes to the http_server class meant that the TLS settings were not getting applied anymore. This fixes so that the minimum TLS version is 1.2 again. --- pkg/api/http_server.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 1e143ef876f..0468b5cbe8a 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -61,7 +61,7 @@ func (hs *HttpServer) Start(ctx context.Context) error { return nil } case setting.HTTPS: - err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile) + err = hs.listenAndServeTLS(setting.CertFile, setting.KeyFile) if err == http.ErrServerClosed { hs.log.Debug("server was shutdown gracefully") return nil @@ -92,7 +92,7 @@ func (hs *HttpServer) Shutdown(ctx context.Context) error { return err } -func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error { +func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error { if certfile == "" { return fmt.Errorf("cert_file cannot be empty when using HTTPS") } @@ -127,14 +127,11 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, }, } - srv := &http.Server{ - Addr: listenAddr, - Handler: hs.macaron, - TLSConfig: tlsCfg, - TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0), - } - return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile) + hs.httpSrv.TLSConfig = tlsCfg + hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0) + + return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile) } func (hs *HttpServer) newMacaron() *macaron.Macaron {