Loading src/webui/service/templates/js/topology.js +88 −58 Original line number Diff line number Diff line Loading @@ -31,24 +31,39 @@ const margin = {top: 5, right: 5, bottom: 5, left: 5}; const icon_width = 40; const icon_height = 40; width = 1400 - margin.left - margin.right; height = 800 - margin.top - margin.bottom; //function handleZoom(e) { // console.dir(e); // d3.select('svg g').attr('transform', e.transform); //} //let zoom = d3.zoom().scaleExtent([0.01, 10]).translateExtent([[0, 0], [width, height]]).on('zoom', handleZoom); // append the svg object to the body of the page const svg = d3.select('#topology') .append('svg') const topologyContainer = d3.select('#topology'); const svg = topologyContainer.append('svg'); const zoomLayer = svg.append('g'); const graphLayer = zoomLayer.append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); let width = 0; let height = 0; let graphInitialized = false; function updateCanvasSize() { const bounds = topologyContainer.node().getBoundingClientRect(); width = Math.max(bounds.width - margin.left - margin.right, 200); height = Math.max(bounds.height - margin.top - margin.bottom, 200); svg .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) //.call(zoom) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`) ; .attr('height', height + margin.top + margin.bottom); } updateCanvasSize(); function handleZoom() { zoomLayer.attr('transform', d3.event.transform); } const zoom = d3.zoom() .scaleExtent([0.2, 8]) .on('zoom', handleZoom); svg.call(zoom); const ZOOM_DURATION_MS = 250; const ZOOM_SCALE_STEP = 1.2; const PAN_OFFSET = 80; // svg objects var link, node, optical_link, labels; Loading @@ -67,10 +82,21 @@ forceProperties = { var simulation = d3.forceSimulation(); function applyDirectionalForces() { simulation .force("center", d3.forceCenter(width * forceProperties.center.x, height * forceProperties.center.y)) .force("forceX", d3.forceX() .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) .x(width * forceProperties.forceX.x)) .force("forceY", d3.forceY() .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) .y(height * forceProperties.forceY.y)); } // load the data d3.json("{{ url_for('main.topology') }}", function(data) { // set the data and properties of link lines and node circles link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.links) .enter() Loading @@ -86,7 +112,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); optical_link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') optical_link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.optical_links) .enter() Loading @@ -102,7 +128,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); node = svg.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') node = graphLayer.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') .selectAll("circle") .data(data.devices) .enter() Loading @@ -117,7 +143,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // node tooltip //node.append("title").text(function(n) { return n.name; }); // persistent node labels labels = svg.append("g").attr("class", "labels") labels = graphLayer.append("g").attr("class", "labels") .selectAll("text") .data(data.devices) .enter() Loading @@ -142,7 +168,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // add forces, associate each with a name, and set their properties // Experimental : Optical link part all_links = data.links.concat(data.optical_links) const all_links = data.links.concat(data.optical_links); simulation .force("link", d3.forceLink() .id(function(d) {return d.id;}) Loading @@ -162,19 +188,13 @@ d3.json("{{ url_for('main.topology') }}", function(data) { .force("collide", d3.forceCollide() .strength(forceProperties.collide.strength * forceProperties.collide.enabled) .radius(forceProperties.collide.radius) .iterations(forceProperties.collide.iterations)) .force("center", d3.forceCenter() .x(width * forceProperties.center.x) .y(height * forceProperties.center.y)) .force("forceX", d3.forceX() .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) .x(width * forceProperties.forceX.x)) .force("forceY", d3.forceY() .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) .y(height * forceProperties.forceY.y)); .iterations(forceProperties.collide.iterations)); applyDirectionalForces(); // after each simulation tick, update the display positions simulation.on("tick", ticked); graphInitialized = true; }); // update the display positions Loading Loading @@ -221,30 +241,40 @@ function dragended(d) { } // update size-related forces d3.select(window).on("resize", function(){ width = +svg.node().getBoundingClientRect().width; height = +svg.node().getBoundingClientRect().height; simulation.alpha(1).restart(); }); function handleResize() { updateCanvasSize(); if (graphInitialized) { applyDirectionalForces(); simulation.alpha(0.3).restart(); } } ///******************** UI ACTIONS *******************/ // //function resetZoom() { // d3.select('svg').transition().call(zoom.scaleTo, 1.0); //} //function zoomIn() { // d3.select('svg').transition().call(zoom.scaleBy, 2.0); //} //function zoomOut() { // d3.select('svg').transition().call(zoom.scaleBy, 0.5); //} // //function center() { // d3.select('svg').transition().call(zoom.translateTo, 0.5 * width, 0.5 * height); //} //function panLeft() { // d3.select('svg').transition().call(zoom.translateBy, -50, 0); //} //function panRight() { // d3.select('svg').transition().call(zoom.translateBy, 50, 0); //} d3.select(window).on("resize", handleResize); /******************** UI ACTIONS *******************/ function resetZoom() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.transform, d3.zoomIdentity); } function zoomIn() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, ZOOM_SCALE_STEP); } function zoomOut() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, 1 / ZOOM_SCALE_STEP); } function center() { const centerX = margin.left + width / 2; const centerY = margin.top + height / 2; svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateTo, centerX, centerY); } function panLeft() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, -PAN_OFFSET, 0); } function panRight() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, PAN_OFFSET, 0); } src/webui/service/templates/main/home.html +26 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,17 @@ {% block content %} <h2>ETSI TeraFlowSDN Controller</h2> <style> #topology { border: 1px solid #b5b5b5; border-radius: 0.25rem; background-color: #fff; width: 100%; min-height: 400px; height: min(75vh, 850px); overflow: hidden; } </style> {% for field, message in context_topology_form.errors.items() %} <div class="alert alert-dismissible fade show" role="alert"> Loading Loading @@ -78,7 +89,21 @@ </form> <script src="https://d3js.org/d3.v4.min.js"></script> <div id="topology"></div> <div class="btn-toolbar mb-3 flex-wrap" role="toolbar" aria-label="Topology controls"> <div class="btn-group btn-group-sm me-2 mb-2" role="group" aria-label="Zoom controls"> <button type="button" class="btn btn-outline-secondary" onclick="zoomIn()">Zoom in</button> <button type="button" class="btn btn-outline-secondary" onclick="zoomOut()">Zoom out</button> <button type="button" class="btn btn-outline-secondary" onclick="resetZoom()">Reset zoom</button> </div> <div class="btn-group btn-group-sm me-2 mb-2" role="group" aria-label="Pan controls"> <button type="button" class="btn btn-outline-secondary" onclick="panLeft()">Pan left</button> <button type="button" class="btn btn-outline-secondary" onclick="panRight()">Pan right</button> </div> <div class="btn-group btn-group-sm mb-2" role="group" aria-label="Center control"> <button type="button" class="btn btn-outline-secondary" onclick="center()">Center</button> </div> </div> <div id="topology" class="mb-4"></div> <script src="{{ url_for('js.topology_js') }}"></script> {% endblock %} Loading
src/webui/service/templates/js/topology.js +88 −58 Original line number Diff line number Diff line Loading @@ -31,24 +31,39 @@ const margin = {top: 5, right: 5, bottom: 5, left: 5}; const icon_width = 40; const icon_height = 40; width = 1400 - margin.left - margin.right; height = 800 - margin.top - margin.bottom; //function handleZoom(e) { // console.dir(e); // d3.select('svg g').attr('transform', e.transform); //} //let zoom = d3.zoom().scaleExtent([0.01, 10]).translateExtent([[0, 0], [width, height]]).on('zoom', handleZoom); // append the svg object to the body of the page const svg = d3.select('#topology') .append('svg') const topologyContainer = d3.select('#topology'); const svg = topologyContainer.append('svg'); const zoomLayer = svg.append('g'); const graphLayer = zoomLayer.append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); let width = 0; let height = 0; let graphInitialized = false; function updateCanvasSize() { const bounds = topologyContainer.node().getBoundingClientRect(); width = Math.max(bounds.width - margin.left - margin.right, 200); height = Math.max(bounds.height - margin.top - margin.bottom, 200); svg .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) //.call(zoom) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`) ; .attr('height', height + margin.top + margin.bottom); } updateCanvasSize(); function handleZoom() { zoomLayer.attr('transform', d3.event.transform); } const zoom = d3.zoom() .scaleExtent([0.2, 8]) .on('zoom', handleZoom); svg.call(zoom); const ZOOM_DURATION_MS = 250; const ZOOM_SCALE_STEP = 1.2; const PAN_OFFSET = 80; // svg objects var link, node, optical_link, labels; Loading @@ -67,10 +82,21 @@ forceProperties = { var simulation = d3.forceSimulation(); function applyDirectionalForces() { simulation .force("center", d3.forceCenter(width * forceProperties.center.x, height * forceProperties.center.y)) .force("forceX", d3.forceX() .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) .x(width * forceProperties.forceX.x)) .force("forceY", d3.forceY() .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) .y(height * forceProperties.forceY.y)); } // load the data d3.json("{{ url_for('main.topology') }}", function(data) { // set the data and properties of link lines and node circles link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.links) .enter() Loading @@ -86,7 +112,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); optical_link = svg.append("g").attr("class", "links")//.style('stroke', '#aaa') optical_link = graphLayer.append("g").attr("class", "links")//.style('stroke', '#aaa') .selectAll("line") .data(data.optical_links) .enter() Loading @@ -102,7 +128,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { return l.name.toLowerCase().includes('mgmt') ? "5,5" : "0"; }); node = svg.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') node = graphLayer.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2') .selectAll("circle") .data(data.devices) .enter() Loading @@ -117,7 +143,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // node tooltip //node.append("title").text(function(n) { return n.name; }); // persistent node labels labels = svg.append("g").attr("class", "labels") labels = graphLayer.append("g").attr("class", "labels") .selectAll("text") .data(data.devices) .enter() Loading @@ -142,7 +168,7 @@ d3.json("{{ url_for('main.topology') }}", function(data) { // add forces, associate each with a name, and set their properties // Experimental : Optical link part all_links = data.links.concat(data.optical_links) const all_links = data.links.concat(data.optical_links); simulation .force("link", d3.forceLink() .id(function(d) {return d.id;}) Loading @@ -162,19 +188,13 @@ d3.json("{{ url_for('main.topology') }}", function(data) { .force("collide", d3.forceCollide() .strength(forceProperties.collide.strength * forceProperties.collide.enabled) .radius(forceProperties.collide.radius) .iterations(forceProperties.collide.iterations)) .force("center", d3.forceCenter() .x(width * forceProperties.center.x) .y(height * forceProperties.center.y)) .force("forceX", d3.forceX() .strength(forceProperties.forceX.strength * forceProperties.forceX.enabled) .x(width * forceProperties.forceX.x)) .force("forceY", d3.forceY() .strength(forceProperties.forceY.strength * forceProperties.forceY.enabled) .y(height * forceProperties.forceY.y)); .iterations(forceProperties.collide.iterations)); applyDirectionalForces(); // after each simulation tick, update the display positions simulation.on("tick", ticked); graphInitialized = true; }); // update the display positions Loading Loading @@ -221,30 +241,40 @@ function dragended(d) { } // update size-related forces d3.select(window).on("resize", function(){ width = +svg.node().getBoundingClientRect().width; height = +svg.node().getBoundingClientRect().height; simulation.alpha(1).restart(); }); function handleResize() { updateCanvasSize(); if (graphInitialized) { applyDirectionalForces(); simulation.alpha(0.3).restart(); } } ///******************** UI ACTIONS *******************/ // //function resetZoom() { // d3.select('svg').transition().call(zoom.scaleTo, 1.0); //} //function zoomIn() { // d3.select('svg').transition().call(zoom.scaleBy, 2.0); //} //function zoomOut() { // d3.select('svg').transition().call(zoom.scaleBy, 0.5); //} // //function center() { // d3.select('svg').transition().call(zoom.translateTo, 0.5 * width, 0.5 * height); //} //function panLeft() { // d3.select('svg').transition().call(zoom.translateBy, -50, 0); //} //function panRight() { // d3.select('svg').transition().call(zoom.translateBy, 50, 0); //} d3.select(window).on("resize", handleResize); /******************** UI ACTIONS *******************/ function resetZoom() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.transform, d3.zoomIdentity); } function zoomIn() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, ZOOM_SCALE_STEP); } function zoomOut() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.scaleBy, 1 / ZOOM_SCALE_STEP); } function center() { const centerX = margin.left + width / 2; const centerY = margin.top + height / 2; svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateTo, centerX, centerY); } function panLeft() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, -PAN_OFFSET, 0); } function panRight() { svg.transition().duration(ZOOM_DURATION_MS).call(zoom.translateBy, PAN_OFFSET, 0); }
src/webui/service/templates/main/home.html +26 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,17 @@ {% block content %} <h2>ETSI TeraFlowSDN Controller</h2> <style> #topology { border: 1px solid #b5b5b5; border-radius: 0.25rem; background-color: #fff; width: 100%; min-height: 400px; height: min(75vh, 850px); overflow: hidden; } </style> {% for field, message in context_topology_form.errors.items() %} <div class="alert alert-dismissible fade show" role="alert"> Loading Loading @@ -78,7 +89,21 @@ </form> <script src="https://d3js.org/d3.v4.min.js"></script> <div id="topology"></div> <div class="btn-toolbar mb-3 flex-wrap" role="toolbar" aria-label="Topology controls"> <div class="btn-group btn-group-sm me-2 mb-2" role="group" aria-label="Zoom controls"> <button type="button" class="btn btn-outline-secondary" onclick="zoomIn()">Zoom in</button> <button type="button" class="btn btn-outline-secondary" onclick="zoomOut()">Zoom out</button> <button type="button" class="btn btn-outline-secondary" onclick="resetZoom()">Reset zoom</button> </div> <div class="btn-group btn-group-sm me-2 mb-2" role="group" aria-label="Pan controls"> <button type="button" class="btn btn-outline-secondary" onclick="panLeft()">Pan left</button> <button type="button" class="btn btn-outline-secondary" onclick="panRight()">Pan right</button> </div> <div class="btn-group btn-group-sm mb-2" role="group" aria-label="Center control"> <button type="button" class="btn btn-outline-secondary" onclick="center()">Center</button> </div> </div> <div id="topology" class="mb-4"></div> <script src="{{ url_for('js.topology_js') }}"></script> {% endblock %}