graph_widget_view.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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 App = require('app');
  19. var fileUtils = require('utils/file_utils');
  20. App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
  21. templateName: require('templates/common/widget/graph_widget'),
  22. /**
  23. * type of metric query from which the widget is comprised
  24. */
  25. metricType: 'TEMPORAL',
  26. /**
  27. * common metrics container
  28. * @type {Array}
  29. */
  30. metrics: [],
  31. /**
  32. * 3600 sec in 1 hour
  33. * @const
  34. */
  35. TIME_FACTOR: 3600,
  36. /**
  37. * custom time range, set when graph opened in popup
  38. * @type {number|null}
  39. */
  40. customTimeRange: null,
  41. /**
  42. * value in seconds
  43. * @type {number}
  44. */
  45. timeRange: function () {
  46. var timeRange = parseInt(this.get('content.properties.time_range'));
  47. if (isNaN(timeRange)) {
  48. //1h - default time range
  49. timeRange = 1;
  50. }
  51. // Custom start and end time is specified by user
  52. if (this.get('exportTargetView.currentTimeIndex') === 8) {
  53. return 0;
  54. }
  55. return this.get('customTimeRange') || timeRange * this.get('TIME_FACTOR');
  56. }.property('content.properties.time_range', 'customTimeRange'),
  57. /**
  58. * value in ms
  59. * @type {number}
  60. */
  61. timeStep: 15,
  62. /**
  63. * @type {Array}
  64. */
  65. data: [],
  66. /**
  67. * time range index for graph
  68. * @type {number}
  69. */
  70. timeIndex: 0,
  71. /**
  72. * custom start time for graph
  73. * @type {number|null}
  74. */
  75. startTime: null,
  76. /**
  77. * custom end time for graph
  78. * @type {number|null}
  79. */
  80. endTime: null,
  81. /**
  82. * graph time range duration in seconds
  83. * @type {number|null}
  84. */
  85. graphSeconds: null,
  86. /**
  87. * time range duration as string
  88. * @type {string|null}
  89. */
  90. durationFormatted: null,
  91. exportTargetView: Em.computed.alias('childViews.lastObject'),
  92. drawWidget: function () {
  93. if (this.get('isLoaded')) {
  94. this.set('data', this.calculateValues());
  95. }
  96. },
  97. /**
  98. * calculate series datasets for graph widgets
  99. */
  100. calculateValues: function () {
  101. var metrics = this.get('metrics');
  102. var seriesData = [];
  103. if (this.get('content.values')) {
  104. this.get('content.values').forEach(function (value) {
  105. var expression = this.extractExpressions(value)[0];
  106. var computedData;
  107. var datasetKey;
  108. if (expression) {
  109. datasetKey = value.value.match(this.get('EXPRESSION_REGEX'))[0];
  110. computedData = this.computeExpression(expression, metrics)[datasetKey];
  111. //exclude empty datasets
  112. if (computedData.length > 0) {
  113. seriesData.push({
  114. name: value.name,
  115. data: computedData
  116. });
  117. }
  118. }
  119. }, this);
  120. }
  121. return seriesData;
  122. },
  123. /**
  124. * compute expression
  125. *
  126. * @param {string} expression
  127. * @param {object} metrics
  128. * @returns {object}
  129. */
  130. computeExpression: function (expression, metrics) {
  131. var validExpression = true,
  132. value = [],
  133. dataLinks = {},
  134. dataLength = -1,
  135. beforeCompute,
  136. result = {},
  137. isDataCorrupted = false,
  138. isPointNull = false;
  139. //replace values with metrics data
  140. expression.match(this.get('VALUE_NAME_REGEX')).forEach(function (match) {
  141. if (isNaN(match)) {
  142. if (metrics.someProperty('name', match)) {
  143. dataLinks[match] = metrics.findProperty('name', match).data;
  144. if (!isDataCorrupted) {
  145. isDataCorrupted = (dataLength !== -1 && dataLength !== dataLinks[match].length);
  146. }
  147. dataLength = (dataLinks[match].length > dataLength) ? dataLinks[match].length : dataLength;
  148. } else {
  149. validExpression = false;
  150. console.warn('Metrics with name "' + match + '" not found to compute expression');
  151. }
  152. }
  153. });
  154. if (validExpression) {
  155. if (isDataCorrupted) {
  156. this.adjustData(dataLinks, dataLength);
  157. }
  158. for (var i = 0, timestamp; i < dataLength; i++) {
  159. isPointNull = false;
  160. beforeCompute = expression.replace(this.get('VALUE_NAME_REGEX'), function (match) {
  161. if (isNaN(match)) {
  162. timestamp = dataLinks[match][i][1];
  163. isPointNull = (isPointNull) ? true : (Em.isNone(dataLinks[match][i][0]));
  164. return dataLinks[match][i][0];
  165. } else {
  166. return match;
  167. }
  168. });
  169. var dataLinkPointValue = isPointNull ? null : Number(window.eval(beforeCompute));
  170. // expression resulting into `0/0` will produce NaN Object which is not a valid series data value for RickShaw graphs
  171. if (isNaN(dataLinkPointValue)) {
  172. dataLinkPointValue = 0;
  173. }
  174. value.push([dataLinkPointValue, timestamp]);
  175. }
  176. }
  177. result['${' + expression + '}'] = value;
  178. return result;
  179. },
  180. /**
  181. * add missing points, with zero value, to series
  182. *
  183. * @param {object} dataLinks
  184. * @param {number} length
  185. */
  186. adjustData: function(dataLinks, length) {
  187. //series with full data taken as original
  188. var original = [];
  189. var substituteValue = null;
  190. for (var i in dataLinks) {
  191. if (dataLinks[i].length === length) {
  192. original = dataLinks[i];
  193. break;
  194. }
  195. }
  196. original.forEach(function(point, index) {
  197. for (var i in dataLinks) {
  198. if (!dataLinks[i][index] || dataLinks[i][index][1] !== point[1]) {
  199. dataLinks[i].splice(index, 0, [substituteValue, point[1]]);
  200. }
  201. }
  202. }, this);
  203. },
  204. /**
  205. * add time properties
  206. * @param {Array} metricPaths
  207. * @returns {Array} result
  208. */
  209. addTimeProperties: function (metricPaths) {
  210. var toSeconds,
  211. fromSeconds,
  212. step = this.get('timeStep'),
  213. timeRange = this.get('timeRange'),
  214. result = [],
  215. targetView = this.get('exportTargetView.isPopup') ? this.get('exportTargetView') : this.get('parentView');
  216. //if view destroyed then no metrics should be asked
  217. if (Em.isNone(targetView)) return result;
  218. if (timeRange === 0 &&
  219. !Em.isNone(targetView.get('customStartTime')) &&
  220. !Em.isNone(targetView.get('customEndTime'))) {
  221. // Custom start/end time is specified by user
  222. toSeconds = targetView.get('customEndTime') / 1000;
  223. fromSeconds = targetView.get('customStartTime') / 1000;
  224. } else {
  225. // Preset time range is specified by user
  226. toSeconds = Math.round(App.dateTime() / 1000);
  227. fromSeconds = toSeconds - timeRange;
  228. }
  229. metricPaths.forEach(function (metricPath) {
  230. result.push(metricPath + '[' + fromSeconds + ',' + toSeconds + ',' + step + ']');
  231. }, this);
  232. return result;
  233. },
  234. /**
  235. * @type {Em.View}
  236. * @class
  237. */
  238. graphView: App.ChartLinearTimeView.extend({
  239. noTitleUnderGraph: true,
  240. inWidget: true,
  241. description: Em.computed.alias('parentView.content.description'),
  242. isPreview: Em.computed.alias('parentView.isPreview'),
  243. displayUnit: Em.computed.alias('parentView.content.properties.display_unit'),
  244. setYAxisFormatter: function () {
  245. var displayUnit = this.get('displayUnit');
  246. if (displayUnit) {
  247. this.set('yAxisFormatter', function (value) {
  248. return App.ChartLinearTimeView.DisplayUnitFormatter(value, displayUnit);
  249. });
  250. }
  251. }.observes('displayUnit'),
  252. /**
  253. * set custom time range for graph widget
  254. */
  255. setTimeRange: function () {
  256. if (this.get('isPopup')) {
  257. if (this.get('currentTimeIndex') === 8) {
  258. // Custom start and end time is specified by user
  259. this.get('parentView').propertyDidChange('customTimeRange');
  260. } else {
  261. // Preset time range is specified by user
  262. this.set('parentView.customTimeRange', this.get('timeUnitSeconds'));
  263. }
  264. } else {
  265. this.set('parentView.customTimeRange', null);
  266. }
  267. }.observes('isPopup', 'timeUnitSeconds'),
  268. /**
  269. * graph height
  270. * @type {number}
  271. */
  272. height: 95,
  273. /**
  274. * @type {string}
  275. */
  276. id: function () {
  277. return 'widget_'+ this.get('parentView.content.id') + '_graph';
  278. }.property('parentView.content.id'),
  279. /**
  280. * @type {string}
  281. */
  282. renderer: function () {
  283. return this.get('parentView.content.properties.graph_type') === 'STACK' ? 'area' : 'line';
  284. }.property('parentView.content.properties.graph_type'),
  285. title: Em.computed.alias('parentView.content.widgetName'),
  286. transformToSeries: function (seriesData) {
  287. var seriesArray = [];
  288. seriesData.forEach(function (_series) {
  289. seriesArray.push(this.transformData(_series.data, _series.name));
  290. }, this);
  291. return seriesArray;
  292. },
  293. loadData: function () {
  294. Em.run.next(this, function () {
  295. this._refreshGraph(this.get('parentView.data'), this.get('parentView'));
  296. });
  297. },
  298. didInsertElement: function () {
  299. var self = this;
  300. this.$().closest('.graph-widget').on('mouseleave', function () {
  301. self.set('parentView.isExportMenuHidden', true);
  302. });
  303. this.setYAxisFormatter();
  304. if (!arguments.length || this.get('parentView.data.length')) {
  305. this.loadData();
  306. }
  307. var self = this;
  308. Em.run.next(function () {
  309. if (self.get('isPreview')) {
  310. App.tooltip(this.$("[rel='ZoomInTooltip']"), 'disable');
  311. } else {
  312. App.tooltip(this.$("[rel='ZoomInTooltip']"), {
  313. placement: 'left',
  314. template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner graph-tooltip"></div></div>'
  315. });
  316. }
  317. });
  318. }.observes('parentView.data')
  319. }),
  320. exportGraphData: function (event) {
  321. this.set('isExportMenuHidden', true);
  322. var data,
  323. isCSV = !!event.context,
  324. fileType = isCSV ? 'csv' : 'json',
  325. fileName = 'data.' + fileType,
  326. metrics = this.get('data'),
  327. hasData = Em.isArray(metrics) && metrics.some(function (item) {
  328. return Em.isArray(item.data);
  329. });
  330. if (hasData) {
  331. data = isCSV ? this.prepareCSV(metrics) : JSON.stringify(metrics, this.jsonReplacer(), 4);
  332. fileUtils.downloadTextFile(data, fileType, fileName);
  333. } else {
  334. App.showAlertPopup(Em.I18n.t('graphs.noData.title'), Em.I18n.t('graphs.noData.tooltip.title'));
  335. }
  336. }
  337. });