pm2-gui-fr
Version:
Une interface web et terminal élégante pour Unitech / PM2.
1,423 lines (1,274 loc) • 33.5 kB
JavaScript
;
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);
}
};