diff options
4 files changed, 259 insertions, 282 deletions
diff --git a/dashboard/src/static/js/dashboard.js b/dashboard/src/static/js/dashboard.js new file mode 100644 index 0000000..66e95d0 --- /dev/null +++ b/dashboard/src/static/js/dashboard.js @@ -0,0 +1,244 @@ +class MultipleSelectFilterWidget { + + constructor(neighbors, items, initial) { + this.inputs = []; + this.graph_neighbors = neighbors; + this.filter_items = items; + this.result = {}; + this.dropdown_count = 0; + + for(let nodeId in this.filter_items) { + const node = this.filter_items[nodeId]; + this.result[node.class] = {} + } + + this.make_selection(initial); + } + + make_selection( initial_data ){ + if(!initial_data || jQuery.isEmptyObject(initial_data)) + return; + for(let item_class in initial_data) { + const selected_items = initial_data[item_class]; + for( let node_id in selected_items ){ + const node = this.filter_items[node_id]; + const selection_data = selected_items[node_id] + if( selection_data.selected ) { + this.select(node); + this.markAndSweep(node); + this.updateResult(node); + } + if(node['multiple']){ + this.make_multiple_selection(node, selection_data); + } + } + } + } + + make_multiple_selection(node, selection_data){ + const prepop_data = selection_data.values; + for(let k in prepop_data){ + const div = this.add_item_prepopulate(node, prepop_data[k]); + this.updateObjectResult(node, div.id, prepop_data[k]); + } + } + + markAndSweep(root){ + for(let i in this.filter_items) { + const node = this.filter_items[i]; + node['marked'] = true; //mark all nodes + } + + const toCheck = [root]; + while(toCheck.length > 0){ + const node = toCheck.pop(); + if(!node['marked']) { + continue; //already visited, just continue + } + node['marked'] = false; //mark as visited + if(node['follow'] || node == root){ //add neighbors if we want to follow this node + const neighbors = this.graph_neighbors[node.id]; + for(let neighId of neighbors) { + const neighbor = this.filter_items[neighId]; + toCheck.push(neighbor); + } + } + } + + //now remove all nodes still marked + for(let i in this.filter_items){ + const node = this.filter_items[i]; + if(node['marked']){ + this.disable_node(node); + } + } + } + + process(node) { + if(node['selected']) { + this.markAndSweep(node); + } + else { //TODO: make this not dumb + const selected = [] + //remember the currently selected, then reset everything and reselect one at a time + for(let nodeId in this.filter_items) { + node = this.filter_items[nodeId]; + if(node['selected']) { + selected.push(node); + } + this.clear(node); + } + for(let node of selected) { + this.select(node); + this.markAndSweep(selected[i]); + } + } + } + + select(node) { + const elem = document.getElementById(node['id']); + node['selected'] = true; + elem.classList.remove('disabled_node', 'cleared_node'); + elem.classList.add('selected_node'); + } + + clear(node) { + const elem = document.getElementById(node['id']); + node['selected'] = false; + node['selectable'] = true; + elem.classList.add('cleared_node') + elem.classList.remove('disabled_node', 'selected_node'); + } + + disable_node(node) { + const elem = document.getElementById(node['id']); + node['selected'] = false; + node['selectable'] = false; + elem.classList.remove('cleared_node', 'selected_node'); + elem.classList.add('disabled_node'); + } + + processClick(id){ + const node = this.filter_items[id]; + if(!node['selectable']) + return; + + if(node['multiple']){ + return this.processClickMultiple(node); + } else { + return this.processClickSingle(node); + } + } + + processClickSingle(node){ + node['selected'] = !node['selected']; //toggle on click + if(node['selected']) { + this.select(node); + } else { + this.clear(node); + } + this.process(node); + this.updateResult(node); + } + + processClickMultiple(node){ + this.select(node); + const div = this.add_item_prepopulate(node, false); + this.process(node); + this.updateObjectResult(node, div.id, ""); + } + + restrictchars(input){ + if( input.validity.patternMismatch ){ + input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"); + input.reportValidity(); + } + input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, ""); + this.checkunique(input); + } + + checkunique(tocheck){ //TODO: use set + const val = tocheck.value; + for( let input of this.inputs ){ + if( input.value == val && input != tocheck){ + tocheck.setCustomValidity("All hostnames must be unique"); + tocheck.reportValidity(); + return; + } + } + tocheck.setCustomValidity(""); + } + + make_remove_button(div, node){ + const button = document.createElement("BUTTON"); + button.type = "button"; + button.appendChild(document.createTextNode("Remove")); + button.classList.add("btn", "btn-danger"); + const that = this; + button.onclick = function(){ that.remove_dropdown(div.id, node.id); } + return button; + } + + make_input(div, node, prepopulate){ + const input = document.createElement("INPUT"); + input.type = node.form.type; + input.name = node.id + node.form.name + input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})"; + input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed" + input.placeholder = node.form.placeholder; + this.inputs.push(input); + const that = this; + input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); }; + input.oninput = function() { that.restrictchars(this); }; + if(prepopulate) + input.value = prepopulate; + return input; + } + + add_item_prepopulate(node, prepopulate){ + const div = document.createElement("DIV"); + div.id = "dropdown_" + this.dropdown_count; + div.classList.add("dropdown_item"); + this.dropdown_count++; + const label = document.createElement("H5") + label.appendChild(document.createTextNode(node['name'])) + div.appendChild(label); + div.appendChild(this.make_input(div, node, prepopulate)); + div.appendChild(this.make_remove_button(div, node)); + document.getElementById("dropdown_wrapper").appendChild(div); + return div; + } + + remove_dropdown(div_id, node_id){ + const div = document.getElementById(div_id); + const node = this.filter_items[node_id] + const parent = div.parentNode; + div.parentNode.removeChild(div); + delete this.result[node.class][node.id]['values'][div.id]; + + //checks if we have removed last item in class + if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){ + delete this.result[node.class][node.id]; + this.clear(node); + } + } + + updateResult(node){ + if(!node['multiple']){ + this.result[node.class][node.id] = {selected: node.selected, id: node.model_id} + if(!node.selected) + delete this.result[node.class][node.id]; + } + } + + updateObjectResult(node, childKey, childValue){ + if(!this.result[node.class][node.id]) + this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}} + + this.result[node.class][node.id]['values'][childKey] = childValue; + } + + finish(){ + document.getElementById("filter_field").value = JSON.stringify(this.result); + } +} diff --git a/dashboard/src/templates/booking/quick_deploy.html b/dashboard/src/templates/booking/quick_deploy.html index ea80af4..07f3d89 100644 --- a/dashboard/src/templates/booking/quick_deploy.html +++ b/dashboard/src/templates/booking/quick_deploy.html @@ -83,7 +83,7 @@ function submit_form() { //formats data for form submission - document.getElementById("filter_field").value = JSON.stringify(result); + multi_filter_widget.finish(); } function hide_dropdown(drop_id) { @@ -104,10 +104,8 @@ } function get_selected_value(key){ - for( var attr in result[key] ){ - if( attr in {} ) - continue; - else + for( var attr in multi_filter_widget.result[key] ){ + if(!(attr in {}) ) return attr; } return null; diff --git a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html index 3a7e148..bfcbed6 100644 --- a/dashboard/src/templates/dashboard/multiple_select_filter_widget.html +++ b/dashboard/src/templates/dashboard/multiple_select_filter_widget.html @@ -1,3 +1,6 @@ +<script src="/static/js/dashboard.js"> +</script> + <style> .object_class_wrapper { display: grid; @@ -102,7 +105,7 @@ <div id="{{ obj.id|default:'not_provided' }}" class="grid-item"> <p class="grid-item-header">{{obj.name}}</p> <p class="grid-item-description">{{obj.description}}</p> - <button type="button" class="btn btn-success grid-item-select-btn" onclick="processClick( + <button type="button" class="btn btn-success grid-item-select-btn" onclick="multi_filter_widget.processClick( '{{obj.id}}');">{% if obj.multiple %}Add{% else %}Select{% endif %}</button> </div> {% endfor %} @@ -113,270 +116,15 @@ <div id="dropdown_wrapper"> </div> - <script> -var initialized = false; -var inputs = []; -var graph_neighbors = {{ neighbors|safe }}; -var filter_items = {{ filter_items|safe }}; -var result = {}; -var dropdown_count = 0; - -{% if initial_value %} - -var initial_value = {{ initial_value|safe }}; - - -function make_selection( initial_data ){ - try_init(); - for(var item_class in initial_data) { - var selected_items = initial_data[item_class]; - for( var node_id in selected_items ){ - var node = filter_items[node_id]; - var selection_data = selected_items[node_id] - if( selection_data.selected ) { - select(node); - markAndSweep(node); - updateResult(node); - } - if(node['multiple']){ - make_multiple_selection(node, selection_data); - } - } - } -} - -function make_multiple_selection(node, selection_data){ - prepop_data = selection_data.values; - for(var k in prepop_data){ - var div = add_item_prepopulate(node, prepop_data[k]); - updateObjectResult(node, div.id, prepop_data[k]); - } -} - -make_selection({{initial_value|safe}}); - -{% endif %} - -function markAndSweep(root){ - for(var i in filter_items) { - node = filter_items[i]; - node['marked'] = true; //mark all nodes - } - - toCheck = [root]; - while(toCheck.length > 0){ - node = toCheck.pop(); - if(!node['marked']) { - //already visited, just continue - continue; - } - node['marked'] = false; //mark as visited - if(node['follow'] || node == root){ //add neighbors if we want to follow this node - var neighbors = graph_neighbors[node.id]; - for(var i in neighbors) { - var neighId = neighbors[i]; - var neighbor = filter_items[neighId]; - toCheck.push(neighbor); - } - } - } - - //now remove all nodes still marked - for(var i in filter_items){ - node = filter_items[i]; - if(node['marked']){ - disable_node(node); - } - } -} - -function process(node) { - if(node['selected']) { - markAndSweep(node); - } - else { //TODO: make this not dumb - var selected = [] - //remember the currently selected, then reset everything and reselect one at a time - for(var nodeId in filter_items) { - node = filter_items[nodeId]; - if(node['selected']) { - selected.push(node); - } - clear(node); - } - for(var i=0; i<selected.length; i++) { - node = selected[i]; - select(node); - markAndSweep(selected[i]); - } - } -} - -function select(node) { - elem = document.getElementById(node['id']); - node['selected'] = true; - elem.classList.remove('cleared_node'); - elem.classList.remove('disabled_node'); - elem.classList.add('selected_node'); -} - -function clear(node) { - elem = document.getElementById(node['id']); - node['selected'] = false; - node['selectable'] = true; - elem.classList.add('cleared_node') - elem.classList.remove('disabled_node'); - elem.classList.remove('selected_node'); -} - -function disable_node(node) { - elem = document.getElementById(node['id']); - node['selected'] = false; - node['selectable'] = false; - elem.classList.remove('cleared_node'); - elem.classList.add('disabled_node'); - elem.classList.remove('selected_node'); -} - -function processClick(id){ - try_init(); - var node = filter_items[id]; - if(!node['selectable']) - return; - - if(node['multiple']){ - return processClickMultiple(node); - } else { - return processClickSingle(node); - } -} - -function processClickSingle(node){ - node['selected'] = !node['selected']; //toggle on click - - if(node['selected']) { - select(node); - } else { - clear(node); - } - process(node); - updateResult(node); -} - -function processClickMultiple(node){ - select(node); - var div = add_node(node); - process(node); - updateObjectResult(node, div.id, ""); -} - -function add_node(node){ - return add_item_prepopulate(node, false); -} - -function restrictchars(input){ - if( input.validity.patternMismatch ){ - input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"); - input.reportValidity(); - } - input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, ""); - checkunique(input); -} - -function checkunique(tocheck){ - val = tocheck.value; - for( var i = 0; i < inputs.length; i++ ) - { - if( inputs[i].value == val && inputs[i] != tocheck) - { - tocheck.setCustomValidity("All hostnames must be unique"); - tocheck.reportValidity(); - return; - } - } - tocheck.setCustomValidity(""); -} - -function make_remove_button(div, node){ - var button = document.createElement("BUTTON"); - button.type = "button"; - button.appendChild(document.createTextNode("Remove")); - button.classList.add("btn-danger"); - button.classList.add("btn"); - button.onclick = function(){ - remove_dropdown(div.id, node.id); - } - return button; -} - -function make_input(div, node, prepopulate){ - var input = document.createElement("INPUT"); - input.type = node.form.type; - input.name = node.id + node.form.name - input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})"; - input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed" - input.placeholder = node.form.placeholder; - inputs.push(input); - input.onchange = function() { updateObjectResult(node, div.id, input.value); restrictchars(this); }; - input.oninput = function() { restrictchars(this); }; - if(prepopulate) - input.value = prepopulate; - return input; -} - -function add_item_prepopulate(node, prepopulate){ - var div = document.createElement("DIV"); - div.id = "dropdown_" + dropdown_count; - div.classList.add("dropdown_item"); - dropdown_count++; - var label = document.createElement("H5") - label.appendChild(document.createTextNode(node['name'])) - div.appendChild(label); - div.appendChild(make_input(div, node, prepopulate)); - div.appendChild(make_remove_button(div, node)); - document.getElementById("dropdown_wrapper").appendChild(div); - return div; -} - -function remove_dropdown(div_id, node_id){ - var div = document.getElementById(div_id); - var node = filter_items[node_id] - var parent = div.parentNode; - div.parentNode.removeChild(div); - delete result[node.class][node.id]['values'][div.id]; - - //checks if we have removed last item in class - if(jQuery.isEmptyObject(result[node.class][node.id]['values'])){ - delete result[node.class][node.id]; - clear(node); - } -} -function updateResult(node){ - try_init(); - if(!node['multiple']){ - result[node.class][node.id] = {selected: node.selected, id: node.model_id} - if(!node.selected) - delete result[node.class][node.id]; - } -} - -function updateObjectResult(node, childKey, childValue){ - try_init(); - if(!result[node.class][node.id]) - result[node.class][node.id] = {selected: true, id: node.model_id, values: {}} - - result[node.class][node.id]['values'][childKey] = childValue; -} +function multipleSelectFilterWidgetEntry() { + const graph_neighbors = {{ neighbors|safe }}; + const filter_items = {{ filter_items|safe }}; + const initial_value = {{ initial_value|default_if_none:"{}"|safe }}; -function try_init() { - if(initialized) return; - for(nodeId in filter_items) { - var element = document.getElementById(nodeId); - var node = filter_items[nodeId]; - result[node.class] = {} - } - initialized = true; + //global variable + multi_filter_widget = new MultipleSelectFilterWidget(graph_neighbors, filter_items, initial_value); } +multipleSelectFilterWidgetEntry(); </script> diff --git a/dashboard/src/templates/resource/steps/define_hardware.html b/dashboard/src/templates/resource/steps/define_hardware.html index 77df5a2..57078e9 100644 --- a/dashboard/src/templates/resource/steps/define_hardware.html +++ b/dashboard/src/templates/resource/steps/define_hardware.html @@ -15,20 +15,7 @@ with your current configuration will become unavailable.</p> </form> {% endblock content %} {% block onleave %} -var normalize = function(data){ - //converts the top level keys in data to map to lists - var normalized = {} - for( var key in data ){ - normalized[key] = []; - for( var subkey in data[key] ){ - normalized[key].push(data[key][subkey]); - } - } - return normalized; -} -var data = result; -data = JSON.stringify(data); -document.getElementById("filter_field").value = data; +multi_filter_widget.finish(); var formData = $("#define_hardware_form").serialize(); req = new XMLHttpRequest(); req.open('POST', '/wf/workflow/', false); |