/*! @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; function Docker() { function same(array1, array2) { if (!array1 && !array2) return true; if (!array1 || !array2) return false; return (array1.length == array2.length) && array1.every(function(element, index) { return element === array2[index]; }); } function quote(text) { if (text.match('"')) { if (!text.match("'")) return "'"+text+"'"; else return '"'+text.replace(/"/g, '\\"')+'"'; } else { if (text.match(' ')) return '"'+text+'"'; else return text; } } var _docker = this; this.Images = function() { var _images = this; var images = []; var nodes = []; function setup() { delete nodes; nodes = []; images.forEach(function(c, i) { if (!nodes[c.Id]) nodes[c.Id] = {}; nodes[c.Id].id = c.Id; nodes[c.Id].tags = c.RepoTags; nodes[c.Id].created = c.Created; nodes[c.Id].author = c.Author; nodes[c.Id].os = c.Os+"/"+c.Architecture; nodes[c.Id].parent = c.Parent; nodes[c.Id].env = c.Config.Env; nodes[c.Id].cmd = c.Config.Cmd; nodes[c.Id].entrypoint = c.Config.Entrypoint; nodes[c.Id].ports = c.Config.ExposedPorts; nodes[c.Id].volumes = c.Config.Volumes; if (c.Parent) { if (!nodes[c.Parent]) nodes[c.Parent] = {}; if (!nodes[c.Parent].children) nodes[c.Parent].children = []; nodes[c.Parent].children.push(c.Id); } }); } this.cleanup = function(id, instance) { if (!nodes[id]) return nodes[id].env.forEach(function(e) { if ((pos=instance.env.indexOf(e))>-1) instance.env.splice(pos, 1) }) if (same(nodes[id].cmd, instance.cmd)) instance.cmd = null if (same(nodes[id].entrypoint, instance.entrypoint)) instance.entrypoint = null } this.set = function(c) { if (typeof c == "string") c = JSON.parse(c); if (typeof c != "object") throw "wrong format: "+(typeof c); images = c; setup(); } } this.Containers = function() { var _containers = this; var Status = Object.freeze({ Error: {color: "red", action1: "start", action2: "remove", bash: false}, Terminated: {color: "yellow", action1: "start", action2: "remove", bash: false}, Restarting: {color: "lightblue", action1: "start", action2: "remove", bash: false}, Paused: {color: "lightgrey", action1: "unpause", action2: null, bash: false}, Running: {color: "lightgreen", action1: "pause", action2: "stop", bash: true} }); var containers = []; var nodes = []; function protocol(port) { if (port.toString().match("443")) return "https://"; if (port.toString().match("3304")) return "mysql://"; if (port.toString().match("22")) return "ssh://"; return "http://"; } this.exists = function(name) { if (nodes[name]) return true; return false; } function getIps(n, ips) { n.ports.forEach(function(p) { if (!ips[p.ip]) ips[p.ip] = []; ips[p.ip].push(p); }); } function graphIpClusters(ips) { var res = "newrank=true;\n"; var i = 0; for (ip in ips) { res += "subgraph clusterIp"+(++i)+' {\nlabel="'+ip+'";\n'; ips[ip].forEach(function(p) { res += '"'+p.ip+":"+p.external +'" [label="'+p.external+'",URL="' +protocol(p.internal)+p.ip+':'+p.external+'",shape=box];\n'; }); res+="}\n"; } res += "{rank=same;\n"; for (ip in ips) { ips[ip].forEach(function(p) { res += '"'+p.ip+":"+p.external+'";\n'; }); } res+="}\n"; return res; } function graphNode(n) { var res = ""; var label = n.name+'\\n'+n.image.name; res += '"'+n.name+'"' +' [label="'+label +'",URL="#'+n.name +'",style=filled,fillcolor='+n.status.color+"];\n"; n.ports.forEach(function(p) { res += '"'+(p.ip?p.ip+":":"")+p.external+'" -> "'+n.name +'" [label="'+p.internal+'"];\n'; }); n.links.forEach(function(l) { res += '"'+n.name+'" -> "'+l.to+'" [label="link: '+l.link+'"];\n' }); return res; } function graphVolumesInside(n) { var res = ""; n.volumes.forEach(function(v) { res += '"'+v.id+'" [label="'+v.inside+'",shape=box];\n'; }); return res; } function graphVolumesOutside(n) { var res = ""; n.volumes.forEach(function(v) { if (v.host) res += '"'+v.outside+'" [label="'+v.host+'",shape=box];\n'; }); return res; } function graphVolumesConnections(n) { var res = ""; n.volumes.forEach(function(v) { if (v.host) res += '"'+v.id+'" -> "'+v.outside+'" [label="mounted from"]\n'; res += '"'+n.name+'" -> "'+v.id+'" [label="volume/'+v.rw+'"]\n'; }); n.volumesfrom.forEach(function(o) { res += '"'+n.name+'" -> "'+nodes[o].name+'" [label="volumes from"]\n'; }); return res; } this.graph = function(n) { var res = ""; var ips = []; n = n || nodes; for (name in n) getIps(n[name], ips); res += graphIpClusters(ips); for (name in n) res += graphNode(n[name]); res += "{rank=same;\n"; for (name in n) res += graphVolumesInside(n[name]); res+="}\n"; res += "{rank=same;\n"; for (name in n) res += graphVolumesOutside(n[name]); res+="}\n"; for (name in n) res += graphVolumesConnections(n[name]); return res; } function addNodes(ns, name) { var n = nodes[name]; ns[name] = n; n.links.forEach(function(peer) { if (!ns[peer.to]) addNodes(ns, peer.to); }); n.usedby.forEach(function(peer) { if (!ns[peer]) addNodes(ns, peer); }); n.volumesfrom.forEach(function(peer) { if (!ns[peer]) addNodes(ns, peer); }); n.volumesto.forEach(function(peer) { if (!ns[peer]) addNodes(ns, peer); }); } this.subnet = function(name) { var ns = {}; addNodes(ns, name); return ns; } this.subgraph = function(name) { return this.graph(this.subnet(name)); } this.configuration = function(name) { var ns = this.subnet(name); var creates = []; for (n in ns) { var instance = { name: ns[n].name, image: ns[n].image.name, ports: ns[n].ports, env: ns[n].env, cmd: ns[n].cmd, entrypoint: ns[n].entrypoint, volumesfrom: ns[n].volumesfrom, links: ns[n].links, volumes: [] }; ns[n].volumes.forEach(function(v) { if (v.host) instance.volumes.push({ inside: v.inside, outside: v.host }); }); _docker.images.cleanup(ns[n].image.id, instance); creates.push(instance); } creates.sort(function(a, b) { if (a.volumesfrom.indexOf(b.name)>=0) return 1; // a after b if (b.volumesfrom.indexOf(a.name)>=0) return -1; // a before b for (var i=0; i') $("#popup").empty(); if (n.status.action1) { $("#popup").append(''); $("#popup1").click(function() { emit(n.status.action1, name); }); } $("#popup").append(''); $("#popup2").click(function() { if (focused) overview(); else details(name); }); if (n.status.action2) { $("#popup").append(''); $("#popup3").click(function() { emit(n.status.action2, name); }); } $("#popup").append('
'); $("#popup").append(''); $("#popup4").click(function() { showLogs(); emit("logs", name); }); if (n.status.bash) { $("#popup").append(''); $("#popup5").click(function() { showConsole(); emit("bash-start", name); $("#bash").submit(function() { emit("bash-input", {name: name, text: $("#command").val()+"\n"}); $("#command").val(""); }) }); } $("#popup").append(''); $("#popup6").click(function() { var download = document.createElement('a'); download.href = 'data:application/json,' + encodeURI(JSON.stringify(_containers.creation(name), null, 2)); download.target = '_blank'; download.download = name+'.json'; var clickEvent = new MouseEvent("click", { "view": window, "bubbles": true, "cancelable": false }); download.dispatchEvent(clickEvent); }); $("#popup").css("position", "fixed"); $("#popup").css("top", e.pageY-$("#popup").height()/4); $("#popup").css("left", e.pageX-$("#popup").width()/2); $("#popup").mouseleave(function() { $("#popup").hide(); }).click(function() { $("#popup").hide(); }); $("#popup").show(); }) } this.set = function(c) { if (typeof c == "string") c = JSON.parse(c); if (typeof c != "object") throw "wrong format: "+(typeof c); containers = c; setup(); } } this.images = new this.Images(); this.containers = new this.Containers(); } var docker = new Docker(); function htmlenc(html) { return $('
').text(html).html(); } function htmldec(data) { return $('
').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); $("#popup").hide(); if (msg) success(msg); else setTimeout("$('#status').fadeOut('slow')", 5000); zoom(0); $("#main").show(); $("form input:first-child").focus(); docker.containers.contextmenu("#main"); } function emit(signal, data) { console.log("<-snd "+signal); 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(); } function about(c) { $("#imagetools").hide(); $.ajax({url: "about.php", success: function(res) { try { var a = JSON.parse(res); status("

"+a.description+"

"+ "

"+a.project+"-"+a.version+"

"+ "

"+a.docker+"

"+ "

README

"+ "
"+a.readme+"
"); } catch(e) { status("
"+res+"
"); error("Exception Caught: "+e); } }}).fail(function() { error("offline"); }); } var zoomlevel = 0; function zoom(incr = 0) { zoomlevel = (zoomlevel+incr)%2; switch (zoomlevel) { case 0: { $("#main svg").css("width", "auto"); $("#main svg").css("height", "auto"); $("#main svg").css("max-width", "100%"); $("#main svg").css("max-height", "100%"); } break; case 1: { $("#main svg").css("width", "100%"); $("#main svg").css("height", "auto"); $("#main svg").css("max-width", "100%"); $("#main svg").css("max-height", "none"); } break; case 2: { $("#main.svg").css("width", "auto"); $("#main.svg").css("height", "100%"); $("#main.svg").css("max-width", "none"); $("#main.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(); } function showviz(vizpath, more) { $("#imagetools").show(); 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)); } catch(e) { (res = res.split("\n")).forEach(function(v, i, a) { a[i] = ("000"+(i+1)).slice(-3)+": "+v; }); status("

Exception Caught:

"+e+"

"+res.join("\n")+"
"); } } function details(name) { if (name) focused = name; else if (!focused) return overview(); showviz(docker.containers.subgraph(focused)); } function images(i) { console.log("->rcv images"); docker.images.set(i); } function containers(c) { console.log("->rcv containers"); docker.containers.set(c); showImage(); if (focused && docker.containers.exists(focused)) details(focused); else overview(); } function showImage() { $("#close").hide(); $("#logs").hide(); $("#console").hide(); $("#main").show(); } function showConsole() { $("#main").hide(); $("#logs").hide(); $("#console").show(); $("#close").show(); $("#command").focus(); $("#command").val(""); if ($("#screen").val()!="") $("#screen").append("\n"); } function showLogs() { $("#main").hide(); $("#logs").show(); $("#console").hide(); $("#close").show(); } function logs(data) { console.log("->rcv logs("+data.name+")"); if (data.type=='done') { $("#logs").append('\nDONE'); } else { $("#logs").append(''+htmlenc(data.text)+''); } } 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, ''); ++spans; break; case 2: res = strInsert(res, pos, ''); ++spans; break; case 4: res = strInsert(res, pos, ''); ++spans; break; case 5: res = strInsert(res, pos, ''); ++spans; break; case 7: res = strInsert(res, pos, ''); ++spans; break; case 8: res = strInsert(res, pos, '"); break; case 21: if (spans) --spans; res = strInsert(res, pos, ""); break; case 22: if (spans) --spans; res = strInsert(res, pos, ""); break; case 23: if (spans) --spans; res = strInsert(res, pos, ""); break; case 25: if (spans) --spans; res = strInsert(res, pos, ""); break; case 27: if (spans) --spans; res = strInsert(res, pos, ""); break; case 28: if (spans) --spans; res = strInsert(res, pos, ""); break; // fg colors case 39: res = strInsert(res, pos, ''); ++spans; break; case 30: res = strInsert(res, pos, ''); ++spans; break; case 31: res = strInsert(res, pos, ''); ++spans; break; case 32: res = strInsert(res, pos, ''); ++spans; break; case 33: res = strInsert(res, pos, ''); ++spans; break; case 34: res = strInsert(res, pos, ''); ++spans; break; case 35: res = strInsert(res, pos, ''); ++spans; break; case 36: res = strInsert(res, pos, ''); ++spans; break; case 37: res = strInsert(res, pos, ''); ++spans; break; case 90: res = strInsert(res, pos, ''); ++spans; break; case 91: res = strInsert(res, pos, ''); ++spans; break; case 92: res = strInsert(res, pos, ''); ++spans; break; case 93: res = strInsert(res, pos, ''); ++spans; break; case 94: res = strInsert(res, pos, ''); ++spans; break; case 95: res = strInsert(res, pos, ''); ++spans; break; case 96: res = strInsert(res, pos, ''); ++spans; break; case 97: res = strInsert(res, pos, ''); ++spans; break; // bg colors case 49: res = strInsert(res, pos, ''); ++spans; break; case 40: res = strInsert(res, pos, ''); ++spans; break; case 41: res = strInsert(res, pos, ''); ++spans; break; case 42: res = strInsert(res, pos, ''); ++spans; break; case 43: res = strInsert(res, pos, ''); ++spans; break; case 44: res = strInsert(res, pos, ''); ++spans; break; case 45: res = strInsert(res, pos, ''); ++spans; break; case 46: res = strInsert(res, pos, ''); ++spans; break; case 47: res = strInsert(res, pos, ''); ++spans; break; case 100: res = strInsert(res, pos, ''); ++spans; break; case 101: res = strInsert(res, pos, ''); ++spans; break; case 102: res = strInsert(res, pos, ''); ++spans; break; case 103: res = strInsert(res, pos, ''); ++spans; break; case 104: res = strInsert(res, pos, ''); ++spans; break; case 105: res = strInsert(res, pos, ''); ++spans; break; case 106: res = strInsert(res, pos, ''); ++spans; break; case 107: res = strInsert(res, pos, ''); ++spans; break; } }); } else { break; } } for (;spans;--spans) res += ""; console.log(res); return res.replace(/\r\r\n/g, '\n'); } function bash_data(data) { console.log("->rcv bash-data("+data.name+")", data); if (data.type=='done') { $("#screen").append('\nDONE'); } else { $("#screen").append(''+ansifilter(htmlenc(data.text))+''); } $("#screen").animate({ scrollTop: $("#screen").scrollHeight() }, 500);} 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(); $("#menu").hide(); $("#username").html(window.location.hostname) try { status("Starting up ..."); emit("images"); emit("containers"); } catch (m) { error(m); } } 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("logs", logs) .on("bash-data", bash_data); start(); } /// On Load, Call @ref start /* $(window.onbeforeunload = function() { return "Are you sure you want to navigate away?"; }); */ $(init);