query_part_editor.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import $ from 'jquery';
  2. import { debounce, each, map, partial, escape, unescape } from 'lodash';
  3. import coreModule from 'app/angular/core_module';
  4. import { promiseToDigest } from '../promiseToDigest';
  5. const template = `
  6. <div class="dropdown cascade-open">
  7. <a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.def.type}}</a>
  8. <span>(</span><span class="query-part-parameters"></span><span>)</span>
  9. <ul class="dropdown-menu">
  10. <li ng-repeat="action in partActions">
  11. <a ng-click="triggerPartAction(action)">{{action.text}}</a>
  12. </li>
  13. </ul>
  14. `;
  15. /** @ngInject */
  16. export function queryPartEditorDirective(templateSrv: any) {
  17. const paramTemplate = '<input type="text" class="hide input-mini tight-form-func-param"></input>';
  18. return {
  19. restrict: 'E',
  20. template: template,
  21. scope: {
  22. part: '=',
  23. handleEvent: '&',
  24. debounce: '@',
  25. },
  26. link: function postLink($scope: any, elem: any) {
  27. const part = $scope.part;
  28. const partDef = part.def;
  29. const $paramsContainer = elem.find('.query-part-parameters');
  30. const debounceLookup = $scope.debounce;
  31. $scope.partActions = [];
  32. function clickFuncParam(this: any, paramIndex: number) {
  33. const $link = $(this);
  34. const $input = $link.next();
  35. $input.val(part.params[paramIndex]);
  36. $input.css('width', $link.width()! + 16 + 'px');
  37. $link.hide();
  38. $input.show();
  39. $input.focus();
  40. $input.select();
  41. const typeahead = $input.data('typeahead');
  42. if (typeahead) {
  43. $input.val('');
  44. typeahead.lookup();
  45. }
  46. }
  47. function inputBlur(this: any, paramIndex: number) {
  48. const $input = $(this);
  49. const $link = $input.prev();
  50. const newValue = $input.val();
  51. if (newValue !== '' || part.def.params[paramIndex].optional) {
  52. $link.html(templateSrv.highlightVariablesAsHtml(newValue));
  53. part.updateParam($input.val(), paramIndex);
  54. $scope.$apply(() => {
  55. $scope.handleEvent({ $event: { name: 'part-param-changed' } });
  56. });
  57. }
  58. $input.hide();
  59. $link.show();
  60. }
  61. function inputKeyPress(this: any, paramIndex: number, e: any) {
  62. if (e.which === 13) {
  63. inputBlur.call(this, paramIndex);
  64. }
  65. }
  66. function inputKeyDown(this: any) {
  67. this.style.width = (3 + this.value.length) * 8 + 'px';
  68. }
  69. function addTypeahead($input: JQuery, param: any, paramIndex: number) {
  70. if (!param.options && !param.dynamicLookup) {
  71. return;
  72. }
  73. const typeaheadSource = (query: string, callback: any) => {
  74. if (param.options) {
  75. let options = param.options;
  76. if (param.type === 'int') {
  77. options = map(options, (val) => {
  78. return val.toString();
  79. });
  80. }
  81. return options;
  82. }
  83. $scope.$apply(() => {
  84. $scope.handleEvent({ $event: { name: 'get-param-options' } }).then((result: any) => {
  85. const dynamicOptions = map(result, (op) => {
  86. return escape(op.value);
  87. });
  88. callback(dynamicOptions);
  89. });
  90. });
  91. };
  92. $input.attr('data-provide', 'typeahead');
  93. $input.typeahead({
  94. source: typeaheadSource,
  95. minLength: 0,
  96. items: 1000,
  97. updater: (value: string) => {
  98. value = unescape(value);
  99. setTimeout(() => {
  100. inputBlur.call($input[0], paramIndex);
  101. }, 0);
  102. return value;
  103. },
  104. });
  105. const typeahead = $input.data('typeahead');
  106. typeahead.lookup = function () {
  107. this.query = this.$element.val() || '';
  108. const items = this.source(this.query, $.proxy(this.process, this));
  109. return items ? this.process(items) : items;
  110. };
  111. if (debounceLookup) {
  112. typeahead.lookup = debounce(typeahead.lookup, 500, { leading: true });
  113. }
  114. }
  115. $scope.showActionsMenu = () => {
  116. promiseToDigest($scope)(
  117. $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => {
  118. $scope.partActions = res;
  119. })
  120. );
  121. };
  122. $scope.triggerPartAction = (action: string) => {
  123. $scope.handleEvent({ $event: { name: 'action', action: action } });
  124. };
  125. function addElementsAndCompile() {
  126. each(partDef.params, (param: any, index: number) => {
  127. if (param.optional && part.params.length <= index) {
  128. return;
  129. }
  130. if (index > 0) {
  131. $('<span>, </span>').appendTo($paramsContainer);
  132. }
  133. const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
  134. const $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
  135. const $input = $(paramTemplate);
  136. $paramLink.appendTo($paramsContainer);
  137. $input.appendTo($paramsContainer);
  138. $input.blur(partial(inputBlur, index));
  139. $input.keyup(inputKeyDown);
  140. $input.keypress(partial(inputKeyPress, index));
  141. $paramLink.click(partial(clickFuncParam, index));
  142. addTypeahead($input, param, index);
  143. });
  144. }
  145. function relink() {
  146. $paramsContainer.empty();
  147. addElementsAndCompile();
  148. }
  149. relink();
  150. },
  151. };
  152. }
  153. coreModule.directive('queryPartEditor', queryPartEditorDirective);