Explorar o código

Merge branch 'metric-segment-remake'

Torkel Ödegaard %!s(int64=8) %!d(string=hai) anos
pai
achega
e9c8881d54

+ 248 - 0
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -0,0 +1,248 @@
+///<reference path="../../../headers/common.d.ts" />
+
+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 =  `
+<input type="text"
+  data-provide="typeahead"
+  class="gf-form-input"
+  spellcheck="false"
+  style="display:none">
+</input>
+<a ng-class="ctrl.cssClasses"
+	 tabindex="1"
+	 ng-click="ctrl.open()"
+	 give-focus="ctrl.focus"
+	 ng-bind-html="ctrl.display">
+</a>
+`;
+
+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);

+ 2 - 0
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,

+ 1 - 1
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;
           }

+ 20 - 31
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;
     }
-  }
 
-  mixedDatasourceChanged() {
-    var target: any = {isNew: true};
-    var ds = _.find(this.datasources, {name: this.mixedDsSegment.value});
+    this.current = option.datasource;
+    this.panelCtrl.setDatasource(option.datasource);
+  }
 
-    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() {

+ 10 - 5
public/app/features/panel/partials/metrics_tab.html

@@ -19,7 +19,10 @@
       </button>
 
       <div class="dropdown" ng-if="ctrl.current.meta.mixed">
-        <metric-segment segment="ctrl.mixedDsSegment" get-options="ctrl.getOptions(false)" on-change="ctrl.mixedDatasourceChanged()"></metric-segment>
+        <gf-form-dropdown model="ctrl.addQueryDropdown"
+                          get-options="ctrl.getOptions(false)"
+                          on-change="ctrl.addMixedQuery($option)">
+        </gf-form-dropdown>
       </div>
     </div>
   </div>
@@ -30,10 +33,12 @@
 <div class="gf-form-group">
   <div class="gf-form-inline">
     <div class="gf-form">
-      <label class="gf-form-label">
-        Panel Data Source
-      </label>
-      <metric-segment segment="ctrl.dsSegment" get-options="ctrl.getOptions(true)" on-change="ctrl.datasourceChanged()"></metric-segment>
+      <label class="gf-form-label">Panel Data Source</label>
+      <gf-form-dropdown model="ctrl.panelDsValue"
+                        lookup-text="true"
+                        get-options="ctrl.getOptions(true)"
+                        on-change="ctrl.datasourceChanged($option)">
+      </gf-form-dropdown>
     </div>
   </div>
 </div>

+ 14 - 7
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() {

+ 44 - 6
public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html

@@ -5,8 +5,22 @@
 			<span ng-hide="isFirst">Then by</span>
 		</label>
 
-		<metric-segment-model property="agg.type" options="bucketAggTypes" on-change="onTypeChanged()" custom="false" css-class="width-10"></metric-segment-model>
-		<metric-segment-model ng-if="agg.field" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
+		<gf-form-dropdown model="agg.type"
+											lookup-text="true"
+											get-options="getBucketAggTypes()"
+											on-change="onTypeChanged()"
+											allow-custom="false"
+											label-mode="true"
+											css-class="width-10">
+		</gf-form-dropdown>
+		<gf-form-dropdown ng-if="agg.field"
+											model="agg.field"
+											get-options="getFieldsInternal()"
+											on-change="onChange()"
+											allow-custom="false"
+											label-mode="true"
+											css-class="width-12">
+		</gf-form-dropdown>
 	</div>
 
 	<div class="gf-form gf-form--grow">
@@ -33,7 +47,13 @@
 	<div ng-if="agg.type === 'date_histogram'">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Interval</label>
-			<metric-segment-model property="agg.settings.interval" get-options="getIntervalOptions()" on-change="onChangeInternal()" css-class="width-12" custom="true"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.interval"
+												get-options="getIntervalOptions()"
+												on-change="onChangeInternal()"
+												allow-custom="true"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 
 		<div class="gf-form offset-width-7">
@@ -66,11 +86,23 @@
 	<div ng-if="agg.type === 'terms'">
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Order</label>
-			<metric-segment-model property="agg.settings.order" options="orderOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.order"
+											  lookup-text="true"
+												get-options="getOrderOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Size</label>
-			<metric-segment-model property="agg.settings.size" options="sizeOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.size"
+											  lookup-text="true"
+												get-options="getSizeOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Min Doc Count</label>
@@ -78,7 +110,13 @@
 		</div>
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">Order By</label>
-			<metric-segment-model property="agg.settings.orderBy" options="orderByOptions" on-change="onChangeInternal()" css-class="width-12"></metric-segment-model>
+			<gf-form-dropdown model="agg.settings.orderBy"
+											  lookup-text="true"
+												get-options="getOrderByOptions()"
+												on-change="onChangeInternal()"
+												label-mode="true"
+												css-class="width-12">
+			</gf-form-dropdown>
 		</div>
 		<div class="gf-form offset-width-7">
 			<label class="gf-form-label width-10">

+ 2 - 2
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');
   }