blob: 88b8b786df8af5227c81fbff8a51b85cf105170f [file] [log] [blame]
/**
* 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 stringUtils = require('utils/string_utils');
App.WidgetMixin = Ember.Mixin.create({
/**
* type of metric query from which the widget is comprised
*/
metricType: 'POINT_IN_TIME',
/**
* @type {RegExp}
* @const
*/
EXPRESSION_REGEX: /\$\{([\w\s\.\,\+\-\*\/\(\)\:\=\[\]]*)\}/g,
/**
* @type {RegExp}
* @const
*/
MATH_EXPRESSION_REGEX: /^[\d\s\+\-\*\/\(\)\.]+$/,
/**
* @type {RegExp}
* @const
*/
VALUE_NAME_REGEX: /(\w+\s+\w+)?[\w\.\,\-\:\=\[\]]+/g,
/**
* @type {string}
* @const
*/
CLONE_SUFFIX: '(Copy)',
/**
* @type {number|null}
*/
timeoutId: null,
/**
* common metrics container
* @type {Array}
*/
metrics: [],
/**
*
*/
aggregatorFunc: ['._sum', '._avg', '._min', '._max', '._rate'],
/**
* @type {boolean}
*/
isLoaded: false,
/**
* @type {App.Widget}
* @default null
*/
content: null,
/**
* color of content calculated by thresholds
* @type {string}
*/
contentColor: Em.computed.ifThenElse('value', 'green', 'grey'),
beforeRender: function () {
this.get('metrics').clear();
this.loadMetrics();
},
/**
* load metrics
*/
loadMetrics: function () {
var requestData = this.getRequestData(this.get('content.metrics')),
request,
requestCounter = 0,
self = this;
for (var i in requestData) {
request = requestData[i];
requestCounter++;
if (this.get('content.widgetType') === 'HEATMAP') {
if (request.service_name === 'STACK') {
this.getHostsMetrics(request).complete(function () {
requestCounter--;
if (requestCounter === 0) self.onMetricsLoaded();
});
} else {
this.getHostComponentsMetrics(request).complete(function () {
requestCounter--;
if (requestCounter === 0) self.onMetricsLoaded();
});
}
} else if (request.host_component_criteria) {
App.WidgetLoadAggregator.add({
data: request,
context: this,
startCallName: 'getHostComponentMetrics',
successCallback: this.getHostComponentMetricsSuccessCallback,
errorCallback: this.getMetricsErrorCallback,
completeCallback: function (xhr) {
requestCounter--;
if (requestCounter === 0) this.onMetricsLoaded();
if (this.get('graphView')) {
var graph = this.get('childViews') && this.get('childViews').findProperty('runningRequests');
if (graph) {
var requestsArrayName = graph.get('isPopup') ? 'runningPopupRequests' : 'runningRequests';
graph.set(requestsArrayName, graph.get(requestsArrayName).reject(function (item) {
return item === xhr;
}));
}
}
}
});
} else {
App.WidgetLoadAggregator.add({
data: request,
context: this,
startCallName: 'getServiceComponentMetrics',
successCallback: this.getMetricsSuccessCallback,
errorCallback: this.getMetricsErrorCallback,
completeCallback: function (xhr) {
requestCounter--;
if (requestCounter === 0) this.onMetricsLoaded();
if (this.get('graphView')) {
var graph = this.get('childViews') && this.get('childViews').findProperty('runningRequests');
if (graph) {
var requestsArrayName = graph.get('isPopup') ? 'runningPopupRequests' : 'runningRequests';
graph.set(requestsArrayName, graph.get(requestsArrayName).reject(function (item) {
return item === xhr;
}));
}
}
}
});
}
}
}.observes('customTimeRange', 'content.properties.time_range'),
/**
* get data formatted for request
* @param {Array} metrics
*/
getRequestData: function (metrics) {
var requestsData = {};
if (metrics) {
metrics.forEach(function (metric, index) {
var key;
if (metric.host_component_criteria) {
key = metric.service_name + '_' + metric.component_name + '_' + metric.host_component_criteria;
} else {
key = metric.service_name + '_' + metric.component_name;
}
var requestMetric = $.extend({}, metric);
if (requestsData[key]) {
requestsData[key]["metric_paths"].push({
metric_path: requestMetric["metric_path"],
metric_type: this.get('metricType'),
id: requestMetric["metric_path"] + "_" + this.get('metricType'),
context: this
});
} else {
requestMetric["metric_paths"] = [{
metric_path: requestMetric["metric_path"],
metric_type: this.get('metricType'),
id: requestMetric["metric_path"] + "_" + this.get('metricType'),
context: this}];
delete requestMetric["metric_path"];
requestsData[key] = requestMetric;
}
}, this);
}
return requestsData;
},
/**
* Tweak host component criteria from the actual host component criteria
* @param {object} request
*/
computeHostComponentCriteria: function (request) {
return request.host_component_criteria.replace('host_components/', '&').trim();
},
/**
* make GET call to server in order to fetch service-component metrics
* @param {object} request
* @returns {$.ajax}
*/
getServiceComponentMetrics: function (request) {
var xhr = App.ajax.send({
name: 'widgets.serviceComponent.metrics.get',
sender: this,
data: {
serviceName: request.service_name,
componentName: request.component_name,
metricPaths: this.prepareMetricPaths(request.metric_paths)
}
});
if (this.get('graphView')) {
var graph = this.get('childViews') && this.get('childViews').findProperty('runningRequests');
if (graph) {
var requestsArrayName = graph.get('isPopup') ? 'runningPopupRequests' : 'runningRequests';
graph.get(requestsArrayName).push(xhr);
}
}
return xhr;
},
/**
* aggregate all metric names in the query. Add time range and step to temporal queries
* @param {Array} metricPaths
* @returns {string}
*/
prepareMetricPaths: function(metricPaths) {
var temporalMetrics = metricPaths.filterProperty('metric_type', 'TEMPORAL');
var pointInTimeMetrics = metricPaths.filterProperty('metric_type', 'POINT_IN_TIME');
var result = temporalMetrics.length ? temporalMetrics[0].context.addTimeProperties(temporalMetrics.mapProperty('metric_path')) : [];
if (pointInTimeMetrics.length) {
result = result.concat(pointInTimeMetrics.mapProperty('metric_path'));
}
return result.join(',');
},
/**
* make GET call to server in order to fetch specific host-component metrics
* @param {object} request
* @returns {$.ajax}
*/
getHostComponentMetrics: function (request) {
var metricPaths = this.prepareMetricPaths(request.metric_paths);
if (metricPaths.length) {
var xhr = App.ajax.send({
name: 'widgets.hostComponent.metrics.get',
sender: this,
data: {
componentName: request.component_name,
metricPaths: this.prepareMetricPaths(request.metric_paths),
hostComponentCriteria: this.computeHostComponentCriteria(request)
}
}),
graph = this.get('graphView') && this.get('childViews') && this.get('childViews').findProperty('runningRequests');
if (graph) {
var requestsArrayName = graph.get('isPopup') ? 'runningPopupRequests' : 'runningRequests';
graph.get(requestsArrayName).push(xhr);
}
return xhr;
}
return jQuery.Deferred().reject().promise();
},
getHostComponentMetricsSuccessCallback: function (data) {
if (data.items[0]) {
this.getMetricsSuccessCallback(data.items[0]);
}
},
/**
* callback on getting aggregated metrics and host component metrics
* @param data
*/
getMetricsSuccessCallback: function (data) {
var metrics = [];
var atLeastOneMetricPresent = false;
if (this.get('content.metrics')) {
this.get('content.metrics').forEach(function (_metric) {
var metric_path = _metric.metric_path;
var isAggregatorFunc = false;
var metric_data = Em.get(data, metric_path.replace(/\//g, '.'));
if (Em.isNone(metric_data)) {
this.aggregatorFunc.forEach(function (_item) {
if (metric_path.endsWith(_item) && !isAggregatorFunc) {
isAggregatorFunc = true;
var metricBeanProperty = metric_path.split("/").pop();
var metricBean;
metric_path = metric_path.substring(0, metric_path.indexOf(metricBeanProperty));
if (metric_path.endsWith("/")) {
metric_path = metric_path.slice(0, -1);
}
metricBean = Em.get(data, metric_path.replace(/\//g, '.'));
if (!Em.isNone(metricBean)) {
metric_data = metricBean[metricBeanProperty];
}
}
}, this);
}
if (!Em.isNone(metric_data)) {
atLeastOneMetricPresent = true;
_metric.data = metric_data;
this.get('metrics').pushObject(_metric);
}
}, this);
if (!atLeastOneMetricPresent) {
this.disableGraph();
}
}
},
/**
* if no metrics were received from server then disable graph
*/
disableGraph: function() {
if (this.get('graphView')) {
var graph = this.get('childViews') && this.get('childViews').findProperty('_showMessage');
if (graph) {
graph.set('hasData', false);
this.set('isExportButtonHidden', true);
graph._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noDataAtTime.message'));
}
}
this.set('metrics', this.get('metrics').reject(function (item) {
return this.get('content.metrics').someProperty('name', item.name);
}, this));
},
/**
* error callback on getting aggregated metrics and host component metrics
* @param {object} xhr
* @param {string} textStatus
* @param {string} errorThrown
*/
getMetricsErrorCallback: function (xhr, textStatus, errorThrown) {
if (this.get('graphView') && !xhr.isForcedAbort) {
var graph = this.get('childViews') && this.get('childViews').findProperty('_showMessage');
if (graph) {
if (xhr.readyState == 4 && xhr.status) {
textStatus = xhr.status + " " + textStatus;
}
graph.set('hasData', false);
this.set('isExportButtonHidden', true);
graph._showMessage('warn', this.t('graphs.error.title'), this.t('graphs.error.message').format(textStatus, errorThrown));
this.set('metrics', this.get('metrics').reject(function (item) {
return this.get('content.metrics').someProperty('name', item.name);
}, this));
}
}
},
/**
* make GET call to get metrics value for all host components
* @param {object} request
* @return {$.ajax}
*/
getHostComponentsMetrics: function (request) {
request.metric_paths.forEach(function (_metric, index) {
request.metric_paths[index] = "host_components/" + _metric.metric_path;
});
return App.ajax.send({
name: 'widgets.serviceComponent.metrics.get',
sender: this,
data: {
serviceName: request.service_name,
componentName: request.component_name,
metricPaths: request.metric_paths.join(',')
},
success: 'getHostComponentsMetricsSuccessCallback'
});
},
getHostComponentsMetricsSuccessCallback: function (data) {
var metrics = this.get('content.metrics');
data.host_components.forEach(function (item) {
metrics.forEach(function (_metric) {
if (!Em.isNone(Em.get(item, _metric.metric_path.replace(/\//g, '.')))) {
var metric = $.extend({}, _metric, true);
metric.data = Em.get(item, _metric.metric_path.replace(/\//g, '.'));
metric.hostName = item.HostRoles.host_name;
this.get('metrics').pushObject(metric);
}
}, this);
}, this);
},
getHostsMetrics: function (request) {
var metricPaths = request.metric_paths.mapProperty('metric_path');
return App.ajax.send({
name: 'widgets.hosts.metrics.get',
sender: this,
data: {
metricPaths: metricPaths.join(',')
},
success: 'getHostsMetricsSuccessCallback'
});
},
getHostsMetricsSuccessCallback: function (data) {
var metrics = this.get('content.metrics');
data.items.forEach(function (item) {
metrics.forEach(function (_metric, index) {
if (!Em.isNone(Em.get(item, _metric.metric_path.replace(/\//g, '.')))) {
var metric = $.extend({}, _metric, true);
metric.data = Em.get(item, _metric.metric_path.replace(/\//g, '.'));
metric.hostName = item.Hosts.host_name;
this.get('metrics').pushObject(metric);
}
}, this);
}, this);
},
/**
* callback on metrics loaded
*/
onMetricsLoaded: function () {
var self = this;
if (!this.get('isLoaded')) this.set('isLoaded', true);
this.drawWidget();
clearTimeout(this.get('timeoutId'));
this.set('timeoutId', setTimeout(function () {
self.loadMetrics();
}, App.contentUpdateInterval));
},
/**
* draw widget
*/
drawWidget: function () {
if (this.get('isLoaded')) {
this.calculateValues();
this.set('value', (this.get('content.values')[0]) ? this.get('content.values')[0].computedValue : '');
}
},
/**
* initialize tooltips
*/
initTooltip: function () {
var self = this;
if (this.get('isLoaded')) {
Em.run.next(function(){
if (self.get('state') === 'inDOM') {
App.tooltip(self.$(".corner-icon > .icon-copy"), {title: Em.I18n.t('common.clone')});
App.tooltip(self.$(".corner-icon > .icon-edit"), {title: Em.I18n.t('common.edit')});
App.tooltip(self.$(".corner-icon > .icon-save"), {title: Em.I18n.t('common.export')});
}
});
}
}.observes('isLoaded'),
willDestroyElement: function() {
this.$(".corner-icon > .icon-copy").tooltip('destroy');
this.$(".corner-icon > .icon-edit").tooltip('destroy');
this.$(".corner-icon > .icon-save").tooltip('destroy');
},
/**
* calculate series datasets for graph widgets
*/
calculateValues: function () {
this.get('content.values').forEach(function (value) {
var computeExpression = this.computeExpression(this.extractExpressions(value), this.get('metrics'));
value.computedValue = value.value.replace(this.get('EXPRESSION_REGEX'), function (match) {
var float = parseFloat(computeExpression[match]);
if (isNaN(float)) {
return computeExpression[match] || "<span class=\"grey\">n/a</span>";
} else {
return String((float % 1 !== 0) ? float.toFixed(2) : float);
}
});
}, this);
},
/**
* extract expressions
* Example:
* input: "${a/b} equal ${b+a}"
* expressions: ['a/b', 'b+a']
*
* @param {object} input
* @returns {Array}
*/
extractExpressions: function (input) {
var pattern = this.get('EXPRESSION_REGEX'),
expressions = [],
match;
if (Em.isNone(input)) return expressions;
while (match = pattern.exec(input.value)) {
expressions.push(match[1]);
}
return expressions;
},
/**
* compute expression
* @param expressions
* @param metrics
* @returns {object}
*/
computeExpression: function (expressions, metrics) {
var result = {};
expressions.forEach(function (_expression) {
var validExpression = true;
var value = "";
//replace values with metrics data
var beforeCompute = _expression.replace(this.get('VALUE_NAME_REGEX'), function (match) {
if (window.isNaN(match)) {
if (metrics.someProperty('name', match)) {
return metrics.findProperty('name', match).data;
} else {
validExpression = false;
}
} else {
return match;
}
});
//check for correct math expression
if (!(validExpression && this.get('MATH_EXPRESSION_REGEX').test(beforeCompute))) {
validExpression = false;
}
result['${' + _expression + '}'] = (validExpression) ? Number(window.eval(beforeCompute)).toString() : value;
}, this);
return result;
},
/*
* make call when clicking on "remove icon" on widget
*/
hideWidget: function (event) {
this.get('controller').hideWidget(
{
context: Em.Object.create({
id: event.context
})
}
);
},
/*
* make call when clicking on "clone icon" on widget
*/
cloneWidget: function (event) {
var self = this;
return App.showConfirmationPopup(
function () {
self.postWidgetDefinition(true);
},
Em.I18n.t('widget.clone.body').format(stringUtils.htmlEntities(self.get('content.widgetName'))),
null,
null,
Em.I18n.t('common.clone')
);
},
/**
* collect all needed data to create new widget
* @returns {{WidgetInfo: {widget_name: *, widget_type: *, description: *, scope: *, metrics: *, values: *, properties: *}}}
*/
collectWidgetData: function () {
return {
WidgetInfo: {
widget_name: this.get('content.widgetName'),
widget_type: this.get('content.widgetType'),
description: this.get('content.widgetDescription'),
scope: this.get('content.scope'),
"metrics": this.get('content.metrics').map(function (metric) {
return {
"name": metric.name,
"service_name": metric.service_name,
"component_name": metric.component_name,
"host_component_criteria": metric.host_component_criteria,
"metric_path": metric.metric_path
}
}),
values: this.get('content.values'),
properties: this.get('content.properties')
}
};
},
/**
* post widget definition to server
* @param {boolean} isClone
* * @param {boolean} isEditClonedWidget
* @returns {$.ajax}
*/
postWidgetDefinition: function (isClone, isEditClonedWidget) {
var data = this.collectWidgetData();
if (isClone) {
data.WidgetInfo.widget_name += this.get('CLONE_SUFFIX');
data.WidgetInfo.scope = 'USER';
}
var successCallback = isEditClonedWidget ? 'editNewClonedWidget' : 'postWidgetDefinitionSuccessCallback';
return App.ajax.send({
name: 'widgets.wizard.add',
sender: this,
data: {
data: data
},
success: successCallback
});
},
postWidgetDefinitionSuccessCallback: function (data) {
var widgets = this.get('content.layout.widgets').toArray();
widgets.pushObject(Em.Object.create({
id: data.resources[0].WidgetInfo.id
}));
var mainServiceInfoSummaryController = App.router.get('mainServiceInfoSummaryController');
mainServiceInfoSummaryController.saveWidgetLayout(widgets).done(function(){
mainServiceInfoSummaryController.updateActiveLayout();
});
},
/*
* enter edit wizard of the newly cloned widget
*/
editNewClonedWidget: function (data) {
var controller = this.get('controller');
var widgets = this.get('content.layout.widgets').toArray();
var id = data.resources[0].WidgetInfo.id;
widgets.pushObject(Em.Object.create({
id: id
}));
var mainServiceInfoSummaryController = App.router.get('mainServiceInfoSummaryController');
mainServiceInfoSummaryController.saveWidgetLayout(widgets).done(function() {
mainServiceInfoSummaryController.getActiveWidgetLayout().done(function() {
var newWidget = App.Widget.find().findProperty('id', id);
controller.editWidget(newWidget);
});
});
},
/*
* make call when clicking on "edit icon" on widget
*/
editWidget: function (event) {
var self = this;
var isShared = this.get('content.scope') == 'CLUSTER';
if (!isShared) {
self.get('controller').editWidget(self.get('content'));
} else {
return App.ModalPopup.show({
header: Em.I18n.t('common.warning'),
bodyClass: Em.View.extend({
template: Ember.Handlebars.compile('{{t widget.edit.body}}')
}),
primary: Em.I18n.t('widget.edit.button.primary'),
secondary: Em.I18n.t('widget.edit.button.secondary'),
third: Em.I18n.t('common.cancel'),
onPrimary: function () {
this.hide();
self.get('controller').editWidget(self.get('content'));
},
onSecondary: function () {
this.hide();
self.postWidgetDefinition(true, true);
},
onThird: function () {
this.hide();
}
});
}
}
});
App.WidgetPreviewMixin = Ember.Mixin.create({
beforeRender: Em.K,
isLoaded: true,
metrics: [],
content: Em.Object.create({
id: 1,
values: []
}),
loadMetrics: function () {
this.get('content').setProperties({
'values': this.get('controller.widgetValues'),
'properties': this.get('controller.widgetProperties'),
'widgetName': this.get('controller.widgetName'),
'metrics': this.get('controller.widgetMetrics')
});
this._super();
}.observes('controller.widgetProperties', 'controller.widgetValues', 'controller.widgetMetrics', 'controller.widgetName'),
onMetricsLoaded: function () {
this.drawWidget();
}
});
/**
* aggregate requests to load metrics by component name
* requests can be added via add method
* input example:
* {
* data: request,
* context: this,
* startCallName: this.getServiceComponentMetrics,
* successCallback: this.getMetricsSuccessCallback,
* completeCallback: function () {
* requestCounter--;
* if (requestCounter === 0) this.onMetricsLoaded();
* }
* }
* @type {Em.Object}
*/
App.WidgetLoadAggregator = Em.Object.create({
/**
* @type {Array}
*/
requests: [],
/**
* @type {number|null}
*/
timeoutId: null,
/**
* @type {number}
* @const
*/
BULK_INTERVAL: 1000,
arrayUtils: require('utils/array_utils'),
/**
* add request
* every {{BULK_INTERVAL}} requests get collected, aggregated and sent to server
*
* @param {object} request
*/
add: function (request) {
var self = this;
this.get('requests').push(request);
if (Em.isNone(this.get('timeoutId'))) {
this.set('timeoutId', window.setTimeout(function () {
//clear requests that are belongs to destroyed views
self.set('requests', self.get('requests').filterProperty('context.state', 'inDOM'));
self.runRequests(self.get('requests'));
self.get('requests').clear();
clearTimeout(self.get('timeoutId'));
self.set('timeoutId', null);
}, this.get('BULK_INTERVAL')));
}
},
/**
* return requests which grouped into bulks
* @param {Array} requests
* @returns {object} bulks
*/
groupRequests: function (requests) {
var bulks = {};
requests.forEach(function (request) {
//poll metrics for graph widgets separately
var graphSuffix = request.context.get('content.widgetType') === "GRAPH" ? "_graph" : '';
var id = request.startCallName + "_" + request.data.component_name + graphSuffix;
if (Em.isNone(bulks[id])) {
bulks[id] = {
data: request.data,
context: request.context,
startCallName: request.startCallName
};
bulks[id].subRequests = [{
context: request.context,
successCallback: request.successCallback,
errorCallback: request.errorCallback,
completeCallback: request.completeCallback
}];
} else {
bulks[id].data.metric_paths.pushObjects(request.data.metric_paths);
bulks[id].subRequests.push({
context: request.context,
successCallback: request.successCallback,
errorCallback: request.errorCallback,
completeCallback: request.completeCallback
});
}
}, this);
return bulks;
},
/**
* run aggregated requests
* @param {Array} requests
*/
runRequests: function (requests) {
var bulks = this.groupRequests(requests);
var self = this;
for (var id in bulks) {
(function (_request) {
_request.data.metric_paths = self.arrayUtils.uniqObjectsbyId(_request.data.metric_paths, "id");
_request.context[_request.startCallName].call(_request.context, _request.data).done(function (response) {
_request.subRequests.forEach(function (subRequest) {
subRequest.successCallback.call(subRequest.context, response);
}, this);
}).fail(function (xhr, textStatus, errorThrown) {
_request.subRequests.forEach(function (subRequest) {
if (subRequest.errorCallback) {
subRequest.errorCallback.call(subRequest.context, xhr, textStatus, errorThrown);
}
}, this);
}).always(function (xhr) {
_request.subRequests.forEach(function (subRequest) {
subRequest.completeCallback.call(subRequest.context, xhr);
}, this);
});
})(bulks[id]);
}
}
});