You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
622 lines
22 KiB
622 lines
22 KiB
/*! @file |
|
|
|
@id $Id$ |
|
|
|
This is the main application as it is fully run in the user's browser. |
|
|
|
*/ |
|
// 1 2 3 4 5 6 7 8 |
|
// 45678901234567890123456789012345678901234567890123456789012345678901234567890 |
|
|
|
var socket = io.connect(); |
|
var focused = null; |
|
|
|
var docker = new Docker(); |
|
|
|
function htmlenc(html) { |
|
return $('<div/>').text(html).html(); |
|
} |
|
|
|
function htmldec(data) { |
|
return $('<div/>').html(data).text(); |
|
} |
|
|
|
/// Show error messsage |
|
/** Fades in an error message and logs to console. |
|
@param data (optional) The error can be a string or any structure. |
|
Strings are shown to the user, structures are logged only. |
|
@param stay (optional) If not given as @c true, reloads page after 5s. */ |
|
function error(data) { |
|
$("#status").fadeOut("slow", function() { |
|
$("#status").addClass("error") |
|
$("#status").removeClass("notice") |
|
$("#status").removeClass("success") |
|
if (data) { |
|
if (typeof data == 'string') { |
|
$("#status").html(data); |
|
console.log("error: "+data); |
|
} else { |
|
$("#status").html('unknown error: '+JSON.stringify(data)); |
|
console.log("error: ", data); |
|
console.log((new Error('stacktrace'))); |
|
} |
|
} else { |
|
$("#status").html('error'); |
|
console.log("error"); |
|
} |
|
$("#status").fadeIn("slow"); |
|
}); |
|
} |
|
|
|
/// Show notice messsage |
|
/** Fades in an notice message and logs to console. |
|
@param text (optional) The data is a string. */ |
|
function notice(text) { |
|
$("#status").fadeOut("slow", function() { |
|
$("#status").addClass("notice") |
|
$("#status").removeClass("error") |
|
$("#status").removeClass("success") |
|
if (text) { |
|
$("#status").html(text); |
|
console.log("notice: "+text); |
|
} else { |
|
$("#status").html(''); |
|
console.log("notice"); |
|
} |
|
$("#status").fadeIn("slow"); |
|
}); |
|
} |
|
|
|
/// Show notice messsage |
|
/** Fades in an success message and logs to console. |
|
@param text (optional) The data is a string. */ |
|
function success(text) { |
|
$("#status").fadeOut("slow", function() { |
|
$("#status").addClass("success") |
|
$("#status").removeClass("error") |
|
$("#status").removeClass("notice") |
|
if (text) { |
|
$("#status").html(text); |
|
console.log("success: "+text); |
|
} else { |
|
$("#status").html(''); |
|
console.log("success"); |
|
} |
|
$("#status").fadeIn("slow"); |
|
}); |
|
} |
|
|
|
/// Show status message in the main screen area |
|
/** @param text Text is a message or some complex HTML from the server. |
|
@param msg The success message text */ |
|
function status(text, msg) { |
|
$("#main").hide(); |
|
$("#main").html(text); |
|
if (msg) success(msg); |
|
else setTimeout("$('#status').fadeOut('slow')", 5000); |
|
zoom(0); |
|
stats(); |
|
$("#main").show(); |
|
$("form input:first-child").focus(); |
|
docker.containers.contextmenu("#main"); |
|
} |
|
|
|
function emit(signal, data) { |
|
console.log("<-snd "+signal, data); |
|
socket.emit(signal, data); |
|
} |
|
|
|
function connected() { |
|
console.log("server connected"); |
|
$("#connectionstatus #bad").hide(); |
|
$("#connectionstatus #good").show(); |
|
success("server connected"); |
|
} |
|
|
|
function disconnected() { |
|
console.log("server disconnected"); |
|
$("#connectionstatus #good").hide(); |
|
$("#connectionstatus #bad").show(); |
|
error("server disconnected", true); |
|
} |
|
|
|
function connectionstatus() { |
|
if (socket.connected) connected(); else disconnected(); |
|
} |
|
|
|
/// Toggle Menu Display |
|
function togglemenu() { |
|
$("#menu").toggle(); |
|
} |
|
|
|
/// Upload a configuration and send it to server |
|
function upload(evt) { |
|
if (!window.FileReader) |
|
return error("your browser does not support file upload", true); |
|
for (var i=0, f; f=evt.target.files[i]; ++i) { |
|
var file = f; |
|
var reader = new FileReader(); |
|
reader.onload = function(evt) { |
|
if (evt.target.error) return error("error reading file", true); |
|
if (evt.target.readyState==0) return notice("waiting for data …"); |
|
if (evt.target.readyState==1) return notice("loading data …"); |
|
tobecreated = JSON.parse(evt.target.result); |
|
showCreate(); |
|
} |
|
reader.readAsText(file); |
|
} |
|
} |
|
|
|
var tobecreated = {}; |
|
function previewCreate() { |
|
var name = $('#name').val(); |
|
var nodes = Object.create(tobecreated); |
|
if (name != '') { |
|
nodes[name] = { |
|
status: docker.containers.Status.Preview, |
|
id: null, |
|
name: name, |
|
image: { |
|
name: $('#image').val(), |
|
id: null |
|
}, |
|
ports: $('#createports option').map(function() {return $(this).data();}).get(), |
|
env: $('#createvars option').map(function() {return $(this).val();}).get(), |
|
volumes: $('#createvolumes option').map(function() {return $(this).data();}).get(), |
|
volumesfrom: $('#createvolumefroms option').map(function() {return $(this).val();}).get(), |
|
links: $('#createlinks option').map(function() {return $(this).data();}).get(), |
|
entrypoint: $('#createentrypoints option').map(function() {return $(this).val();}).get(), |
|
cmd: $('#createcommands option').map(function() {return $(this).val();}).get(), |
|
}; |
|
$('#doappend').unbind().click(function() { |
|
tobecreated[name] = Object.create(nodes[name]); |
|
tobecreated[name].status = docker.containers.Status.Prepared; |
|
$('#createpreview').append('<option>'+name+'</option>'); |
|
$('#create form input[type=text]').val(''); |
|
$('#create form select.collect option').remove(); |
|
previewCreate(); |
|
}); |
|
$('#doremove').unbind().click(function() { |
|
$(this).siblings('#createpreview').children('option:selected').each(function(a,b,c) { |
|
delete tobecreated[b.innerHTML]; |
|
}).remove(); |
|
previewCreate(); |
|
}); |
|
$('#dosend').unbind().click(function() { |
|
if (Object.keys(tobecreated).length>0) { |
|
emit("create", docker.containers.configuration(tobecreated)); |
|
tobecreated = {}; |
|
showImage(); |
|
} |
|
}); |
|
} |
|
if ($('#image').val()!='') { |
|
var img = docker.images.get($('#image').val()); |
|
if (img) { |
|
$('#portintdata').empty().append(img.ports.map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i.replace(/\/.*/g, ''); |
|
return option; |
|
})); |
|
$('#varnamedata').empty().append(img.env.map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i.replace(/=.*/g, ''); |
|
return option; |
|
})); |
|
$('#varvaluedata').empty().append(img.env.map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i.replace(/^[^=]*=/g, ''); |
|
return option; |
|
})); |
|
$('#volumeextdata').empty(); |
|
$('#volumeintdata').empty(); |
|
for (i in img.volumes) { |
|
var option = document.createElement('option'); |
|
option.value = i; |
|
$('#volumeextdata').append(option); |
|
$('#volumeintdata').append(option); |
|
} |
|
$('#volumesfromdata').empty().append(docker.containers.names(tobecreated).map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i; |
|
return option; |
|
})); |
|
$('#linkcontainerdata').empty().append(docker.containers.names(tobecreated).map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i; |
|
return option; |
|
})); |
|
} |
|
} |
|
if (name=='' && nodes) for (name in nodes) break; |
|
if (name && name!='') $('#preview').html(Viz("digraph {\n"+" rankdir="+rankdir+";\n" |
|
+docker.containers.subgraph(name, nodes) |
|
+"\n}")); |
|
else $('#preview').html(''); |
|
} |
|
|
|
function create() { |
|
} |
|
|
|
var zoomlevel = 0; |
|
function zoom(incr = 0) { |
|
zoomlevel = (zoomlevel+incr)%2; |
|
switch (zoomlevel) { |
|
case 0: { |
|
$("#main svg, #preview svg").css("width", "auto"); |
|
$("#main svg, #preview svg").css("height", "auto"); |
|
$("#main svg, #preview svg").css("max-width", "100%"); |
|
$("#main svg, #preview svg").css("max-height", "100%"); |
|
} break; |
|
case 1: { |
|
$("#main svg, #preview svg").css("width", "100%"); |
|
$("#main svg, #preview svg").css("height", "auto"); |
|
$("#main svg, #preview svg").css("max-width", "100%"); |
|
$("#main svg, #preview svg").css("max-height", "none"); |
|
} break; |
|
case 2: { |
|
$("#main.svg, #preview svg").css("width", "auto"); |
|
$("#main.svg, #preview svg").css("height", "100%"); |
|
$("#main.svg, #preview svg").css("max-width", "none"); |
|
$("#main.svg, #preview svg").css("max-height", "100%"); |
|
} break; |
|
} |
|
} |
|
|
|
var viz = null; |
|
var vizmore = null; |
|
var rankdir = "LR"; |
|
function rotateviz() { |
|
if (!viz) return; |
|
if (rankdir == "LR") |
|
rankdir = "TB"; |
|
else |
|
rankdir = "LR"; |
|
showviz(); |
|
previewCreate(); |
|
} |
|
function showviz(vizpath, more) { |
|
if (!vizpath) { |
|
vizpath = viz; |
|
more = vizmore; |
|
} else { |
|
viz = vizpath; |
|
vizmore = more; |
|
} |
|
res = "digraph {\n"+" rankdir="+rankdir+";\n"+viz+"\n}"; |
|
try { |
|
status(more?Viz(res)+more:Viz(res)); |
|
$('#main a > ellipse + text').attr('font-size', '12'); |
|
$('#main a > ellipse + text + text') |
|
.attr('font-weight', 'bold') |
|
.attr('font-size', '16') |
|
.each(function() {$(this).attr('y', parseFloat($(this).attr('y'))+1.0)}); |
|
$('#main a > ellipse + text + text + text').attr('font-size', '12'); |
|
} catch(e) { |
|
(res = res.split("\n")).forEach(function(v, i, a) { |
|
a[i] = ("000"+(i+1)).slice(-3)+": "+v; |
|
}); |
|
status("<h2>Exception Caught:</h2><p>"+e+"<p><pre>"+res.join("\n")+"</pre>"); |
|
} |
|
} |
|
|
|
function details(name) { |
|
if (name) focused = name; |
|
else if (!focused) return overview(); |
|
showviz(docker.containers.subgraph(focused)); |
|
} |
|
|
|
/// Convert number of bytes to readable text |
|
function size(num) { |
|
if (num>0.6*1024) { |
|
if (num>0.6*1024*1024) { |
|
if (num>0.6*1024*1024*1024) { |
|
if (num>0.6*1024*1024*1024*1024) { |
|
return Math.round(num/1024/1024/1024/1024)+"TB"; |
|
} else { |
|
return Math.round(num/1024/1024/1024)+"GB"; |
|
} |
|
} else { |
|
return Math.round(num/1024/1024)+"MB"; |
|
} |
|
} else { |
|
return Math.round(num/1024)+"kB"; |
|
} |
|
} else { |
|
return num+"B"; |
|
} |
|
} |
|
|
|
var oldoldstats = null; |
|
var oldstats = null; |
|
function stats(data) { |
|
console.log("->rcv stats"); |
|
if (!data && oldstats && oldoldstats) { |
|
data = oldstats; |
|
oldstats = oldoldstats; |
|
} |
|
if (oldstats) for (name in data) { |
|
var s = data[name]; |
|
var o = oldstats[name]; |
|
if (!o|| !s) continue; |
|
$('#main text + text:contains("'+name+'") + text') |
|
.html('cpu: ' |
|
+(Math.round((s.cpuacct.usage.data-o.cpuacct.usage.data) |
|
/(s.cpuacct.usage.date-o.cpuacct.usage.date) |
|
/100) |
|
/100) |
|
+'% mem: ' |
|
+size(s.memory.usage_in_bytes.data)); |
|
} |
|
oldoldstats = oldstats; |
|
oldstats = data; |
|
} |
|
|
|
function images(i) { |
|
console.log("->rcv images"); |
|
docker.images.set(i); |
|
$('#imagedata').empty().append(docker.images.tags().map(function(i) { |
|
var option = document.createElement('option'); |
|
option.value = i; |
|
return option; |
|
})); |
|
} |
|
|
|
function containers(c) { |
|
console.log("->rcv containers"); |
|
docker.containers.set(c); |
|
if (focused && docker.containers.exists(focused)) |
|
details(focused); |
|
else |
|
overview(); |
|
} |
|
|
|
function showImage() { |
|
$("#create").hide(); |
|
$("#logs").hide(); |
|
$("#console").hide(); |
|
$("#close").hide(); |
|
$("#imagetools").show(); |
|
$("#main").show(); |
|
} |
|
|
|
function showCreate() { |
|
$("#main").hide(); |
|
$("#logs").hide(); |
|
$("#console").hide(); |
|
$("#imagetools").show(); |
|
$("#close").show(); |
|
$("#create").show(); |
|
previewCreate(); |
|
} |
|
|
|
function showConsole() { |
|
$("#main").hide(); |
|
$("#create").hide(); |
|
$("#logs").hide(); |
|
$("#imagetools").hide(); |
|
$("#console").show(); |
|
$("#close").show(); |
|
// $("#command").focus(); |
|
// $("#command").val(""); |
|
// if ($("#screen").val()!="") $("#screen").append("\n"); |
|
} |
|
|
|
function showLogs() { |
|
$("#main").hide(); |
|
$("#create").hide(); |
|
$("#console").hide(); |
|
$("#imagetools").hide(); |
|
$("#close").show(); |
|
$("#logs").show(); |
|
} |
|
|
|
function logs(data) { |
|
console.log("->rcv logs("+data.name+")"); |
|
if (data.type=='done') { |
|
$("#logs").append('<span class="'+data.type+'">\nDONE</span>'); |
|
} else { |
|
$("#logs").append('<span class="'+data.type+'">'+htmlenc(data.text)+'</span>'); |
|
} |
|
} |
|
|
|
function strInsert(str, pos, txt) { |
|
return str.slice(0, pos)+txt+str.slice(pos); |
|
} |
|
|
|
function ansifilter(data) { |
|
console.log("ansifilter"); |
|
var res = data; |
|
var pos = -1; |
|
var spans = 0; |
|
while ((pos=res.indexOf("\x1B[")) >= 0) { |
|
var end = res.indexOf("m", pos+2); |
|
if (end>0) { |
|
var control= res.slice(pos+2, end); |
|
res = res.slice(0, pos)+res.slice(end+1); |
|
control.split(';').forEach(function(c) { |
|
switch (parseInt(c)) { |
|
// set |
|
case 1: res = strInsert(res, pos, '<span class="bold">'); ++spans; break; |
|
case 2: res = strInsert(res, pos, '<span class="dim">'); ++spans; break; |
|
case 4: res = strInsert(res, pos, '<span class="underlined">'); ++spans; break; |
|
case 5: res = strInsert(res, pos, '<span class="blink">'); ++spans; break; |
|
case 7: res = strInsert(res, pos, '<span class="reverse">'); ++spans; break; |
|
case 8: res = strInsert(res, pos, '<span class="hidden">'); ++spans; break; |
|
// reset |
|
case 0: for (;spans;--spans) res = strInsert(res, pos, "</span>"); break; |
|
case 21: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
case 22: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
case 23: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
case 25: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
case 27: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
case 28: if (spans) --spans; res = strInsert(res, pos, "</span>"); break; |
|
// fg colors |
|
case 39: res = strInsert(res, pos, '<span class="fgdefault">'); ++spans; break; |
|
case 30: res = strInsert(res, pos, '<span class="fgblack">'); ++spans; break; |
|
case 31: res = strInsert(res, pos, '<span class="fgred">'); ++spans; break; |
|
case 32: res = strInsert(res, pos, '<span class="fggreen">'); ++spans; break; |
|
case 33: res = strInsert(res, pos, '<span class="fgyellow">'); ++spans; break; |
|
case 34: res = strInsert(res, pos, '<span class="fgblue">'); ++spans; break; |
|
case 35: res = strInsert(res, pos, '<span class="fgmagenta">'); ++spans; break; |
|
case 36: res = strInsert(res, pos, '<span class="fgcyan">'); ++spans; break; |
|
case 37: res = strInsert(res, pos, '<span class="fglightgrey">'); ++spans; break; |
|
case 90: res = strInsert(res, pos, '<span class="fgdarkgrey">'); ++spans; break; |
|
case 91: res = strInsert(res, pos, '<span class="fglightred">'); ++spans; break; |
|
case 92: res = strInsert(res, pos, '<span class="fglightgreen">'); ++spans; break; |
|
case 93: res = strInsert(res, pos, '<span class="fglightyellow">'); ++spans; break; |
|
case 94: res = strInsert(res, pos, '<span class="fglightblue">'); ++spans; break; |
|
case 95: res = strInsert(res, pos, '<span class="fglightmagenta">'); ++spans; break; |
|
case 96: res = strInsert(res, pos, '<span class="fglightcyan">'); ++spans; break; |
|
case 97: res = strInsert(res, pos, '<span class="fgwhite">'); ++spans; break; |
|
// bg colors |
|
case 49: res = strInsert(res, pos, '<span class="bgdefault">'); ++spans; break; |
|
case 40: res = strInsert(res, pos, '<span class="bgblack">'); ++spans; break; |
|
case 41: res = strInsert(res, pos, '<span class="bgred">'); ++spans; break; |
|
case 42: res = strInsert(res, pos, '<span class="bggreen">'); ++spans; break; |
|
case 43: res = strInsert(res, pos, '<span class="bgyellow">'); ++spans; break; |
|
case 44: res = strInsert(res, pos, '<span class="bgblue">'); ++spans; break; |
|
case 45: res = strInsert(res, pos, '<span class="bgmagenta">'); ++spans; break; |
|
case 46: res = strInsert(res, pos, '<span class="bgcyan">'); ++spans; break; |
|
case 47: res = strInsert(res, pos, '<span class="bglightgrey">'); ++spans; break; |
|
case 100: res = strInsert(res, pos, '<span class="bgdarkgrey">'); ++spans; break; |
|
case 101: res = strInsert(res, pos, '<span class="bglightred">'); ++spans; break; |
|
case 102: res = strInsert(res, pos, '<span class="bglightgreen">'); ++spans; break; |
|
case 103: res = strInsert(res, pos, '<span class="bglightyellow">'); ++spans; break; |
|
case 104: res = strInsert(res, pos, '<span class="bglightblue">'); ++spans; break; |
|
case 105: res = strInsert(res, pos, '<span class="bglightmagenta">'); ++spans; break; |
|
case 106: res = strInsert(res, pos, '<span class="bglightcyan">'); ++spans; break; |
|
case 107: res = strInsert(res, pos, '<span class="bgwhite">'); ++spans; break; |
|
|
|
} |
|
}); |
|
} else { |
|
break; |
|
} |
|
} |
|
for (;spans;--spans) res += "</span>"; |
|
console.log(res); |
|
return res.replace(/\r\r\n/g, '\n'); |
|
} |
|
|
|
function ascii(txt) { |
|
var res = ""; |
|
for (i=0; i<txt.length; ++i) { |
|
if (res) res += ","; |
|
res += txt.charCodeAt(i).toString(); |
|
} |
|
return res; |
|
} |
|
|
|
function bash_data(data) { |
|
console.log("->rcv bash-data("+data.name+")", data); |
|
if (data.type=='done') { |
|
$("#screen").append('<span class="'+data.type+'">\nDONE</span>'); |
|
} else { |
|
var buff = ""; |
|
console.log("ASCII: ", ascii(data.text)); |
|
for (var i=0; i<data.text.length; ++i) { |
|
switch (data.text.charCodeAt(i)) { |
|
case 7: |
|
case 8: |
|
if (buff.length) $("#screen").append(ansifilter(htmlenc(buff))); |
|
buff = ""; |
|
$('#screen').html(function (_,txt) { |
|
return txt.slice(0, -1); |
|
}); |
|
break; |
|
default: buff += data.text[i]; |
|
} |
|
} |
|
if (buff.length) $("#screen").append(ansifilter(htmlenc(buff))); |
|
} |
|
$("#screen").animate({ |
|
scrollTop: $("#screen").prop('scrollHeight') |
|
}, 500); |
|
$("#screen").focus(); |
|
} |
|
|
|
function overview() { |
|
focused = null; |
|
showviz(docker.containers.graph()); |
|
} |
|
|
|
/// Initial Function: Startup |
|
/** Decide whether to login or to create a new user */ |
|
function start() { |
|
$("#imagetools").hide(); |
|
$("#close").hide(); |
|
$("#popup").hide(); |
|
$("#menu").hide(); |
|
$("#username").html(window.location.hostname) |
|
try { |
|
status("Starting up ..."); |
|
emit("images"); |
|
emit("containers"); |
|
showImage(); |
|
} catch (m) { |
|
error(m); |
|
} |
|
} |
|
|
|
function initForms() { |
|
$('#create form *').change(previewCreate); |
|
$("#create form fieldset .add").unbind().click(function() { |
|
$(this).siblings("select.collect") |
|
.append('<option '+$(this).siblings("input") |
|
.map(function() { |
|
if (this.hasAttribute('data-name')) { |
|
var res = ""; |
|
var obj = this; |
|
this.getAttribute('data-name').split(' ').forEach(function(n) { |
|
res += 'data-'+n+'="'+ |
|
(obj.type!='checkbox'||obj.checked |
|
?obj.value:obj.getAttribute('data-false')) |
|
.replace(/&/g, '&') |
|
.replace(/"/g, '"') |
|
+'"'; |
|
}) |
|
return res; |
|
} else return ''; |
|
}).get().join(' ')+'>'+$(this).siblings("input") |
|
.map(function() { |
|
var val = this.value; |
|
var sep = this.getAttribute('data-separator') || ''; |
|
if (this.type=='checkbox' && !this.checked) { |
|
val=''; sep='' |
|
} |
|
return sep+val; |
|
}).get().join('')+'</option>'); |
|
$(this).siblings("input[type=text]").val(''); |
|
previewCreate(); |
|
}); |
|
$("#create form fieldset .remove").unbind().click(function() { |
|
$(this).siblings('select.collect').children('option:selected').remove(); |
|
previewCreate(); |
|
}); |
|
} |
|
|
|
function init() { |
|
socket.io |
|
.on("connect", connected) |
|
.on("reconnect", connected) |
|
.on("disconnect", disconnected) |
|
.on("error", disconnected); |
|
socket |
|
.on("fail", error) |
|
.on("containers", containers) |
|
.on("images", images) |
|
.on("stats", stats) |
|
.on("logs", logs) |
|
.on("bash-data", bash_data); |
|
initForms(); |
|
start(); |
|
} |
|
|
|
/// On Load, Call @ref start |
|
/* |
|
$(window.onbeforeunload = function() { |
|
return "Are you sure you want to navigate away?"; |
|
}); |
|
*/ |
|
$(init);
|
|
|