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
-
-
+
+
+
+