1423 lines
33 KiB
JavaScript
1423 lines
33 KiB
JavaScript
"use strict";
|
|
|
|
var sysStat,
|
|
sockets = {},
|
|
pageIndex = 1,
|
|
pageLoaded,
|
|
procAnimated,
|
|
procs,
|
|
prevProcs,
|
|
tmps = {},
|
|
eles = {},
|
|
NSP = {
|
|
SYS: '/system',
|
|
LOG: '/log',
|
|
PROCESS: '/proccess'
|
|
},
|
|
SOCKET_EVENTS = {
|
|
ERROR: 'error',
|
|
CONNECT: 'connect',
|
|
CONNECT_ERROR: 'connect_error',
|
|
DATA: 'data',
|
|
DATA_PROCESSES: 'data.processes',
|
|
DATA_SYSTEM_STATS: 'data.sysstat',
|
|
DATA_PM2_VERSION: 'data.pm2version',
|
|
DATA_ACTION: 'data.action',
|
|
PULL_LOGS: 'pull.log',
|
|
PULL_PROCESS: 'pull.process',
|
|
PULL_ACTION: 'pull.action'
|
|
},
|
|
timer,
|
|
popupShown,
|
|
popupProc,
|
|
scrolled;
|
|
|
|
$(window).ready(function() {
|
|
if (!Array.isArray(GUI.connections) || GUI.connections.length == 0) {
|
|
info('No agent is online, can not start it.');
|
|
} else {
|
|
GUI.connection = GUI.connections[GUI.connections.length - 1];
|
|
}
|
|
prepareDOM();
|
|
initFullPage();
|
|
listenSocket();
|
|
renderFanavi();
|
|
});
|
|
/**
|
|
* Prepare DOM, cache elements, templates...
|
|
*/
|
|
function prepareDOM() {
|
|
eles = {
|
|
fpNav: $('#fp-nav'),
|
|
procs: $('.procs').eq(0),
|
|
procsHintContainer: $('.procs-hint-container').eq(0)
|
|
};
|
|
|
|
eles.procsHint = eles.procsHintContainer.find('div').eq(0);
|
|
eles.procsHintNum = eles.procsHintContainer.find('span').eq(0);
|
|
eles.procsAction = $('#procs_action');
|
|
|
|
// Enable/Disable when mouseenter/mouseleave processes list.
|
|
eles.procs.hover(function() {
|
|
!popupShown && setFPEnable(false, true);
|
|
}, function() {
|
|
!popupShown && setFPEnable(true, true);
|
|
});
|
|
|
|
tmps = {
|
|
proc: _.template($('#procTmp').html()),
|
|
noproc: $('#noProcTmp').html(),
|
|
popup: _.template($('#popupTmp').html())
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize fullPage plugin.
|
|
*/
|
|
function initFullPage() {
|
|
$('#fullpage').fullpage({
|
|
sectionsColor: ['#303552', '#3b4163'],
|
|
navigation: true,
|
|
navigationPosition: 'right',
|
|
navigationTooltips: ['System Stat', 'Processes'],
|
|
afterLoad: function() {
|
|
pageLoaded = true;
|
|
},
|
|
onLeave: function(index, nextIndex, direction) {
|
|
pageIndex = nextIndex;
|
|
pageLoaded = false;
|
|
|
|
if (nextIndex == 2) {
|
|
// Update processes' layout without animation.
|
|
updateProcsLayout(true);
|
|
|
|
if (!procAnimated) {
|
|
// Animate processes' layout with bounceInDown.
|
|
procAnimated = true;
|
|
animate(eles.procs, 'bounceInDown');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Disable fullPage.
|
|
setFPEnable(false);
|
|
}
|
|
|
|
/**
|
|
* Set fullPage enable or disable.
|
|
* @param {Boolean} enable
|
|
* @param {Boolean} unscrollable
|
|
*/
|
|
function setFPEnable(enable, unscrollable) {
|
|
$.fn.fullpage.setAllowScrolling(enable);
|
|
if (!unscrollable) {
|
|
$.fn.fullpage.setKeyboardScrolling(enable);
|
|
eles.fpNav[enable ? 'fadeIn' : 'fadeOut']();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connect to socket server.
|
|
*/
|
|
function connectSocketServer(ns) {
|
|
var uri = GUI.connection.value;
|
|
if (GUI.connection.short == 'localhost') {
|
|
uri = uri.replace(/^http:\/\/[^\?\/]+/, location.host);
|
|
}
|
|
var index = uri.indexOf('?'),
|
|
query = '';
|
|
if (index > 0) {
|
|
query = uri.slice(index);
|
|
uri = uri.slice(0, index);
|
|
}
|
|
|
|
uri = _.trimRight(uri, '/') + (ns || '') + query;
|
|
|
|
var socket = io.connect(uri, {
|
|
forceNew: true,
|
|
timeout: 3000
|
|
});
|
|
socket.on(SOCKET_EVENTS.ERROR, onError);
|
|
socket.on(SOCKET_EVENTS.CONNECT_ERROR, onError);
|
|
return socket;
|
|
}
|
|
|
|
/**
|
|
* Fires on error.
|
|
* @param {String} err
|
|
*/
|
|
function onError(err) {
|
|
if (err == 'unauthorized') {
|
|
err = 'There was an error with the authentication: ' + err;
|
|
} else {
|
|
err = 'Can not connect to the server due to ' + err;
|
|
}
|
|
info(err);
|
|
}
|
|
|
|
/**
|
|
* Initialize socket.io client and add listeners.
|
|
*/
|
|
function listenSocket() {
|
|
sockets._root = connectSocketServer();
|
|
sockets.sys = connectSocketServer(NSP.SYS);
|
|
// information from server.
|
|
sockets.sys.on(SOCKET_EVENTS.ERROR, info);
|
|
// processes
|
|
sockets.sys.on(SOCKET_EVENTS.DATA_PROCESSES, onProcsChange);
|
|
|
|
// The first time to request system state.
|
|
sockets.sys.on(SOCKET_EVENTS.DATA_SYSTEM_STATS, onSysStat);
|
|
|
|
function onSysStat(data) {
|
|
// Remove listen immediately.
|
|
sockets.sys.removeEventListener(SOCKET_EVENTS.DATA_SYSTEM_STATS, onSysStat);
|
|
|
|
// Store system states.
|
|
sysStat = data;
|
|
|
|
// Render polar chart.
|
|
polarUsage();
|
|
|
|
// Bind system information.
|
|
var tmp = _.template($('#sysInfoTmp').html());
|
|
$('.system-info').html(tmp({
|
|
data: {
|
|
cpu: sysStat.cpus.length,
|
|
arch: sysStat.arch,
|
|
uptime: fromNow(sysStat.uptime),
|
|
memory: getMem(sysStat.memory.total)
|
|
}
|
|
})).css('opacity', 0.01).show().animate({
|
|
opacity: 1,
|
|
marginTop: -40
|
|
});
|
|
|
|
// Enable fullPage.
|
|
setFPEnable(true);
|
|
|
|
// Remove loading.
|
|
$('.spinner').remove();
|
|
}
|
|
|
|
sockets.sys.on(SOCKET_EVENTS.DATA_PM2_VERSION, function(ver) {
|
|
$('.repo > span').text('PM2 v' + ver);
|
|
});
|
|
|
|
// Show alert when stopping process by pm_id failed.
|
|
sockets.sys.on(SOCKET_EVENTS.DATA_ACTION, function(id, errMsg) {
|
|
info(errMsg);
|
|
$('#proc_' + id).find('.proc-ops').find('.load').fadeOut(function() {
|
|
$(this).prev().fadeIn().end().fadeOut(function() {
|
|
$(this).remove();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render the fanavi component.
|
|
*/
|
|
function renderFanavi() {
|
|
if (GUI.readonly) {
|
|
return;
|
|
}
|
|
var icons = [{
|
|
icon: 'img/restart.png',
|
|
title: 'Restart All'
|
|
}, {
|
|
icon: 'img/stop.png',
|
|
title: 'Stop All'
|
|
}, {
|
|
icon: 'img/save.png',
|
|
title: 'Save All'
|
|
}, {
|
|
icon: 'img/delete.png',
|
|
title: 'Delete All'
|
|
}];
|
|
|
|
d3.menu('#procs_action')
|
|
.option({
|
|
backgroundColor: '#303552',
|
|
buttonForegroundColor: '#fff',
|
|
startAngle: -90,
|
|
endAngle: 90,
|
|
innerRadius: 36,
|
|
shadow: {
|
|
color: '#4e5786',
|
|
x: 1,
|
|
y: 1
|
|
},
|
|
iconSize: 24,
|
|
speed: 500,
|
|
hideTooltip: true
|
|
})
|
|
.load(icons)
|
|
.on('click', function(index, data) {
|
|
sockets.sys.emit(SOCKET_EVENTS.PULL_ACTION, ['restart', 'stop', 'save', 'delete'][index], 'all');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reset the status of navigator.
|
|
*/
|
|
function resetFanavi() {
|
|
var isVisible = eles.procsAction.is(':visible');
|
|
if (procs.data.length > 0 && !isVisible) {
|
|
eles.procsAction.css({
|
|
opacity: 0.01,
|
|
display: 'inherit'
|
|
}).stop().animate({
|
|
opacity: 1
|
|
});
|
|
} else if (procs.data.length == 0 && isVisible) {
|
|
eles.procsAction.stop().animate({
|
|
opacity: 0.01
|
|
}, function() {
|
|
$(this).css('display', 'none');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render polar charset of usage (CPU and memory).
|
|
*/
|
|
function polarUsage() {
|
|
if (!sysStat) {
|
|
return;
|
|
}
|
|
var width = 520,
|
|
height = 520,
|
|
radius = Math.min(width, height) / 2,
|
|
spacing = .15;
|
|
|
|
// Usage colors - green to red.
|
|
var color = d3.scale.linear()
|
|
.range(['hsl(-270,50%,50%)', 'hsl(0,50%,50%)'])
|
|
.interpolate(function(a, b) {
|
|
var i = d3.interpolateString(a, b);
|
|
return function(t) {
|
|
return d3.hsl(i(t));
|
|
};
|
|
});
|
|
|
|
// Transform percentage to angle.
|
|
var arc = d3.svg.arc()
|
|
.startAngle(0)
|
|
.endAngle(function(d) {
|
|
return d.value * 2 * Math.PI;
|
|
})
|
|
.innerRadius(function(d) {
|
|
return d.index * radius;
|
|
})
|
|
.outerRadius(function(d) {
|
|
return (d.index + spacing) * radius;
|
|
});
|
|
|
|
// Initialize polar.
|
|
$('.polar-usage').find('svg').remove();
|
|
var svg = d3.select('.polar-usage').style({
|
|
height: height + 'px',
|
|
width: width + 'px'
|
|
}).append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
.append('g')
|
|
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
|
|
|
|
// Text of hostname.
|
|
svg.append('text')
|
|
.attr('dy', 0)
|
|
.text(sysStat.hostname);
|
|
|
|
// Text of platform and release
|
|
svg.append('text')
|
|
.attr('dy', 20)
|
|
.style('fill', '#ccc')
|
|
.text(sysStat.platform + ' ' + sysStat.release);
|
|
|
|
// Initialize CPU and Memory fields.
|
|
var field = svg.selectAll('g')
|
|
.data(fields)
|
|
.enter().append('g');
|
|
|
|
field.append('path');
|
|
field.append('text');
|
|
|
|
// Render it.
|
|
d3.transition().duration(0).each(refresh);
|
|
|
|
// arcTween
|
|
function arcTween(d) {
|
|
var i = d3.interpolateNumber(d.previousValue, d.value);
|
|
return function(t) {
|
|
d.value = i(t);
|
|
return arc(d);
|
|
};
|
|
}
|
|
|
|
// Real-time.
|
|
function fields() {
|
|
return [{
|
|
index: .7,
|
|
text: 'CPU ' + sysStat.cpu + '%',
|
|
value: sysStat.cpu / 100
|
|
}, {
|
|
index: .4,
|
|
text: 'MEM ' + sysStat.memory.percentage + '%',
|
|
value: sysStat.memory.percentage / 100
|
|
}];
|
|
}
|
|
|
|
// Refresh system states.
|
|
function refresh() {
|
|
field = field
|
|
.each(function(d) {
|
|
this._value = d.value;
|
|
})
|
|
.data(fields)
|
|
.each(function(d) {
|
|
d.previousValue = this._value;
|
|
});
|
|
|
|
field.select('path')
|
|
.transition()
|
|
.ease('elastic')
|
|
.attrTween('d', arcTween)
|
|
.style('fill', function(d) {
|
|
return color(d.value);
|
|
});
|
|
|
|
field.select('text')
|
|
.attr('dy', function(d) {
|
|
return d.value < .5 ? '0' : '10px';
|
|
})
|
|
.text(function(d) {
|
|
return d.text;
|
|
})
|
|
.transition()
|
|
.ease('elastic')
|
|
.attr('transform', function(d) {
|
|
return 'rotate(' + 360 * d.value + ') ' +
|
|
'translate(0,' + -(d.index + spacing / 2) * radius + ') ' +
|
|
'rotate(' + (d.value < .5 ? -90 : 90) + ')'
|
|
});
|
|
}
|
|
|
|
// When receiving data from server, refresh polar.
|
|
sockets.sys.on(SOCKET_EVENTS.DATA_SYSTEM_STATS, function(data) {
|
|
if (pageIndex != 1) {
|
|
return;
|
|
}
|
|
var changed = sysStat.cpu != data.cpu || sysStat.memory.percentage != data.memory.percentage;
|
|
sysStat = data;
|
|
|
|
changed && refresh();
|
|
});
|
|
|
|
addChooser({
|
|
width: width,
|
|
height: height,
|
|
radius: radius
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add server chooser to the UI.
|
|
*/
|
|
function addChooser(options) {
|
|
if (addChooser._added == true || !Array.isArray(GUI.connections) || GUI.connections.length == 1) {
|
|
return;
|
|
}
|
|
addChooser._added = true;
|
|
var width = 100,
|
|
height = 30,
|
|
style = {
|
|
width: 100,
|
|
height: height,
|
|
left: (options.width - width) / 2,
|
|
top: (options.height - height) / 2 + 50
|
|
};
|
|
|
|
var chooser = $('<div>', {
|
|
'class': 'chooser dropdown',
|
|
css: style
|
|
});
|
|
|
|
var conns = _.clone(GUI.connections);
|
|
if (conns[conns.length - 1].short == 'localhost') {
|
|
conns.splice(conns.length - 1, 0, '-');
|
|
}
|
|
var html = '<button id="dropdownChooser" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"><i class="glyphicon glyphicon-random"></i> CHANGE <span class="caret"></span></button>';
|
|
html += '<ul class="dropdown-menu" aria-labelledby="dropdownChooser">';
|
|
conns.forEach(function(conn) {
|
|
if (conn == '-') {
|
|
html += '<li role = "separator" class="divider"></li>';
|
|
} else {
|
|
html += '<li ' + (GUI.connection.value == conn.value ? 'class="active"' : '') + '><a href="javascript:void(0);" data-value="' + conn.value + '" data-short="' + conn.short + '">' + conn.name + '</a></li>';
|
|
}
|
|
});
|
|
html += '</ul>';
|
|
chooser.html(html);
|
|
chooser.on('click', 'a', function() {
|
|
var ele = $(this),
|
|
val = ele.data('value');
|
|
if (val && val != GUI.connection.value) {
|
|
GUI.connection = {
|
|
name: ele.text(),
|
|
value: val,
|
|
short: ele.data('short')
|
|
};
|
|
chooser.find('li.active').removeClass('active');
|
|
ele.parent().addClass('active');
|
|
changeConnection();
|
|
}
|
|
}).on('show.bs.dropdown', function() {
|
|
setFPEnable(false, false);
|
|
}).on('hide.bs.dropdown', function() {
|
|
setFPEnable(true, false);
|
|
});
|
|
chooser.appendTo('.polar-usage');
|
|
}
|
|
|
|
/**
|
|
* Change the connection.
|
|
* @param {[type]} connection [description]
|
|
* @return {[type]} [description]
|
|
*/
|
|
function changeConnection(connection) {
|
|
for (var ns in sockets) {
|
|
sockets[ns].disconnect();
|
|
sockets[ns].io.close();
|
|
delete sockets[ns];
|
|
}
|
|
|
|
listenSocket();
|
|
}
|
|
|
|
/**
|
|
* Be triggered after processes have been changed.
|
|
* @param _procs
|
|
*/
|
|
function onProcsChange(_procs) {
|
|
// Stora processes.
|
|
procs = {
|
|
data: _procs.filter(function(p) {
|
|
return !!p;
|
|
}),
|
|
tick: Date.now()
|
|
};
|
|
|
|
// Compare ticks to make sure there has any change.
|
|
var isProcsChanged = eles.procsHint.data('tick') != procs.tick;
|
|
|
|
if (!isProcsChanged) {
|
|
return;
|
|
}
|
|
|
|
if (pageIndex == 1) {
|
|
// Update processes count only on fullPage 1 (with animation).
|
|
updateProcsCount(true);
|
|
} else if (pageIndex == 2) {
|
|
// Update processes count on fullPage 1 (without animation).
|
|
updateProcsCount();
|
|
// Update processes' layout on fullPage 2.
|
|
updateProcsLayout();
|
|
}
|
|
|
|
resetFanavi();
|
|
}
|
|
|
|
/**
|
|
* Update processes count.
|
|
* @param {Boolean} withAnimation
|
|
*/
|
|
function updateProcsCount(withAnimation) {
|
|
var len = procs.data.length;
|
|
// If there has no change, return it.
|
|
if (eles.procsHintContainer.data('count') == len) {
|
|
return;
|
|
}
|
|
// Store count to element.
|
|
eles.procsHintContainer.data('count', len).removeClass('hide');
|
|
// Reset count.
|
|
eles.procsHintNum.text(len);
|
|
// Shake it if necessary.
|
|
withAnimation && animate(eles.procsHint, 'shake');
|
|
}
|
|
|
|
/**
|
|
* Update the processes' layout.
|
|
* @param {Boolean} noAnimation
|
|
* @returns {*}
|
|
*/
|
|
function updateProcsLayout(noAnimation) {
|
|
// If has no change, return it.
|
|
if (!procs || eles.procs.data('tick') == procs.tick) {
|
|
return cloneProcs();
|
|
}
|
|
|
|
// Store tick.
|
|
eles.procs.data('tick', procs.tick);
|
|
|
|
// Has process or not.
|
|
var noprocRendered = eles.procs.data('empty'),
|
|
isEmpty = eles.procs.is(':empty');
|
|
|
|
// If there has no process.
|
|
if (procs.data.length == 0) {
|
|
// And the `empty tip` is rendered, return it.
|
|
if (noprocRendered) {
|
|
return cloneProcs();
|
|
}
|
|
// It's an empty list.
|
|
eles.procs.data('empty', true);
|
|
|
|
// destroy slimScroll if necessary.
|
|
destroySlimScroll();
|
|
|
|
// Render `empty tip`.
|
|
$(tmps.noproc).prependTo(eles.procs);
|
|
|
|
// Remove previous processes list.
|
|
if (!isEmpty) {
|
|
!noAnimation && animate(eles.procs, 'flip');
|
|
eles.procs.find('.proc,.proc-div').not('.proc-empty').remove();
|
|
}
|
|
return cloneProcs();
|
|
}
|
|
|
|
// If there have processes and the `empty tip` is rendered.
|
|
if (noprocRendered) {
|
|
// Remove empty data.
|
|
eles.procs.removeData('empty');
|
|
// Create processes' layout.
|
|
createProcs(procs.data, noprocRendered, noAnimation);
|
|
return cloneProcs();
|
|
}
|
|
|
|
// If there has no process and never render `empty tip`.
|
|
if (isEmpty) {
|
|
// Create processes' layout.
|
|
createProcs(procs.data, noprocRendered, noAnimation);
|
|
return cloneProcs();
|
|
}
|
|
|
|
// Read existing processes' Uids.
|
|
var rps = [];
|
|
eles.procs.find('div.proc').each(function() {
|
|
rps.push(parseInt(this.id.substr(5)));
|
|
});
|
|
// Processes that waiting to be created.
|
|
var cps = procs.data;
|
|
// Processes that should be deleted.
|
|
var dps = _.difference(rps, cps.map(function(p) {
|
|
return p.pm_id;
|
|
}));
|
|
// Processes that should be updated.
|
|
var ups = [];
|
|
|
|
// Remove the processes to be deleted.
|
|
rps = _.difference(rps, dps);
|
|
|
|
if (rps.length > 0) {
|
|
// Remove existing.
|
|
cps = cps.filter(function(p) {
|
|
return !~rps.indexOf(p.pm_id);
|
|
});
|
|
|
|
// Compare with previous processes to grep `ups`.
|
|
if (prevProcs) {
|
|
rps.forEach(function(pm_id) {
|
|
var proc1 = _.find(prevProcs.data, function(p) {
|
|
return p.pm_id == pm_id;
|
|
}),
|
|
proc2 = _.find(procs.data, function(p) {
|
|
return p.pm_id == pm_id;
|
|
});
|
|
|
|
if (proc1 && proc2 &&
|
|
(proc1.pm2_env.status != proc2.pm2_env.status ||
|
|
proc1.pm2_env.pm_uptime != proc2.pm2_env.pm_uptime ||
|
|
proc1.pm2_env.restart_time != proc2.pm2_env.restart_time)) {
|
|
ups.push(proc2);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var animated = false;
|
|
// Create.
|
|
if (cps.length > 0) {
|
|
animated = true;
|
|
createProcs(cps, noprocRendered, noAnimation);
|
|
}
|
|
// Delete
|
|
if (dps.length > 0) {
|
|
removeProcs(dps, animated || noAnimation);
|
|
animated = true;
|
|
}
|
|
// Update
|
|
if (ups.length > 0) {
|
|
updateProcs(ups, animated || noAnimation);
|
|
}
|
|
cloneProcs();
|
|
}
|
|
|
|
/**
|
|
* Create processes' layout.
|
|
* @param {Array} _procs
|
|
* @param {Boolean} noproc `empty tip` is rendered before.
|
|
* @param {Boolean} noAnimation
|
|
*/
|
|
function createProcs(_procs, noproc, noAnimation) {
|
|
var html = '';
|
|
_.sortBy(_procs, 'pm_id').forEach(function(p, i) {
|
|
html += tmps.proc({
|
|
proc: p,
|
|
noDiv: false,
|
|
index: i
|
|
});
|
|
});
|
|
$(html).appendTo(eles.procs)
|
|
// Attach events of process.
|
|
attachProcEvents();
|
|
|
|
// Flip in if necessary.
|
|
!noAnimation && flipProcs();
|
|
|
|
// Remove `empty tip` if necessary.
|
|
noproc && eles.procs.find('.proc-empty').remove();
|
|
|
|
// slimScroll if processes length is greater than 10.
|
|
if (eles.procs.find('div.proc').length > 10) {
|
|
if (eles.procs.data('slimScroll')) {
|
|
return;
|
|
}
|
|
eles.procs.data('slimScroll', true);
|
|
eles.procs.slimScroll({
|
|
height: '600px',
|
|
width: '720px',
|
|
color: '#fff',
|
|
opacity: 0.8,
|
|
railVisible: true,
|
|
railColor: '#fff'
|
|
});
|
|
} else {
|
|
destroySlimScroll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update processes' layout.
|
|
* @param {Array} _procs
|
|
* @param {Boolean} noAnimation
|
|
*/
|
|
function updateProcs(_procs, noAnimation) {
|
|
// Find elements and replace them new ones.
|
|
eles.procs.find(_procs.map(function(p) {
|
|
return '#proc_' + p.pm_id;
|
|
}).join(',')).each(function(i) {
|
|
var ele = $(this),
|
|
placement = ele.find('.proc-ops i').eq(0).data('placement'),
|
|
_id = parseInt(ele.attr('id').substr(5)),
|
|
proc = _.find(_procs, function(p) {
|
|
return p.pm_id == _id;
|
|
});
|
|
|
|
// HTML
|
|
var procHTML = tmps.proc({
|
|
proc: proc,
|
|
noDiv: true,
|
|
index: placement !== 'top' ? 0 : 1
|
|
});
|
|
var procEle = $(procHTML);
|
|
procEle.data({
|
|
'event-avgrund': null,
|
|
'event-click': null
|
|
});
|
|
|
|
var ele = $(this);
|
|
// Animate it or not.
|
|
if (!noAnimation) {
|
|
animate(ele, 'flipOutX', function() {
|
|
ele.replaceWith(procEle);
|
|
attachProcEvents();
|
|
animate(procEle, 'flipInX', startTimer);
|
|
});
|
|
} else {
|
|
ele.replaceWith(procEle);
|
|
attachProcEvents();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove processes from layout.
|
|
* @param {Array} pm_ids pm_ids of processes.
|
|
* @param {Boolean} noAnimation
|
|
*/
|
|
function removeProcs(pm_ids, noAnimation) {
|
|
// Find elements and remove them directly.
|
|
eles.procs.find(pm_ids.map(function(id) {
|
|
return '#proc_' + id;
|
|
}).join(',')).each(function() {
|
|
var ele = $(this);
|
|
ele.next().remove();
|
|
ele.remove();
|
|
});
|
|
|
|
// Flip it if necessary.
|
|
!noAnimation && flipProcs();
|
|
|
|
// Destroy slimScroll if necessary.
|
|
if (eles.procs.find('div.proc').length <= 10) {
|
|
destroySlimScroll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clone processes and count uptime from now.
|
|
*/
|
|
function cloneProcs() {
|
|
// Clone processes.
|
|
prevProcs = _.clone(procs);
|
|
|
|
// Timer of uptime.
|
|
startTimer();
|
|
}
|
|
|
|
/**
|
|
* Timer of uptime.
|
|
*/
|
|
function startTimer() {
|
|
timer && clearTimeout(timer);
|
|
updateUptime();
|
|
}
|
|
|
|
/**
|
|
* Update the uptimes of processes.
|
|
*/
|
|
function updateUptime() {
|
|
var spans = eles.procs.find('span[data-ctime][data-running=YES]');
|
|
if (spans.length == 0) {
|
|
return;
|
|
}
|
|
var now = Date.now();
|
|
spans.each(function() {
|
|
var ele = $(this);
|
|
ele.text(fromNow(Math.ceil((now - ele.data('ctime')) / 1000), true));
|
|
});
|
|
|
|
// Only do this job on fullPage 2.
|
|
(pageIndex == 2) && (timer = setTimeout(updateUptime, 1000));
|
|
}
|
|
|
|
/**
|
|
* Flip processes' layout.
|
|
*/
|
|
function flipProcs() {
|
|
var p = eles.procs.parent();
|
|
animate(p.hasClass('slimScrollDiv') ? p : eles.procs, 'flip');
|
|
}
|
|
|
|
/**
|
|
* Destroy slimScroll of processes' layout.
|
|
*/
|
|
function destroySlimScroll() {
|
|
if (!eles.procs.data('slimScroll')) {
|
|
return;
|
|
}
|
|
eles.procs.slimScroll({
|
|
destroy: true
|
|
});
|
|
eles.procs.data('slimScroll', false).css('height', 'auto');
|
|
}
|
|
|
|
/**
|
|
* Attach events to process layout.
|
|
*/
|
|
function attachProcEvents() {
|
|
bindPopup();
|
|
procEvents();
|
|
}
|
|
|
|
/**
|
|
* Bind process events.
|
|
*/
|
|
function procEvents() {
|
|
eles.procs.find('.proc-ops i').each(function() {
|
|
var ele = $(this);
|
|
if (ele.data('event-click') == 'BOUND') {
|
|
return;
|
|
}
|
|
ele.data('event-click', 'BOUND').click(function() {
|
|
var ele = $(this),
|
|
method = (ele.data('original-title') || ele.attr('title')).toLowerCase(),
|
|
pm_id = parseInt(ele.closest('.proc').attr('id').substr(5));
|
|
|
|
var ops = ele.closest('.proc-ops');
|
|
$('<div class="load"></div>').css({
|
|
opacity: 0.01
|
|
}).appendTo(ops);
|
|
|
|
ops.find('ul').fadeOut().next().animate({
|
|
opacity: 1
|
|
});
|
|
|
|
sockets.sys.emit(SOCKET_EVENTS.PULL_ACTION, method, pm_id);
|
|
});
|
|
});
|
|
eles.procs.find('[data-toggle="tooltip"]').tooltip({
|
|
container: 'body'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Popup dialog to display full information of processes.
|
|
* @param {jQuery} o
|
|
*/
|
|
function bindPopup(o) {
|
|
eles.procs.find('.proc-name').each(function() {
|
|
var ele = $(this);
|
|
if (ele.data('event-avgrund') == 'BOUND') {
|
|
return;
|
|
}
|
|
ele.data('event-avgrund', 'BOUND').avgrund({
|
|
width: 640,
|
|
height: 350,
|
|
showClose: true,
|
|
holderClass: 'proc-popup',
|
|
showCloseText: 'CLOSE',
|
|
onBlurContainer: '.section',
|
|
onLoad: function(ele) {
|
|
if (popupShown) {
|
|
return;
|
|
}
|
|
popupShown = true;
|
|
setFPEnable(false, false);
|
|
showPopupTab(getProcByEle(ele));
|
|
},
|
|
onUnload: function(ele) {
|
|
if (!popupShown) {
|
|
return;
|
|
}
|
|
scrolled = false;
|
|
popupShown = false;
|
|
setFPEnable(true, false);
|
|
destroyTail();
|
|
destroyMonitor();
|
|
popupProc = null;
|
|
},
|
|
template: '<div id="popup"><div class="load"></div></div>'
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reset tabcontent of popup.
|
|
* @param {Object} proc
|
|
* @returns {*}
|
|
*/
|
|
function showPopupTab(proc, delayed) {
|
|
if (!proc) {
|
|
return info('Process does not exist, try to refresh current page manually (F5 or COMMAND+R)');
|
|
}
|
|
// Do this after popup is shown.
|
|
if (!delayed) {
|
|
return setTimeout(showPopupTab, 800, proc, true);
|
|
}
|
|
|
|
// Resort keys.
|
|
var clonedProc = {};
|
|
_.sortBy(Object.keys(proc)).forEach(function(key) {
|
|
// Omit memory, just keep the original data.
|
|
if (key == 'monit') {
|
|
var monit = proc[key];
|
|
monit.memory = getMem(monit.memory);
|
|
return clonedProc[key] = monit;
|
|
}
|
|
clonedProc[key] = proc[key];
|
|
});
|
|
|
|
// Reset content HTML.
|
|
var popup = $('#popup').html(tmps.popup({
|
|
info: highlight(clonedProc)
|
|
}));
|
|
// Find tabcontent.
|
|
var tabContent = popup.find('.tab-content').eq(0);
|
|
// Bind slimScroll.
|
|
tabContent.slimScroll({
|
|
height: '300px',
|
|
color: '#000',
|
|
opacity: 0.8,
|
|
railVisible: true,
|
|
railColor: '#f0f0f0'
|
|
});
|
|
|
|
// Bing tab change event.
|
|
popup.find('li').click(function() {
|
|
var ele = $(this);
|
|
if (ele.hasClass('active')) {
|
|
return;
|
|
}
|
|
|
|
// Scroll to y: 0
|
|
tabContent.slimScroll({
|
|
scrollTo: 0
|
|
});
|
|
|
|
var tab = $(this).text().trim();
|
|
|
|
if (tab != 'Log') {
|
|
destroyTail();
|
|
$('#log').html('<div class="load"></div>');
|
|
scrolled = false;
|
|
popupProc = null;
|
|
}
|
|
|
|
if (tab != 'Monitor') {
|
|
destroyMonitor();
|
|
$('#monitor').html('<div class="load"></div>');
|
|
popupProc = null;
|
|
}
|
|
|
|
// Tail logs.
|
|
if (tab == 'Log') {
|
|
popupProc = proc;
|
|
return tailLogs();
|
|
}
|
|
if (tab == 'Monitor') {
|
|
popupProc = proc;
|
|
return monitorProc();
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Tail log of process
|
|
* @returns {*}
|
|
*/
|
|
function tailLogs() {
|
|
if (!popupProc) {
|
|
$('#log').html('<span style="color:#ff0000">Process does not exist.</span>')
|
|
return;
|
|
}
|
|
if (!sockets.log) {
|
|
sockets.log = connectSocketServer(NSP.LOG);
|
|
sockets.log.on(SOCKET_EVENTS.ERROR, appendLogs);
|
|
sockets.log.on(SOCKET_EVENTS.DATA, appendLogs);
|
|
sockets.log.on(SOCKET_EVENTS.CONNECT, function() {
|
|
sockets.log.emit(SOCKET_EVENTS.PULL_LOGS, popupProc.pm_id);
|
|
});
|
|
} else {
|
|
sockets.log.connect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append logs to DOM.
|
|
* @param {Object} log
|
|
*/
|
|
function appendLogs(log) {
|
|
// Check process and pm_id should be equalled.
|
|
if (!popupProc || popupProc.pm_id != log.pm_id) {
|
|
return;
|
|
}
|
|
|
|
// Remove `loading` status.
|
|
$('#log>.load').remove();
|
|
|
|
var lo = $('#log'),
|
|
loDom = lo.get(0);
|
|
|
|
var offset = loDom.scrollHeight - 300,
|
|
poffset = lo.parent().scrollTop() || 0,
|
|
scrollable = false;
|
|
|
|
// Scroll down if necessary.
|
|
if (!scrolled || poffset >= offset - 30) {
|
|
!scrolled && (scrolled = poffset < offset - 30);
|
|
scrollable = true;
|
|
}
|
|
$(log.msg || log.error).appendTo(lo);
|
|
|
|
if (scrollable) {
|
|
lo.parent().slimScroll({
|
|
scrollTo: loDom.scrollHeight - 300
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy tail socket.
|
|
*/
|
|
function destroyTail() {
|
|
if (!sockets.log) {
|
|
return;
|
|
}
|
|
sockets.log.disconnect();
|
|
}
|
|
|
|
/**
|
|
* Monitor the memory && CPU usage of process.
|
|
*/
|
|
function monitorProc() {
|
|
if (!popupProc || popupProc.pid == 0) {
|
|
$('#monitor').html('<span style="color:#ff0000">Process does not exist or is not running.</span>')
|
|
return;
|
|
}
|
|
if (!sockets.proc) {
|
|
sockets.proc = connectSocketServer(NSP.PROCESS);
|
|
sockets.proc.on(SOCKET_EVENTS.ERROR, appendData);
|
|
sockets.proc.on(SOCKET_EVENTS.DATA, appendData);
|
|
sockets.proc.on(SOCKET_EVENTS.CONNECT, function() {
|
|
sockets.proc.emit(SOCKET_EVENTS.PULL_PROCESS, popupProc.pid);
|
|
});
|
|
} else {
|
|
sockets.proc.connect();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append data to lineChart.
|
|
* @param proc
|
|
*/
|
|
function appendData(proc) {
|
|
if (!popupProc || popupProc.pid != proc.pid) {
|
|
return;
|
|
}
|
|
var loadEl = $('#monitor>.load');
|
|
if (lineChart.data.length == 0) {
|
|
var now = proc.time || Date.now(),
|
|
len = lineChart.settings.queueLength;
|
|
|
|
lineChart.data = d3.range(len).map(function(n) {
|
|
return {
|
|
time: now - (len - n) * 3000,
|
|
usage: {
|
|
cpu: 0,
|
|
memory: 0
|
|
}
|
|
};
|
|
});
|
|
}
|
|
// handle error
|
|
if (proc.error) {
|
|
delete proc.error;
|
|
proc.time = Date.now();
|
|
proc.usage = {
|
|
cpu: 0,
|
|
memory: 0
|
|
};
|
|
}
|
|
lineChart.data.push(proc);
|
|
if (loadEl.length > 0) {
|
|
loadEl.remove();
|
|
lineChart.next();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy monitor socket.
|
|
*/
|
|
function destroyMonitor() {
|
|
if (!sockets.proc) {
|
|
return;
|
|
}
|
|
sockets.proc.disconnect();
|
|
lineChart.destroy();
|
|
}
|
|
|
|
/**
|
|
* Get process by Uid span element.
|
|
* @param {jQuery} ele
|
|
* @returns {*}
|
|
*/
|
|
function getProcByEle(ele) {
|
|
var id = parseInt(ele.data('pmid'));
|
|
return _.find(procs.data, function(p) {
|
|
return p.pm_id == id;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Animate element with animation from animate.css
|
|
* @param {jQuery} o element
|
|
* @param {String} a animation name
|
|
* @param {Function} cb callback
|
|
*/
|
|
function animate(o, a, cb) {
|
|
a += ' animated';
|
|
o.removeClass(a).addClass(a).one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
|
|
var ele = $(this);
|
|
ele.removeClass(a)
|
|
cb && cb.call(ele);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show sticky information
|
|
* @param {String} msg
|
|
*/
|
|
function info(msg) {
|
|
if (msg instanceof Error) {
|
|
msg = msg.message;
|
|
}
|
|
if (_.isObject(msg)) {
|
|
msg = msg.error
|
|
}
|
|
$.sticky({
|
|
body: msg,
|
|
icon: './img/info.png',
|
|
useAnimateCss: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wrap memory.
|
|
* @param {Float} mem
|
|
* @returns {string}
|
|
*/
|
|
function getMem(mem) {
|
|
if (typeof mem == 'string') {
|
|
return mem;
|
|
}
|
|
|
|
if (mem < 1024) {
|
|
return mem + 'B';
|
|
}
|
|
if (mem < 1048576) {
|
|
return Math.round(mem / 1024) + 'K';
|
|
}
|
|
if (mem < 1073741824) {
|
|
return Math.round(mem / 1048576) + 'M';
|
|
}
|
|
return Math.round(mem / 1073741824) + 'G';
|
|
}
|
|
|
|
/**
|
|
* Wrap tick from now.
|
|
* @param {Float} tick
|
|
* @param {Boolean} tiny show all of it.
|
|
* @returns {string}
|
|
*/
|
|
function fromNow(tick, tiny) {
|
|
if (tick < 60) {
|
|
return tick + 's';
|
|
}
|
|
var s = tick % 60 + 's';
|
|
if (tick < 3600) {
|
|
return parseInt(tick / 60) + 'm ' + s;
|
|
}
|
|
var m = parseInt((tick % 3600) / 60) + 'm ';
|
|
if (tick < 86400) {
|
|
return parseInt(tick / 3600) + 'h ' + m + (!tiny ? '' : s);
|
|
}
|
|
var h = parseInt((tick % 86400) / 3600) + 'h ';
|
|
return parseInt(tick / 86400) + 'd ' + h + (!tiny ? '' : m + s);
|
|
}
|
|
|
|
/**
|
|
* Hightlight JSON
|
|
* @param {JSON} data
|
|
* @param {Int} indent
|
|
* @returns {string}
|
|
*/
|
|
function highlight(data, indent) {
|
|
indent = indent || 2;
|
|
|
|
data = JSON.stringify(typeof data != 'string' ? data : JSON.parse(data), undefined, indent);
|
|
|
|
[
|
|
[/&/g, '&'],
|
|
[/</g, '<'],
|
|
[/>/g, '>']
|
|
].forEach(function(rep) {
|
|
data = String.prototype.replace.apply(data, rep);
|
|
});
|
|
|
|
return data.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(m) {
|
|
var color = '1e297e';
|
|
if (/^"/.test(m)) {
|
|
color = ['440a4d', '0d660a'][/:$/.test(m) ? 0 : 1];
|
|
} else if (/true|false/.test(m)) {
|
|
color = '1e297e';
|
|
} else if (/null|undefined/.test(m)) {
|
|
color = '14193c';
|
|
}
|
|
return '<span style="color: #' + color + '">' + m + '</span>';
|
|
}).replace(/\n/, '<br />');
|
|
};
|
|
|
|
/**
|
|
* Line chart represents memory / CPU usage.
|
|
*/
|
|
var lineChart = {
|
|
settings: {
|
|
id: '#monitor',
|
|
width: 580,
|
|
height: 270,
|
|
ticks: 5,
|
|
tension: 0.8,
|
|
padding: 10,
|
|
queueLength: 20,
|
|
transitionDelay: 3000,
|
|
fancyDelay: 1000,
|
|
tickFormat: '%H:%M:%S',
|
|
series: ['cpu', 'memory'],
|
|
colors: {
|
|
line: {
|
|
cpu: 'rgba(0, 200, 0, 1)',
|
|
memory: 'rgba(200, 200, 0, 1)'
|
|
},
|
|
dot: '#ff5400'
|
|
}
|
|
},
|
|
data: [],
|
|
eles: {},
|
|
destroy: function() {
|
|
this.eles.path && this.eles.path.interrupt().transition();
|
|
this.eles.xAxis && this.eles.xAxis.interrupt().transition();
|
|
d3.timer.flush();
|
|
this.data = [];
|
|
this.eles = {};
|
|
},
|
|
next: function(forceQuit) {
|
|
var ng = !this.eles.svg;
|
|
if (ng && forceQuit) {
|
|
return;
|
|
}
|
|
|
|
if (ng) {
|
|
this._graph();
|
|
}
|
|
var st = this.settings;
|
|
|
|
if (this.data.length < st.queueLength) {
|
|
return;
|
|
}
|
|
|
|
this.eles.path.attr('transform', 'translate(0, ' + st.padding + ')');
|
|
this.eles.xAxis.call(this.eles.x.axis);
|
|
|
|
this.eles.x.domain([this.data[1].time, this.data[st.queueLength - 1].time]);
|
|
|
|
st.series.forEach(function(key) {
|
|
lineChart.eles[key + 'LineEl']
|
|
.attr('d', lineChart.eles[key + 'Line'])
|
|
.attr('transform', null);
|
|
});
|
|
|
|
if (ng) {
|
|
return setTimeout(function(ctx) {
|
|
ctx.next(true);
|
|
}, 10, this);
|
|
}
|
|
|
|
this.eles.path
|
|
.transition()
|
|
.duration(st.transitionDelay)
|
|
.ease('linear')
|
|
.attr('transform', 'translate(' + this.eles.x(this.data[0].time) + ', ' + st.padding + ')')
|
|
.each('end', function() {
|
|
lineChart.next(true);
|
|
});
|
|
|
|
this.eles.xAxis.transition()
|
|
.duration(st.transitionDelay)
|
|
.ease('basic')
|
|
.call(this.eles.x.axis);
|
|
|
|
this.data.shift();
|
|
},
|
|
_graph: function() {
|
|
var st = this.settings;
|
|
st.gWidth = st.width;
|
|
st.gHeight = st.height - 50;
|
|
|
|
var series = '<ul>';
|
|
st.series.forEach(function(key) {
|
|
series += '<li style="color:' + st.colors.line[key] + '">' + key + '</li>';
|
|
});
|
|
series += '</ul>';
|
|
|
|
$(series).appendTo(st.id);
|
|
|
|
this.eles.x = d3.time
|
|
.scale()
|
|
.range([0, st.gWidth]);
|
|
|
|
this.eles.x.axis = d3.svg.axis()
|
|
.scale(this.eles.x)
|
|
.tickFormat(d3.time.format(st.tickFormat))
|
|
.ticks(st.ticks)
|
|
.orient('bottom');
|
|
|
|
this.eles.y = d3.scale
|
|
.linear()
|
|
.domain([0, 100])
|
|
.range([st.gHeight, 0])
|
|
.clamp(true);
|
|
|
|
this.eles.y.axis = d3.svg
|
|
.axis()
|
|
.scale(this.eles.y)
|
|
.orient('right')
|
|
.ticks(st.ticks);
|
|
|
|
this.eles.svg = d3
|
|
.select(lineChart.settings.id)
|
|
.append('svg')
|
|
.attr('width', st.width)
|
|
.attr('height', st.height);
|
|
|
|
this.eles.svg.append('defs').append('clipPath')
|
|
.attr('id', 'clip')
|
|
.append('rect')
|
|
.attr('width', st.gWidth)
|
|
.attr('height', st.height);
|
|
|
|
this.eles.g = this.eles.svg
|
|
.append('g')
|
|
.attr('clip-path', 'url(#clip)')
|
|
.selectAll('g')
|
|
.data([this.data])
|
|
.enter()
|
|
.append('g')
|
|
.attr('transform', 'translate(0, 0)');
|
|
|
|
this.eles.path = this.eles.g.append('g')
|
|
.attr('transform', 'translate(0, ' + st.padding + ')');
|
|
|
|
st.series.forEach(function(key) {
|
|
lineChart.eles[key + 'Line'] = d3.svg
|
|
.line()
|
|
.interpolate('cardinal')
|
|
.tension(st.tension)
|
|
.x(function(d) {
|
|
return lineChart.eles.x(d.time || Date.now());
|
|
})
|
|
.y(function(d) {
|
|
return lineChart.eles.y(!d.usage ? 0 : d.usage[key]);
|
|
});
|
|
|
|
lineChart.eles[key + 'LineEl'] = lineChart.eles.path.append('path')
|
|
.attr('class', 'line')
|
|
.style('stroke', st.colors.line[key])
|
|
.attr('d', lineChart.eles[key + 'Line']);
|
|
});
|
|
|
|
this.eles.g.append('g')
|
|
.attr('class', 'y axis')
|
|
.attr('transform', 'translate(1, ' + st.padding + ')')
|
|
.call(this.eles.y.axis);
|
|
|
|
this.eles.xAxis = this.eles.g.append('g')
|
|
.attr('class', 'x axis')
|
|
.attr('transform', 'translate(0,' + (st.gHeight + st.padding) + ')')
|
|
.call(this.eles.x.axis);
|
|
}
|
|
};
|