Add start of POC
This commit is contained in:
parent
7228f8b7c0
commit
25adf0f374
7 changed files with 1082 additions and 0 deletions
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module git.faercol.me/faercol/topology-map
|
||||||
|
|
||||||
|
go 1.23.1
|
212
main.go
Normal file
212
main.go
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiKey = "4e75b8927940adc29e2e1eac042bf92bcddd57fe"
|
||||||
|
const netboxBaseURL = "https://netbox.internal.faercol.me/api/"
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Object struct {
|
||||||
|
Device Device `json:"device"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CableTermination struct {
|
||||||
|
Object Object `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cable struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
ATerminations []CableTermination `json:"a_terminations"`
|
||||||
|
BTerminations []CableTermination `json:"b_terminations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VM struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Device Device `json:"device"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type vmsResponse struct {
|
||||||
|
Results []VM `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceResponse struct {
|
||||||
|
Results []Device `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cableResponse struct {
|
||||||
|
Results []Cable `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ElementData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
Parent string `json:"parent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Element struct {
|
||||||
|
Data ElementData `json:"data"`
|
||||||
|
Classes []string `json:"classes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDevices() ([]Device, error) {
|
||||||
|
query, err := http.NewRequest("GET", netboxBaseURL+"dcim/devices", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Header.Set("Authorization", "Token "+apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res deviceResponse
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCables() ([]Cable, error) {
|
||||||
|
query, err := http.NewRequest("GET", netboxBaseURL+"dcim/cables", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Header.Set("Authorization", "Token "+apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res cableResponse
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVMs() ([]VM, error) {
|
||||||
|
query, err := http.NewRequest("GET", netboxBaseURL+"virtualization/virtual-machines", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Header.Set("Authorization", "Token "+apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res vmsResponse
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
srv := http.NewServeMux()
|
||||||
|
|
||||||
|
srv.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("Serving static file")
|
||||||
|
fs := http.FileServer(http.Dir("./static"))
|
||||||
|
http.StripPrefix("/static", fs).ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
srv.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("Serving API route")
|
||||||
|
devices, err := GetDevices()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to get devices: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cables, err := GetCables()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to get cables: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vms, err := GetVMs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to get VMs: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := []Element{}
|
||||||
|
for _, d := range devices {
|
||||||
|
resp = append(resp, Element{Data: ElementData{ID: d.Name}})
|
||||||
|
}
|
||||||
|
for _, c := range cables {
|
||||||
|
resp = append(resp, Element{Data: ElementData{ID: "link-" + strconv.FormatInt(int64(c.ID), 10), Source: c.ATerminations[0].Object.Device.Name, Target: c.BTerminations[0].Object.Device.Name}})
|
||||||
|
}
|
||||||
|
for _, v := range vms {
|
||||||
|
resp = append(resp, Element{Data: ElementData{ID: v.Name}}, Element{Data: ElementData{ID: "vm-" + strconv.FormatInt(int64(v.ID), 10), Source: v.Name, Target: v.Device.Name}, Classes: []string{"edge", "virtual-edge"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to serialize data: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(respBody)
|
||||||
|
})
|
||||||
|
srv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("Serving main root")
|
||||||
|
tpl, err := template.New("index.html").ParseFiles("templates/index.html")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to read template: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tpl.Execute(w, nil); err != nil {
|
||||||
|
fmt.Printf("Failed to execute template: %s\n", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := http.ListenAndServe("127.0.0.1:5000", srv); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
753
static/scripts/cytoscape-cola.js
Normal file
753
static/scripts/cytoscape-cola.js
Normal file
|
@ -0,0 +1,753 @@
|
||||||
|
(function webpackUniversalModuleDefinition(root, factory) {
|
||||||
|
if (typeof exports === 'object' && typeof module === 'object')
|
||||||
|
module.exports = factory(require("webcola"));
|
||||||
|
else if (typeof define === 'function' && define.amd)
|
||||||
|
define(["webcola"], factory);
|
||||||
|
else if (typeof exports === 'object')
|
||||||
|
exports["cytoscapeCola"] = factory(require("webcola"));
|
||||||
|
else
|
||||||
|
root["cytoscapeCola"] = factory(root["webcola"]);
|
||||||
|
})(this, function (__WEBPACK_EXTERNAL_MODULE_5__) {
|
||||||
|
return /******/ (function (modules) { // webpackBootstrap
|
||||||
|
/******/ // The module cache
|
||||||
|
/******/ var installedModules = {};
|
||||||
|
/******/
|
||||||
|
/******/ // The require function
|
||||||
|
/******/ function __webpack_require__(moduleId) {
|
||||||
|
/******/
|
||||||
|
/******/ // Check if module is in cache
|
||||||
|
/******/ if (installedModules[moduleId]) {
|
||||||
|
/******/ return installedModules[moduleId].exports;
|
||||||
|
/******/
|
||||||
|
}
|
||||||
|
/******/ // Create a new module (and put it into the cache)
|
||||||
|
/******/ var module = installedModules[moduleId] = {
|
||||||
|
/******/ i: moduleId,
|
||||||
|
/******/ l: false,
|
||||||
|
/******/ exports: {}
|
||||||
|
/******/
|
||||||
|
};
|
||||||
|
/******/
|
||||||
|
/******/ // Execute the module function
|
||||||
|
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||||
|
/******/
|
||||||
|
/******/ // Flag the module as loaded
|
||||||
|
/******/ module.l = true;
|
||||||
|
/******/
|
||||||
|
/******/ // Return the exports of the module
|
||||||
|
/******/ return module.exports;
|
||||||
|
/******/
|
||||||
|
}
|
||||||
|
/******/
|
||||||
|
/******/
|
||||||
|
/******/ // expose the modules object (__webpack_modules__)
|
||||||
|
/******/ __webpack_require__.m = modules;
|
||||||
|
/******/
|
||||||
|
/******/ // expose the module cache
|
||||||
|
/******/ __webpack_require__.c = installedModules;
|
||||||
|
/******/
|
||||||
|
/******/ // identity function for calling harmony imports with the correct context
|
||||||
|
/******/ __webpack_require__.i = function (value) { return value; };
|
||||||
|
/******/
|
||||||
|
/******/ // define getter function for harmony exports
|
||||||
|
/******/ __webpack_require__.d = function (exports, name, getter) {
|
||||||
|
/******/ if (!__webpack_require__.o(exports, name)) {
|
||||||
|
/******/ Object.defineProperty(exports, name, {
|
||||||
|
/******/ configurable: false,
|
||||||
|
/******/ enumerable: true,
|
||||||
|
/******/ get: getter
|
||||||
|
/******/
|
||||||
|
});
|
||||||
|
/******/
|
||||||
|
}
|
||||||
|
/******/
|
||||||
|
};
|
||||||
|
/******/
|
||||||
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||||
|
/******/ __webpack_require__.n = function (module) {
|
||||||
|
/******/ var getter = module && module.__esModule ?
|
||||||
|
/******/ function getDefault() { return module['default']; } :
|
||||||
|
/******/ function getModuleExports() { return module; };
|
||||||
|
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||||
|
/******/ return getter;
|
||||||
|
/******/
|
||||||
|
};
|
||||||
|
/******/
|
||||||
|
/******/ // Object.prototype.hasOwnProperty.call
|
||||||
|
/******/ __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||||
|
/******/
|
||||||
|
/******/ // __webpack_public_path__
|
||||||
|
/******/ __webpack_require__.p = "";
|
||||||
|
/******/
|
||||||
|
/******/ // Load entry module and return exports
|
||||||
|
/******/ return __webpack_require__(__webpack_require__.s = 3);
|
||||||
|
/******/
|
||||||
|
})
|
||||||
|
/************************************************************************/
|
||||||
|
/******/([
|
||||||
|
/* 0 */
|
||||||
|
/***/ (function (module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
||||||
|
|
||||||
|
var assign = __webpack_require__(1);
|
||||||
|
var defaults = __webpack_require__(2);
|
||||||
|
var cola = __webpack_require__(5) || (typeof window !== 'undefined' ? window.cola : null);
|
||||||
|
var raf = __webpack_require__(4);
|
||||||
|
var isString = function isString(o) {
|
||||||
|
return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof('');
|
||||||
|
};
|
||||||
|
var isNumber = function isNumber(o) {
|
||||||
|
return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof(0);
|
||||||
|
};
|
||||||
|
var isObject = function isObject(o) {
|
||||||
|
return o != null && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof({});
|
||||||
|
};
|
||||||
|
var isFunction = function isFunction(o) {
|
||||||
|
return o != null && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof(function () { });
|
||||||
|
};
|
||||||
|
var nop = function nop() { };
|
||||||
|
|
||||||
|
var getOptVal = function getOptVal(val, ele) {
|
||||||
|
if (isFunction(val)) {
|
||||||
|
var fn = val;
|
||||||
|
return fn.apply(ele, [ele]);
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// constructor
|
||||||
|
// options : object containing layout options
|
||||||
|
function ColaLayout(options) {
|
||||||
|
this.options = assign({}, defaults, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// runs the layout
|
||||||
|
ColaLayout.prototype.run = function () {
|
||||||
|
var layout = this;
|
||||||
|
var options = this.options;
|
||||||
|
|
||||||
|
layout.manuallyStopped = false;
|
||||||
|
|
||||||
|
var cy = options.cy; // cy is automatically populated for us in the constructor
|
||||||
|
var eles = options.eles;
|
||||||
|
var nodes = eles.nodes();
|
||||||
|
var edges = eles.edges();
|
||||||
|
var ready = false;
|
||||||
|
|
||||||
|
var isParent = function isParent(ele) {
|
||||||
|
return ele.isParent();
|
||||||
|
};
|
||||||
|
|
||||||
|
var parentNodes = nodes.filter(isParent);
|
||||||
|
|
||||||
|
var nonparentNodes = nodes.subtract(parentNodes);
|
||||||
|
|
||||||
|
var bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() };
|
||||||
|
if (bb.x2 === undefined) {
|
||||||
|
bb.x2 = bb.x1 + bb.w;
|
||||||
|
}
|
||||||
|
if (bb.w === undefined) {
|
||||||
|
bb.w = bb.x2 - bb.x1;
|
||||||
|
}
|
||||||
|
if (bb.y2 === undefined) {
|
||||||
|
bb.y2 = bb.y1 + bb.h;
|
||||||
|
}
|
||||||
|
if (bb.h === undefined) {
|
||||||
|
bb.h = bb.y2 - bb.y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateNodePositions = function updateNodePositions() {
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
var node = nodes[i];
|
||||||
|
var dimensions = node.layoutDimensions(options);
|
||||||
|
var scratch = node.scratch('cola');
|
||||||
|
|
||||||
|
// update node dims
|
||||||
|
if (!scratch.updatedDims) {
|
||||||
|
var padding = getOptVal(options.nodeSpacing, node);
|
||||||
|
|
||||||
|
scratch.width = dimensions.w + 2 * padding;
|
||||||
|
scratch.height = dimensions.h + 2 * padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.positions(function (node) {
|
||||||
|
var scratch = node.scratch().cola;
|
||||||
|
var retPos = void 0;
|
||||||
|
|
||||||
|
if (!node.grabbed() && nonparentNodes.contains(node)) {
|
||||||
|
retPos = {
|
||||||
|
x: bb.x1 + scratch.x,
|
||||||
|
y: bb.y1 + scratch.y
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isNumber(retPos.x) || !isNumber(retPos.y)) {
|
||||||
|
retPos = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retPos;
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.updateCompoundBounds(); // because the way this layout sets positions is buggy for some reason; ref #878
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
onReady();
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fit) {
|
||||||
|
cy.fit(options.padding);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var onDone = function onDone() {
|
||||||
|
if (options.ungrabifyWhileSimulating) {
|
||||||
|
grabbableNodes.grabify();
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.off('destroy', destroyHandler);
|
||||||
|
|
||||||
|
nodes.off('grab free position', grabHandler);
|
||||||
|
nodes.off('lock unlock', lockHandler);
|
||||||
|
|
||||||
|
// trigger layoutstop when the layout stops (e.g. finishes)
|
||||||
|
layout.one('layoutstop', options.stop);
|
||||||
|
layout.trigger({ type: 'layoutstop', layout: layout });
|
||||||
|
};
|
||||||
|
|
||||||
|
var onReady = function onReady() {
|
||||||
|
// trigger layoutready when each node has had its position set at least once
|
||||||
|
layout.one('layoutready', options.ready);
|
||||||
|
layout.trigger({ type: 'layoutready', layout: layout });
|
||||||
|
};
|
||||||
|
|
||||||
|
var ticksPerFrame = options.refresh;
|
||||||
|
|
||||||
|
if (options.refresh < 0) {
|
||||||
|
ticksPerFrame = 1;
|
||||||
|
} else {
|
||||||
|
ticksPerFrame = Math.max(1, ticksPerFrame); // at least 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var adaptor = layout.adaptor = cola.adaptor({
|
||||||
|
trigger: function trigger(e) {
|
||||||
|
// on sim event
|
||||||
|
var TICK = cola.EventType ? cola.EventType.tick : null;
|
||||||
|
var END = cola.EventType ? cola.EventType.end : null;
|
||||||
|
|
||||||
|
switch (e.type) {
|
||||||
|
case 'tick':
|
||||||
|
case TICK:
|
||||||
|
if (options.animate) {
|
||||||
|
updateNodePositions();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'end':
|
||||||
|
case END:
|
||||||
|
updateNodePositions();
|
||||||
|
if (!options.infinite) {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
kick: function kick() {
|
||||||
|
// kick off the simulation
|
||||||
|
//let skip = 0;
|
||||||
|
|
||||||
|
var firstTick = true;
|
||||||
|
|
||||||
|
var inftick = function inftick() {
|
||||||
|
if (layout.manuallyStopped) {
|
||||||
|
onDone();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = adaptor.tick();
|
||||||
|
|
||||||
|
if (!options.infinite && !firstTick) {
|
||||||
|
adaptor.convergenceThreshold(options.convergenceThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
firstTick = false;
|
||||||
|
|
||||||
|
if (ret && options.infinite) {
|
||||||
|
// resume layout if done
|
||||||
|
adaptor.resume(); // resume => new kick
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret; // allow regular finish b/c of new kick
|
||||||
|
};
|
||||||
|
|
||||||
|
var multitick = function multitick() {
|
||||||
|
// multiple ticks in a row
|
||||||
|
var ret = void 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < ticksPerFrame && !ret; i++) {
|
||||||
|
ret = ret || inftick(); // pick up true ret vals => sim done
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.animate) {
|
||||||
|
var frame = function frame() {
|
||||||
|
if (multitick()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
raf(frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
raf(frame);
|
||||||
|
} else {
|
||||||
|
while (!inftick()) {
|
||||||
|
// keep going...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
on: nop, // dummy; not needed
|
||||||
|
|
||||||
|
drag: nop // not needed for our case
|
||||||
|
});
|
||||||
|
layout.adaptor = adaptor;
|
||||||
|
|
||||||
|
// if set no grabbing during layout
|
||||||
|
var grabbableNodes = nodes.filter(':grabbable');
|
||||||
|
if (options.ungrabifyWhileSimulating) {
|
||||||
|
grabbableNodes.ungrabify();
|
||||||
|
}
|
||||||
|
|
||||||
|
var destroyHandler = void 0;
|
||||||
|
cy.one('destroy', destroyHandler = function destroyHandler() {
|
||||||
|
layout.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle node dragging
|
||||||
|
var grabHandler = void 0;
|
||||||
|
nodes.on('grab free position', grabHandler = function grabHandler(e) {
|
||||||
|
var node = this;
|
||||||
|
var scrCola = node.scratch().cola;
|
||||||
|
var pos = node.position();
|
||||||
|
var nodeIsTarget = e.cyTarget === node || e.target === node;
|
||||||
|
|
||||||
|
if (!nodeIsTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.type) {
|
||||||
|
case 'grab':
|
||||||
|
adaptor.dragstart(scrCola);
|
||||||
|
break;
|
||||||
|
case 'free':
|
||||||
|
adaptor.dragend(scrCola);
|
||||||
|
break;
|
||||||
|
case 'position':
|
||||||
|
// only update when different (i.e. manual .position() call or drag) so we don't loop needlessly
|
||||||
|
if (scrCola.px !== pos.x - bb.x1 || scrCola.py !== pos.y - bb.y1) {
|
||||||
|
scrCola.px = pos.x - bb.x1;
|
||||||
|
scrCola.py = pos.y - bb.y1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var lockHandler = void 0;
|
||||||
|
nodes.on('lock unlock', lockHandler = function lockHandler() {
|
||||||
|
var node = this;
|
||||||
|
var scrCola = node.scratch().cola;
|
||||||
|
|
||||||
|
scrCola.fixed = node.locked();
|
||||||
|
|
||||||
|
if (node.locked()) {
|
||||||
|
adaptor.dragstart(scrCola);
|
||||||
|
} else {
|
||||||
|
adaptor.dragend(scrCola);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add nodes to cola
|
||||||
|
adaptor.nodes(nonparentNodes.map(function (node, i) {
|
||||||
|
var padding = getOptVal(options.nodeSpacing, node);
|
||||||
|
var pos = node.position();
|
||||||
|
var dimensions = node.layoutDimensions(options);
|
||||||
|
|
||||||
|
var struct = node.scratch().cola = {
|
||||||
|
x: options.randomize && !node.locked() || pos.x === undefined ? Math.round(Math.random() * bb.w) : pos.x,
|
||||||
|
y: options.randomize && !node.locked() || pos.y === undefined ? Math.round(Math.random() * bb.h) : pos.y,
|
||||||
|
width: dimensions.w + 2 * padding,
|
||||||
|
height: dimensions.h + 2 * padding,
|
||||||
|
index: i,
|
||||||
|
fixed: node.locked()
|
||||||
|
};
|
||||||
|
|
||||||
|
return struct;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// the constraints to be added on nodes
|
||||||
|
var constraints = [];
|
||||||
|
|
||||||
|
if (options.alignment) {
|
||||||
|
// then set alignment constraints
|
||||||
|
|
||||||
|
if (options.alignment.vertical) {
|
||||||
|
var verticalAlignments = options.alignment.vertical;
|
||||||
|
verticalAlignments.forEach(function (alignment) {
|
||||||
|
var offsetsX = [];
|
||||||
|
alignment.forEach(function (nodeData) {
|
||||||
|
var node = nodeData.node;
|
||||||
|
var scrCola = node.scratch().cola;
|
||||||
|
var index = scrCola.index;
|
||||||
|
offsetsX.push({
|
||||||
|
node: index,
|
||||||
|
offset: nodeData.offset ? nodeData.offset : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
constraints.push({
|
||||||
|
type: 'alignment',
|
||||||
|
axis: 'x',
|
||||||
|
offsets: offsetsX
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.alignment.horizontal) {
|
||||||
|
var horizontalAlignments = options.alignment.horizontal;
|
||||||
|
horizontalAlignments.forEach(function (alignment) {
|
||||||
|
var offsetsY = [];
|
||||||
|
alignment.forEach(function (nodeData) {
|
||||||
|
var node = nodeData.node;
|
||||||
|
var scrCola = node.scratch().cola;
|
||||||
|
var index = scrCola.index;
|
||||||
|
offsetsY.push({
|
||||||
|
node: index,
|
||||||
|
offset: nodeData.offset ? nodeData.offset : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
constraints.push({
|
||||||
|
type: 'alignment',
|
||||||
|
axis: 'y',
|
||||||
|
offsets: offsetsY
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if gapInequalities variable is set add each inequality constraint to list of constraints
|
||||||
|
if (options.gapInequalities) {
|
||||||
|
options.gapInequalities.forEach(function (inequality) {
|
||||||
|
|
||||||
|
// for the constraints to be passed to cola layout adaptor use indices of nodes,
|
||||||
|
// not the nodes themselves
|
||||||
|
var leftIndex = inequality.left.scratch().cola.index;
|
||||||
|
var rightIndex = inequality.right.scratch().cola.index;
|
||||||
|
|
||||||
|
constraints.push({
|
||||||
|
axis: inequality.axis,
|
||||||
|
left: leftIndex,
|
||||||
|
right: rightIndex,
|
||||||
|
gap: inequality.gap,
|
||||||
|
equality: inequality.equality
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add constraints if any
|
||||||
|
if (constraints.length > 0) {
|
||||||
|
adaptor.constraints(constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add compound nodes to cola
|
||||||
|
adaptor.groups(parentNodes.map(function (node, i) {
|
||||||
|
// add basic group incl leaf nodes
|
||||||
|
var optPadding = getOptVal(options.nodeSpacing, node);
|
||||||
|
var getPadding = function getPadding(d) {
|
||||||
|
return parseFloat(node.style('padding-' + d));
|
||||||
|
};
|
||||||
|
|
||||||
|
var pleft = getPadding('left') + optPadding;
|
||||||
|
var pright = getPadding('right') + optPadding;
|
||||||
|
var ptop = getPadding('top') + optPadding;
|
||||||
|
var pbottom = getPadding('bottom') + optPadding;
|
||||||
|
|
||||||
|
node.scratch().cola = {
|
||||||
|
index: i,
|
||||||
|
|
||||||
|
padding: Math.max(pleft, pright, ptop, pbottom),
|
||||||
|
|
||||||
|
// leaves should only contain direct descendants (children),
|
||||||
|
// not the leaves of nested compound nodes or any nodes that are compounds themselves
|
||||||
|
leaves: node.children().intersection(nonparentNodes).map(function (child) {
|
||||||
|
return child[0].scratch().cola.index;
|
||||||
|
}),
|
||||||
|
|
||||||
|
fixed: node.locked()
|
||||||
|
};
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}).map(function (node) {
|
||||||
|
// add subgroups
|
||||||
|
node.scratch().cola.groups = node.children().intersection(parentNodes).map(function (child) {
|
||||||
|
return child.scratch().cola.index;
|
||||||
|
});
|
||||||
|
|
||||||
|
return node.scratch().cola;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// get the edge length setting mechanism
|
||||||
|
var length = void 0;
|
||||||
|
var lengthFnName = void 0;
|
||||||
|
if (options.edgeLength != null) {
|
||||||
|
length = options.edgeLength;
|
||||||
|
lengthFnName = 'linkDistance';
|
||||||
|
} else if (options.edgeSymDiffLength != null) {
|
||||||
|
length = options.edgeSymDiffLength;
|
||||||
|
lengthFnName = 'symmetricDiffLinkLengths';
|
||||||
|
} else if (options.edgeJaccardLength != null) {
|
||||||
|
length = options.edgeJaccardLength;
|
||||||
|
lengthFnName = 'jaccardLinkLengths';
|
||||||
|
} else {
|
||||||
|
length = 100;
|
||||||
|
lengthFnName = 'linkDistance';
|
||||||
|
}
|
||||||
|
|
||||||
|
var lengthGetter = function lengthGetter(link) {
|
||||||
|
return link.calcLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the edges to cola
|
||||||
|
adaptor.links(edges.stdFilter(function (edge) {
|
||||||
|
return nonparentNodes.contains(edge.source()) && nonparentNodes.contains(edge.target());
|
||||||
|
}).map(function (edge) {
|
||||||
|
var c = edge.scratch().cola = {
|
||||||
|
source: edge.source()[0].scratch().cola.index,
|
||||||
|
target: edge.target()[0].scratch().cola.index
|
||||||
|
};
|
||||||
|
|
||||||
|
if (length != null) {
|
||||||
|
c.calcLength = getOptVal(length, edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}));
|
||||||
|
|
||||||
|
adaptor.size([bb.w, bb.h]);
|
||||||
|
|
||||||
|
if (length != null) {
|
||||||
|
adaptor[lengthFnName](lengthGetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the flow of cola
|
||||||
|
if (options.flow) {
|
||||||
|
var flow = void 0;
|
||||||
|
var defAxis = 'y';
|
||||||
|
var defMinSep = 50;
|
||||||
|
|
||||||
|
if (isString(options.flow)) {
|
||||||
|
flow = {
|
||||||
|
axis: options.flow,
|
||||||
|
minSeparation: defMinSep
|
||||||
|
};
|
||||||
|
} else if (isNumber(options.flow)) {
|
||||||
|
flow = {
|
||||||
|
axis: defAxis,
|
||||||
|
minSeparation: options.flow
|
||||||
|
};
|
||||||
|
} else if (isObject(options.flow)) {
|
||||||
|
flow = options.flow;
|
||||||
|
|
||||||
|
flow.axis = flow.axis || defAxis;
|
||||||
|
flow.minSeparation = flow.minSeparation != null ? flow.minSeparation : defMinSep;
|
||||||
|
} else {
|
||||||
|
// e.g. options.flow: true
|
||||||
|
flow = {
|
||||||
|
axis: defAxis,
|
||||||
|
minSeparation: defMinSep
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
adaptor.flowLayout(flow.axis, flow.minSeparation);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.trigger({ type: 'layoutstart', layout: layout });
|
||||||
|
|
||||||
|
adaptor.avoidOverlaps(options.avoidOverlap).handleDisconnected(options.handleDisconnected).start(options.unconstrIter, options.userConstIter, options.allConstIter, undefined, // gridSnapIterations = 0
|
||||||
|
undefined, // keepRunning = true
|
||||||
|
options.centerGraph);
|
||||||
|
|
||||||
|
if (!options.infinite) {
|
||||||
|
setTimeout(function () {
|
||||||
|
if (!layout.manuallyStopped) {
|
||||||
|
adaptor.stop();
|
||||||
|
}
|
||||||
|
}, options.maxSimulationTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
// called on continuous layouts to stop them before they finish
|
||||||
|
ColaLayout.prototype.stop = function () {
|
||||||
|
if (this.adaptor) {
|
||||||
|
this.manuallyStopped = true;
|
||||||
|
this.adaptor.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this; // chaining
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ColaLayout;
|
||||||
|
|
||||||
|
/***/
|
||||||
|
}),
|
||||||
|
/* 1 */
|
||||||
|
/***/ (function (module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
// Simple, internal Object.assign() polyfill for options objects etc.
|
||||||
|
|
||||||
|
module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
|
||||||
|
for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||||
|
srcs[_key - 1] = arguments[_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
srcs.filter(function (src) {
|
||||||
|
return src != null;
|
||||||
|
}).forEach(function (src) {
|
||||||
|
Object.keys(src).forEach(function (k) {
|
||||||
|
return tgt[k] = src[k];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return tgt;
|
||||||
|
};
|
||||||
|
|
||||||
|
/***/
|
||||||
|
}),
|
||||||
|
/* 2 */
|
||||||
|
/***/ (function (module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
// default layout options
|
||||||
|
var defaults = {
|
||||||
|
animate: true, // whether to show the layout as it's running
|
||||||
|
refresh: 1, // number of ticks per frame; higher is faster but more jerky
|
||||||
|
maxSimulationTime: 4000, // max length in ms to run the layout
|
||||||
|
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
|
||||||
|
fit: true, // on every layout reposition of nodes, fit the viewport
|
||||||
|
padding: 30, // padding around the simulation
|
||||||
|
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||||
|
nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
|
||||||
|
|
||||||
|
// layout event callbacks
|
||||||
|
ready: function ready() { }, // on layoutready
|
||||||
|
stop: function stop() { }, // on layoutstop
|
||||||
|
|
||||||
|
// positioning options
|
||||||
|
randomize: false, // use random node positions at beginning of layout
|
||||||
|
avoidOverlap: true, // if true, prevents overlap of node bounding boxes
|
||||||
|
handleDisconnected: true, // if true, avoids disconnected components from overlapping
|
||||||
|
convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops
|
||||||
|
nodeSpacing: function nodeSpacing(node) {
|
||||||
|
return 10;
|
||||||
|
}, // extra spacing around nodes
|
||||||
|
flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
|
||||||
|
alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
|
||||||
|
gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
|
||||||
|
centerGraph: true, // adjusts the node positions initially to center the graph (pass false if you want to start the layout from the current position)
|
||||||
|
|
||||||
|
|
||||||
|
// different methods of specifying edge length
|
||||||
|
// each can be a constant numerical value or a function like `function( edge ){ return 2; }`
|
||||||
|
edgeLength: undefined, // sets edge length directly in simulation
|
||||||
|
edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
|
||||||
|
edgeJaccardLength: undefined, // jaccard edge length in simulation
|
||||||
|
|
||||||
|
// iterations of cola algorithm; uses default values on undefined
|
||||||
|
unconstrIter: undefined, // unconstrained initial layout iterations
|
||||||
|
userConstIter: undefined, // initial layout iterations with user-specified constraints
|
||||||
|
allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
|
||||||
|
|
||||||
|
// infinite layout options
|
||||||
|
infinite: false // overrides all other options for a forces-all-the-time mode
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = defaults;
|
||||||
|
|
||||||
|
/***/
|
||||||
|
}),
|
||||||
|
/* 3 */
|
||||||
|
/***/ (function (module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
var impl = __webpack_require__(0);
|
||||||
|
|
||||||
|
// registers the extension on a cytoscape lib ref
|
||||||
|
var register = function register(cytoscape) {
|
||||||
|
if (!cytoscape) {
|
||||||
|
return;
|
||||||
|
} // can't register if cytoscape unspecified
|
||||||
|
|
||||||
|
cytoscape('layout', 'cola', impl); // register with cytoscape.js
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof cytoscape !== 'undefined') {
|
||||||
|
// expose to global cytoscape (i.e. window.cytoscape)
|
||||||
|
register(cytoscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = register;
|
||||||
|
|
||||||
|
/***/
|
||||||
|
}),
|
||||||
|
/* 4 */
|
||||||
|
/***/ (function (module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
||||||
|
|
||||||
|
var raf = void 0;
|
||||||
|
|
||||||
|
if ((typeof window === "undefined" ? "undefined" : _typeof(window)) !== (true ? "undefined" : _typeof(undefined))) {
|
||||||
|
raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function (fn) {
|
||||||
|
return setTimeout(fn, 16);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// if not available, all you get is immediate calls
|
||||||
|
raf = function raf(cb) {
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = raf;
|
||||||
|
|
||||||
|
/***/
|
||||||
|
}),
|
||||||
|
/* 5 */
|
||||||
|
/***/ (function (module, exports) {
|
||||||
|
|
||||||
|
module.exports = __WEBPACK_EXTERNAL_MODULE_5__;
|
||||||
|
|
||||||
|
/***/
|
||||||
|
})
|
||||||
|
/******/]);
|
||||||
|
});
|
||||||
|
|
77
static/scripts/index.js
Normal file
77
static/scripts/index.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// import cola from 'cytoscape-cola';
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/data");
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`Response status: ${resp.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await resp.json();
|
||||||
|
return json;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGraph() {
|
||||||
|
|
||||||
|
getData()
|
||||||
|
.catch(function (err) { console.error(err) })
|
||||||
|
.then(function (elements) {
|
||||||
|
|
||||||
|
var cy = window.cy = cytoscape({
|
||||||
|
|
||||||
|
container: document.getElementById('map-container'), // container to render in
|
||||||
|
|
||||||
|
elements: elements,
|
||||||
|
// elements: [ // list of graph elements to start with
|
||||||
|
// { // node a
|
||||||
|
// data: { id: 'a' }
|
||||||
|
// },
|
||||||
|
// { // node b
|
||||||
|
// data: { id: 'b' }
|
||||||
|
// },
|
||||||
|
// { // edge ab
|
||||||
|
// data: { id: 'ab', source: 'a', target: 'b' }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
|
||||||
|
style: [ // the stylesheet for the graph
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-color': '#666',
|
||||||
|
'label': 'data(id)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': 1,
|
||||||
|
'line-color': '#1E88E5',
|
||||||
|
'target-arrow-color': '#1E88E5',
|
||||||
|
'target-arrow-shape': 'none',
|
||||||
|
'curve-style': 'bezier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".virtual-edge",
|
||||||
|
style: {
|
||||||
|
"line-color": "#BDBDBD",
|
||||||
|
"line-style": "dashed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
layout: {
|
||||||
|
name: 'cola',
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', setupGraph);
|
9
static/style/main.css
Normal file
9
static/style/main.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#map-container {
|
||||||
|
width: 80%;
|
||||||
|
height: 600px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge {
|
||||||
|
color: pink;
|
||||||
|
}
|
28
templates/index.html
Normal file
28
templates/index.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/style/main.css">
|
||||||
|
|
||||||
|
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.2/cytoscape.min.js"
|
||||||
|
integrity="sha512-EY3U1MWdgKx0P1dqTE4inlKz2cpXtWpsR1YUyD855Hs6RL/A0cyvrKh60EpE8wDZ814cTe1KgRK+sG0Rn792vQ=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script> -->
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.2/cytoscape.umd.js"
|
||||||
|
integrity="sha512-j0/d+W9EL6BFFGcM3YGwI10MsaTihhvlrvZoTLff+plAOEC78IgiJZGx/0ZGMpjHqrlp5si4JpdNtcDMywep4A=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/webcola/WebCola/cola.min.js"></script>
|
||||||
|
<script src="/static/scripts/cytoscape-cola.js"></script>
|
||||||
|
<script src="/static/scripts/index.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Coucou</h1>
|
||||||
|
|
||||||
|
<div id="map-container"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
BIN
topology-map
Executable file
BIN
topology-map
Executable file
Binary file not shown.
Loading…
Reference in a new issue