expression_view.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  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. var number_utils = require("utils/number_utils");
  20. App.WidgetWizardExpressionView = Em.View.extend({
  21. templateName: require('templates/main/service/widgets/create/expression'),
  22. /**
  23. * @type {Array}
  24. */
  25. classNames: ['metric-container'],
  26. /**
  27. * @type {Array}
  28. */
  29. classNameBindings: ['isInvalid'],
  30. /**
  31. * list of operators that can be used in expression
  32. * @type {Array}
  33. * @constant
  34. */
  35. OPERATORS: ["+", "-", "*", "/", "(", ")"],
  36. /**
  37. * @type {Array}
  38. * @const
  39. */
  40. AGGREGATE_FUNCTIONS: ['avg', 'sum', 'min', 'max'],
  41. /**
  42. * @type {RegExp}
  43. * @const
  44. */
  45. VALID_EXPRESSION_REGEX: /^((\(\s)*[\d]+)[\(\)\+\-\*\/\.\d\s]*[\d\)]*$/,
  46. /**
  47. * contains expression data before editing in order to restore previous state
  48. */
  49. dataBefore: [],
  50. /**
  51. * @type {Ember.Object}
  52. */
  53. expression: null,
  54. /**
  55. * @type {boolean}
  56. */
  57. isInvalid: false,
  58. /**
  59. * contains value of number added to expression
  60. * @type {string}
  61. */
  62. numberValue: "",
  63. /**
  64. * @type {boolean}
  65. */
  66. isNumberValueInvalid: function () {
  67. return this.get('numberValue').trim() === "" || !number_utils.isPositiveNumber(this.get('numberValue').trim());
  68. }.property('numberValue'),
  69. /**
  70. * add operator to expression data
  71. * @param event
  72. */
  73. addOperator: function (event) {
  74. var data = this.get('expression.data');
  75. var lastId = (data.length > 0) ? Math.max.apply(this, data.mapProperty('id')) : 0;
  76. data.pushObject(Em.Object.create({
  77. id: ++lastId,
  78. name: event.context,
  79. isOperator: true
  80. }));
  81. },
  82. /**
  83. * add operator to expression data
  84. * @param event
  85. */
  86. addNumber: function (event) {
  87. var data = this.get('expression.data');
  88. var lastId = (data.length > 0) ? Math.max.apply(this, data.mapProperty('id')) : 0;
  89. data.pushObject(Em.Object.create({
  90. id: ++lastId,
  91. name: this.get('numberValue'),
  92. isNumber: true
  93. }));
  94. this.set('numberValue', "");
  95. },
  96. /**
  97. * redraw expression
  98. * NOTE: needed in order to avoid collision between scrollable lib and metric action event
  99. */
  100. redrawField: function () {
  101. this.set('expression.data', misc.sortByOrder($(this.get('element')).find('.metric-instance').map(function () {
  102. return this.id;
  103. }), this.get('expression.data')));
  104. },
  105. /**
  106. * enable metric edit area
  107. */
  108. didInsertElement: function () {
  109. var self = this;
  110. this.propertyDidChange('expression');
  111. Em.run.next(function () {
  112. $(self.get('element')).find('.metric-field').sortable({
  113. items: "> .metric-instance",
  114. tolerance: "pointer",
  115. scroll: false,
  116. update: function () {
  117. self.redrawField();
  118. }
  119. }).disableSelection();
  120. });
  121. },
  122. /**
  123. * remove metric or operator from expression
  124. * @param {object} event
  125. */
  126. removeElement: function (event) {
  127. this.get('expression.data').removeObject(event.context);
  128. },
  129. validate: function () {
  130. //number 1 used as substitute to test expression to be mathematically correct
  131. var testNumber = 1;
  132. var isInvalid = true;
  133. var expression = this.get('expression.data').map(function (element) {
  134. if (element.isMetric) {
  135. return testNumber;
  136. } else {
  137. return element.name;
  138. }
  139. }, this).join(" ");
  140. if (expression.length > 0) {
  141. if (this.get('VALID_EXPRESSION_REGEX').test(expression)) {
  142. try {
  143. isInvalid = !isFinite(window.eval(expression));
  144. } catch (e) {
  145. isInvalid = true;
  146. }
  147. }
  148. } else {
  149. isInvalid = false;
  150. }
  151. this.set('isInvalid', isInvalid);
  152. this.set('expression.isInvalid', isInvalid);
  153. this.get('controller').propertyDidChange('isSubmitDisabled');
  154. if (!isInvalid) {
  155. this.get('controller').updateExpressions();
  156. }
  157. }.observes('expression.data.length')
  158. });
  159. /**
  160. * input used to add number to expression
  161. * @type {Em.TextField}
  162. * @class
  163. */
  164. App.AddNumberExpressionView = Em.TextField.extend({
  165. classNameBindings: ['isInvalid'],
  166. /**
  167. * @type {boolean}
  168. */
  169. isInvalid: function () {
  170. return this.get('value').trim().length > 0 && !number_utils.isPositiveNumber(this.get('value').trim());
  171. }.property('value')
  172. });
  173. /**
  174. * show menu view that provide ability to add metric
  175. */
  176. App.AddMetricExpressionView = Em.View.extend({
  177. templateName: require('templates/main/service/widgets/create/step2_add_metric'),
  178. controller: function () {
  179. return this.get('parentView.controller');
  180. }.property('parentView.controller'),
  181. elementId: 'add-metric-menu',
  182. didInsertElement: function () {
  183. //prevent dropdown closing on click select
  184. $('html').on('click.dropdown', '.dropdown-menu li', function (e) {
  185. $(this).hasClass('keep-open') && e.stopPropagation();
  186. });
  187. },
  188. metricsSelectionObj: function () {
  189. var self = this;
  190. return Em.Object.create({
  191. placeholder_text: Em.I18n.t('dashboard.widgets.wizard.step2.selectMetric'),
  192. no_results_text: Em.I18n.t('widget.create.wizard.step2.noMetricFound'),
  193. onChangeCallback: function (event, obj) {
  194. var filteredComponentMetrics = self.get('controller.filteredMetrics').filterProperty('component_name', self.get('currentSelectedComponent.componentName')).filterProperty('level', self.get('currentSelectedComponent.level'));
  195. var filteredMetric = filteredComponentMetrics.findProperty('name', obj.selected);
  196. var selectedMetric = Em.Object.create({
  197. name: obj.selected,
  198. componentName: self.get('currentSelectedComponent.componentName'),
  199. serviceName: self.get('currentSelectedComponent.serviceName'),
  200. metricPath: filteredMetric.widget_id,
  201. isMetric: true
  202. });
  203. if (self.get('currentSelectedComponent.hostComponentCriteria')) {
  204. selectedMetric.hostComponentCriteria = self.get('currentSelectedComponent.hostComponentCriteria');
  205. }
  206. self.set('currentSelectedComponent.selectedMetric', selectedMetric);
  207. }
  208. })
  209. }.property(),
  210. aggregateFnSelectionObj: function () {
  211. var self = this;
  212. return Em.Object.create({
  213. placeholder_text: Em.I18n.t('dashboard.widgets.wizard.step2.aggregateFunction.scanOps'),
  214. onChangeCallback: function (event, obj) {
  215. self.set('currentSelectedComponent.selectedAggregation', obj.selected);
  216. }
  217. })
  218. }.property(),
  219. /**
  220. * @type {Ember.Object}
  221. * @default null
  222. */
  223. currentSelectedComponent: null,
  224. /**
  225. * select component
  226. * @param {object} event
  227. */
  228. selectComponents: function (event) {
  229. var component = this.get('componentMap').findProperty('serviceName', event.context.get('serviceName'))
  230. .get('components').findProperty('id', event.context.get('id'));
  231. this.set('currentSelectedComponent', component);
  232. },
  233. /**
  234. * add current metrics and aggregation to expression
  235. * @param event
  236. */
  237. addMetric: function (event) {
  238. var selectedMetric = event.context.get('selectedMetric'),
  239. aggregateFunction = event.context.get('selectedAggregation'),
  240. result = Em.Object.create(selectedMetric);
  241. if (event.context.get('isAddEnabled')) {
  242. var data = this.get('parentView').get('expression.data'),
  243. id = (data.length > 0) ? Math.max.apply(this.get('parentView'), data.mapProperty('id')) + 1 : 1;
  244. result.set('id', id);
  245. if (event.context.get('showAggregateSelect')) {
  246. result.set('metricPath', result.get('metricPath') + '._' + aggregateFunction);
  247. result.set('name', result.get('name') + '._' + aggregateFunction);
  248. }
  249. data.pushObject(result);
  250. this.cancel();
  251. }
  252. },
  253. /**
  254. * cancel adding metric, close add metric menu
  255. */
  256. cancel: function () {
  257. $(".service-level-dropdown").parent().removeClass('open');
  258. },
  259. /**
  260. * map of components
  261. * has following hierarchy: service -> component -> metrics
  262. */
  263. componentMap: function () {
  264. var servicesMap = {};
  265. var result = [];
  266. var components = [];
  267. var masterNames = App.StackServiceComponent.find().filterProperty('isMaster').mapProperty('componentName');
  268. var parentView = this.get('parentView');
  269. if (this.get('controller.filteredMetrics')) {
  270. this.get('controller.filteredMetrics').forEach(function (metric) {
  271. var service = servicesMap[metric.service_name];
  272. var componentId = masterNames.contains(metric.component_name) ? metric.component_name + '_' + metric.level : metric.component_name;
  273. if (service) {
  274. service.count++;
  275. if (service.components[componentId]) {
  276. service.components[componentId].count++;
  277. service.components[componentId].metrics.push(metric.name);
  278. } else {
  279. service.components[componentId] = {
  280. component_name: metric.component_name,
  281. level: metric.level,
  282. count: 1,
  283. hostComponentCriteria: metric.host_component_criteria,
  284. metrics: [metric.name]
  285. };
  286. }
  287. } else {
  288. servicesMap[metric.service_name] = {
  289. count: 1,
  290. components: {}
  291. };
  292. }
  293. }, this);
  294. }
  295. for (var serviceName in servicesMap) {
  296. components = [];
  297. for (var componentId in servicesMap[serviceName].components) {
  298. //HBase service should not show "Active HBase master"
  299. if (servicesMap[serviceName].components[componentId].component_name === 'HBASE_MASTER' &&
  300. servicesMap[serviceName].components[componentId].level === 'HOSTCOMPONENT') continue;
  301. var component = Em.Object.create({
  302. componentName: servicesMap[serviceName].components[componentId].component_name,
  303. level: servicesMap[serviceName].components[componentId].level,
  304. displayName: function() {
  305. var stackComponent = App.StackServiceComponent.find(this.get('componentName'));
  306. if (stackComponent.get('isMaster')) {
  307. if (this.get('level') === 'HOSTCOMPONENT') {
  308. return Em.I18n.t('widget.create.wizard.step2.activeComponents').format(stackComponent.get('displayName'));
  309. }
  310. }
  311. return Em.I18n.t('widget.create.wizard.step2.allComponents').format(stackComponent.get('displayName'));
  312. }.property('componentName', 'level'),
  313. count: servicesMap[serviceName].components[componentId].count,
  314. metrics: servicesMap[serviceName].components[componentId].metrics.uniq().sort(),
  315. selected: false,
  316. id: componentId,
  317. aggregatorId: componentId + '_aggregator',
  318. serviceName: serviceName,
  319. showAggregateSelect: function () {
  320. return this.get('level') === 'COMPONENT';
  321. }.property('level'),
  322. selectedMetric: null,
  323. selectedAggregation: Em.I18n.t('dashboard.widgets.wizard.step2.aggregateFunction.scanOps'),
  324. isAddEnabled: function () {
  325. var selectedMetric = this.get('selectedMetric'),
  326. aggregateFunction = this.get('selectedAggregation');
  327. if (this.get('showAggregateSelect')) {
  328. return (!!selectedMetric && !!aggregateFunction &&
  329. aggregateFunction != Em.I18n.t('dashboard.widgets.wizard.step2.aggregateFunction.scanOps'));
  330. } else {
  331. return (!!selectedMetric);
  332. }
  333. }.property('selectedMetric', 'selectedAggregation')
  334. });
  335. if (component.get('level') === 'HOSTCOMPONENT') {
  336. component.set('hostComponentCriteria', servicesMap[serviceName].components[componentId].hostComponentCriteria);
  337. }
  338. components.push(component);
  339. }
  340. result.push(Em.Object.create({
  341. serviceName: serviceName,
  342. //in order to support accordion lists
  343. href: '#' + serviceName,
  344. displayName: App.StackService.find(serviceName).get('displayName'),
  345. count: servicesMap[serviceName].count,
  346. components: components
  347. }));
  348. }
  349. return result;
  350. }.property('controller.filteredMetrics')
  351. });
  352. App.InputCursorTextfieldView = Ember.TextField.extend({
  353. placeholder: "",
  354. classNameBindings: ['isInvalid'],
  355. isInvalid: false,
  356. didInsertElement: function () {
  357. this.focusCursor();
  358. },
  359. focusCursor: function () {
  360. var self = this;
  361. Em.run.next( function() {
  362. if (self.$()) {
  363. self.$().focus();
  364. }
  365. });
  366. }.observes('parentView.expression.data.length'),
  367. focusOut: function(evt) {
  368. this.saveNumber();
  369. },
  370. validateInput: function () {
  371. var value = this.get('value');
  372. var parentView = this.get('parentView');
  373. var isInvalid = false;
  374. if (!number_utils.isPositiveNumber(value)) {
  375. if (value && parentView.get('OPERATORS').contains(value)) {
  376. // add operator
  377. var data = parentView.get('expression.data');
  378. var lastId = (data.length > 0) ? Math.max.apply(parentView, data.mapProperty('id')) : 0;
  379. data.pushObject(Em.Object.create({
  380. id: ++lastId,
  381. name: value,
  382. isOperator: true
  383. }));
  384. this.set('value', '');
  385. } else if (value && value == 'm') {
  386. // open add metric menu
  387. $('#add-metric-menu > div > a').click();
  388. this.set('value', '');
  389. } else if (value) {
  390. // invalid operator
  391. isInvalid = true;
  392. }
  393. }
  394. this.set('isInvalid', isInvalid);
  395. }.observes('value'),
  396. keyDown: function (event) {
  397. if ((event.keyCode == 8 || event.which == 8) && !this.get('value')) { // backspace
  398. var data = this.get('parentView.expression.data');
  399. if (data.length >= 1) {
  400. data.removeObject(data[data.length - 1]);
  401. }
  402. } else if (event.keyCode == 13) { //Enter
  403. this.saveNumber();
  404. }
  405. },
  406. saveNumber: function() {
  407. var number_utils = require("utils/number_utils");
  408. var value = this.get('value');
  409. if (number_utils.isPositiveNumber(value)) {
  410. var parentView = this.get('parentView');
  411. var data = parentView.get('expression.data');
  412. var lastId = (data.length > 0) ? Math.max.apply(this, data.mapProperty('id')) : 0;
  413. data.pushObject(Em.Object.create({
  414. id: ++lastId,
  415. name: this.get('value'),
  416. isNumber: true
  417. }));
  418. this.set('numberValue', "");
  419. this.set('isInvalid', false);
  420. this.set('value', '');
  421. }
  422. }
  423. });