5 form_submission_callbacks = []; //all runnables will be executed before form submission
11 // Taken from https://docs.djangoproject.com/en/3.0/ref/csrf/
12 function getCookie(name) {
13 var cookieValue = null;
14 if (document.cookie && document.cookie !== '') {
15 var cookies = document.cookie.split(';');
16 for (var i = 0; i < cookies.length; i++) {
17 var cookie = cookies[i].trim();
18 // Does this cookie string begin with the name we want?
19 if (cookie.substring(0, name.length + 1) === (name + '=')) {
20 cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
28 function update_page(response) {
29 if( response.redirect )
31 window.location.replace(response.redirect);
34 draw_breadcrumbs(response.meta);
35 update_exit_button(response.meta);
36 update_side_buttons(response.meta);
37 $("#formContainer").html(response.content);
40 function update_side_buttons(meta) {
41 const step = meta.active;
42 const page_count = meta.steps.length;
44 const back_button = document.getElementById("workflow-nav-back");
46 back_button.classList.add("disabled");
47 back_button.disabled = true;
49 back_button.classList.remove("disabled");
50 back_button.disabled = false;
53 const forward_btn = document.getElementById("workflow-nav-next");
54 if (step == page_count - 1) {
55 forward_btn.classList.add("disabled");
56 forward_btn.disabled = true;
58 forward_btn.classList.remove("disabled");
59 forward_btn.disabled = false;
63 function update_exit_button(meta) {
64 if (meta.workflow_count == 1) {
65 document.getElementById("cancel_btn").innerText = "Exit Workflow";
67 document.getElementById("cancel_btn").innerText = "Return to Parent";
71 function draw_breadcrumbs(meta) {
72 $("#topPagination").children().not(".page-control").remove();
74 for (const i in meta.steps) {
75 const step_btn = create_step(meta.steps[i], i == meta["active"]);
76 $("#topPagination li:last-child").before(step_btn);
80 function create_step(step_json, active) {
81 const step_dom = document.createElement("li");
82 // First create the dom object depending on active or not
83 step_dom.className = "topcrumb";
85 step_dom.classList.add("active");
87 $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
89 const code = step_json.valid;
94 $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
97 } else if (code < 200) {
98 $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>")
100 msg = step_json.message;
101 } else if (code < 300) {
102 $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>")
104 msg = step_json.message;
107 if (step_json.enabled == false) {
108 step_dom.classList.add("disabled");
111 update_message(msg, stat);
117 function update_description(title, desc) {
118 document.getElementById("view_title").innerText = title;
119 document.getElementById("view_desc").innerText = desc;
122 function update_message(message, stepstatus) {
124 if (stepstatus == 'valid') {
125 color_code = 'text-success';
126 } else if (stepstatus == 'invalid') {
127 color_code = 'text-danger';
131 document.getElementById("view_message").innerText = message;
132 document.getElementById("view_message").className = "step_message";
133 document.getElementById("view_message").classList.add("message_" + stepstatus);
134 document.getElementById("view_message").classList.add(color_code);
137 function submitStepForm(next_step = "current"){
138 run_form_callbacks();
139 const step_form_data = $("#step_form").serialize();
140 const form_data = $.param({
142 "step_form": step_form_data,
143 "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
146 '/workflow/manager/',
148 (data) => update_page(data),
150 ).fail(() => alert("failure"));
153 function run_form_callbacks(){
154 for(f of form_submission_callbacks)
156 form_submission_callbacks = [];
159 function create_workflow(type) {
162 url: "/workflow/create/",
164 "workflow_type": type
167 "X-CSRFToken": getCookie('csrftoken')
169 }).done(function (data, textStatus, jqXHR) {
170 window.location = "/workflow/";
171 }).fail(function (jqxHR, textstatus) {
172 alert("Something went wrong...");
176 function add_workflow(type) {
179 url: "/workflow/add/",
181 "workflow_type": type
184 "X-CSRFToken": getCookie('csrftoken')
186 }).done(function (data, textStatus, jqXHR) {
188 }).fail(function (jqxHR, textstatus) {
189 alert("Something went wrong...");
193 function pop_workflow() {
196 url: "/workflow/pop/",
198 "X-CSRFToken": getCookie('csrftoken')
200 }).done(function (data, textStatus, jqXHR) {
202 }).fail(function (jqxHR, textstatus) {
203 alert("Something went wrong...");
207 function continue_workflow() {
208 window.location.replace("/workflow/");
215 class MultipleSelectFilterWidget {
217 constructor(neighbors, items, initial) {
219 this.graph_neighbors = neighbors;
220 this.filter_items = items;
221 this.currentLab = null;
222 this.available_resources = {};
224 this.dropdown_count = 0;
226 for(let nodeId in this.filter_items) {
227 const node = this.filter_items[nodeId];
228 this.result[node.class] = {}
231 this.make_selection(initial);
234 make_selection(initial_data){
235 if(!initial_data || jQuery.isEmptyObject(initial_data))
238 // Need to sort through labs first
239 let initial_lab = initial_data['lab'];
240 let initial_resources = initial_data['resource'];
242 for( let node_id in initial_lab) { // This should only be length one
243 const node = this.filter_items[node_id];
244 const selection_data = initial_lab[node_id];
245 if( selection_data.selected ) {
247 this.markAndSweep(node);
248 this.updateResult(node);
250 if(node['multiple']){
251 this.make_multiple_selection(node, selection_data);
253 this.currentLab = node;
254 this.available_resources = JSON.parse(node['available_resources']);
257 for( let node_id in initial_resources){
258 const node = this.filter_items[node_id];
259 const selection_data = initial_resources[node_id];
260 if( selection_data.selected ) {
262 this.markAndSweep(node);
263 this.updateResult(node);
265 if(node['multiple']){
266 this.make_multiple_selection(node, selection_data);
269 this.updateAvailibility();
272 make_multiple_selection(node, selection_data){
273 const prepop_data = selection_data.values;
274 for(let k in prepop_data){
275 const div = this.add_item_prepopulate(node, prepop_data[k]);
276 this.updateObjectResult(node, div.id, prepop_data[k]);
281 for(let i in this.filter_items) {
282 const node = this.filter_items[i];
283 node['marked'] = true; //mark all nodes
286 const toCheck = [root];
287 while(toCheck.length > 0){
288 const node = toCheck.pop();
290 if(!node['marked']) {
291 continue; //already visited, just continue
294 node['marked'] = false; //mark as visited
295 if(node['follow'] || node == root){ //add neighbors if we want to follow this node
296 const neighbors = this.graph_neighbors[node.id];
297 for(let neighId of neighbors) {
298 const neighbor = this.filter_items[neighId];
299 toCheck.push(neighbor);
304 //now remove all nodes still marked
305 for(let i in this.filter_items){
306 const node = this.filter_items[i];
308 this.disable_node(node);
314 if(node['selected']) {
315 this.markAndSweep(node);
317 else { //TODO: make this not dumb
319 //remember the currently selected, then reset everything and reselect one at a time
320 for(let nodeId in this.filter_items) {
321 node = this.filter_items[nodeId];
322 if(node['selected']) {
327 for(let node of selected) {
329 this.markAndSweep(node);
335 const elem = document.getElementById(node['id']);
336 node['selected'] = true;
337 elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
338 elem.classList.add('selected_node');
340 if(node['class'] == 'resource')
341 this.reserveResource(node);
346 const elem = document.getElementById(node['id']);
347 node['selected'] = false;
348 node['selectable'] = true;
349 elem.classList.add('bg-white')
350 elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
354 const elem = document.getElementById(node['id']);
355 node['selected'] = false;
356 node['selectable'] = false;
357 elem.classList.remove('bg-white', 'selected_node');
358 elem.classList.add('not-allowed', 'bg-light');
362 // if lab is not already selected update available resources
363 if(!node['selected']) {
364 this.currentLab = node;
365 this.available_resources = JSON.parse(node['available_resources']);
366 this.updateAvailibility();
368 // a lab is already selected, clear already selected resources
369 if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) {
377 updateAvailibility() {
378 const lab_resources = this.graph_neighbors[this.currentLab.id];
380 // need to loop through and update all quantities
381 for(let i in lab_resources) {
382 const resource_node = this.filter_items[lab_resources[i]];
383 const required_resources = JSON.parse(resource_node['required_resources']);
384 let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0];
385 let leastAvailable = 100;
387 let quantityDescription;
390 for(let resource in required_resources) {
391 currCount = Math.floor(this.available_resources[resource] / required_resources[resource]);
392 if(currCount < leastAvailable)
393 leastAvailable = currCount;
395 if(!currCount || currCount < 0) {
401 if (elem.children[0]){
402 elem.removeChild(elem.children[0]);
405 quantityDescription = '<br> Quantity Currently Available: ' + leastAvailable;
406 quantityNode = document.createElement('P');
407 if (leastAvailable > 0) {
408 quantityDescription = quantityDescription.fontcolor('green');
410 quantityDescription = quantityDescription.fontcolor('red');
413 quantityNode.innerHTML = quantityDescription;
414 elem.appendChild(quantityNode)
418 reserveResource(node){
419 const required_resources = JSON.parse(node['required_resources']);
420 let hostname = document.getElementById('id_hostname');
421 let image = document.getElementById('id_image');
425 for(let resource in required_resources){
426 this.available_resources[resource] -= required_resources[resource];
427 cnt += required_resources[resource];
430 if (cnt > 1 && hostname) {
431 hostname.readOnly = true;
432 // we only disable hostname modification because there is no sane case where you want all hosts to have the same hostname
433 // image is still allowed to be set across all hosts, but is filtered to the set of images that are commonly applicable still
434 // if no images exist that would apply to all hosts in a pod, then the user is restricted to not setting an image
435 // and the default image for each host is used
438 this.updateAvailibility();
441 releaseResource(node){
442 const required_resources = JSON.parse(node['required_resources']);
443 let hostname = document.getElementById('id_hostname');
444 let image = document.getElementById('id_image');
446 for(let resource in required_resources){
447 this.available_resources[resource] += required_resources[resource];
450 if (hostname && image) {
451 hostname.readOnly = false;
452 image.disabled = false;
455 this.updateAvailibility();
460 const node = this.filter_items[id];
461 if(!node['selectable'])
464 // If they are selecting a lab, update accordingly
465 if (node['class'] == 'lab') {
466 lab_check = this.labCheck(node);
471 // Can only select a resource if a lab is selected
472 if (!this.currentLab) {
473 alert('You must select a lab before selecting a resource');
477 if(node['multiple']){
478 return this.processClickMultiple(node);
480 return this.processClickSingle(node);
484 processClickSingle(node){
485 node['selected'] = !node['selected']; //toggle on click
486 if(node['selected']) {
490 this.releaseResource(node); // can't do this in clear since clear removes border
493 this.updateResult(node);
496 processClickMultiple(node){
498 const div = this.add_item_prepopulate(node, false);
500 this.updateObjectResult(node, div.id, "");
503 restrictchars(input){
504 if( input.validity.patternMismatch ){
505 input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
506 input.reportValidity();
508 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
509 this.checkunique(input);
512 checkunique(tocheck){ //TODO: use set
513 const val = tocheck.value;
514 for( let input of this.inputs ){
515 if( input.value == val && input != tocheck){
516 tocheck.setCustomValidity("All hostnames must be unique");
517 tocheck.reportValidity();
521 tocheck.setCustomValidity("");
524 make_remove_button(div, node){
525 const button = document.createElement("BUTTON");
526 button.type = "button";
527 button.appendChild(document.createTextNode("Remove"));
528 button.classList.add("btn", "btn-danger", "d-inline-block");
530 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
534 make_input(div, node, prepopulate){
535 const input = document.createElement("INPUT");
536 input.type = node.form.type;
537 input.name = node.id + node.form.name
538 input.classList.add("form-control", "w-auto", "d-inline-block");
539 input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
540 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
541 input.placeholder = node.form.placeholder;
542 this.inputs.push(input);
544 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
545 input.oninput = function() { that.restrictchars(this); };
547 input.value = prepopulate;
551 add_item_prepopulate(node, prepopulate){
552 const div = document.createElement("DIV");
553 div.id = "dropdown_" + this.dropdown_count;
554 div.classList.add("card", "flex-row", "d-flex", "mb-2");
555 this.dropdown_count++;
556 const label = document.createElement("H5")
557 label.appendChild(document.createTextNode(node['name']))
558 label.classList.add("p-1", "m-1", "flex-grow-1");
559 div.appendChild(label);
560 let remove_btn = this.make_remove_button(div, node);
561 remove_btn.classList.add("p-1", "m-1");
562 div.appendChild(remove_btn);
563 document.getElementById("dropdown_wrapper").appendChild(div);
567 remove_dropdown(div_id, node_id){
568 const div = document.getElementById(div_id);
569 const node = this.filter_items[node_id]
570 const parent = div.parentNode;
571 div.parentNode.removeChild(div);
572 this.result[node.class][node.id]['count']--;
573 this.releaseResource(node); // This can't be done on clear b/c clear removes border
575 //checks if we have removed last item in class
576 if(this.result[node.class][node.id]['count'] == 0){
577 delete this.result[node.class][node.id];
583 if(!node['multiple']){
584 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
586 delete this.result[node.class][node.id];
590 updateObjectResult(node, childKey, childValue){
591 if(!this.result[node.class][node.id])
592 this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0}
594 this.result[node.class][node.id]['count']++;
598 document.getElementById("filter_field").value = JSON.stringify(this.result);
610 // description: string,
618 // network: int, [networks.id]
633 constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){
634 if(!this.check_support()) {
635 console.log("Aborting, browser is not supported");
639 this.currentWindow = null;
641 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
643 this.lastHostBottom = 100;
644 this.networks = new Set();
645 this.has_public_net = false;
647 this.editor = new mxEditor();
648 this.graph = this.editor.graph;
650 window.global_graph = this.graph;
651 window.network_rr_index = 5;
653 this.editor.setGraphContainer(graphContainer);
654 this.doGlobalConfig();
658 for(const network_id in networks) {
659 let network = networks[network_id];
661 mx_networks[network_id] = this.populateNetwork(network);
664 this.prefillHosts(resources, mx_networks);
666 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
667 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
668 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus');
669 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus');
672 this.editor.addAction('printXML', function(editor, cell) {
673 mxLog.write(this.encodeGraph());
676 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code');
679 new mxOutline(this.graph, overviewContainer);
680 //sets the edge color to be the same as the network
681 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
682 //hooks up double click functionality
683 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
687 if (!mxClient.isBrowserSupported()) {
688 mxUtils.error('Browser is not supported', 200, false);
696 * mx_interface: mxCell for the interface itself
697 * network: mxCell for the outer network
700 connectNetwork(mx_interface, network, tagged) {
701 var cell = new mxCell(
702 "connection from " + network + " to " + mx_interface,
703 new mxGeometry(0, 0, 50, 50));
705 cell.geometry.relative = true;
706 cell.setValue(JSON.stringify({tagged: tagged}));
708 let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network);
709 let edge = this.graph.addEdge(cell, null, mx_interface, terminal);
710 this.colorEdge(edge, terminal, true);
711 this.graph.refresh(edge);
717 * to: desired y axis position of the matching cell
718 * within: graph cell for a full network, with all child cells
721 * an mx cell, the one vertically closest to the desired value
724 * modifies the <rr_index> on the <within> parameter
726 getClosestNetworkCell(to, within) {
727 if(window.network_rr_index === undefined) {
728 window.network_rr_index = 5;
731 let child_keys = within.children.keys();
732 let children = Array.from(within.children);
733 let index = (window.network_rr_index++) % children.length;
735 let child = within.children[child_keys[index]];
737 return children[index];
746 * description: string,
754 * network: int, [networks.id]
762 * network_mappings: {
763 * <django network id>: <mxnetwork id>
766 * draws given hosts into the mxgraph
768 prefillHosts(hosts, network_mappings){
769 for(const host_id in hosts) {
770 this.makeHost(hosts[host_id], network_mappings);
774 cellConnectionHandler(sender, event){
775 const edge = event.getProperty('edge');
776 const terminal = event.getProperty('terminal')
777 const source = event.getProperty('source');
778 if(this.checkAllowed(edge, terminal, source)) {
779 this.colorEdge(edge, terminal, source);
780 this.alertVlan(edge, terminal, source);
784 doubleClickHandler(evt, cell) {
786 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
787 cell = cell.getParent();
789 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
790 this.createDeleteDialog(cell.getId());
793 this.showDetailWindow(cell);
798 alertVlan(edge, terminal, source) {
799 if( terminal == null || edge.getTerminal(!source) == null) {
802 const form = document.createElement("form");
803 const tagged = document.createElement("input");
804 tagged.type = "radio";
805 tagged.name = "tagged";
806 tagged.value = "True";
807 tagged.checked = "True";
808 form.appendChild(tagged);
809 form.appendChild(document.createTextNode(" Tagged"));
810 form.appendChild(document.createElement("br"));
812 const untagged = document.createElement("input");
813 untagged.type = "radio";
814 untagged.name = "tagged";
815 untagged.value = "False";
816 form.appendChild(untagged);
817 form.appendChild(document.createTextNode(" Untagged"));
818 form.appendChild(document.createElement("br"));
820 const yes_button = document.createElement("button");
821 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
822 yes_button.appendChild(document.createTextNode("Okay"));
824 const cancel_button = document.createElement("button");
825 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
826 cancel_button.appendChild(document.createTextNode("Cancel"));
828 const error_div = document.createElement("div");
829 error_div.id = "current_window_errors";
830 form.appendChild(error_div);
832 const content = document.createElement('div');
833 content.appendChild(form);
834 content.appendChild(yes_button);
835 content.appendChild(cancel_button);
836 this.showWindow("Vlan Selection", content, 200, 200);
839 createDeleteDialog(id) {
840 const content = document.createElement('div');
841 const remove_button = document.createElement("button");
842 remove_button.style.width = '46%';
843 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
844 remove_button.appendChild(document.createTextNode("Remove"));
845 const cancel_button = document.createElement("button");
846 cancel_button.style.width = '46%';
847 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
848 cancel_button.appendChild(document.createTextNode("Cancel"));
850 content.appendChild(remove_button);
851 content.appendChild(cancel_button);
852 this.showWindow('Do you want to delete this network?', content, 200, 62);
855 checkAllowed(edge, terminal, source) {
856 //check if other terminal is null, and that they are different
857 const otherTerminal = edge.getTerminal(!source);
858 if(terminal != null && otherTerminal != null) {
859 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
860 otherTerminal.getParent().getId().split('_')[0] ) {
862 this.graph.removeCells([edge]);
869 colorEdge(edge, terminal, source) {
870 if(terminal.getParent().getId().indexOf('network') >= 0) {
871 const styles = terminal.getParent().getStyle().split(';');
873 for(let style of styles){
874 const kvp = style.split('=');
875 if(kvp[0] == "fillColor"){
880 edge.setStyle('strokeColor=' + color);
882 console.log("Failed to color " + edge + ", " + terminal + ", " + source);
886 showDetailWindow(cell) {
887 const info = JSON.parse(cell.getValue());
888 const content = document.createElement("div");
889 const pre_tag = document.createElement("pre");
890 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
891 const ok_button = document.createElement("button");
892 ok_button.onclick = function() { this.closeWindow();};
893 content.appendChild(pre_tag);
894 content.appendChild(ok_button);
895 this.showWindow('Details', content, 400, 400);
898 restoreFromXml(xml, editor) {
899 const doc = mxUtils.parseXml(xml);
900 const node = doc.documentElement;
901 editor.readGraphModel(node);
903 //Iterate over all children, and parse the networks to add them to the sidebar
904 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
905 if(cell.getId().indexOf("network") > -1) {
906 const info = JSON.parse(cell.getValue());
907 const name = info['name'];
908 this.networks.add(name);
909 const styles = cell.getStyle().split(";");
911 for(const style of styles){
912 const kvp = style.split('=');
913 if(kvp[0] == "fillColor") {
919 this.has_public_net = true;
922 this.makeSidebarNetwork(name, color, cell.getId());
928 var cell = this.graph.getModel().getCell(cellId);
929 if( cellId.indexOf("network") > -1 ) {
930 let elem = document.getElementById(cellId);
931 elem.parentElement.removeChild(elem);
933 this.graph.removeCells([cell]);
934 this.currentWindow.destroy();
938 const input = document.createElement("input");
940 input.name = "net_name";
941 input.maxlength = 100;
942 input.id = "net_name_input";
943 input.style.margin = "5px";
945 const yes_button = document.createElement("button");
946 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
947 yes_button.appendChild(document.createTextNode("Okay"));
949 const cancel_button = document.createElement("button");
950 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
951 cancel_button.appendChild(document.createTextNode("Cancel"));
953 const error_div = document.createElement("div");
954 error_div.id = "current_window_errors";
956 const content = document.createElement("div");
957 content.appendChild(document.createTextNode("Name: "));
958 content.appendChild(input);
959 content.appendChild(document.createElement("br"));
960 content.appendChild(yes_button);
961 content.appendChild(cancel_button);
962 content.appendChild(document.createElement("br"));
963 content.appendChild(error_div);
965 this.showWindow("Network Creation", content, 300, 300);
968 parseNetworkWindow() {
969 const net_name = document.getElementById("net_name_input").value
970 const error_div = document.getElementById("current_window_errors");
971 if( this.networks.has(net_name) ){
972 error_div.innerHTML = "All network names must be unique";
975 this.addNetwork(net_name);
976 this.currentWindow.destroy();
979 addToolbarButton(editor, toolbar, action, image) {
980 const button = document.createElement('button');
981 button.setAttribute('class', 'btn btn-sm m-1');
983 const icon = document.createElement('i');
984 icon.setAttribute('class', 'fas ' + image);
985 button.appendChild(icon);
987 mxEvent.addListener(button, 'click', function(evt) {
988 editor.execute(action);
990 mxUtils.write(button, '');
991 toolbar.appendChild(button);
995 const encoder = new mxCodec();
996 const xml = encoder.encode(this.graph.getModel());
997 return mxUtils.getXml(xml);
1001 //general graph stuff
1002 this.graph.setMultigraph(false);
1003 this.graph.setCellsSelectable(false);
1004 this.graph.setCellsMovable(false);
1007 this.graph.vertexLabelIsMovable = true;
1010 this.graph.setConnectable(true);
1011 this.graph.setAllowDanglingEdges(false);
1012 mxEdgeHandler.prototype.snapToTerminals = true;
1013 mxConstants.MIN_HOTSPOT_SIZE = 16;
1014 mxConstants.DEFAULT_HOTSPOT = 1;
1015 //edge 'style' (still affects behavior greatly)
1016 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
1017 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
1018 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
1019 style[mxConstants.STYLE_ROUNDED] = true;
1020 style[mxConstants.STYLE_FONTCOLOR] = 'black';
1021 style[mxConstants.STYLE_STROKECOLOR] = 'red';
1022 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
1023 style[mxConstants.STYLE_STROKEWIDTH] = '3';
1024 style[mxConstants.STYLE_ROUNDED] = true;
1025 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
1027 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
1028 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
1030 this.graph.convertValueToString = function(cell) {
1032 //changes value for edges with xml value
1034 if(JSON.parse(cell.getValue())["tagged"]) {
1040 return JSON.parse(cell.getValue())['name'];
1044 return cell.getValue();
1049 showWindow(title, content, width, height) {
1050 //create transparent black background
1051 const background = document.createElement('div');
1052 background.style.position = 'absolute';
1053 background.style.left = '0px';
1054 background.style.top = '0px';
1055 background.style.right = '0px';
1056 background.style.bottom = '0px';
1057 background.style.background = 'black';
1058 mxUtils.setOpacity(background, 50);
1059 document.body.appendChild(background);
1061 const x = Math.max(0, document.body.scrollWidth/2-width/2);
1062 const y = Math.max(10, (document.body.scrollHeight ||
1063 document.documentElement.scrollHeight)/2-height*2/3);
1065 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
1066 wnd.setClosable(false);
1068 wnd.addListener(mxEvent.DESTROY, function(evt) {
1069 this.graph.setEnabled(true);
1070 mxEffects.fadeOut(background, 50, true, 10, 30, true);
1072 this.currentWindow = wnd;
1074 this.graph.setEnabled(false);
1075 this.currentWindow.setVisible(true);
1079 //allows the current window to be destroyed
1080 this.currentWindow.destroy();
1083 othersUntagged(edgeID) {
1084 const edge = this.graph.getModel().getCell(edgeID);
1085 const end1 = edge.getTerminal(true);
1086 const end2 = edge.getTerminal(false);
1088 if( end1.getParent().getId().split('_')[0] == 'host' ){
1094 var edges = netint.edges;
1095 for( let edge of edges) {
1096 if( edge.getValue() ) {
1097 var tagged = JSON.parse(edge.getValue()).tagged;
1110 deleteVlanWindow(edgeID) {
1111 const cell = this.graph.getModel().getCell(edgeID);
1112 this.graph.removeCells([cell]);
1113 this.currentWindow.destroy();
1116 parseVlanWindow(edgeID) {
1117 //do parsing and data manipulation
1118 const radios = document.getElementsByName("tagged");
1119 const edge = this.graph.getModel().getCell(edgeID);
1121 for(let radio of radios){
1123 //set edge to be tagged or untagged
1124 if( radio.value == "False") {
1125 if( this.othersUntagged(edgeID) ) {
1126 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
1130 const edgeVal = {tagged: radio.value == "True"};
1131 edge.setValue(JSON.stringify(edgeVal));
1135 this.graph.refresh(edge);
1139 makeMxNetwork(net_name, is_public = false) {
1140 const model = this.graph.getModel();
1142 const height = 1700;
1143 const xoff = 400 + (30 * this.netCount);
1145 let color = this.netColors[this.netCount];
1146 if( this.netCount > (this.netColors.length - 1)) {
1147 color = Math.floor(Math.random() * 16777215); //int in possible color space
1148 color = '#' + color.toString(16).toUpperCase(); //convert to hex
1150 const net_val = { name: net_name, public: is_public};
1151 const net = this.graph.insertVertex(
1152 this.graph.getDefaultParent(),
1153 'network_' + this.netCount,
1154 JSON.stringify(net_val),
1159 'fillColor=' + color,
1162 const num_ports = 45;
1163 for(var i=0; i<num_ports; i++){
1164 let port = this.graph.insertVertex(
1172 'fillColor=black;opacity=0',
1177 const ret_val = { color: color, element_id: "network_" + this.netCount };
1179 this.networks.add(net_name);
1193 // mxgraph id of network
1194 populateNetwork(network) {
1195 let mxNet = this.makeMxNetwork(network.name, network.public);
1196 this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id);
1198 if( network.public ) {
1199 this.has_public_net = true;
1202 return mxNet.element_id;
1205 addPublicNetwork() {
1206 const net = this.makeMxNetwork("public", true);
1207 this.makeSidebarNetwork("public", net['color'], net['element_id']);
1208 this.has_public_net = true;
1211 addNetwork(net_name) {
1212 const ret = this.makeMxNetwork(net_name);
1213 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
1216 updateHosts(removed) {
1218 for(const hostID of removed) {
1219 cells.push(this.graph.getModel().getCell("host_" + hostID));
1221 this.graph.removeCells(cells);
1223 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
1225 for(const i in hosts) {
1226 const host = hosts[i];
1227 if(host.id.startsWith("host_")){
1228 const geometry = host.getGeometry();
1229 geometry.y = topdist + 50;
1230 topdist = geometry.y + geometry.height;
1231 host.setGeometry(geometry);
1236 makeSidebarNetwork(net_name, color, net_id){
1237 const colorBlob = document.createElement("div");
1238 colorBlob.className = "square-20 rounded-circle";
1239 colorBlob.style['background'] = color;
1241 const textContainer = document.createElement("span");
1242 textContainer.className = "ml-2";
1243 textContainer.appendChild(document.createTextNode(net_name));
1245 const timesIcon = document.createElement("i");
1246 timesIcon.classList.add("fas", "fa-times");
1248 const deletebutton = document.createElement("button");
1249 deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
1250 deletebutton.appendChild(timesIcon);
1251 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
1253 const newNet = document.createElement("li");
1254 newNet.classList.add("list-group-item", "d-flex", "bg-light");
1256 newNet.appendChild(colorBlob);
1257 newNet.appendChild(textContainer);
1259 if( net_name != "public" ) {
1260 newNet.appendChild(deletebutton);
1262 document.getElementById("network_list").appendChild(newNet);
1270 * 'description': string,
1279 * network: int, <django network id>,
1287 * network_mappings: {
1288 * <django network id>: <mxnetwork id>
1291 makeHost(hostInfo, network_mappings) {
1292 const value = JSON.stringify(hostInfo['value']);
1293 const interfaces = hostInfo['interfaces'];
1295 const height = (25 * interfaces.length) + 25;
1297 const yoff = this.lastHostBottom + 50;
1298 this.lastHostBottom = yoff + height;
1299 const host = this.graph.insertVertex(
1300 this.graph.getDefaultParent(),
1301 'host_' + hostInfo['id'],
1310 host.getGeometry().offset = new mxPoint(-50,0);
1311 host.setConnectable(false);
1314 for(var i=0; i<interfaces.length; i++) {
1315 const port = this.graph.insertVertex(
1318 JSON.stringify(interfaces[i]),
1323 'fillColor=blue;editable=0',
1326 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
1327 const iface = interfaces[i];
1328 for( const connection of iface.connections ) {
1329 const network = this
1332 .getCell(network_mappings[connection.network]);
1334 this.connectNetwork(port, network, connection.tagged);
1336 this.graph.refresh(port);
1338 this.graph.refresh(host);
1342 const input_elem = document.getElementById("hidden_xml_input");
1343 input_elem.value = this.encodeGraph(this.graph);
1347 class SearchableSelectMultipleWidget {
1348 constructor(format_vars, field_dataset, field_initial) {
1349 this.format_vars = format_vars;
1350 this.items = field_dataset;
1351 this.initial = field_initial;
1353 this.expanded_name_trie = {"isComplete": false};
1354 this.small_name_trie = {"isComplete": false};
1355 this.string_trie = {"isComplete": false};
1357 this.added_items = new Set();
1359 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
1361 this[e] = format_vars[e];
1364 this.search_field_init();
1366 if( this.show_from_noentry )
1373 const textfield = document.getElementById("user_field");
1374 const drop = document.getElementById("drop_results");
1376 textfield.disabled = "True";
1377 drop.style.display = "none";
1379 const btns = document.getElementsByClassName("btn-remove");
1380 for( const btn of btns )
1382 btn.classList.add("disabled");
1387 search_field_init() {
1388 this.build_all_tries(this.items);
1390 for( const elem of this.initial )
1392 this.select_item(elem);
1394 if(this.initial.length == 1)
1396 this.search(this.items[this.initial[0]]["small_name"]);
1397 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
1401 build_all_tries(dict)
1403 for( const key in dict )
1405 this.add_item(dict[key]);
1411 const id = item['id'];
1412 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
1413 this.add_to_tree(item['small_name'], id, this.small_name_trie);
1414 this.add_to_tree(item['string'], id, this.string_trie);
1417 add_to_tree(str, id, trie)
1419 let inner_trie = trie;
1422 if( !inner_trie[str.charAt(0)] )
1425 inner_trie[str.charAt(0)] = new_trie;
1429 var new_trie = inner_trie[str.charAt(0)];
1432 if( str.length == 1 )
1434 new_trie.isComplete = true;
1439 new_trie.ids.push(id);
1441 inner_trie = new_trie;
1442 str = str.substring(1);
1448 if( input.length == 0 && !this.show_from_noentry){
1452 else if( input.length == 0 && this.show_from_noentry)
1454 this.dropdown(this.items); //show all items
1459 const tr1 = this.getSubtree(input, this.expanded_name_trie);
1461 const tr2 = this.getSubtree(input, this.small_name_trie);
1463 const tr3 = this.getSubtree(input, this.string_trie);
1465 const results = this.collate(trees);
1466 this.dropdown(results);
1470 getSubtree(input, given_trie)
1473 recursive function to return the trie accessed at input
1476 if( input.length == 0 ){
1481 const substr = input.substring(0, input.length - 1);
1482 const last_char = input.charAt(input.length-1);
1483 const subtrie = this.getSubtree(substr, given_trie);
1485 if( !subtrie ) //substr not in the trie
1490 const indexed_trie = subtrie[last_char];
1491 return indexed_trie;
1498 takes in a trie and returns a list of its item id's
1503 return itemIDs; //empty, base case
1505 for( const key in trie )
1511 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1513 if ( trie.isComplete )
1515 itemIDs.push(...trie.ids);
1524 takes a list of tries
1525 returns a list of ids of objects that are available
1528 for( const tree of trees )
1530 const available_IDs = this.serialize(tree);
1532 for( const itemID of available_IDs ) {
1533 results[itemID] = this.items[itemID];
1539 generate_element_text(obj)
1541 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1542 const result = content_strings.shift();
1543 if( result == null || content_strings.length < 1) {
1546 return result + " (" + content_strings.join(", ") + ")";
1553 takes in a mapping of ids to objects in items
1554 and displays them in the dropdown
1556 const drop = document.getElementById("drop_results");
1557 while(drop.firstChild)
1559 drop.removeChild(drop.firstChild);
1562 for( const id in ids )
1564 const obj = this.items[id];
1565 const result_text = this.generate_element_text(obj);
1566 const result_entry = document.createElement("a");
1567 result_entry.href = "#";
1568 result_entry.innerText = result_text;
1569 result_entry.title = result_text;
1570 result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1571 result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1572 const tooltip = document.createElement("span");
1573 const tooltiptext = document.createTextNode(result_text);
1574 tooltip.appendChild(tooltiptext);
1575 tooltip.classList.add("d-none");
1576 result_entry.appendChild(tooltip);
1577 drop.appendChild(result_entry);
1580 const scroll_restrictor = document.getElementById("scroll_restrictor");
1582 if( !drop.firstChild )
1584 scroll_restrictor.style.visibility = 'hidden';
1588 scroll_restrictor.style.visibility = 'inherit';
1592 select_item(item_id)
1594 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1596 this.added_items.add(item_id);
1598 this.update_selected_list();
1599 // clear search bar contents
1600 document.getElementById("user_field").value = "";
1601 document.getElementById("user_field").focus();
1605 remove_item(item_id)
1607 this.added_items.delete(item_id);
1609 this.update_selected_list()
1610 document.getElementById("user_field").focus();
1613 update_selected_list()
1615 document.getElementById("added_number").innerText = this.added_items.size;
1616 const selector = document.getElementById('selector');
1617 selector.value = JSON.stringify([...this.added_items]);
1618 const added_list = document.getElementById('added_list');
1620 while(selector.firstChild)
1622 selector.removeChild(selector.firstChild);
1624 while(added_list.firstChild)
1626 added_list.removeChild(added_list.firstChild);
1629 const list_html = document.createElement("div");
1630 list_html.classList.add("list-group");
1632 for( const item_id of this.added_items )
1634 const times = document.createElement("li");
1635 times.classList.add("fas", "fa-times");
1637 const deleteButton = document.createElement("a");
1638 deleteButton.href = "#";
1639 deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1640 // Setting .onclick/.addEventListener does not work,
1641 // which is why I took the setAttribute approach
1642 // If anyone knows why, please let me know :]
1643 deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1644 deleteButton.classList.add("btn");
1645 const deleteColumn = document.createElement("div");
1646 deleteColumn.classList.add("col-auto");
1647 deleteColumn.append(deleteButton);
1649 const item = this.items[item_id];
1650 const element_entry_text = this.generate_element_text(item);
1651 const textColumn = document.createElement("div");
1652 textColumn.classList.add("col", "overflow-ellipsis");
1653 textColumn.innerText = element_entry_text;
1654 textColumn.title = element_entry_text;
1656 const itemRow = document.createElement("div");
1657 itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1658 itemRow.append(textColumn, deleteColumn);
1660 list_html.append(itemRow);
1662 added_list.innerHTML = list_html.innerHTML;