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..72a101388b7 --- /dev/null +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -0,0 +1,248 @@ +/// + +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; + model: any; + display: any; + text: any; + options: any; + cssClass: any; + cssClasses: any; + allowCustom: any; + labelMode: boolean; + linkMode: boolean; + cancelBlur: any; + onChange: any; + getOptions: any; + optionCache: any; + lookupText: boolean; + + 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; + this.cancelBlur = null; + + // 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), + 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.keydown(evt => { + if (evt.keyCode === 13) { + this.inputElement.blur(); + } + }); + + this.inputElement.blur(this.inputBlur.bind(this)); + } + + 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.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.getOptionsInternal(query).then(options => { + this.optionCache = options; + + // extract texts + let optionTexts = _.map(options, 'text'); + + // add custom values + if (this.allowCustom) { + if (_.indexOf(optionTexts, this.text) === -1) { + options.unshift(this.text); + } + } + + callback(optionTexts); + }); + } + + 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.optionCache, {text: text}); + + if (option) { + if (_.isObject(this.model)) { + this.model = option; + } else { + this.model = option.value; + } + this.text = option.text; + } else if (this.allowCustom) { + 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({$option: option}); + }); + }); + + }); + } + + 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(); + } + } +} + +const template = ` + + + + +`; + +export function formDropdownDirective() { + return { + restrict: 'E', + template: template, + controller: FormDropdownCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + model: "=", + getOptions: "&", + onChange: "&", + cssClass: "@", + allowCustom: "@", + labelMode: "@", + lookupText: "@", + }, + }; +} + +coreModule.directive('gfFormDropdown', formDropdownDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index ede1e819c1a..3f7867f41fa 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -34,6 +34,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'; @@ -68,6 +69,7 @@ export { queryPartEditorDirective, WizardFlow, colors, + formDropdownDirective, assignModelProperties, contextSrv, KeybindingSrv, 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/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..bc0bcf7c6b2 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 @@
- - + + +
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 8c14f3ef356..a180f97c994 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -5,8 +5,22 @@ Then by - - + + + +
@@ -33,7 +47,13 @@
- + +
@@ -66,11 +86,23 @@
- + +
- + +
@@ -78,7 +110,13 @@
- + +