Commit 2394134e authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Implemented WebUI topology view.

- Each device type has a particular icon
- Icon color represents real/emulated devices
- Nodes support drag/drop
- Nodes are auto-placed in the topology
- Nodes have a tooltip showing their name/id
parent 07759b7d
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@
import json, logging, sys
from common.Settings import get_setting
from context.client.ContextClient import ContextClient
from context.proto.context_pb2 import Context, ContextId, Device, Empty, Link, Topology
from context.proto.context_pb2 import Context, Device, Link, Topology
from device.client.DeviceClient import DeviceClient

LOGGER = logging.getLogger(__name__)
+27 −12
Original line number Diff line number Diff line
@@ -15,7 +15,7 @@
import json
import logging
import sys
from flask import redirect, render_template, Blueprint, flash, session, url_for, request
from flask import jsonify, redirect, render_template, Blueprint, flash, session, url_for, request
from webui.Config import (CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT,
                DEVICE_SERVICE_ADDRESS, DEVICE_SERVICE_PORT)
from context.client.ContextClient import ContextClient
@@ -67,35 +67,50 @@ def home():
    context_client.connect()
    device_client.connect()
    try:
        logger.warning('home: {:s}'.format(str(request)))
        # flash('This is an info message', 'info')
        # flash('This is a danger message', 'danger')
        response = context_client.ListContextIds(Empty())
        context_form: ContextForm = ContextForm()
        context_form.context.choices.append(('', 'Select...'))
        for context in response.context_ids:
            context_form.context.choices.append((context.context_uuid.uuid, context.context_uuid))
        logger.warning('context_form.data = {:s}'.format(str(context_form.data)))
        logger.warning('before validate_on_submit')
        if context_form.validate_on_submit():
            logger.warning('inside validate_on_submit')
            logger.warning('context_form.context.data = {:s}'.format(str(context_form.context.data)))
            logger.warning('context_form.descriptors.data = {:s}'.format(str(context_form.descriptors.data)))
            if context_form.context.data:
                session['context_uuid'] = context_form.context.data
                flash(f'The context was successfully set to `{context_form.context.data}`.', 'success')
            if context_form.descriptors.data:
                process_descriptors(context_form.descriptors)
        logger.warning('context_form.errors = {:s}'.format(str(context_form.errors)))
        if 'context_uuid' in session:
            context_form.context.data = session['context_uuid']
        return render_template('main/home.html', context_form=context_form)
    except:
        logger.exception('Something failed')
    except Exception as e:
        logger.exception('Descriptor load failed')
        flash(f'Descriptor load failed: `{str(e)}`', 'danger')
    finally:
        context_client.close()
        device_client.close()

@main.route('/topology', methods=['GET'])
def topology():
    context_client.connect()
    try:
        response = context_client.ListDevices(Empty())
        devices = [{
            'id': device.device_id.device_uuid.uuid,
            'name': device.device_id.device_uuid.uuid,
            'type': device.device_type,
        } for device in response.devices]

        response = context_client.ListLinks(Empty())
        links = [{
            'id': link.link_id.link_uuid.uuid,
            'source': link.link_endpoint_ids[0].device_id.device_uuid.uuid,
            'target': link.link_endpoint_ids[1].device_id.device_uuid.uuid,
        } for link in response.links]

        return jsonify({'devices': devices, 'links': links})
    except:
        logger.exception('Error retrieving topology')
    finally:
        context_client.close()

@main.get('/about')
def about():
+148 −0
Original line number Diff line number Diff line
// Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Based on:
//   https://www.d3-graph-gallery.com/graph/network_basic.html
//   https://bl.ocks.org/steveharoz/8c3e2524079a8c440df60c1ab72b5d03

// set the dimensions and margins of the graph
const margin = {top: 5, right: 5, bottom: 5, left: 5};

const icon_width  = 40;
const icon_height = 40;

width = 800 - margin.left - margin.right;
height = 500 - margin.top - margin.bottom;

