diff options
Diffstat (limited to 'src/static')
-rw-r--r-- | src/static/js/dashboard.js | 588 |
1 files changed, 588 insertions, 0 deletions
diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index 66e95d0..e51a219 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -242,3 +242,591 @@ class MultipleSelectFilterWidget { document.getElementById("filter_field").value = JSON.stringify(this.result); } } + +class NetworkStep { + constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){ + if(!this.check_support()) + return; + + this.currentWindow = null; + this.netCount = 0; + this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC']; + this.hostCount = 0; + this.lastHostBottom = 100; + this.networks = new Set(); + this.has_public_net = false; + this.debug = debug; + this.editor = new mxEditor(); + this.graph = this.editor.graph; + + this.editor.setGraphContainer(graphContainer); + this.doGlobalConfig(); + this.prefill(xml, hosts, added_hosts, removed_host_ids); + this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true); + this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true); + + if(this.debug){ + this.editor.addAction('printXML', function(editor, cell) { + mxLog.write(this.encodeGraph()); + mxLog.show(); + }.bind(this)); + this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true); + } + + new mxOutline(this.graph, overviewContainer); + //sets the edge color to be the same as the network + this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this)); + //hooks up double click functionality + this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this); + + if(!this.has_public_net){ + this.addPublicNetwork(); + } + } + + check_support(){ + if (!mxClient.isBrowserSupported()) { + mxUtils.error('Browser is not supported', 200, false); + return false; + } + return true; + } + + prefill(xml, hosts, added_hosts, removed_host_ids){ + //populate existing data + if(xml){ + this.restoreFromXml(xml, this.editor); + } else if(hosts){ + for(const host of hosts) + this.makeHost(host); + } + + //apply any changes + if(added_hosts){ + for(const host of added_hosts) + this.makeHost(host); + this.updateHosts([]); //TODO: why? + } + this.updateHosts(removed_host_ids); + } + + cellConnectionHandler(sender, event){ + const edge = event.getProperty('edge'); + const terminal = event.getProperty('terminal') + const source = event.getProperty('source'); + if(this.checkAllowed(edge, terminal, source)) { + this.colorEdge(edge, terminal, source); + this.alertVlan(edge, terminal, source); + } + } + + doubleClickHandler(evt, cell) { + if( cell != null ){ + if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) { + cell = cell.getParent(); + } + if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) { + this.createDeleteDialog(cell.getId()); + } + else { + this.showDetailWindow(cell); + } + } + } + + alertVlan(edge, terminal, source) { + if( terminal == null || edge.getTerminal(!source) == null) { + return; + } + const form = document.createElement("form"); + const tagged = document.createElement("input"); + tagged.type = "radio"; + tagged.name = "tagged"; + tagged.value = "True"; + form.appendChild(tagged); + form.appendChild(document.createTextNode(" Tagged")); + form.appendChild(document.createElement("br")); + + const untagged = document.createElement("input"); + untagged.type = "radio"; + untagged.name = "tagged"; + untagged.value = "False"; + form.appendChild(untagged); + form.appendChild(document.createTextNode(" Untagged")); + form.appendChild(document.createElement("br")); + + const yes_button = document.createElement("button"); + yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this); + yes_button.appendChild(document.createTextNode("Okay")); + + const cancel_button = document.createElement("button"); + cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this); + cancel_button.appendChild(document.createTextNode("Cancel")); + + const error_div = document.createElement("div"); + error_div.id = "current_window_errors"; + form.appendChild(error_div); + + const content = document.createElement('div'); + content.appendChild(form); + content.appendChild(yes_button); + content.appendChild(cancel_button); + this.showWindow("Vlan Selection", content, 200, 200); + } + + createDeleteDialog(id) { + const content = document.createElement('div'); + const remove_button = document.createElement("button"); + remove_button.style.width = '46%'; + remove_button.onclick = function() { this.deleteCell(id);}.bind(this); + remove_button.appendChild(document.createTextNode("Remove")); + const cancel_button = document.createElement("button"); + cancel_button.style.width = '46%'; + cancel_button.onclick = function() { this.closeWindow();}.bind(this); + cancel_button.appendChild(document.createTextNode("Cancel")); + + content.appendChild(remove_button); + content.appendChild(cancel_button); + this.showWindow('Do you want to delete this network?', content, 200, 62); + } + + checkAllowed(edge, terminal, source) { + //check if other terminal is null, and that they are different + const otherTerminal = edge.getTerminal(!source); + if(terminal != null && otherTerminal != null) { + if( terminal.getParent().getId().split('_')[0] == //'host' or 'network' + otherTerminal.getParent().getId().split('_')[0] ) { + //not allowed + this.graph.removeCells([edge]); + return false; + } + } + return true; + } + + colorEdge(edge, terminal, source) { + if(terminal.getParent().getId().indexOf('network') >= 0) { + const styles = terminal.getParent().getStyle().split(';'); + let color = 'black'; + for(let style of styles){ + const kvp = style.split('='); + if(kvp[0] == "fillColor"){ + color = kvp[1]; + } + } + edge.setStyle('strokeColor=' + color); + } + } + + showDetailWindow(cell) { + const info = JSON.parse(cell.getValue()); + const content = document.createElement("div"); + const pre_tag = document.createElement("pre"); + pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description)); + const ok_button = document.createElement("button"); + ok_button.onclick = function() { this.closeWindow();}; + content.appendChild(pre_tag); + content.appendChild(ok_button); + this.showWindow('Details', content, 400, 400); + } + + restoreFromXml(xml, editor) { + const doc = mxUtils.parseXml(xml); + const node = doc.documentElement; + editor.readGraphModel(node); + + //Iterate over all children, and parse the networks to add them to the sidebar + for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) { + if(cell.getId().indexOf("network") > -1) { + const info = JSON.parse(cell.getValue()); + const name = info['name']; + this.networks.add(name); + const styles = cell.getStyle().split(";"); + let color = null; + for(const style of styles){ + const kvp = style.split('='); + if(kvp[0] == "fillColor") { + color = kvp[1]; + break; + } + } + if(info.public){ + this.has_public_net = true; + } + this.netCount++; + this.makeSidebarNetwork(name, color, cell.getId()); + } + } + } + + deleteCell(cellId) { + var cell = this.graph.getModel().getCell(cellId); + if( cellId.indexOf("network") > -1 ) { + let elem = document.getElementById(cellId); + elem.parentElement.removeChild(elem); + } + this.graph.removeCells([cell]); + this.currentWindow.destroy(); + } + + newNetworkWindow() { + const input = document.createElement("input"); + input.type = "text"; + input.name = "net_name"; + input.maxlength = 100; + input.id = "net_name_input"; + input.style.margin = "5px"; + + const yes_button = document.createElement("button"); + yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this); + yes_button.appendChild(document.createTextNode("Okay")); + + const cancel_button = document.createElement("button"); + cancel_button.onclick = function() {thid.closeWindow();}.bind(this); + cancel_button.appendChild(document.createTextNode("Cancel")); + + const error_div = document.createElement("div"); + error_div.id = "current_window_errors"; + + const content = document.createElement("div"); + content.appendChild(document.createTextNode("Name: ")); + content.appendChild(input); + content.appendChild(document.createElement("br")); + content.appendChild(yes_button); + content.appendChild(cancel_button); + content.appendChild(document.createElement("br")); + content.appendChild(error_div); + + this.showWindow("Network Creation", content, 300, 300); + } + + parseNetworkWindow() { + const net_name = document.getElementById("net_name_input").value + const error_div = document.getElementById("current_window_errors"); + if( this.networks.has(net_name) ){ + error_div.innerHTML = "All network names must be unique"; + return; + } + this.addNetwork(net_name); + this.currentWindow.destroy(); + } + + addToolbarButton(editor, toolbar, action, label, image, isTransparent) { + const button = document.createElement('button'); + button.style.fontSize = '10'; + if (image != null) { + const img = document.createElement('img'); + img.setAttribute('src', image); + img.style.width = '16px'; + img.style.height = '16px'; + img.style.verticalAlign = 'middle'; + img.style.marginRight = '2px'; + button.appendChild(img); + } + if (isTransparent) { + button.style.background = 'transparent'; + button.style.color = '#FFFFFF'; + button.style.border = 'none'; + } + mxEvent.addListener(button, 'click', function(evt) { + editor.execute(action); + }); + mxUtils.write(button, label); + toolbar.appendChild(button); + }; + + encodeGraph() { + const encoder = new mxCodec(); + const xml = encoder.encode(this.graph.getModel()); + return mxUtils.getXml(xml); + } + + doGlobalConfig() { + //general graph stuff + this.graph.setMultigraph(false); + this.graph.setCellsSelectable(false); + this.graph.setCellsMovable(false); + + //testing + this.graph.vertexLabelIsMovable = true; + + //edge behavior + this.graph.setConnectable(true); + this.graph.setAllowDanglingEdges(false); + mxEdgeHandler.prototype.snapToTerminals = true; + mxConstants.MIN_HOTSPOT_SIZE = 16; + mxConstants.DEFAULT_HOTSPOT = 1; + //edge 'style' (still affects behavior greatly) + const style = this.graph.getStylesheet().getDefaultEdgeStyle(); + style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW; + style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE; + style[mxConstants.STYLE_ROUNDED] = true; + style[mxConstants.STYLE_FONTCOLOR] = 'black'; + style[mxConstants.STYLE_STROKECOLOR] = 'red'; + style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF'; + style[mxConstants.STYLE_STROKEWIDTH] = '3'; + style[mxConstants.STYLE_ROUNDED] = true; + style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation; + + const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle(); + hostStyle[mxConstants.STYLE_ROUNDED] = 1; + + this.graph.convertValueToString = function(cell) { + try{ + //changes value for edges with xml value + if(cell.isEdge()) { + if(JSON.parse(cell.getValue())["tagged"]) { + return "tagged"; + } + return "untagged"; + } + else{ + return JSON.parse(cell.getValue())['name']; + } + } + catch(e){ + return cell.getValue(); + } + }; + } + + showWindow(title, content, width, height) { + //create transparent black background + const background = document.createElement('div'); + background.style.position = 'absolute'; + background.style.left = '0px'; + background.style.top = '0px'; + background.style.right = '0px'; + background.style.bottom = '0px'; + background.style.background = 'black'; + mxUtils.setOpacity(background, 50); + document.body.appendChild(background); + + const x = Math.max(0, document.body.scrollWidth/2-width/2); + const y = Math.max(10, (document.body.scrollHeight || + document.documentElement.scrollHeight)/2-height*2/3); + + const wnd = new mxWindow(title, content, x, y, width, height, false, true); + wnd.setClosable(false); + + wnd.addListener(mxEvent.DESTROY, function(evt) { + this.graph.setEnabled(true); + mxEffects.fadeOut(background, 50, true, 10, 30, true); + }.bind(this)); + this.currentWindow = wnd; + + this.graph.setEnabled(false); + this.currentWindow.setVisible(true); + }; + + closeWindow() { + //allows the current window to be destroyed + this.currentWindow.destroy(); + }; + + othersUntagged(edgeID) { + const edge = this.graph.getModel().getCell(edgeID); + const end1 = edge.getTerminal(true); + const end2 = edge.getTerminal(false); + + if( end1.getParent().getId().split('_')[0] == 'host' ){ + var netint = end1; + } else { + var netint = end2; + } + + var edges = netint.edges; + for( let edge of edges) { + if( edge.getValue() ) { + var tagged = JSON.parse(edge.getValue()).tagged; + } else { + var tagged = true; + } + if( !tagged ) { + return true; + } + } + return false; + }; + + + deleteVlanWindow(edgeID) { + const cell = this.graph.getModel().getCell(edgeID); + this.graph.removeCells([cell]); + this.currentWindow.destroy(); + } + + parseVlanWindow(edgeID) { + //do parsing and data manipulation + const radios = document.getElementsByName("tagged"); + const edge = this.graph.getModel().getCell(edgeID); + + for(let radio of radios){ + if(radio.checked) { + //set edge to be tagged or untagged + if( radio.value == "False") { + if( this.othersUntagged(edgeID) ) { + document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed."; + return; + } + } + const edgeVal = {tagged: radio.value == "True"}; + edge.setValue(JSON.stringify(edgeVal)); + break; + } + } + this.graph.refresh(edge); + this.closeWindow(); + } + + makeMxNetwork(net_name, is_public = false) { + const model = this.graph.getModel(); + const width = 10; + const height = 1700; + const xoff = 400 + (30 * this.netCount); + const yoff = -10; + let color = this.netColors[this.netCount]; + if( this.netCount > (this.netColors.length - 1)) { + color = Math.floor(Math.random() * 16777215); //int in possible color space + color = '#' + color.toString(16).toUpperCase(); //convert to hex + } + const net_val = { name: net_name, public: is_public}; + const net = this.graph.insertVertex( + this.graph.getDefaultParent(), + 'network_' + this.netCount, + JSON.stringify(net_val), + xoff, + yoff, + width, + height, + 'fillColor=' + color, + false + ); + const num_ports = 45; + for(var i=0; i<num_ports; i++){ + let port = this.graph.insertVertex( + net, + null, + '', + 0, + (1/num_ports) * i, + 10, + height / num_ports, + 'fillColor=black;opacity=0', + true + ); + } + + const ret_val = { color: color, element_id: "network_" + this.netCount }; + + this.networks.add(net_name); + this.netCount++; + return ret_val; + } + + addPublicNetwork() { + const net = this.makeMxNetwork("public", true); + this.makeSidebarNetwork("public", net['color'], net['element_id']); + this.has_public_net = true; + } + + addNetwork(net_name) { + const ret = this.makeMxNetwork(net_name); + this.makeSidebarNetwork(...ret); + } + + updateHosts(removed) { + const cells = [] + for(const hostID of removed) { + cells.push(this.graph.getModel().getCell("host_" + hostID)); + } + this.graph.removeCells(cells); + + const hosts = this.graph.getChildVertices(this.graph.getDefaultParent()); + let topdist = 100; + for(const i in hosts) { + const host = hosts[i]; + if(host.id.startsWith("host_")){ + const geometry = host.getGeometry(); + geometry.y = topdist + 50; + topdist = geometry.y + geometry.height; + host.setGeometry(geometry); + } + } + } + + makeSidebarNetwork(net_name, color, net_id){ + const newNet = document.createElement("li"); + const colorBlob = document.createElement("div"); + colorBlob.className = "colorblob"; + const textContainer = document.createElement("p"); + textContainer.className = "network_innertext"; + newNet.id = net_id; + const deletebutton = document.createElement("button"); + deletebutton.className = "btn btn-danger"; + deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;"; + deletebutton.appendChild(document.createTextNode("X")); + deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false); + textContainer.appendChild(document.createTextNode(net_name)); + colorBlob.style['background'] = color; + newNet.appendChild(colorBlob); + newNet.appendChild(textContainer); + if( net_name != "public" ) { + newNet.appendChild(deletebutton); + } + document.getElementById("network_list").appendChild(newNet); + } + + makeHost(hostInfo) { + const value = JSON.stringify(hostInfo['value']); + const interfaces = hostInfo['interfaces']; + const width = 100; + const height = (25 * interfaces.length) + 25; + const xoff = 75; + const yoff = this.lastHostBottom + 50; + this.lastHostBottom = yoff + height; + const host = this.graph.insertVertex( + this.graph.getDefaultParent(), + 'host_' + hostInfo['id'], + value, + xoff, + yoff, + width, + height, + 'editable=0', + false + ); + host.getGeometry().offset = new mxPoint(-50,0); + host.setConnectable(false); + this.hostCount++; + + for(var i=0; i<interfaces.length; i++) { + const port = this.graph.insertVertex( + host, + null, + JSON.stringify(interfaces[i]), + 90, + (i * 25) + 12, + 20, + 20, + 'fillColor=blue;editable=0', + false + ); + port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0); + this.graph.refresh(port); + } + this.graph.refresh(host); + } + + submitForm() { + const form = document.getElementById("xml_form"); + const input_elem = document.getElementById("hidden_xml_input"); + input_elem.value = this.encodeGraph(this.graph); + const req = new XMLHttpRequest(); + req.open("POST", "/wf/workflow/", false); + req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + req.onerror = function() { alert("problem with form submission"); } + const formData = $("#xml_form").serialize(); + req.send(formData); + } +} |