blob: 6f6f4a7e1864b4375a86048a82f4de95008de63f [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.
*/
/* global define, module, require, exports */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery',
'd3',
'nf.Common',
'nf.Dialog',
'nf.ErrorHandler'],
function ($, d3, nfCommon, nfDialog, nfErrorHandler) {
return (nf.StatusHistory = factory($, d3, nfCommon, nfDialog, nfErrorHandler));
});
} else if (typeof exports === 'object' && typeof module === 'object') {
module.exports = (nf.StatusHistory = factory(require('jquery'),
require('d3'),
require('nf.Common'),
require('nf.Dialog'),
require('nf.ErrorHandler')));
} else {
nf.StatusHistory = factory(root.$,
root.d3,
root.nf.Common,
root.nf.Dialog,
root.nf.ErrorHandler);
}
}(this, function ($, d3, nfCommon, nfDialog, nfErrorHandler) {
var config = {
nifiInstanceId: 'nifi-instance-id',
nifiInstanceLabel: 'NiFi',
type: {
processor: 'Processor',
inputPort: 'Input Port',
outputPort: 'Output Port',
processGroup: 'Process Group',
remoteProcessGroup: 'Remote Process Group',
connection: 'Connection',
funnel: 'Funnel',
template: 'Template',
label: 'Label',
node: 'Node'
},
urls: {
api: '../nifi-api'
}
};
/**
* Mapping of formatters to use for the chart's y axis.
*/
var formatters = {
'DURATION': function (d) {
return nfCommon.formatDuration(d);
},
'COUNT': function (d) {
// need to handle floating point number since this formatter
// will also be used for average values
if (d % 1 === 0) {
return nfCommon.formatInteger(d);
} else {
return nfCommon.formatFloat(d);
}
},
'DATA_SIZE': function (d) {
return nfCommon.formatDataSize(d);
},
'FRACTION': function (d) {
return nfCommon.formatFloat(d / 1000000);
}
};
/**
* The time offset of the server.
*/
var serverTimeOffset = null;
/**
* The current selection of the brush.
*/
var brushSelection = null;
/**
* The currently selected descriptor.
*/
var descriptor = null;
/**
* The instances and whether they are visible or not
*/
var instances = null;
/**
* Handles the status history response.
*
* @param {string} groupId
* @param {string} id
* @param {object} componentStatusHistory
* @param {string} componentType
* @param {object} selectedDescriptor
*/
var handleStatusHistoryResponse = function (groupId, id, componentStatusHistory, componentType, selectedDescriptor) {
// update the last refreshed
$('#status-history-last-refreshed').text(componentStatusHistory.generated);
// initialize the status history
var statusHistory = {
groupId: groupId,
id: id,
type: componentType,
instances: []
};
// get the descriptors
var descriptors = componentStatusHistory.fieldDescriptors;
statusHistory.details = componentStatusHistory.componentDetails;
statusHistory.selectedDescriptor = nfCommon.isUndefined(selectedDescriptor) ? descriptors[0] : selectedDescriptor;
// ensure enough status snapshots
if (nfCommon.isDefinedAndNotNull(componentStatusHistory.aggregateSnapshots) && componentStatusHistory.aggregateSnapshots.length > 1) {
statusHistory.instances.push({
id: config.nifiInstanceId,
label: config.nifiInstanceLabel,
snapshots: componentStatusHistory.aggregateSnapshots
});
} else {
insufficientHistory();
return;
}
// get the status for each node in the cluster if applicable
$.each(componentStatusHistory.nodeSnapshots, function (_, nodeSnapshots) {
// ensure enough status snapshots
if (nfCommon.isDefinedAndNotNull(nodeSnapshots.statusSnapshots) && nodeSnapshots.statusSnapshots.length > 1) {
statusHistory.instances.push({
id: nodeSnapshots.nodeId,
label: nodeSnapshots.address + ':' + nodeSnapshots.apiPort,
snapshots: nodeSnapshots.statusSnapshots
});
}
});
// ensure we found eligible status history
if (statusHistory.instances.length > 0) {
// store the status history
$('#status-history-dialog').data('status-history', statusHistory);
// chart the status history
chart(statusHistory, descriptors);
} else {
insufficientHistory();
}
};
/**
* Handles the status history response for node status history.
*
* @param {object} componentStatusHistory
*/
var handleNodeStatusHistoryResponse = function (componentStatusHistory) {
// update the last refreshed
$('#status-history-last-refreshed').text(componentStatusHistory.generated);
// initialize the status history
var statusHistory = {
type: 'Node',
instances: []
};
// get the descriptors
var descriptors = componentStatusHistory.fieldDescriptors;
statusHistory.details = componentStatusHistory.componentDetails;
statusHistory.selectedDescriptor = descriptors[0];
// ensure enough status snapshots
if (nfCommon.isDefinedAndNotNull(componentStatusHistory.aggregateSnapshots) && componentStatusHistory.aggregateSnapshots.length > 1) {
statusHistory.instances.push({
id: config.nifiInstanceId,
label: config.nifiInstanceLabel,
snapshots: componentStatusHistory.aggregateSnapshots
});
} else {
insufficientHistory();
return;
}
// get the status for each node in the cluster if applicable
$.each(componentStatusHistory.nodeSnapshots, function (_, nodeSnapshots) {
// ensure enough status snapshots
if (nfCommon.isDefinedAndNotNull(nodeSnapshots.statusSnapshots) && nodeSnapshots.statusSnapshots.length > 1) {
statusHistory.instances.push({
id: nodeSnapshots.nodeId,
label: nodeSnapshots.address + ':' + nodeSnapshots.apiPort,
snapshots: nodeSnapshots.statusSnapshots
});
}
});
// ensure we found eligible status history
if (statusHistory.instances.length > 0) {
// store the status history
$('#status-history-dialog').data('status-history', statusHistory);
// chart the status history
chart(statusHistory, descriptors);
} else {
insufficientHistory();
}
};
/**
* Shows an error message stating there is insufficient history available.
*/
var insufficientHistory = function () {
// notify the user
nfDialog.showOkDialog({
headerText: 'Status History',
dialogContent: 'Insufficient history, please try again later.'
});
};
/**
* Builds the chart using the statusHistory and the available fields.
*
* @param {type} statusHistory
* @param {type} descriptors
*/
var chart = function (statusHistory, descriptors) {
if (instances === null) {
instances = {};
}
// go through each instance of this status history
$.each(statusHistory.instances, function (_, instance) {
// and convert each timestamp accordingly
instance.snapshots.forEach(function (d) {
// get the current user time to properly convert the server time
var now = new Date();
// convert the user offset to millis
var userTimeOffset = now.getTimezoneOffset() * 60 * 1000;
// create the proper date by adjusting by the offsets
d.timestamp = new Date(d.timestamp + userTimeOffset + serverTimeOffset);
});
});
// this will trigger the chart to be updated
setAvailableFields(descriptors, statusHistory.selectedDescriptor);
};
/**
* Sets the available fields.
*
* @param {type} descriptors
* @param {type} selected
*/
var setAvailableFields = function (descriptors, selected) {
var options = [];
$.each(descriptors, function (_, d) {
options.push({
text: d.label,
value: d.field,
description: nfCommon.escapeHtml(d.description)
});
});
// build the combo with the available fields
$('#status-history-metric-combo').combo({
selectedOption: {
value: selected.field
},
options: options,
select: function (selectedOption) {
// find the corresponding descriptor
var selectedDescriptor = null;
$.each(descriptors, function (_, d) {
if (selectedOption.value === d.field) {
selectedDescriptor = d;
return false;
}
});
// ensure the descriptor was found
if (selectedDescriptor !== null) {
// clear the current extent if this is a newly selected descriptor
if (descriptor === null || descriptor.field !== selectedDescriptor.field) {
brushSelection = null;
}
// record the currently selected descriptor
descriptor = selectedDescriptor;
// update the selected chart
var statusHistory = $('#status-history-dialog').data('status-history');
statusHistory.selectedDescriptor = selectedDescriptor;
$('#status-history-dialog').data('status-history', statusHistory);
// update the chart
updateChart();
}
}
});
$('#status-history-dialog').modal('show');
};
var getChartMinHeight = function () {
return $('#status-history-chart-container').parent().outerHeight() - $('#status-history-chart-control-container').outerHeight() -
parseInt($('#status-history-chart-control-container').css('margin-top'), 10);
};
var getChartMaxHeight = function () {
return $('body').height() - $('#status-history-chart-control-container').outerHeight() -
$('#status-history-refresh-container').outerHeight() -
parseInt($('#status-history-chart-control-container').css('margin-top'), 10) -
parseInt($('.dialog-content').css('top')) - parseInt($('.dialog-content').css('bottom'));
};
var getChartMaxWidth = function () {
return $('body').width() - $('#status-history-details').innerWidth() -
parseInt($('#status-history-dialog').css('left'), 10) -
parseInt($('.dialog-content').css('left')) - parseInt($('.dialog-content').css('right'));
};
/**
* Updates the chart with the specified status history and the selected field.
*/
var updateChart = function () {
var statusHistoryDialog = $('#status-history-dialog');
if (statusHistoryDialog.is(':visible')) {
var statusHistory = statusHistoryDialog.data('status-history');
// get the selected descriptor
var selectedDescriptor = statusHistory.selectedDescriptor;
// remove current details
$('#status-history-details').empty();
// add status history details
var detailsContainer = buildDetailsContainer('Status History');
d3.map(statusHistory.details).each(function (value, label) {
addDetailItem(detailsContainer, label, value);
});
var margin = {
top: 15,
right: 20,
bottom: 25,
left: 75
};
// -------------
// prep the data
// -------------
// available colors
var color = d3.scaleOrdinal(d3.schemeCategory10);
// determine the available instances
var instanceLabels = [];
$.each(statusHistory.instances, function (_, instance) {
instanceLabels.push(instance.label);
});
// specify the domain based on the detected instances
color.domain(instanceLabels);
// data for the chart
var statusData = [];
// go through each instance of this status history
$.each(statusHistory.instances, function (_, instance) {
// if this is the first time this instance is being rendered, make it visible
if (nfCommon.isUndefinedOrNull(instances[instance.id])) {
instances[instance.id] = true;
}
// and convert the model
statusData.push({
id: instance.id,
label: instance.label,
values: $.map(instance.snapshots, function (d) {
return {
timestamp: d.timestamp,
value: d.statusMetrics[selectedDescriptor.field]
};
}),
visible: instances[instance.id] === true
});
});
// --------------------------
// custom time axis formatter
// --------------------------
var customTimeFormat = function (d) {
if (d.getMilliseconds()) {
return d3.timeFormat(':%S.%L')(d);
} else if (d.getSeconds()) {
return d3.timeFormat(':%S')(d);
} else if (d.getMinutes() || d.getHours()) {
return d3.timeFormat('%H:%M')(d);
} else if (d.getDay() && d.getDate() !== 1) {
return d3.timeFormat('%a %d')(d);
} else if (d.getDate() !== 1) {
return d3.timeFormat('%b %d')(d);
} else if (d.getMonth()) {
return d3.timeFormat('%B')(d);
} else {
return d3.timeFormat('%Y')(d);
}
};
// ----------
// main chart
// ----------
// the container for the main chart
var chartContainer = $('#status-history-chart-container').empty();
if (chartContainer.hasClass('ui-resizable')) {
chartContainer.resizable('destroy');
chartContainer.removeAttr("style");
}
// calculate the dimensions
chartContainer.height(getChartMinHeight());
// determine the new width/height
var width = chartContainer.outerWidth() - margin.left - margin.right;
var height = chartContainer.outerHeight() - margin.top - margin.bottom;
maxWidth = $('#status-history-container').width();
if (width > maxWidth) {
width = maxWidth;
}
maxHeight = getChartMaxHeight();
if (height > maxHeight) {
height = maxHeight;
}
// define the x axis for the main chart
var x = d3.scaleTime()
.range([0, width]);
var xAxis = d3.axisBottom(x)
.ticks(5)
.tickFormat(customTimeFormat);
// define the y axis
var y = d3.scaleLinear()
.range([height, 0]);
var yAxis = d3.axisLeft(y)
.tickFormat(formatters[selectedDescriptor.formatter]);
// status line
var line = d3.line()
.curve(d3.curveMonotoneX)
.x(function (d) {
return x(d.timestamp);
})
.y(function (d) {
return y(d.value);
});
// build the chart svg
var chartSvg = d3.select('#status-history-chart-container').append('svg')
.attr('style', 'pointer-events: none;')
.attr('width', chartContainer.parent().width())
.attr('height', chartContainer.innerHeight());
// define a clip the path
var clipPath = chartSvg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width)
.attr('height', height);
// build the chart
var chart = chartSvg.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
// determine the min/max date
var minDate = d3.min(statusData, function (d) {
return d3.min(d.values, function (s) {
return s.timestamp;
});
});
var maxDate = d3.max(statusData, function (d) {
return d3.max(d.values, function (s) {
return s.timestamp;
});
});
addDetailItem(detailsContainer, 'Start', nfCommon.formatDateTime(minDate));
addDetailItem(detailsContainer, 'End', nfCommon.formatDateTime(maxDate));
// determine the x axis range
x.domain([minDate, maxDate]);
// determine the y axis range
y.domain([getMinValue(statusData), getMaxValue(statusData)]);
// build the x axis
chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
// build the y axis
chart.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.attr('text-anchor', 'end')
.text(selectedDescriptor.label);
// build the chart
var status = chart.selectAll('.status')
.data(statusData)
.enter()
.append('g')
.attr('clip-path', 'url(#clip)')
.attr('class', 'status');
// draw the lines
status.append('path')
.attr('class', function (d) {
return 'chart-line chart-line-' + d.id;
})
.attr('d', function (d) {
return line(d.values);
})
.attr('stroke', function (d) {
return color(d.label);
})
.classed('hidden', function (d) {
return d.visible === false;
})
.append('title')
.text(function (d) {
return d.label;
});
// draw the control points for each line
status.each(function (d) {
// create a group for the control points
var markGroup = d3.select(this).append('g')
.attr('class', function () {
return 'mark-group mark-group-' + d.id;
})
.classed('hidden', function (d) {
return d.visible === false;
});
// draw the control points
markGroup.selectAll('circle.mark')
.data(d.values)
.enter()
.append('circle')
.attr('style', 'pointer-events: all;')
.attr('class', 'mark')
.attr('cx', function (v) {
return x(v.timestamp);
})
.attr('cy', function (v) {
return y(v.value);
})
.attr('fill', function () {
return color(d.label);
})
.attr('r', 1.5)
.append('title')
.text(function (v) {
return d.label + ' -- ' + formatters[selectedDescriptor.formatter](v.value);
});
});
// update the size of the chart
chartSvg.attr('width', chartContainer.parent().width())
.attr('height', chartContainer.innerHeight());
// update the size of the clipper
clipPath.attr('width', width)
.attr('height', height);
// update the position of the x axis
chart.select('.x.axis').attr('transform', 'translate(0, ' + height + ')');
// -------------
// control chart
// -------------
// the container for the main chart control
var chartControlContainer = $('#status-history-chart-control-container').empty();
var controlHeight = chartControlContainer.innerHeight() - margin.top - margin.bottom;
var xControl = d3.scaleTime()
.range([0, width]);
var xControlAxis = d3.axisBottom(xControl)
.ticks(5)
.tickFormat(customTimeFormat);
var yControl = d3.scaleLinear()
.range([controlHeight, 0]);
var yControlAxis = d3.axisLeft(yControl)
.tickValues(y.domain())
.tickFormat(formatters[selectedDescriptor.formatter]);
// status line
var controlLine = d3.line()
.curve(d3.curveMonotoneX)
.x(function (d) {
return xControl(d.timestamp);
})
.y(function (d) {
return yControl(d.value);
});
// build the svg
var controlChartSvg = d3.select('#status-history-chart-control-container').append('svg')
.attr('width', chartContainer.parent().width())
.attr('height', controlHeight + margin.top + margin.bottom);
// build the control chart
var control = controlChartSvg.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
// define the domain for the control chart
xControl.domain(x.domain());
yControl.domain(y.domain());
// build the control x axis
control.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + controlHeight + ')')
.call(xControlAxis);
// build the control y axis
control.append('g')
.attr('class', 'y axis')
.call(yControlAxis);
// build the control chart
var controlStatus = control.selectAll('.status')
.data(statusData)
.enter()
.append('g')
.attr('class', 'status');
// draw the lines
controlStatus.append('path')
.attr('class', function (d) {
return 'chart-line chart-line-' + d.id;
})
.attr('d', function (d) {
return controlLine(d.values);
})
.attr('stroke', function (d) {
return color(d.label);
})
.classed('hidden', function (d) {
return instances[d.id] === false;
})
.append('title')
.text(function (d) {
return d.label;
});
// --------------------
// aggregate statistics
// --------------------
var updateAggregateStatistics = function () {
// locate the instances that have data points within the current brush
var withinBrush = $.map(statusData, function (d) {
var xDomain = x.domain();
var yDomain = y.domain();
// copy to avoid modifying the original
var copy = $.extend({}, d);
// update the copy to only include values within the brush
return $.extend(copy, {
values: $.grep(d.values, function (s) {
return s.timestamp.getTime() >= xDomain[0].getTime() && s.timestamp.getTime() <= xDomain[1].getTime() && s.value >= yDomain[0] && s.value <= yDomain[1];
})
});
});
// consider visible nodes with data in the brush
var nodes = $.grep(withinBrush, function (d) {
return d.id !== config.nifiInstanceId && d.visible && d.values.length > 0;
});
var nodeMinValue = nodes.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMinValue(nodes));
var nodeMeanValue = nodes.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMeanValue(nodes));
var nodeMaxValue = nodes.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMaxValue(nodes));
// update the currently displayed min/max/mean
$('#node-aggregate-statistics').text(nodeMinValue + ' / ' + nodeMaxValue + ' / ' + nodeMeanValue);
// only consider the cluster with data in the brush
var cluster = $.grep(withinBrush, function (d) {
return d.id === config.nifiInstanceId && d.visible && d.values.length > 0;
});
// determine the cluster values
var clusterMinValue = cluster.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMinValue(cluster));
var clusterMeanValue = cluster.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMeanValue(cluster));
var clusterMaxValue = cluster.length === 0 ? 'NA' : formatters[selectedDescriptor.formatter](getMaxValue(cluster));
// update the cluster min/max/mean
$('#cluster-aggregate-statistics').text(clusterMinValue + ' / ' + clusterMaxValue + ' / ' + clusterMeanValue);
};
// -------------------
// configure the brush
// -------------------
/**
* Updates the axis for the main chart.
*
* @param {array} xDomain The new domain for the x axis
* @param {array} yDomain The new domain for the y axis
*/
var updateAxes = function (xDomain, yDomain) {
// update the domain of the main chart
x.domain(xDomain);
y.domain(yDomain);
// update the chart lines
status.selectAll('.chart-line')
.attr('d', function (d) {
return line(d.values);
});
status.selectAll('circle.mark')
.attr('cx', function (v) {
return x(v.timestamp);
})
.attr('cy', function (v) {
return y(v.value);
})
.attr('r', function () {
return d3.brushSelection(brushNode.node()) === null ? 1.5 : 4;
});
// update the x axis
chart.select('.x.axis').call(xAxis);
chart.select('.y.axis').call(yAxis);
};
/**
* Handles brush events by updating the main chart according to the context window
* or the control domain if there is no context window.
*/
var brushed = function () {
brushSelection = d3.brushSelection(brushNode.node());
// determine the new x and y domains
var xContextDomain, yContextDomain;
if (brushSelection === null) {
// get the all visible instances
var visibleInstances = $.grep(statusData, function (d) {
return d.visible;
});
// determine the appropriate y domain
if (visibleInstances.length === 0) {
yContextDomain = yControl.domain();
} else {
yContextDomain = [
d3.min(visibleInstances, function (d) {
return d3.min(d.values, function (s) {
return s.value;
});
}),
d3.max(visibleInstances, function (d) {
return d3.max(d.values, function (s) {
return s.value;
});
})
];
}
xContextDomain = xControl.domain();
} else {
xContextDomain = [brushSelection[0][0], brushSelection[1][0]].map(xControl.invert, xControl);
yContextDomain = [brushSelection[1][1], brushSelection[0][1]].map(yControl.invert, yControl);
}
// update the axes accordingly
updateAxes(xContextDomain, yContextDomain);
// update the aggregate statistics according to the new domain
updateAggregateStatistics();
};
// build the brush
var brush = d3.brush()
.extent([[xControl.range()[0], yControl.range()[1]], [xControl.range()[1], yControl.range()[0]]])
.on('brush', brushed);
// context area
var brushNode = control.append('g')
.attr('class', 'brush')
.on('click', brushed)
.call(brush);
// conditionally set the brush extent
if (nfCommon.isDefinedAndNotNull(brushSelection)) {
brush = brush.move(brushNode, brushSelection);
}
// add expansion to the extent
control.select('rect.selection')
.attr('style', 'pointer-events: all;')
.on('dblclick', function () {
if (brushSelection !== null) {
// get the y range (this value does not change from the original y domain)
var yRange = yControl.range();
// expand the extent vertically
brush.move(brushNode, [[brushSelection[0][0], yRange[1]], [brushSelection[1][0], yRange[0]]]);
}
});
// ----------------
// build the legend
// ----------------
// identify all nodes and sort
var nodes = $.grep(statusData, function (status) {
return status.id !== config.nifiInstanceId;
}).sort(function (a, b) {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
});
// adds a legend entry for the specified instance
var addLegendEntry = function (legend, instance) {
// create the label and the checkbox
var instanceLabelElement = $('<div></div>').addClass('legend-label nf-checkbox-label').css('color', color(instance.label)).text(instance.label).ellipsis();
var instanceCheckboxElement = $('<div class="nf-checkbox"></div>').on('click', function () {
// get the line and the control points for this instance (select all for the line to update control and main charts)
var chartLine = d3.selectAll('path.chart-line-' + instance.id);
var markGroup = d3.select('g.mark-group-' + instance.id);
// determine if it was hidden
var isHidden = markGroup.classed('hidden');
// toggle the visibility
chartLine.classed('hidden', function () {
return !isHidden;
});
markGroup.classed('hidden', function () {
return !isHidden;
});
// update whether its visible
instance.visible = isHidden;
// record the current status so it persists across refreshes
instances[instance.id] = instance.visible;
// update the brush
brushed();
}).addClass(instance.visible ? 'checkbox-checked' : 'checkbox-unchecked');
// add the legend entry
$('<div class="legend-entry"></div>').append(instanceCheckboxElement).append(instanceLabelElement).on('mouseenter', function () {
d3.selectAll('path.chart-line-' + instance.id).classed('over', true);
}).on('mouseleave', function () {
d3.selectAll('path.chart-line-' + instance.id).classed('over', false);
}).appendTo(legend);
};
// get the cluster instance
var cluster = $.grep(statusData, function (status) {
return status.id === config.nifiInstanceId;
});
// build the cluster container
var clusterDetailsContainer = buildDetailsContainer('NiFi');
// add the total cluster values
addDetailItem(clusterDetailsContainer, 'Min / Max / Mean', '', 'cluster-aggregate-statistics');
// build the cluster legend
addLegendEntry(clusterDetailsContainer, cluster[0]);
// if there are entries to render
if (nodes.length > 0) {
// build the cluster container
var nodeDetailsContainer = buildDetailsContainer('Nodes');
// add the total cluster values
addDetailItem(nodeDetailsContainer, 'Min / Max / Mean', '', 'node-aggregate-statistics');
// add each legend entry
$.each(nodes, function (_, instance) {
addLegendEntry(nodeDetailsContainer, instance);
});
}
// update the brush
brushed();
// ---------------
// handle resizing
// ---------------
var maxWidth, maxHeight, minHeight, dialog;
chartContainer.append('<div class="ui-resizable-handle ui-resizable-se"></div>').resizable({
minWidth: 425,
minHeight: 150,
handles: {
'se': '.ui-resizable-se'
},
resize: function (e, ui) {
// -----------
// containment
// -----------
dialog = $('#status-history-dialog');
var nfDialogData = {};
if (nfCommon.isDefinedAndNotNull(dialog.data('nf-dialog'))) {
nfDialogData = dialog.data('nf-dialog');
}
nfDialogData['min-width'] = (dialog.width() / $(window).width()) * 100 + '%';
nfDialogData['min-height'] = (dialog.height() / $(window).height()) * 100 + '%';
nfDialogData.responsive['fullscreen-width'] = dialog.outerWidth() + 'px';
nfDialogData.responsive['fullscreen-height'] = dialog.outerHeight() + 'px';
maxWidth = getChartMaxWidth();
if (ui.helper.width() > maxWidth) {
ui.helper.width(maxWidth);
nfDialogData.responsive['fullscreen-width'] = $(window).width() + 'px';
nfDialogData['min-width'] = '100%';
}
maxHeight = getChartMaxHeight();
if (ui.helper.height() > maxHeight) {
ui.helper.height(maxHeight);
nfDialogData.responsive['fullscreen-height'] = $(window).height() + 'px';
nfDialogData['min-height'] = '100%';
}
minHeight = getChartMinHeight();
if (ui.helper.height() < minHeight) {
ui.helper.height(minHeight);
}
nfDialogData['min-width'] = (parseInt(nfDialogData['min-width'], 10) >= 100) ? '100%' : nfDialogData['min-width'];
nfDialogData['min-height'] = (parseInt(nfDialogData['min-height'], 10) >= 100) ? '100%' : nfDialogData['min-height'];
//persist data attribute
dialog.data('nfDialog', nfDialogData);
// ----------------------
// status history dialog
// ----------------------
dialog.css('min-width', (chartContainer.outerWidth() + $('#status-history-details').outerWidth() + 40));
dialog.css('min-height', (chartContainer.outerHeight() +
$('#status-history-refresh-container').outerHeight() + $('#status-history-chart-control-container').outerHeight() +
$('.dialog-buttons').outerHeight() + $('.dialog-header').outerHeight() + 40 + 5));
dialog.center();
},
stop: updateChart
});
}
};
/**
* Gets the minimum value from the specified instances.
*
* @param {type} nodeInstances
*/
var getMinValue = function (nodeInstances) {
return d3.min(nodeInstances, function (d) {
return d3.min(d.values, function (s) {
return s.value;
});
});
};
/**
* Gets the maximum value from the specified instances.
*
* @param {type} nodeInstances
*/
var getMaxValue = function (nodeInstances) {
return d3.max(nodeInstances, function (d) {
return d3.max(d.values, function (s) {
return s.value;
});
});
};
/**
* Gets the mean value from the specified instances.
*
* @param {type} nodeInstances
*/
var getMeanValue = function (nodeInstances) {
var snapshotCount = 0;
var totalValue = d3.sum(nodeInstances, function (d) {
snapshotCount += d.values.length;
return d3.sum(d.values, function (s) {
return s.value;
});
});
return totalValue / snapshotCount;
};
/**
* Builds a details container.
*
* @param {type} label
*/
var buildDetailsContainer = function (label) {
var detailContainer = $('<div class="status-history-detail"></div>').appendTo('#status-history-details');
$('<div class="detail-container-label"></div>').text(label).appendTo(detailContainer);
return $('<div></div>').appendTo(detailContainer);
};
/**
* Adds a detail item and specified container.
*
* @param {type} container
* @param {type} label
* @param {type} value
* @param {type} valueElementId
*/
var addDetailItem = function (container, label, value, valueElementId) {
var detailContainer = $('<div class="detail-item"></div>').appendTo(container);
$('<div class="setting-name"></div>').text(label).appendTo(detailContainer);
var detailElement = $('<div class="setting-field"></div>').text(value).appendTo(detailContainer);
if (nfCommon.isDefinedAndNotNull(valueElementId)) {
detailElement.attr('id', valueElementId);
}
};
var nfStatusHistory = {
/**
* Initializes the lineage graph.
*
* @param {integer} timeOffset The time offset of the server
*/
init: function (timeOffset) {
serverTimeOffset = timeOffset;
$('#status-history-refresh-button').click(function () {
var statusHistory = $('#status-history-dialog').data('status-history');
if (statusHistory !== null) {
if (statusHistory.type === config.type.processor) {
nfStatusHistory.showProcessorChart(statusHistory.groupId, statusHistory.id, statusHistory.selectedDescriptor);
} else if (statusHistory.type === config.type.processGroup) {
nfStatusHistory.showProcessGroupChart(statusHistory.groupId, statusHistory.id, statusHistory.selectedDescriptor);
} else if (statusHistory.type === config.type.remoteProcessGroup) {
nfStatusHistory.showRemoteProcessGroupChart(statusHistory.groupId, statusHistory.id, statusHistory.selectedDescriptor);
} else if (statusHistory.type === config.type.node) {
nfStatusHistory.showNodeChart();
} else {
nfStatusHistory.showConnectionChart(statusHistory.groupId, statusHistory.id, statusHistory.selectedDescriptor);
}
}
});
// configure the dialog and make it draggable
$('#status-history-dialog').modal({
scrollableContentStyle: 'scrollable',
headerText: "Status History",
buttons: [{
buttonText: 'Close',
color: {
base: '#728E9B',
hover: '#004849',
text: '#ffffff'
},
handler: {
click: function () {
this.modal('hide');
}
}
}],
handler: {
close: function () {
// remove the current status history
$('#status-history-dialog').removeData('status-history');
// reset the dom
$('#status-history-chart-container').empty();
$('#status-history-chart-control-container').empty();
$('#status-history-details').empty();
// clear the extent and selected descriptor
brushSelection = null;
descriptor = null;
instances = null;
},
open: updateChart
}
});
$(window).on('resize', function (e) {
if (e.target === window) {
updateChart();
}
nfCommon.toggleScrollable($('#status-history-details').get(0));
})
},
/**
* Shows the status history for the specified connection in this instance.
*
* @param {type} groupId
* @param {type} connectionId
* @param {type} selectedDescriptor
*/
showConnectionChart: function (groupId, connectionId, selectedDescriptor) {
$.ajax({
type: 'GET',
url: config.urls.api + '/flow/connections/' + encodeURIComponent(connectionId) + '/status/history',
dataType: 'json'
}).done(function (response) {
handleStatusHistoryResponse(groupId, connectionId, response.statusHistory, config.type.connection, selectedDescriptor);
}).fail(nfErrorHandler.handleAjaxError);
},
/**
* Shows the status history for the specified processor in this instance.
*
* @param {type} groupId
* @param {type} processorId
* @param {type} selectedDescriptor
*/
showProcessorChart: function (groupId, processorId, selectedDescriptor) {
$.ajax({
type: 'GET',
url: config.urls.api + '/flow/processors/' + encodeURIComponent(processorId) + '/status/history',
dataType: 'json'
}).done(function (response) {
handleStatusHistoryResponse(groupId, processorId, response.statusHistory, config.type.processor, selectedDescriptor);
}).fail(nfErrorHandler.handleAjaxError);
},
/**
* Shows the status history for the node.
*/
showNodeChart: function () {
$.ajax({
type: 'GET',
url: config.urls.api + '/controller/status/history',
dataType: 'json'
}).done(function (response) {
handleNodeStatusHistoryResponse(response.statusHistory);
}).fail(nfErrorHandler.handleAjaxError);
},
/**
* Shows the status history for the specified process group in this instance.
*
* @param {type} groupId
* @param {type} processGroupId
* @param {type} selectedDescriptor
*/
showProcessGroupChart: function (groupId, processGroupId, selectedDescriptor) {
$.ajax({
type: 'GET',
url: config.urls.api + '/flow/process-groups/' + encodeURIComponent(processGroupId) + '/status/history',
dataType: 'json'
}).done(function (response) {
handleStatusHistoryResponse(groupId, processGroupId, response.statusHistory, config.type.processGroup, selectedDescriptor);
}).fail(nfErrorHandler.handleAjaxError);
},
/**
* Shows the status history for the specified remote process group in this instance.
*
* @param {type} groupId
* @param {type} remoteProcessGroupId
* @param {type} selectedDescriptor
*/
showRemoteProcessGroupChart: function (groupId, remoteProcessGroupId, selectedDescriptor) {
$.ajax({
type: 'GET',
url: config.urls.api + '/flow/remote-process-groups/' + encodeURIComponent(remoteProcessGroupId) + '/status/history',
dataType: 'json'
}).done(function (response) {
handleStatusHistoryResponse(groupId, remoteProcessGroupId, response.statusHistory, config.type.remoteProcessGroup, selectedDescriptor);
}).fail(nfErrorHandler.handleAjaxError);
}
};
return nfStatusHistory;
}));