// append the svg object to the body of the page
const svg = d3.select('#topology')
    .append('svg')
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
    .append('g')
        .attr('transform', `translate(${margin.left}, ${margin.top})`);

// svg objects
var link, node;

// values for all forces
forceProperties = {
    center: {x: 0.5, y: 0.5},
    charge: {enabled: true, strength: -500, distanceMin: 10, distanceMax: 2000},
    collide: {enabled: true, strength: 0.7, iterations: 1, radius: 5},
    forceX: {enabled: false, strength: 0.1, x: 0.5},
    forceY: {enabled: false, strength: 0.1, y: 0.5},
    link: {enabled: true, distance: 100, iterations: 1}
}

/**************** FORCE SIMULATION *****************/

var simulation = d3.forceSimulation();

// load the data
d3.json('/topology', function(data) {
    // set the data and properties of link lines and node circles
    link = svg.append("g").attr("class", "links").style('stroke', '#aaa')
        .selectAll("line")
        .data(data.links)
        .enter()
        .append("line");
    node = svg.append("g").attr("class", "devices").attr('r', 20).style('fill', '#69b3a2')
        .selectAll("circle")
        .data(data.devices)
        .enter()
        .append("image")
        .attr('xlink:href', function(d) {return '/static/topology_icons/' + d.type + '.png';})
        .attr('width',  icon_width)
        .attr('height', icon_height)
        .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));

    // node tooltip
    node.append("title").text(function(d) { return d.id; });

    // link style
    link
        .attr("stroke-width", forceProperties.link.enabled ? 2 : 1)
        .attr("opacity", forceProperties.link.enabled ? 1 : 0);
    
    // set up the simulation and event to update locations after each tick
    simulation.nodes(data.devices);

    // add forces, associate each with a name, and set their properties
    simulation
        .force("link", d3.forceLink()
            .id(function(d) {return d.id;})
            .distance(forceProperties.link.distance)
            .iterations(forceProperties.link.iterations)
            .links(forceProperties.link.enabled ? data.links : []))
        .force("charge", d3.forceManyBody()
            .strength(forceProperties.charge.strength * forceProperties.charge.enabled)
            .distanceMin(forceProperties.charge.distanceMin)
            .distanceMax(forceProperties.charge.distanceMax))
        .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));
    
    // after each simulation tick, update the display positions
    simulation.on("tick", ticked);
});

// update the display positions
function ticked() {
    link
        .attr('x1', function(d) { return d.source.x; })
        .attr('y1', function(d) { return d.source.y; })
        .attr('x2', function(d) { return d.target.x; })
        .attr('y2', function(d) { return d.target.y; });

    node
        .attr('x', function(d) { return d.x-icon_width/2; })
        .attr('y', function(d) { return d.y-icon_height/2; });
}

/******************** UI EVENTS ********************/

function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
}

function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
}

function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0.0001);
    d.fx = null;
    d.fy = null;
}

// update size-related forces
d3.select(window).on("resize", function(){
    width = +svg.node().getBoundingClientRect().width;
    height = +svg.node().getBoundingClientRect().height;
    simulation.alpha(1).restart();
});
+12 −0
Original line number Diff line number Diff line
Network Topology Icons taken from https://vecta.io/symbols

https://symbols.getvecta.com/stencil_240/51_cloud.4d0a827676.png => cloud.png

https://symbols.getvecta.com/stencil_240/15_atm-switch.1bbf9a7cca.png => packet-switch.png
https://symbols.getvecta.com/stencil_241/45_atm-switch.6a7362c1df.png => emu-packet-switch.png

https://symbols.getvecta.com/stencil_240/204_router.7b208c1133.png => packet-router.png
https://symbols.getvecta.com/stencil_241/224_router.be30fb87e7.png => emu-packet-router.png

https://symbols.getvecta.com/stencil_240/269_virtual-layer-switch.ed10fdede6.png => optical-line-system.png
https://symbols.getvecta.com/stencil_241/281_virtual-layer-switch.29420aff2f.png => emu-optical-line-system.png
+8.78 KiB
Loading image diff...
Loading