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) {
422 hostname.readOnly = true;
423 // we only disable hostname modification because there is no sane case where you want all hosts to have the same hostname
424 // image is still allowed to be set across all hosts, but is filtered to the set of images that are commonly applicable still
425 // if no images exist that would apply to all hosts in a pod, then the user is restricted to not setting an image
426 // and the default image for each host is used
429 this.updateAvailibility();
432 releaseResource(node){
433 const required_resources = JSON.parse(node['required_resources']);
434 let hostname = document.getElementById('id_hostname');
435 let image = document.getElementById('id_image');
437 for(let resource in required_resources){
438 this.available_resources[resource] += required_resources[resource];
441 if (hostname && image) {
442 hostname.readOnly = false;
443 image.disabled = false;
446 this.updateAvailibility();
451 const node = this.filter_items[id];
452 if(!node['selectable'])
455 // If they are selecting a lab, update accordingly
456 if (node['class'] == 'lab') {
457 lab_check = this.labCheck(node);
462 // Can only select a resource if a lab is selected
463 if (!this.currentLab) {
464 alert('You must select a lab before selecting a resource');
468 if(node['multiple']){
469 return this.processClickMultiple(node);
471 return this.processClickSingle(node);
475 processClickSingle(node){
476 node['selected'] = !node['selected']; //toggle on click
477 if(node['selected']) {
481 this.releaseResource(node); // can't do this in clear since clear removes border
484 this.updateResult(node);
487 processClickMultiple(node){
489 const div = this.add_item_prepopulate(node, false);
491 this.updateObjectResult(node, div.id, "");
494 restrictchars(input){
495 if( input.validity.patternMismatch ){
496 input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
497 input.reportValidity();
499 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
500 this.checkunique(input);
503 checkunique(tocheck){ //TODO: use set
504 const val = tocheck.value;
505 for( let input of this.inputs ){
506 if( input.value == val && input != tocheck){
507 tocheck.setCustomValidity("All hostnames must be unique");
508 tocheck.reportValidity();
512 tocheck.setCustomValidity("");
515 make_remove_button(div, node){
516 const button = document.createElement("BUTTON");
517 button.type = "button";
518 button.appendChild(document.createTextNode("Remove"));
519 button.classList.add("btn", "btn-danger", "d-inline-block");
521 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
525 make_input(div, node, prepopulate){
526 const input = document.createElement("INPUT");
527 input.type = node.form.type;
528 input.name = node.id + node.form.name
529 input.classList.add("form-control", "w-auto", "d-inline-block");
530 input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
531 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
532 input.placeholder = node.form.placeholder;
533 this.inputs.push(input);
535 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
536 input.oninput = function() { that.restrictchars(this); };
538 input.value = prepopulate;
542 add_item_prepopulate(node, prepopulate){
543 const div = document.createElement("DIV");
544 div.id = "dropdown_" + this.dropdown_count;
545 div.classList.add("card", "flex-row", "d-flex", "mb-2");
546 this.dropdown_count++;
547 const label = document.createElement("H5")
548 label.appendChild(document.createTextNode(node['name']))
549 label.classList.add("p-1", "m-1", "flex-grow-1");
550 div.appendChild(label);
551 let remove_btn = this.make_remove_button(div, node);
552 remove_btn.classList.add("p-1", "m-1");
553 div.appendChild(remove_btn);
554 document.getElementById("dropdown_wrapper").appendChild(div);
558 remove_dropdown(div_id, node_id){
559 const div = document.getElementById(div_id);
560 const node = this.filter_items[node_id]
561 const parent = div.parentNode;
562 div.parentNode.removeChild(div);
563 this.result[node.class][node.id]['count']--;
564 this.releaseResource(node); // This can't be done on clear b/c clear removes border
566 //checks if we have removed last item in class
567 if(this.result[node.class][node.id]['count'] == 0){
568 delete this.result[node.class][node.id];
574 if(!node['multiple']){
575 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
577 delete this.result[node.class][node.id];
581 updateObjectResult(node, childKey, childValue){
582 if(!this.result[node.class][node.id])
583 this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0}
585 this.result[node.class][node.id]['count']++;
589 document.getElementById("filter_field").value = JSON.stringify(this.result);
601 // description: string,
609 // network: int, [networks.id]
624 constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){
625 if(!this.check_support()) {
626 console.log("Aborting, browser is not supported");
630 this.currentWindow = null;
632 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
634 this.lastHostBottom = 100;
635 this.networks = new Set();
636 this.has_public_net = false;
638 this.editor = new mxEditor();
639 this.graph = this.editor.graph;
641 window.global_graph = this.graph;
642 window.network_rr_index = 5;
644 this.editor.setGraphContainer(graphContainer);
645 this.doGlobalConfig();
649 for(const network_id in networks) {
650 let network = networks[network_id];
652 mx_networks[network_id] = this.populateNetwork(network);
655 this.prefillHosts(resources, mx_networks);
657 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
658 //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
659 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus');
660 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus');
663 this.editor.addAction('printXML', function(editor, cell) {
664 mxLog.write(this.encodeGraph());
667 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code');
670 new mxOutline(this.graph, overviewContainer);
671 //sets the edge color to be the same as the network
672 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
673 //hooks up double click functionality
674 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
678 if (!mxClient.isBrowserSupported()) {
679 mxUtils.error('Browser is not supported', 200, false);
687 * mx_interface: mxCell for the interface itself
688 * network: mxCell for the outer network
691 connectNetwork(mx_interface, network, tagged) {
692 var cell = new mxCell(
693 "connection from " + network + " to " + mx_interface,
694 new mxGeometry(0, 0, 50, 50));
696 cell.geometry.relative = true;
697 cell.setValue(JSON.stringify({tagged: tagged}));
699 let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network);
700 let edge = this.graph.addEdge(cell, null, mx_interface, terminal);
701 this.colorEdge(edge, terminal, true);
702 this.graph.refresh(edge);
708 * to: desired y axis position of the matching cell
709 * within: graph cell for a full network, with all child cells
712 * an mx cell, the one vertically closest to the desired value
715 * modifies the <rr_index> on the <within> parameter
717 getClosestNetworkCell(to, within) {
718 if(window.network_rr_index === undefined) {
719 window.network_rr_index = 5;
722 let child_keys = within.children.keys();
723 let children = Array.from(within.children);
724 let index = (window.network_rr_index++) % children.length;
726 let child = within.children[child_keys[index]];
728 return children[index];
737 * description: string,
745 * network: int, [networks.id]
753 * network_mappings: {
754 * <django network id>: <mxnetwork id>
757 * draws given hosts into the mxgraph
759 prefillHosts(hosts, network_mappings){
760 for(const host_id in hosts) {
761 this.makeHost(hosts[host_id], network_mappings);
765 cellConnectionHandler(sender, event){
766 const edge = event.getProperty('edge');
767 const terminal = event.getProperty('terminal')
768 const source = event.getProperty('source');
769 if(this.checkAllowed(edge, terminal, source)) {
770 this.colorEdge(edge, terminal, source);
771 this.alertVlan(edge, terminal, source);
775 doubleClickHandler(evt, cell) {
777 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
778 cell = cell.getParent();
780 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
781 this.createDeleteDialog(cell.getId());
784 this.showDetailWindow(cell);
789 alertVlan(edge, terminal, source) {
790 if( terminal == null || edge.getTerminal(!source) == null) {
793 const form = document.createElement("form");
794 const tagged = document.createElement("input");
795 tagged.type = "radio";
796 tagged.name = "tagged";
797 tagged.value = "True";
798 form.appendChild(tagged);
799 form.appendChild(document.createTextNode(" Tagged"));
800 form.appendChild(document.createElement("br"));
802 const untagged = document.createElement("input");
803 untagged.type = "radio";
804 untagged.name = "tagged";
805 untagged.value = "False";
806 form.appendChild(untagged);
807 form.appendChild(document.createTextNode(" Untagged"));
808 form.appendChild(document.createElement("br"));
810 const yes_button = document.createElement("button");
811 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
812 yes_button.appendChild(document.createTextNode("Okay"));
814 const cancel_button = document.createElement("button");
815 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
816 cancel_button.appendChild(document.createTextNode("Cancel"));
818 const error_div = document.createElement("div");
819 error_div.id = "current_window_errors";
820 form.appendChild(error_div);
822 const content = document.createElement('div');
823 content.appendChild(form);
824 content.appendChild(yes_button);
825 content.appendChild(cancel_button);
826 this.showWindow("Vlan Selection", content, 200, 200);
829 createDeleteDialog(id) {
830 const content = document.createElement('div');
831 const remove_button = document.createElement("button");
832 remove_button.style.width = '46%';
833 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
834 remove_button.appendChild(document.createTextNode("Remove"));
835 const cancel_button = document.createElement("button");
836 cancel_button.style.width = '46%';
837 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
838 cancel_button.appendChild(document.createTextNode("Cancel"));
840 content.appendChild(remove_button);
841 content.appendChild(cancel_button);
842 this.showWindow('Do you want to delete this network?', content, 200, 62);
845 checkAllowed(edge, terminal, source) {
846 //check if other terminal is null, and that they are different
847 const otherTerminal = edge.getTerminal(!source);
848 if(terminal != null && otherTerminal != null) {
849 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
850 otherTerminal.getParent().getId().split('_')[0] ) {
852 this.graph.removeCells([edge]);
859 colorEdge(edge, terminal, source) {
860 if(terminal.getParent().getId().indexOf('network') >= 0) {
861 const styles = terminal.getParent().getStyle().split(';');
863 for(let style of styles){
864 const kvp = style.split('=');
865 if(kvp[0] == "fillColor"){
870 edge.setStyle('strokeColor=' + color);
872 console.log("Failed to color " + edge + ", " + terminal + ", " + source);
876 showDetailWindow(cell) {
877 const info = JSON.parse(cell.getValue());
878 const content = document.createElement("div");
879 const pre_tag = document.createElement("pre");
880 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
881 const ok_button = document.createElement("button");
882 ok_button.onclick = function() { this.closeWindow();};
883 content.appendChild(pre_tag);
884 content.appendChild(ok_button);
885 this.showWindow('Details', content, 400, 400);
888 restoreFromXml(xml, editor) {
889 const doc = mxUtils.parseXml(xml);
890 const node = doc.documentElement;
891 editor.readGraphModel(node);
893 //Iterate over all children, and parse the networks to add them to the sidebar
894 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
895 if(cell.getId().indexOf("network") > -1) {
896 const info = JSON.parse(cell.getValue());
897 const name = info['name'];
898 this.networks.add(name);
899 const styles = cell.getStyle().split(";");
901 for(const style of styles){
902 const kvp = style.split('=');
903 if(kvp[0] == "fillColor") {
909 this.has_public_net = true;
912 this.makeSidebarNetwork(name, color, cell.getId());
918 var cell = this.graph.getModel().getCell(cellId);
919 if( cellId.indexOf("network") > -1 ) {
920 let elem = document.getElementById(cellId);
921 elem.parentElement.removeChild(elem);
923 this.graph.removeCells([cell]);
924 this.currentWindow.destroy();
928 const input = document.createElement("input");
930 input.name = "net_name";
931 input.maxlength = 100;
932 input.id = "net_name_input";
933 input.style.margin = "5px";
935 const yes_button = document.createElement("button");
936 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
937 yes_button.appendChild(document.createTextNode("Okay"));
939 const cancel_button = document.createElement("button");
940 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
941 cancel_button.appendChild(document.createTextNode("Cancel"));
943 const error_div = document.createElement("div");
944 error_div.id = "current_window_errors";
946 const content = document.createElement("div");
947 content.appendChild(document.createTextNode("Name: "));
948 content.appendChild(input);
949 content.appendChild(document.createElement("br"));
950 content.appendChild(yes_button);
951 content.appendChild(cancel_button);
952 content.appendChild(document.createElement("br"));
953 content.appendChild(error_div);
955 this.showWindow("Network Creation", content, 300, 300);
958 parseNetworkWindow() {
959 const net_name = document.getElementById("net_name_input").value
960 const error_div = document.getElementById("current_window_errors");
961 if( this.networks.has(net_name) ){
962 error_div.innerHTML = "All network names must be unique";
965 this.addNetwork(net_name);
966 this.currentWindow.destroy();
969 addToolbarButton(editor, toolbar, action, image) {
970 const button = document.createElement('button');
971 button.setAttribute('class', 'btn btn-sm m-1');
973 const icon = document.createElement('i');
974 icon.setAttribute('class', 'fas ' + image);
975 button.appendChild(icon);
977 mxEvent.addListener(button, 'click', function(evt) {
978 editor.execute(action);
980 mxUtils.write(button, '');
981 toolbar.appendChild(button);
985 const encoder = new mxCodec();
986 const xml = encoder.encode(this.graph.getModel());
987 return mxUtils.getXml(xml);
991 //general graph stuff
992 this.graph.setMultigraph(false);
993 this.graph.setCellsSelectable(false);
994 this.graph.setCellsMovable(false);
997 this.graph.vertexLabelIsMovable = true;
1000 this.graph.setConnectable(true);
1001 this.graph.setAllowDanglingEdges(false);
1002 mxEdgeHandler.prototype.snapToTerminals = true;
1003 mxConstants.MIN_HOTSPOT_SIZE = 16;
1004 mxConstants.DEFAULT_HOTSPOT = 1;
1005 //edge 'style' (still affects behavior greatly)
1006 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
1007 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
1008 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
1009 style[mxConstants.STYLE_ROUNDED] = true;
1010 style[mxConstants.STYLE_FONTCOLOR] = 'black';
1011 style[mxConstants.STYLE_STROKECOLOR] = 'red';
1012 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
1013 style[mxConstants.STYLE_STROKEWIDTH] = '3';
1014 style[mxConstants.STYLE_ROUNDED] = true;
1015 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
1017 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
1018 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
1020 this.graph.convertValueToString = function(cell) {
1022 //changes value for edges with xml value
1024 if(JSON.parse(cell.getValue())["tagged"]) {
1030 return JSON.parse(cell.getValue())['name'];
1034 return cell.getValue();
1039 showWindow(title, content, width, height) {
1040 //create transparent black background
1041 const background = document.createElement('div');
1042 background.style.position = 'absolute';
1043 background.style.left = '0px';
1044 background.style.top = '0px';
1045 background.style.right = '0px';
1046 background.style.bottom = '0px';
1047 background.style.background = 'black';
1048 mxUtils.setOpacity(background, 50);
1049 document.body.appendChild(background);
1051 const x = Math.max(0, document.body.scrollWidth/2-width/2);
1052 const y = Math.max(10, (document.body.scrollHeight ||
1053 document.documentElement.scrollHeight)/2-height*2/3);
1055 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
1056 wnd.setClosable(false);
1058 wnd.addListener(mxEvent.DESTROY, function(evt) {
1059 this.graph.setEnabled(true);
1060 mxEffects.fadeOut(background, 50, true, 10, 30, true);
1062 this.currentWindow = wnd;
1064 this.graph.setEnabled(false);
1065 this.currentWindow.setVisible(true);
1069 //allows the current window to be destroyed
1070 this.currentWindow.destroy();
1073 othersUntagged(edgeID) {
1074 const edge = this.graph.getModel().getCell(edgeID);
1075 const end1 = edge.getTerminal(true);
1076 const end2 = edge.getTerminal(false);
1078 if( end1.getParent().getId().split('_')[0] == 'host' ){
1084 var edges = netint.edges;
1085 for( let edge of edges) {
1086 if( edge.getValue() ) {
1087 var tagged = JSON.parse(edge.getValue()).tagged;
1100 deleteVlanWindow(edgeID) {
1101 const cell = this.graph.getModel().getCell(edgeID);
1102 this.graph.removeCells([cell]);
1103 this.currentWindow.destroy();
1106 parseVlanWindow(edgeID) {
1107 //do parsing and data manipulation
1108 const radios = document.getElementsByName("tagged");
1109 const edge = this.graph.getModel().getCell(edgeID);
1111 for(let radio of radios){
1113 //set edge to be tagged or untagged
1114 if( radio.value == "False") {
1115 if( this.othersUntagged(edgeID) ) {
1116 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
1120 const edgeVal = {tagged: radio.value == "True"};
1121 edge.setValue(JSON.stringify(edgeVal));
1125 this.graph.refresh(edge);
1129 makeMxNetwork(net_name, is_public = false) {
1130 const model = this.graph.getModel();
1132 const height = 1700;
1133 const xoff = 400 + (30 * this.netCount);
1135 let color = this.netColors[this.netCount];
1136 if( this.netCount > (this.netColors.length - 1)) {
1137 color = Math.floor(Math.random() * 16777215); //int in possible color space
1138 color = '#' + color.toString(16).toUpperCase(); //convert to hex
1140 const net_val = { name: net_name, public: is_public};
1141 const net = this.graph.insertVertex(
1142 this.graph.getDefaultParent(),
1143 'network_' + this.netCount,
1144 JSON.stringify(net_val),
1149 'fillColor=' + color,
1152 const num_ports = 45;
1153 for(var i=0; i<num_ports; i++){
1154 let port = this.graph.insertVertex(
1162 'fillColor=black;opacity=0',
1167 const ret_val = { color: color, element_id: "network_" + this.netCount };
1169 this.networks.add(net_name);
1183 // mxgraph id of network
1184 populateNetwork(network) {
1185 let mxNet = this.makeMxNetwork(network.name, network.public);
1186 this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id);
1188 if( network.public ) {
1189 this.has_public_net = true;
1192 return mxNet.element_id;
1195 addPublicNetwork() {
1196 const net = this.makeMxNetwork("public", true);
1197 this.makeSidebarNetwork("public", net['color'], net['element_id']);
1198 this.has_public_net = true;
1201 addNetwork(net_name) {
1202 const ret = this.makeMxNetwork(net_name);
1203 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
1206 updateHosts(removed) {
1208 for(const hostID of removed) {
1209 cells.push(this.graph.getModel().getCell("host_" + hostID));
1211 this.graph.removeCells(cells);
1213 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
1215 for(const i in hosts) {
1216 const host = hosts[i];
1217 if(host.id.startsWith("host_")){
1218 const geometry = host.getGeometry();
1219 geometry.y = topdist + 50;
1220 topdist = geometry.y + geometry.height;
1221 host.setGeometry(geometry);
1226 makeSidebarNetwork(net_name, color, net_id){
1227 const colorBlob = document.createElement("div");
1228 colorBlob.className = "square-20 rounded-circle";
1229 colorBlob.style['background'] = color;
1231 const textContainer = document.createElement("span");
1232 textContainer.className = "ml-2";
1233 textContainer.appendChild(document.createTextNode(net_name));
1235 const timesIcon = document.createElement("i");
1236 timesIcon.classList.add("fas", "fa-times");
1238 const deletebutton = document.createElement("button");
1239 deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
1240 deletebutton.appendChild(timesIcon);
1241 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
1243 const newNet = document.createElement("li");
1244 newNet.classList.add("list-group-item", "d-flex", "bg-light");
1246 newNet.appendChild(colorBlob);
1247 newNet.appendChild(textContainer);
1249 if( net_name != "public" ) {
1250 newNet.appendChild(deletebutton);
1252 document.getElementById("network_list").appendChild(newNet);
1260 * 'description': string,
1269 * network: int, <django network id>,
1277 * network_mappings: {
1278 * <django network id>: <mxnetwork id>
1281 makeHost(hostInfo, network_mappings) {
1282 const value = JSON.stringify(hostInfo['value']);
1283 const interfaces = hostInfo['interfaces'];
1285 const height = (25 * interfaces.length) + 25;
1287 const yoff = this.lastHostBottom + 50;
1288 this.lastHostBottom = yoff + height;
1289 const host = this.graph.insertVertex(
1290 this.graph.getDefaultParent(),
1291 'host_' + hostInfo['id'],
1300 host.getGeometry().offset = new mxPoint(-50,0);
1301 host.setConnectable(false);
1304 for(var i=0; i<interfaces.length; i++) {
1305 const port = this.graph.insertVertex(
1308 JSON.stringify(interfaces[i]),
1313 'fillColor=blue;editable=0',
1316 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
1317 const iface = interfaces[i];
1318 for( const connection of iface.connections ) {
1319 const network = this
1322 .getCell(network_mappings[connection.network]);
1324 this.connectNetwork(port, network, connection.tagged);
1326 this.graph.refresh(port);
1328 this.graph.refresh(host);
1332 const input_elem = document.getElementById("hidden_xml_input");
1333 input_elem.value = this.encodeGraph(this.graph);
1337 class SearchableSelectMultipleWidget {
1338 constructor(format_vars, field_dataset, field_initial) {
1339 this.format_vars = format_vars;
1340 this.items = field_dataset;
1341 this.initial = field_initial;
1343 this.expanded_name_trie = {"isComplete": false};
1344 this.small_name_trie = {"isComplete": false};
1345 this.string_trie = {"isComplete": false};
1347 this.added_items = new Set();
1349 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
1351 this[e] = format_vars[e];
1354 this.search_field_init();
1356 if( this.show_from_noentry )
1363 const textfield = document.getElementById("user_field");
1364 const drop = document.getElementById("drop_results");
1366 textfield.disabled = "True";
1367 drop.style.display = "none";
1369 const btns = document.getElementsByClassName("btn-remove");
1370 for( const btn of btns )
1372 btn.classList.add("disabled");
1377 search_field_init() {
1378 this.build_all_tries(this.items);
1380 for( const elem of this.initial )
1382 this.select_item(elem);
1384 if(this.initial.length == 1)
1386 this.search(this.items[this.initial[0]]["small_name"]);
1387 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
1391 build_all_tries(dict)
1393 for( const key in dict )
1395 this.add_item(dict[key]);
1401 const id = item['id'];
1402 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
1403 this.add_to_tree(item['small_name'], id, this.small_name_trie);
1404 this.add_to_tree(item['string'], id, this.string_trie);
1407 add_to_tree(str, id, trie)
1409 let inner_trie = trie;
1412 if( !inner_trie[str.charAt(0)] )
1415 inner_trie[str.charAt(0)] = new_trie;
1419 var new_trie = inner_trie[str.charAt(0)];
1422 if( str.length == 1 )
1424 new_trie.isComplete = true;
1429 new_trie.ids.push(id);
1431 inner_trie = new_trie;
1432 str = str.substring(1);
1438 if( input.length == 0 && !this.show_from_noentry){
1442 else if( input.length == 0 && this.show_from_noentry)
1444 this.dropdown(this.items); //show all items
1449 const tr1 = this.getSubtree(input, this.expanded_name_trie);
1451 const tr2 = this.getSubtree(input, this.small_name_trie);
1453 const tr3 = this.getSubtree(input, this.string_trie);
1455 const results = this.collate(trees);
1456 this.dropdown(results);
1460 getSubtree(input, given_trie)
1463 recursive function to return the trie accessed at input
1466 if( input.length == 0 ){
1471 const substr = input.substring(0, input.length - 1);
1472 const last_char = input.charAt(input.length-1);
1473 const subtrie = this.getSubtree(substr, given_trie);
1475 if( !subtrie ) //substr not in the trie
1480 const indexed_trie = subtrie[last_char];
1481 return indexed_trie;
1488 takes in a trie and returns a list of its item id's
1493 return itemIDs; //empty, base case
1495 for( const key in trie )
1501 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1503 if ( trie.isComplete )
1505 itemIDs.push(...trie.ids);
1514 takes a list of tries
1515 returns a list of ids of objects that are available
1518 for( const tree of trees )
1520 const available_IDs = this.serialize(tree);
1522 for( const itemID of available_IDs ) {
1523 results[itemID] = this.items[itemID];
1529 generate_element_text(obj)
1531 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1532 const result = content_strings.shift();
1533 if( result == null || content_strings.length < 1) {
1536 return result + " (" + content_strings.join(", ") + ")";
1543 takes in a mapping of ids to objects in items
1544 and displays them in the dropdown
1546 const drop = document.getElementById("drop_results");
1547 while(drop.firstChild)
1549 drop.removeChild(drop.firstChild);
1552 for( const id in ids )
1554 const obj = this.items[id];
1555 const result_text = this.generate_element_text(obj);
1556 const result_entry = document.createElement("a");
1557 result_entry.href = "#";
1558 result_entry.innerText = result_text;
1559 result_entry.title = result_text;
1560 result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1561 result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1562 const tooltip = document.createElement("span");
1563 const tooltiptext = document.createTextNode(result_text);
1564 tooltip.appendChild(tooltiptext);
1565 tooltip.classList.add("d-none");
1566 result_entry.appendChild(tooltip);
1567 drop.appendChild(result_entry);
1570 const scroll_restrictor = document.getElementById("scroll_restrictor");
1572 if( !drop.firstChild )
1574 scroll_restrictor.style.visibility = 'hidden';
1578 scroll_restrictor.style.visibility = 'inherit';
1582 select_item(item_id)
1584 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1586 this.added_items.add(item_id);
1588 this.update_selected_list();
1589 // clear search bar contents
1590 document.getElementById("user_field").value = "";
1591 document.getElementById("user_field").focus();
1595 remove_item(item_id)
1597 this.added_items.delete(item_id);
1599 this.update_selected_list()
1600 document.getElementById("user_field").focus();
1603 update_selected_list()
1605 document.getElementById("added_number").innerText = this.added_items.size;
1606 const selector = document.getElementById('selector');
1607 selector.value = JSON.stringify([...this.added_items]);
1608 const added_list = document.getElementById('added_list');
1610 while(selector.firstChild)
1612 selector.removeChild(selector.firstChild);
1614 while(added_list.firstChild)
1616 added_list.removeChild(added_list.firstChild);
1619 const list_html = document.createElement("div");
1620 list_html.classList.add("list-group");
1622 for( const item_id of this.added_items )
1624 const times = document.createElement("li");
1625 times.classList.add("fas", "fa-times");
1627 const deleteButton = document.createElement("a");
1628 deleteButton.href = "#";
1629 deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1630 // Setting .onclick/.addEventListener does not work,
1631 // which is why I took the setAttribute approach
1632 // If anyone knows why, please let me know :]
1633 deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1634 deleteButton.classList.add("btn");
1635 const deleteColumn = document.createElement("div");
1636 deleteColumn.classList.add("col-auto");
1637 deleteColumn.append(deleteButton);
1639 const item = this.items[item_id];
1640 const element_entry_text = this.generate_element_text(item);
1641 const textColumn = document.createElement("div");
1642 textColumn.classList.add("col", "overflow-ellipsis");
1643 textColumn.innerText = element_entry_text;
1644 textColumn.title = element_entry_text;
1646 const itemRow = document.createElement("div");
1647 itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1648 itemRow.append(textColumn, deleteColumn);
1650 list_html.append(itemRow);
1652 added_list.innerHTML = list_html.innerHTML;