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("gob");
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("gof");
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) {
123 document.getElementById("view_message").innerText = message;
124 document.getElementById("view_message").className = "step_message";
125 document.getElementById("view_message").classList.add("message_" + stepstatus);
128 function submitStepForm(next_step = "current"){
129 run_form_callbacks();
130 const step_form_data = $("#step_form").serialize();
131 const form_data = $.param({
133 "step_form": step_form_data,
134 "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
137 '/workflow/manager/',
139 (data) => update_page(data),
141 ).fail(() => alert("failure"));
144 function run_form_callbacks(){
145 for(f of form_submission_callbacks)
147 form_submission_callbacks = [];
150 function create_workflow(type) {
153 url: "/workflow/create/",
155 "workflow_type": type
158 "X-CSRFToken": getCookie('csrftoken')
160 }).done(function (data, textStatus, jqXHR) {
161 window.location = "/workflow/";
162 }).fail(function (jqxHR, textstatus) {
163 alert("Something went wrong...");
167 function add_workflow(type) {
170 url: "/workflow/add/",
172 "workflow_type": type
175 "X-CSRFToken": getCookie('csrftoken')
177 }).done(function (data, textStatus, jqXHR) {
179 }).fail(function (jqxHR, textstatus) {
180 alert("Something went wrong...");
184 function pop_workflow() {
187 url: "/workflow/pop/",
189 "X-CSRFToken": getCookie('csrftoken')
191 }).done(function (data, textStatus, jqXHR) {
193 }).fail(function (jqxHR, textstatus) {
194 alert("Something went wrong...");
198 function continue_workflow() {
199 window.location.replace("/workflow/");
206 class MultipleSelectFilterWidget {
208 constructor(neighbors, items, initial) {
210 this.graph_neighbors = neighbors;
211 this.filter_items = items;
212 this.currentLab = null;
213 this.available_resources = {};
215 this.dropdown_count = 0;
217 for(let nodeId in this.filter_items) {
218 const node = this.filter_items[nodeId];
219 this.result[node.class] = {}
222 this.make_selection(initial);
225 make_selection(initial_data){
226 if(!initial_data || jQuery.isEmptyObject(initial_data))
229 // Need to sort through labs first
230 let initial_lab = initial_data['lab'];
231 let initial_resources = initial_data['resource'];
233 for( let node_id in initial_lab) { // This should only be length one
234 const node = this.filter_items[node_id];
235 const selection_data = initial_lab[node_id];
236 if( selection_data.selected ) {
238 this.markAndSweep(node);
239 this.updateResult(node);
241 if(node['multiple']){
242 this.make_multiple_selection(node, selection_data);
244 this.currentLab = node;
245 this.available_resources = JSON.parse(node['available_resources']);
248 for( let node_id in initial_resources){
249 const node = this.filter_items[node_id];
250 const selection_data = initial_resources[node_id];
251 if( selection_data.selected ) {
253 this.markAndSweep(node);
254 this.updateResult(node);
256 if(node['multiple']){
257 this.make_multiple_selection(node, selection_data);
260 this.updateAvailibility();
263 make_multiple_selection(node, selection_data){
264 const prepop_data = selection_data.values;
265 for(let k in prepop_data){
266 const div = this.add_item_prepopulate(node, prepop_data[k]);
267 this.updateObjectResult(node, div.id, prepop_data[k]);
272 for(let i in this.filter_items) {
273 const node = this.filter_items[i];
274 node['marked'] = true; //mark all nodes
277 const toCheck = [root];
278 while(toCheck.length > 0){
279 const node = toCheck.pop();
281 if(!node['marked']) {
282 continue; //already visited, just continue
285 node['marked'] = false; //mark as visited
286 if(node['follow'] || node == root){ //add neighbors if we want to follow this node
287 const neighbors = this.graph_neighbors[node.id];
288 for(let neighId of neighbors) {
289 const neighbor = this.filter_items[neighId];
290 toCheck.push(neighbor);
295 //now remove all nodes still marked
296 for(let i in this.filter_items){
297 const node = this.filter_items[i];
299 this.disable_node(node);
305 if(node['selected']) {
306 this.markAndSweep(node);
308 else { //TODO: make this not dumb
310 //remember the currently selected, then reset everything and reselect one at a time
311 for(let nodeId in this.filter_items) {
312 node = this.filter_items[nodeId];
313 if(node['selected']) {
318 for(let node of selected) {
320 this.markAndSweep(node);
326 const elem = document.getElementById(node['id']);
327 node['selected'] = true;
328 elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
329 elem.classList.add('selected_node');
331 if(node['class'] == 'resource')
332 this.reserveResource(node);
337 const elem = document.getElementById(node['id']);
338 node['selected'] = false;
339 node['selectable'] = true;
340 elem.classList.add('bg-white')
341 elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
345 const elem = document.getElementById(node['id']);
346 node['selected'] = false;
347 node['selectable'] = false;
348 elem.classList.remove('bg-white', 'selected_node');
349 elem.classList.add('not-allowed', 'bg-light');
353 // if lab is not already selected update available resources
354 if(!node['selected']) {
355 this.currentLab = node;
356 this.available_resources = JSON.parse(node['available_resources']);
357 this.updateAvailibility();
359 // a lab is already selected, clear already selected resources
360 if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) {
368 updateAvailibility() {
369 const lab_resources = this.graph_neighbors[this.currentLab.id];
371 // need to loop through and update all quantities
372 for(let i in lab_resources) {
373 const resource_node = this.filter_items[lab_resources[i]];
374 const required_resources = JSON.parse(resource_node['required_resources']);
375 let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0];
376 let leastAvailable = 100;
378 let quantityDescription;
381 for(let resource in required_resources) {
382 currCount = Math.floor(this.available_resources[resource] / required_resources[resource]);
383 if(currCount < leastAvailable)
384 leastAvailable = currCount;
386 if(!currCount || currCount < 0) {
392 if (elem.children[0]){
393 elem.removeChild(elem.children[0]);
396 quantityDescription = '<br> Quantity Currently Available: ' + leastAvailable;
397 quantityNode = document.createElement('P');
398 if (leastAvailable > 0) {
399 quantityDescription = quantityDescription.fontcolor('green');
401 quantityDescription = quantityDescription.fontcolor('red');
404 quantityNode.innerHTML = quantityDescription;
405 elem.appendChild(quantityNode)
409 reserveResource(node){
410 const required_resources = JSON.parse(node['required_resources']);
411 let hostname = document.getElementById('id_hostname');
412 let image = document.getElementById('id_image');
416 for(let resource in required_resources){
417 this.available_resources[resource] -= required_resources[resource];
418 cnt += required_resources[resource];
421 if (cnt > 1 && hostname && image) {
422 hostname.readOnly = true;
423 image.disabled = true;
426 this.updateAvailibility();
429 releaseResource(node){
430 const required_resources = JSON.parse(node['required_resources']);
431 let hostname = document.getElementById('id_hostname');
432 let image = document.getElementById('id_image');
434 for(let resource in required_resources){
435 this.available_resources[resource] += required_resources[resource];
438 if (hostname && image) {
439 hostname.readOnly = false;
440 image.disabled = false;
443 this.updateAvailibility();
448 const node = this.filter_items[id];
449 if(!node['selectable'])
452 // If they are selecting a lab, update accordingly
453 if (node['class'] == 'lab') {
454 lab_check = this.labCheck(node);
459 // Can only select a resource if a lab is selected
460 if (!this.currentLab) {
461 alert('You must select a lab before selecting a resource');
465 if(node['multiple']){
466 return this.processClickMultiple(node);
468 return this.processClickSingle(node);
472 processClickSingle(node){
473 node['selected'] = !node['selected']; //toggle on click
474 if(node['selected']) {
478 this.releaseResource(node); // can't do this in clear since clear removes border
481 this.updateResult(node);
484 processClickMultiple(node){
486 const div = this.add_item_prepopulate(node, false);
488 this.updateObjectResult(node, div.id, "");
491 restrictchars(input){
492 if( input.validity.patternMismatch ){
493 input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
494 input.reportValidity();
496 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
497 this.checkunique(input);
500 checkunique(tocheck){ //TODO: use set
501 const val = tocheck.value;
502 for( let input of this.inputs ){
503 if( input.value == val && input != tocheck){
504 tocheck.setCustomValidity("All hostnames must be unique");
505 tocheck.reportValidity();
509 tocheck.setCustomValidity("");
512 make_remove_button(div, node){
513 const button = document.createElement("BUTTON");
514 button.type = "button";
515 button.appendChild(document.createTextNode("Remove"));
516 button.classList.add("btn", "btn-danger", "d-inline-block");
518 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
522 make_input(div, node, prepopulate){
523 const input = document.createElement("INPUT");
524 input.type = node.form.type;
525 input.name = node.id + node.form.name
526 input.classList.add("form-control", "w-auto", "d-inline-block");
527 input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
528 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
529 input.placeholder = node.form.placeholder;
530 this.inputs.push(input);
532 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
533 input.oninput = function() { that.restrictchars(this); };
535 input.value = prepopulate;
539 add_item_prepopulate(node, prepopulate){
540 const div = document.createElement("DIV");
541 div.id = "dropdown_" + this.dropdown_count;
542 div.classList.add("card", "flex-row", "d-flex", "mb-2");
543 this.dropdown_count++;
544 const label = document.createElement("H5")
545 label.appendChild(document.createTextNode(node['name']))
546 label.classList.add("p-1", "m-1", "flex-grow-1");
547 div.appendChild(label);
548 let remove_btn = this.make_remove_button(div, node);
549 remove_btn.classList.add("p-1", "m-1");
550 div.appendChild(remove_btn);
551 document.getElementById("dropdown_wrapper").appendChild(div);
555 remove_dropdown(div_id, node_id){
556 const div = document.getElementById(div_id);
557 const node = this.filter_items[node_id]
558 const parent = div.parentNode;
559 div.parentNode.removeChild(div);
560 this.result[node.class][node.id]['count']--;
561 this.releaseResource(node); // This can't be done on clear b/c clear removes border
563 //checks if we have removed last item in class
564 if(this.result[node.class][node.id]['count'] == 0){
565 delete this.result[node.class][node.id];
571 if(!node['multiple']){
572 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
574 delete this.result[node.class][node.id];
578 updateObjectResult(node, childKey, childValue){
579 if(!this.result[node.class][node.id])
580 this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0}
582 this.result[node.class][node.id]['count']++;
586 document.getElementById("filter_field").value = JSON.stringify(this.result);
598 // description: string,
606 // network: int, [networks.id]
621 constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){
622 if(!this.check_support()) {
623 console.log("Aborting, browser is not supported");
627 this.currentWindow = null;
629 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
631 this.lastHostBottom = 100;
632 this.networks = new Set();
633 this.has_public_net = false;
635 this.editor = new mxEditor();
636 this.graph = this.editor.graph;
638 window.global_graph = this.graph;
639 window.network_rr_index = 5;
641 this.editor.setGraphContainer(graphContainer);
642 this.doGlobalConfig();
646 for(const network_id in networks) {
647 let network = networks[network_id];
649 mx_networks[network_id] = this.populateNetwork(network);
652 this.prefillHosts(resources, mx_networks);
654 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
655 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
656 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus');
657 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus');
660 this.editor.addAction('printXML', function(editor, cell) {
661 mxLog.write(this.encodeGraph());
664 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code');
667 new mxOutline(this.graph, overviewContainer);
668 //sets the edge color to be the same as the network
669 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
670 //hooks up double click functionality
671 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
675 if (!mxClient.isBrowserSupported()) {
676 mxUtils.error('Browser is not supported', 200, false);
684 * mx_interface: mxCell for the interface itself
685 * network: mxCell for the outer network
688 connectNetwork(mx_interface, network, tagged) {
689 var cell = new mxCell(
690 "connection from " + network + " to " + mx_interface,
691 new mxGeometry(0, 0, 50, 50));
693 cell.geometry.relative = true;
694 cell.setValue(JSON.stringify({tagged: tagged}));
696 let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network);
697 let edge = this.graph.addEdge(cell, null, mx_interface, terminal);
698 this.colorEdge(edge, terminal, true);
699 this.graph.refresh(edge);
705 * to: desired y axis position of the matching cell
706 * within: graph cell for a full network, with all child cells
709 * an mx cell, the one vertically closest to the desired value
712 * modifies the <rr_index> on the <within> parameter
714 getClosestNetworkCell(to, within) {
715 if(window.network_rr_index === undefined) {
716 window.network_rr_index = 5;
719 let child_keys = within.children.keys();
720 let children = Array.from(within.children);
721 let index = (window.network_rr_index++) % children.length;
723 let child = within.children[child_keys[index]];
725 return children[index];
734 * description: string,
742 * network: int, [networks.id]
750 * network_mappings: {
751 * <django network id>: <mxnetwork id>
754 * draws given hosts into the mxgraph
756 prefillHosts(hosts, network_mappings){
757 for(const host_id in hosts) {
758 this.makeHost(hosts[host_id], network_mappings);
762 cellConnectionHandler(sender, event){
763 const edge = event.getProperty('edge');
764 const terminal = event.getProperty('terminal')
765 const source = event.getProperty('source');
766 if(this.checkAllowed(edge, terminal, source)) {
767 this.colorEdge(edge, terminal, source);
768 this.alertVlan(edge, terminal, source);
772 doubleClickHandler(evt, cell) {
774 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
775 cell = cell.getParent();
777 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
778 this.createDeleteDialog(cell.getId());
781 this.showDetailWindow(cell);
786 alertVlan(edge, terminal, source) {
787 if( terminal == null || edge.getTerminal(!source) == null) {
790 const form = document.createElement("form");
791 const tagged = document.createElement("input");
792 tagged.type = "radio";
793 tagged.name = "tagged";
794 tagged.value = "True";
795 form.appendChild(tagged);
796 form.appendChild(document.createTextNode(" Tagged"));
797 form.appendChild(document.createElement("br"));
799 const untagged = document.createElement("input");
800 untagged.type = "radio";
801 untagged.name = "tagged";
802 untagged.value = "False";
803 form.appendChild(untagged);
804 form.appendChild(document.createTextNode(" Untagged"));
805 form.appendChild(document.createElement("br"));
807 const yes_button = document.createElement("button");
808 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
809 yes_button.appendChild(document.createTextNode("Okay"));
811 const cancel_button = document.createElement("button");
812 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
813 cancel_button.appendChild(document.createTextNode("Cancel"));
815 const error_div = document.createElement("div");
816 error_div.id = "current_window_errors";
817 form.appendChild(error_div);
819 const content = document.createElement('div');
820 content.appendChild(form);
821 content.appendChild(yes_button);
822 content.appendChild(cancel_button);
823 this.showWindow("Vlan Selection", content, 200, 200);
826 createDeleteDialog(id) {
827 const content = document.createElement('div');
828 const remove_button = document.createElement("button");
829 remove_button.style.width = '46%';
830 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
831 remove_button.appendChild(document.createTextNode("Remove"));
832 const cancel_button = document.createElement("button");
833 cancel_button.style.width = '46%';
834 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
835 cancel_button.appendChild(document.createTextNode("Cancel"));
837 content.appendChild(remove_button);
838 content.appendChild(cancel_button);
839 this.showWindow('Do you want to delete this network?', content, 200, 62);
842 checkAllowed(edge, terminal, source) {
843 //check if other terminal is null, and that they are different
844 const otherTerminal = edge.getTerminal(!source);
845 if(terminal != null && otherTerminal != null) {
846 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
847 otherTerminal.getParent().getId().split('_')[0] ) {
849 this.graph.removeCells([edge]);
856 colorEdge(edge, terminal, source) {
857 if(terminal.getParent().getId().indexOf('network') >= 0) {
858 const styles = terminal.getParent().getStyle().split(';');
860 for(let style of styles){
861 const kvp = style.split('=');
862 if(kvp[0] == "fillColor"){
867 edge.setStyle('strokeColor=' + color);
869 console.log("Failed to color " + edge + ", " + terminal + ", " + source);
873 showDetailWindow(cell) {
874 const info = JSON.parse(cell.getValue());
875 const content = document.createElement("div");
876 const pre_tag = document.createElement("pre");
877 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
878 const ok_button = document.createElement("button");
879 ok_button.onclick = function() { this.closeWindow();};
880 content.appendChild(pre_tag);
881 content.appendChild(ok_button);
882 this.showWindow('Details', content, 400, 400);
885 restoreFromXml(xml, editor) {
886 const doc = mxUtils.parseXml(xml);
887 const node = doc.documentElement;
888 editor.readGraphModel(node);
890 //Iterate over all children, and parse the networks to add them to the sidebar
891 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
892 if(cell.getId().indexOf("network") > -1) {
893 const info = JSON.parse(cell.getValue());
894 const name = info['name'];
895 this.networks.add(name);
896 const styles = cell.getStyle().split(";");
898 for(const style of styles){
899 const kvp = style.split('=');
900 if(kvp[0] == "fillColor") {
906 this.has_public_net = true;
909 this.makeSidebarNetwork(name, color, cell.getId());
915 var cell = this.graph.getModel().getCell(cellId);
916 if( cellId.indexOf("network") > -1 ) {
917 let elem = document.getElementById(cellId);
918 elem.parentElement.removeChild(elem);
920 this.graph.removeCells([cell]);
921 this.currentWindow.destroy();
925 const input = document.createElement("input");
927 input.name = "net_name";
928 input.maxlength = 100;
929 input.id = "net_name_input";
930 input.style.margin = "5px";
932 const yes_button = document.createElement("button");
933 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
934 yes_button.appendChild(document.createTextNode("Okay"));
936 const cancel_button = document.createElement("button");
937 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
938 cancel_button.appendChild(document.createTextNode("Cancel"));
940 const error_div = document.createElement("div");
941 error_div.id = "current_window_errors";
943 const content = document.createElement("div");
944 content.appendChild(document.createTextNode("Name: "));
945 content.appendChild(input);
946 content.appendChild(document.createElement("br"));
947 content.appendChild(yes_button);
948 content.appendChild(cancel_button);
949 content.appendChild(document.createElement("br"));
950 content.appendChild(error_div);
952 this.showWindow("Network Creation", content, 300, 300);
955 parseNetworkWindow() {
956 const net_name = document.getElementById("net_name_input").value
957 const error_div = document.getElementById("current_window_errors");
958 if( this.networks.has(net_name) ){
959 error_div.innerHTML = "All network names must be unique";
962 this.addNetwork(net_name);
963 this.currentWindow.destroy();
966 addToolbarButton(editor, toolbar, action, image) {
967 const button = document.createElement('button');
968 button.setAttribute('class', 'btn btn-sm m-1');
970 const icon = document.createElement('i');
971 icon.setAttribute('class', 'fas ' + image);
972 button.appendChild(icon);
974 mxEvent.addListener(button, 'click', function(evt) {
975 editor.execute(action);
977 mxUtils.write(button, '');
978 toolbar.appendChild(button);
982 const encoder = new mxCodec();
983 const xml = encoder.encode(this.graph.getModel());
984 return mxUtils.getXml(xml);
988 //general graph stuff
989 this.graph.setMultigraph(false);
990 this.graph.setCellsSelectable(false);
991 this.graph.setCellsMovable(false);
994 this.graph.vertexLabelIsMovable = true;
997 this.graph.setConnectable(true);
998 this.graph.setAllowDanglingEdges(false);
999 mxEdgeHandler.prototype.snapToTerminals = true;
1000 mxConstants.MIN_HOTSPOT_SIZE = 16;
1001 mxConstants.DEFAULT_HOTSPOT = 1;
1002 //edge 'style' (still affects behavior greatly)
1003 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
1004 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
1005 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
1006 style[mxConstants.STYLE_ROUNDED] = true;
1007 style[mxConstants.STYLE_FONTCOLOR] = 'black';
1008 style[mxConstants.STYLE_STROKECOLOR] = 'red';
1009 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
1010 style[mxConstants.STYLE_STROKEWIDTH] = '3';
1011 style[mxConstants.STYLE_ROUNDED] = true;
1012 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
1014 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
1015 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
1017 this.graph.convertValueToString = function(cell) {
1019 //changes value for edges with xml value
1021 if(JSON.parse(cell.getValue())["tagged"]) {
1027 return JSON.parse(cell.getValue())['name'];
1031 return cell.getValue();
1036 showWindow(title, content, width, height) {
1037 //create transparent black background
1038 const background = document.createElement('div');
1039 background.style.position = 'absolute';
1040 background.style.left = '0px';
1041 background.style.top = '0px';
1042 background.style.right = '0px';
1043 background.style.bottom = '0px';
1044 background.style.background = 'black';
1045 mxUtils.setOpacity(background, 50);
1046 document.body.appendChild(background);
1048 const x = Math.max(0, document.body.scrollWidth/2-width/2);
1049 const y = Math.max(10, (document.body.scrollHeight ||
1050 document.documentElement.scrollHeight)/2-height*2/3);
1052 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
1053 wnd.setClosable(false);
1055 wnd.addListener(mxEvent.DESTROY, function(evt) {
1056 this.graph.setEnabled(true);
1057 mxEffects.fadeOut(background, 50, true, 10, 30, true);
1059 this.currentWindow = wnd;
1061 this.graph.setEnabled(false);
1062 this.currentWindow.setVisible(true);
1066 //allows the current window to be destroyed
1067 this.currentWindow.destroy();
1070 othersUntagged(edgeID) {
1071 const edge = this.graph.getModel().getCell(edgeID);
1072 const end1 = edge.getTerminal(true);
1073 const end2 = edge.getTerminal(false);
1075 if( end1.getParent().getId().split('_')[0] == 'host' ){
1081 var edges = netint.edges;
1082 for( let edge of edges) {
1083 if( edge.getValue() ) {
1084 var tagged = JSON.parse(edge.getValue()).tagged;
1097 deleteVlanWindow(edgeID) {
1098 const cell = this.graph.getModel().getCell(edgeID);
1099 this.graph.removeCells([cell]);
1100 this.currentWindow.destroy();
1103 parseVlanWindow(edgeID) {
1104 //do parsing and data manipulation
1105 const radios = document.getElementsByName("tagged");
1106 const edge = this.graph.getModel().getCell(edgeID);
1108 for(let radio of radios){
1110 //set edge to be tagged or untagged
1111 if( radio.value == "False") {
1112 if( this.othersUntagged(edgeID) ) {
1113 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
1117 const edgeVal = {tagged: radio.value == "True"};
1118 edge.setValue(JSON.stringify(edgeVal));
1122 this.graph.refresh(edge);
1126 makeMxNetwork(net_name, is_public = false) {
1127 const model = this.graph.getModel();
1129 const height = 1700;
1130 const xoff = 400 + (30 * this.netCount);
1132 let color = this.netColors[this.netCount];
1133 if( this.netCount > (this.netColors.length - 1)) {
1134 color = Math.floor(Math.random() * 16777215); //int in possible color space
1135 color = '#' + color.toString(16).toUpperCase(); //convert to hex
1137 const net_val = { name: net_name, public: is_public};
1138 const net = this.graph.insertVertex(
1139 this.graph.getDefaultParent(),
1140 'network_' + this.netCount,
1141 JSON.stringify(net_val),
1146 'fillColor=' + color,
1149 const num_ports = 45;
1150 for(var i=0; i<num_ports; i++){
1151 let port = this.graph.insertVertex(
1159 'fillColor=black;opacity=0',
1164 const ret_val = { color: color, element_id: "network_" + this.netCount };
1166 this.networks.add(net_name);
1180 // mxgraph id of network
1181 populateNetwork(network) {
1182 let mxNet = this.makeMxNetwork(network.name, network.public);
1183 this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id);
1185 if( network.public ) {
1186 this.has_public_net = true;
1189 return mxNet.element_id;
1192 addPublicNetwork() {
1193 const net = this.makeMxNetwork("public", true);
1194 this.makeSidebarNetwork("public", net['color'], net['element_id']);
1195 this.has_public_net = true;
1198 addNetwork(net_name) {
1199 const ret = this.makeMxNetwork(net_name);
1200 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
1203 updateHosts(removed) {
1205 for(const hostID of removed) {
1206 cells.push(this.graph.getModel().getCell("host_" + hostID));
1208 this.graph.removeCells(cells);
1210 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
1212 for(const i in hosts) {
1213 const host = hosts[i];
1214 if(host.id.startsWith("host_")){
1215 const geometry = host.getGeometry();
1216 geometry.y = topdist + 50;
1217 topdist = geometry.y + geometry.height;
1218 host.setGeometry(geometry);
1223 makeSidebarNetwork(net_name, color, net_id){
1224 const colorBlob = document.createElement("div");
1225 colorBlob.className = "square-20 rounded-circle";
1226 colorBlob.style['background'] = color;
1228 const textContainer = document.createElement("span");
1229 textContainer.className = "ml-2";
1230 textContainer.appendChild(document.createTextNode(net_name));
1232 const timesIcon = document.createElement("i");
1233 timesIcon.classList.add("fas", "fa-times");
1235 const deletebutton = document.createElement("button");
1236 deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
1237 deletebutton.appendChild(timesIcon);
1238 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
1240 const newNet = document.createElement("li");
1241 newNet.classList.add("list-group-item", "d-flex", "bg-light");
1243 newNet.appendChild(colorBlob);
1244 newNet.appendChild(textContainer);
1246 if( net_name != "public" ) {
1247 newNet.appendChild(deletebutton);
1249 document.getElementById("network_list").appendChild(newNet);
1257 * 'description': string,
1266 * network: int, <django network id>,
1274 * network_mappings: {
1275 * <django network id>: <mxnetwork id>
1278 makeHost(hostInfo, network_mappings) {
1279 const value = JSON.stringify(hostInfo['value']);
1280 const interfaces = hostInfo['interfaces'];
1282 const height = (25 * interfaces.length) + 25;
1284 const yoff = this.lastHostBottom + 50;
1285 this.lastHostBottom = yoff + height;
1286 const host = this.graph.insertVertex(
1287 this.graph.getDefaultParent(),
1288 'host_' + hostInfo['id'],
1297 host.getGeometry().offset = new mxPoint(-50,0);
1298 host.setConnectable(false);
1301 for(var i=0; i<interfaces.length; i++) {
1302 const port = this.graph.insertVertex(
1305 JSON.stringify(interfaces[i]),
1310 'fillColor=blue;editable=0',
1313 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
1314 const iface = interfaces[i];
1315 for( const connection of iface.connections ) {
1316 const network = this
1319 .getCell(network_mappings[connection.network]);
1321 this.connectNetwork(port, network, connection.tagged);
1323 this.graph.refresh(port);
1325 this.graph.refresh(host);
1329 const input_elem = document.getElementById("hidden_xml_input");
1330 input_elem.value = this.encodeGraph(this.graph);
1334 class SearchableSelectMultipleWidget {
1335 constructor(format_vars, field_dataset, field_initial) {
1336 this.format_vars = format_vars;
1337 this.items = field_dataset;
1338 this.initial = field_initial;
1340 this.expanded_name_trie = {"isComplete": false};
1341 this.small_name_trie = {"isComplete": false};
1342 this.string_trie = {"isComplete": false};
1344 this.added_items = new Set();
1346 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
1348 this[e] = format_vars[e];
1351 this.search_field_init();
1353 if( this.show_from_noentry )
1360 const textfield = document.getElementById("user_field");
1361 const drop = document.getElementById("drop_results");
1363 textfield.disabled = "True";
1364 drop.style.display = "none";
1366 const btns = document.getElementsByClassName("btn-remove");
1367 for( const btn of btns )
1369 btn.classList.add("disabled");
1374 search_field_init() {
1375 this.build_all_tries(this.items);
1377 for( const elem of this.initial )
1379 this.select_item(elem);
1381 if(this.initial.length == 1)
1383 this.search(this.items[this.initial[0]]["small_name"]);
1384 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
1388 build_all_tries(dict)
1390 for( const key in dict )
1392 this.add_item(dict[key]);
1398 const id = item['id'];
1399 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
1400 this.add_to_tree(item['small_name'], id, this.small_name_trie);
1401 this.add_to_tree(item['string'], id, this.string_trie);
1404 add_to_tree(str, id, trie)
1406 let inner_trie = trie;
1409 if( !inner_trie[str.charAt(0)] )
1412 inner_trie[str.charAt(0)] = new_trie;
1416 var new_trie = inner_trie[str.charAt(0)];
1419 if( str.length == 1 )
1421 new_trie.isComplete = true;
1426 new_trie.ids.push(id);
1428 inner_trie = new_trie;
1429 str = str.substring(1);
1435 if( input.length == 0 && !this.show_from_noentry){
1439 else if( input.length == 0 && this.show_from_noentry)
1441 this.dropdown(this.items); //show all items
1446 const tr1 = this.getSubtree(input, this.expanded_name_trie);
1448 const tr2 = this.getSubtree(input, this.small_name_trie);
1450 const tr3 = this.getSubtree(input, this.string_trie);
1452 const results = this.collate(trees);
1453 this.dropdown(results);
1457 getSubtree(input, given_trie)
1460 recursive function to return the trie accessed at input
1463 if( input.length == 0 ){
1468 const substr = input.substring(0, input.length - 1);
1469 const last_char = input.charAt(input.length-1);
1470 const subtrie = this.getSubtree(substr, given_trie);
1472 if( !subtrie ) //substr not in the trie
1477 const indexed_trie = subtrie[last_char];
1478 return indexed_trie;
1485 takes in a trie and returns a list of its item id's
1490 return itemIDs; //empty, base case
1492 for( const key in trie )
1498 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1500 if ( trie.isComplete )
1502 itemIDs.push(...trie.ids);
1511 takes a list of tries
1512 returns a list of ids of objects that are available
1515 for( const tree of trees )
1517 const available_IDs = this.serialize(tree);
1519 for( const itemID of available_IDs ) {
1520 results[itemID] = this.items[itemID];
1526 generate_element_text(obj)
1528 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1529 const result = content_strings.shift();
1530 if( result == null || content_strings.length < 1) {
1533 return result + " (" + content_strings.join(", ") + ")";
1540 takes in a mapping of ids to objects in items
1541 and displays them in the dropdown
1543 const drop = document.getElementById("drop_results");
1544 while(drop.firstChild)
1546 drop.removeChild(drop.firstChild);
1549 for( const id in ids )
1551 const obj = this.items[id];
1552 const result_text = this.generate_element_text(obj);
1553 const result_entry = document.createElement("a");
1554 result_entry.href = "#";
1555 result_entry.innerText = result_text;
1556 result_entry.title = result_text;
1557 result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1558 result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1559 const tooltip = document.createElement("span");
1560 const tooltiptext = document.createTextNode(result_text);
1561 tooltip.appendChild(tooltiptext);
1562 tooltip.classList.add("d-none");
1563 result_entry.appendChild(tooltip);
1564 drop.appendChild(result_entry);
1567 const scroll_restrictor = document.getElementById("scroll_restrictor");
1569 if( !drop.firstChild )
1571 scroll_restrictor.style.visibility = 'hidden';
1575 scroll_restrictor.style.visibility = 'inherit';
1579 select_item(item_id)
1581 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1583 this.added_items.add(item_id);
1585 this.update_selected_list();
1586 // clear search bar contents
1587 document.getElementById("user_field").value = "";
1588 document.getElementById("user_field").focus();
1592 remove_item(item_id)
1594 this.added_items.delete(item_id);
1596 this.update_selected_list()
1597 document.getElementById("user_field").focus();
1600 update_selected_list()
1602 document.getElementById("added_number").innerText = this.added_items.size;
1603 const selector = document.getElementById('selector');
1604 selector.value = JSON.stringify([...this.added_items]);
1605 const added_list = document.getElementById('added_list');
1607 while(selector.firstChild)
1609 selector.removeChild(selector.firstChild);
1611 while(added_list.firstChild)
1613 added_list.removeChild(added_list.firstChild);
1616 const list_html = document.createElement("div");
1617 list_html.classList.add("list-group");
1619 for( const item_id of this.added_items )
1621 const times = document.createElement("li");
1622 times.classList.add("fas", "fa-times");
1624 const deleteButton = document.createElement("a");
1625 deleteButton.href = "#";
1626 deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1627 // Setting .onclick/.addEventListener does not work,
1628 // which is why I took the setAttribute approach
1629 // If anyone knows why, please let me know :]
1630 deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1631 deleteButton.classList.add("btn");
1632 const deleteColumn = document.createElement("div");
1633 deleteColumn.classList.add("col-auto");
1634 deleteColumn.append(deleteButton);
1636 const item = this.items[item_id];
1637 const element_entry_text = this.generate_element_text(item);
1638 const textColumn = document.createElement("div");
1639 textColumn.classList.add("col", "overflow-ellipsis");
1640 textColumn.innerText = element_entry_text;
1641 textColumn.title = element_entry_text;
1643 const itemRow = document.createElement("div");
1644 itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1645 itemRow.append(textColumn, deleteColumn);
1647 list_html.append(itemRow);
1649 added_list.innerHTML = list_html.innerHTML;