diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3950a3c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.faercol.me/faercol/topology-map + +go 1.23.1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..2603bc4 --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/static/scripts/cytoscape-cola.js b/static/scripts/cytoscape-cola.js new file mode 100644 index 0000000..ef7078c --- /dev/null +++ b/static/scripts/cytoscape-cola.js @@ -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__; + + /***/ + }) +/******/]); +}); + diff --git a/static/scripts/index.js b/static/scripts/index.js new file mode 100644 index 0000000..2c08c13 --- /dev/null +++ b/static/scripts/index.js @@ -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); diff --git a/static/style/main.css b/static/style/main.css new file mode 100644 index 0000000..731f286 --- /dev/null +++ b/static/style/main.css @@ -0,0 +1,9 @@ +#map-container { + width: 80%; + height: 600px; + display: block; +} + +.edge { + color: pink; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5f9b192 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + +

Coucou

+ +
+ + + \ No newline at end of file diff --git a/topology-map b/topology-map new file mode 100755 index 0000000..060453a Binary files /dev/null and b/topology-map differ