expression_view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. var misc = require('utils/misc');
  19. App.WidgetWizardExpressionView = Em.View.extend({
  20. templateName: require('templates/main/service/widgets/create/expression'),
  21. /**
  22. * @type {Array}
  23. */
  24. classNames: ['metric-container'],
  25. /**
  26. * @type {Array}
  27. */
  28. classNameBindings: ['isInvalid'],
  29. /**
  30. * list of operators that can be used in expression
  31. * @type {Array}
  32. * @constant
  33. */
  34. OPERATORS: ["+", "-", "*", "/", "(", ")"],
  35. /**
  36. * contains expression data before editing in order to restore previous state
  37. */
  38. dataBefore: [],
  39. /**
  40. * @type {Ember.Object}
  41. */
  42. expression: null,
  43. /**
  44. * @type {boolean}
  45. */
  46. isInvalid: false,
  47. /**
  48. * add operator to expression data
  49. * @param event
  50. */
  51. addOperator: function (event) {
  52. var data = this.get('expression.data');
  53. var lastId = (data.length > 0) ? Math.max.apply(this, data.mapProperty('id')) : 0;
  54. data.pushObject(Em.Object.create({
  55. id: ++lastId,
  56. name: event.context,
  57. isOperator: true
  58. }));
  59. },
  60. /**
  61. * redraw expression
  62. * NOTE: needed in order to avoid collision between scrollable lib and metric action event
  63. */
  64. redrawField: function () {
  65. this.set('expression.data', misc.sortByOrder($(this.get('element')).find('.metric-instance').map(function () {
  66. return this.id;
  67. }), this.get('expression.data')));
  68. },
  69. /**
  70. * enable metric edit area
  71. */
  72. didInsertElement: function () {
  73. var self = this;
  74. this.propertyDidChange('expression');
  75. Em.run.next(function () {
  76. $(self.get('element')).find('.metric-field').sortable({
  77. items: "> div",
  78. tolerance: "pointer",
  79. scroll: false,
  80. update: function () {
  81. self.redrawField();
  82. }
  83. }).disableSelection();
  84. });
  85. },
  86. /**
  87. * discard changes and disable metric edit area
  88. */
  89. cancelEdit: function () {
  90. this.set('expression.data', []);
  91. this.propertyDidChange('expression');
  92. },
  93. /**
  94. * remove metric or operator from expression
  95. * @param {object} event
  96. */
  97. removeElement: function (event) {
  98. this.get('expression.data').removeObject(event.context);
  99. },
  100. validate: function () {
  101. //number 1 used as substitute to test expression to be mathematically correct
  102. var testNumber = 1;
  103. var isInvalid = true;
  104. var expression = this.get('expression.data').map(function (element) {
  105. if (element.isMetric) {
  106. return testNumber;
  107. } else {
  108. return element.name;
  109. }
  110. }, this).join(" ");
  111. if (expression.length > 0) {
  112. if (/^((\(\s)*[\d]+)[\(\)\+\-\*\/\d\s]*[\d\)]*$/.test(expression)) {
  113. try {
  114. isInvalid = !isFinite(window.eval(expression));
  115. } catch (e) {
  116. isInvalid = true;
  117. }
  118. }
  119. } else {
  120. isInvalid = false;
  121. }
  122. this.set('isInvalid', isInvalid);
  123. this.set('expression.isInvalid', isInvalid);
  124. this.set('expression.isEmpty', this.get('expression.data.length') == 0);
  125. if (!isInvalid) this.get('controller').updateExpressions();
  126. }.observes('expression.data.length'),
  127. /**
  128. * show popup that provide ability to add metric
  129. */
  130. addMetric: function () {
  131. return App.ModalPopup.show({
  132. header: Em.I18n.t('dashboard.widgets.wizard.step2.addMetric'),
  133. classNames: ['modal-690px-width', 'add-metric-modal'],
  134. disablePrimary: function () {
  135. return Em.isNone(this.get('selectedMetric'));
  136. }.property('selectedMetric'),
  137. isHideBodyScroll: true,
  138. expression: this.get('expression'),
  139. /**
  140. * @type {Array}
  141. * @const
  142. */
  143. AGGREGATE_FUNCTIONS: ['avg', 'sum', 'min', 'max'],
  144. /**
  145. * @type {object|null}
  146. */
  147. selectedMetric: null,
  148. /**
  149. * @type {string|null}
  150. */
  151. aggregateFunction: null,
  152. /**
  153. * @type {Ember.View}
  154. * @class
  155. */
  156. bodyClass: Em.View.extend({
  157. templateName: require('templates/main/service/widgets/create/step2_add_metric'),
  158. controller: this.get('controller'),
  159. elementId: 'add-metric-popup',
  160. didInsertElement: function () {
  161. var self = this;
  162. //prevent dropdown closing on checkbox click
  163. $('html').on('click.dropdown', '.dropdown-menu li', function (e) {
  164. $(this).hasClass('keep-open') && e.stopPropagation();
  165. });
  166. App.tooltip($('#' + this.get('elementId') + ' .aggregator-select'));
  167. this.propertyDidChange('selectedComponent');
  168. $(".chosen-select").chosen({
  169. placeholder_text: Em.I18n.t('dashboard.widgets.wizard.step2.selectMetric'),
  170. no_results_text: Em.I18n.t('widget.create.wizard.step2.noMetricFound')
  171. }).change(function (event, obj) {
  172. self.set('parentView.selectedMetric', Em.Object.create({
  173. name: obj.selected,
  174. componentName: self.get('selectedComponent.componentName'),
  175. serviceName: self.get('selectedComponent.serviceName'),
  176. metricPath: self.get('controller.filteredMetrics').findProperty('name', obj.selected).widget_id,
  177. isMetric: true
  178. }));
  179. });
  180. },
  181. /**
  182. * @type {Ember.Object}
  183. * @default null
  184. */
  185. selectedComponent: null,
  186. /**
  187. * @type {boolean}
  188. */
  189. showAggregateSelect: function () {
  190. return Boolean(this.get('selectedComponent') && this.get('selectedComponent.level') === 'COMPONENT');
  191. }.property('selectedComponent.level'),
  192. /**
  193. * select component
  194. * @param {object} event
  195. */
  196. selectComponents: function (event) {
  197. var component = this.get('componentMap').findProperty('serviceName', event.context.get('serviceName'))
  198. .get('components').findProperty('id', event.context.get('id'));
  199. $('#add-metric-popup .component-select').removeClass('open');
  200. this.set('selectedComponent', component);
  201. if (this.get('showAggregateSelect')) {
  202. this.set('parentView.aggregateFunction', this.get('parentView.AGGREGATE_FUNCTIONS').objectAt(0));
  203. } else {
  204. this.set('parentView.aggregateFunction', null);
  205. }
  206. this.set('parentView.selectedMetric', null);
  207. Em.run.next(function () {
  208. $('.chosen-select').trigger('chosen:updated');
  209. });
  210. },
  211. /**
  212. * select aggregation function
  213. * @param {object} event
  214. */
  215. selectAggregation: function(event) {
  216. this.set('parentView.aggregateFunction', event.context);
  217. },
  218. /**
  219. * map of components
  220. * has following hierarchy: service -> component -> metrics
  221. */
  222. componentMap: function () {
  223. var servicesMap = {};
  224. var result = [];
  225. var components = [];
  226. var masterNames = App.StackServiceComponent.find().filterProperty('isMaster').mapProperty('componentName');
  227. this.get('controller.filteredMetrics').forEach(function (metric) {
  228. var service = servicesMap[metric.service_name];
  229. var componentId = masterNames.contains(metric.component_name) ? metric.component_name + '_' + metric.level : metric.component_name;
  230. if (service) {
  231. service.count++;
  232. if (service.components[componentId]) {
  233. service.components[componentId].count++;
  234. service.components[componentId].metrics.push(metric.name);
  235. } else {
  236. service.components[componentId] = {
  237. component_name: metric.component_name,
  238. level: metric.level,
  239. count: 1,
  240. metrics: [metric.name]
  241. };
  242. }
  243. } else {
  244. servicesMap[metric.service_name] = {
  245. count: 1,
  246. components: {}
  247. };
  248. }
  249. }, this);
  250. for (var serviceName in servicesMap) {
  251. components = [];
  252. for (var componentId in servicesMap[serviceName].components) {
  253. components.push(Em.Object.create({
  254. componentName: servicesMap[serviceName].components[componentId].component_name,
  255. level: servicesMap[serviceName].components[componentId].level,
  256. displayName: function() {
  257. var stackComponent = App.StackServiceComponent.find(this.get('componentName'));
  258. if (stackComponent.get('isMaster')) {
  259. if (this.get('level') === 'COMPONENT') {
  260. return Em.I18n.t('widget.create.wizard.step2.allComponents').format(stackComponent.get('displayName'));
  261. } else {
  262. return Em.I18n.t('widget.create.wizard.step2.activeComponents').format(stackComponent.get('displayName'));
  263. }
  264. }
  265. return stackComponent.get('displayName');
  266. }.property('componentName', 'level'),
  267. count: servicesMap[serviceName].components[componentId].count,
  268. metrics: servicesMap[serviceName].components[componentId].metrics.uniq().sort(),
  269. selected: false,
  270. id: componentId,
  271. serviceName: serviceName
  272. }));
  273. }
  274. result.push(Em.Object.create({
  275. serviceName: serviceName,
  276. //in order to support accordion lists
  277. href: '#' + serviceName,
  278. displayName: App.StackService.find(serviceName).get('displayName'),
  279. count: servicesMap[serviceName].count,
  280. components: components
  281. }));
  282. }
  283. return result;
  284. }.property('controller.filteredMetrics')
  285. }),
  286. primary: Em.I18n.t('common.add'),
  287. onPrimary: function () {
  288. var data = this.get('expression.data'),
  289. id = (data.length > 0) ? Math.max.apply(this, data.mapProperty('id')) + 1 : 1,
  290. selectedMetric = this.get('selectedMetric'),
  291. aggregateFunction = this.get('aggregateFunction');
  292. selectedMetric.set('id', id);
  293. if (aggregateFunction && aggregateFunction !== 'avg') {
  294. selectedMetric.set('metricPath', selectedMetric.get('metricPath') + '._' + aggregateFunction);
  295. selectedMetric.set('name', selectedMetric.get('name') + '._' + aggregateFunction);
  296. }
  297. data.pushObject(selectedMetric);
  298. this.hide();
  299. }
  300. })
  301. }
  302. });