/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var App = require('app'); var fileUtils = require('utils/file_utils'); App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, { templateName: require('templates/common/widget/graph_widget'), /** * type of metric query from which the widget is comprised */ metricType: 'TEMPORAL', /** * common metrics container * @type {Array} */ metrics: [], /** * 3600 sec in 1 hour * @const */ TIME_FACTOR: 3600, /** * custom time range, set when graph opened in popup * @type {number|null} */ customTimeRange: null, /** * value in seconds * @type {number} */ timeRange: function () { var timeRange = parseInt(this.get('content.properties.time_range')); if (isNaN(timeRange)) { //1h - default time range timeRange = 1; } // Custom start and end time is specified by user if (this.get('exportTargetView.currentTimeIndex') === 8) { return 0; } return this.get('customTimeRange') || timeRange * this.get('TIME_FACTOR'); }.property('content.properties.time_range', 'customTimeRange'), /** * value in ms * @type {number} */ timeStep: 15, /** * @type {Array} */ data: [], /** * time range index for graph * @type {number} */ timeIndex: 0, /** * custom start time for graph * @type {number|null} */ startTime: null, /** * custom end time for graph * @type {number|null} */ endTime: null, /** * graph time range duration in seconds * @type {number|null} */ graphSeconds: null, /** * time range duration as string * @type {string|null} */ durationFormatted: null, exportTargetView: Em.computed.alias('childViews.lastObject'), drawWidget: function () { if (this.get('isLoaded')) { this.set('data', this.calculateValues()); } }, /** * calculate series datasets for graph widgets */ calculateValues: function () { var metrics = this.get('metrics'); var seriesData = []; if (this.get('content.values')) { this.get('content.values').forEach(function (value) { var expression = this.extractExpressions(value)[0]; var computedData; var datasetKey; if (expression) { datasetKey = value.value.match(this.get('EXPRESSION_REGEX'))[0]; computedData = this.computeExpression(expression, metrics)[datasetKey]; //exclude empty datasets if (computedData.length > 0) { seriesData.push({ name: value.name, data: computedData }); } } }, this); } return seriesData; }, /** * compute expression * * @param {string} expression * @param {object} metrics * @returns {object} */ computeExpression: function (expression, metrics) { var validExpression = true, value = [], dataLinks = {}, dataLength = -1, beforeCompute, result = {}, isDataCorrupted = false, isPointNull = false; //replace values with metrics data expression.match(this.get('VALUE_NAME_REGEX')).forEach(function (match) { if (isNaN(match)) { if (metrics.someProperty('name', match)) { dataLinks[match] = metrics.findProperty('name', match).data; if (!isDataCorrupted) { isDataCorrupted = (dataLength !== -1 && dataLength !== dataLinks[match].length); } dataLength = (dataLinks[match].length > dataLength) ? dataLinks[match].length : dataLength; } else { validExpression = false; console.warn('Metrics with name "' + match + '" not found to compute expression'); } } }); if (validExpression) { if (isDataCorrupted) { this.adjustData(dataLinks, dataLength); } for (var i = 0, timestamp; i < dataLength; i++) { isPointNull = false; beforeCompute = expression.replace(this.get('VALUE_NAME_REGEX'), function (match) { if (isNaN(match)) { timestamp = dataLinks[match][i][1]; isPointNull = (isPointNull) ? true : (Em.isNone(dataLinks[match][i][0])); return dataLinks[match][i][0]; } else { return match; } }); var dataLinkPointValue = isPointNull ? null : Number(window.eval(beforeCompute)); // expression resulting into `0/0` will produce NaN Object which is not a valid series data value for RickShaw graphs if (isNaN(dataLinkPointValue)) { dataLinkPointValue = 0; } value.push([dataLinkPointValue, timestamp]); } } result['${' + expression + '}'] = value; return result; }, /** * add missing points, with zero value, to series * * @param {object} dataLinks * @param {number} length */ adjustData: function(dataLinks, length) { //series with full data taken as original var original = []; var substituteValue = null; for (var i in dataLinks) { if (dataLinks[i].length === length) { original = dataLinks[i]; break; } } original.forEach(function(point, index) { for (var i in dataLinks) { if (!dataLinks[i][index] || dataLinks[i][index][1] !== point[1]) { dataLinks[i].splice(index, 0, [substituteValue, point[1]]); } } }, this); }, /** * add time properties * @param {Array} metricPaths * @returns {Array} result */ addTimeProperties: function (metricPaths) { var toSeconds, fromSeconds, step = this.get('timeStep'), timeRange = this.get('timeRange'), result = [], targetView = this.get('exportTargetView.isPopup') ? this.get('exportTargetView') : this.get('parentView'); //if view destroyed then no metrics should be asked if (Em.isNone(targetView)) return result; if (timeRange === 0 && !Em.isNone(targetView.get('customStartTime')) && !Em.isNone(targetView.get('customEndTime'))) { // Custom start/end time is specified by user toSeconds = targetView.get('customEndTime') / 1000; fromSeconds = targetView.get('customStartTime') / 1000; } else { // Preset time range is specified by user toSeconds = Math.round(App.dateTime() / 1000); fromSeconds = toSeconds - timeRange; } metricPaths.forEach(function (metricPath) { result.push(metricPath + '[' + fromSeconds + ',' + toSeconds + ',' + step + ']'); }, this); return result; }, /** * @type {Em.View} * @class */ graphView: App.ChartLinearTimeView.extend({ noTitleUnderGraph: true, inWidget: true, description: Em.computed.alias('parentView.content.description'), isPreview: Em.computed.alias('parentView.isPreview'), displayUnit: Em.computed.alias('parentView.content.properties.display_unit'), setYAxisFormatter: function () { var displayUnit = this.get('displayUnit'); if (displayUnit) { this.set('yAxisFormatter', function (value) { return App.ChartLinearTimeView.DisplayUnitFormatter(value, displayUnit); }); } }.observes('displayUnit'), /** * set custom time range for graph widget */ setTimeRange: function () { if (this.get('isPopup')) { if (this.get('currentTimeIndex') === 8) { // Custom start and end time is specified by user this.get('parentView').propertyDidChange('customTimeRange'); } else { // Preset time range is specified by user this.set('parentView.customTimeRange', this.get('timeUnitSeconds')); } } else { this.set('parentView.customTimeRange', null); } }.observes('isPopup', 'timeUnitSeconds'), /** * graph height * @type {number} */ height: 95, /** * @type {string} */ id: function () { return 'widget_'+ this.get('parentView.content.id') + '_graph'; }.property('parentView.content.id'), /** * @type {string} */ renderer: function () { return this.get('parentView.content.properties.graph_type') === 'STACK' ? 'area' : 'line'; }.property('parentView.content.properties.graph_type'), title: Em.computed.alias('parentView.content.widgetName'), transformToSeries: function (seriesData) { var seriesArray = []; seriesData.forEach(function (_series) { seriesArray.push(this.transformData(_series.data, _series.name)); }, this); return seriesArray; }, loadData: function () { Em.run.next(this, function () { this._refreshGraph(this.get('parentView.data'), this.get('parentView')); }); }, didInsertElement: function () { var self = this; this.$().closest('.graph-widget').on('mouseleave', function () { self.set('parentView.isExportMenuHidden', true); }); this.setYAxisFormatter(); if (!arguments.length || this.get('parentView.data.length')) { this.loadData(); } var self = this; Em.run.next(function () { if (self.get('isPreview')) { App.tooltip(this.$("[rel='ZoomInTooltip']"), 'disable'); } else { App.tooltip(this.$("[rel='ZoomInTooltip']"), { placement: 'left', template: '