mirror of
https://github.com/opennetworkinglab/onos.git
synced 2025-10-22 21:01:00 +02:00
GUI -- Implemented Instance Panel.
- handling addInstance event. Change-Id: Ic98a3291bd37ecf1155dbe1696167d0635a31972
This commit is contained in:
parent
0ce220af4f
commit
4b66859f8d
@ -75,7 +75,8 @@
|
||||
append: appendPanel,
|
||||
width: panelWidth,
|
||||
height: panelHeight,
|
||||
isVisible: panelIsVisible
|
||||
isVisible: panelIsVisible,
|
||||
el: panelEl
|
||||
};
|
||||
|
||||
p.el = panelLayer.append('div')
|
||||
@ -136,6 +137,10 @@
|
||||
return p.on;
|
||||
}
|
||||
|
||||
function panelEl() {
|
||||
return p.el;
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
|
@ -218,8 +218,7 @@
|
||||
if (xns) {
|
||||
atr.transform = sus.translate(trans);
|
||||
}
|
||||
elem.append('use').attr(atr).classed('overlay', ovr);
|
||||
|
||||
return elem.append('use').attr(atr).classed('overlay', ovr);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
@ -143,11 +143,16 @@
|
||||
return 'translate(' + x + ',' + y + ')';
|
||||
}
|
||||
|
||||
function stripPx(s) {
|
||||
return s.replace(/px$/,'');
|
||||
}
|
||||
|
||||
return {
|
||||
createDragBehavior: createDragBehavior,
|
||||
loadGlow: loadGlow,
|
||||
cat7: cat7,
|
||||
translate: translate
|
||||
translate: translate,
|
||||
stripPx: stripPx
|
||||
};
|
||||
}]);
|
||||
}());
|
||||
|
@ -78,6 +78,7 @@
|
||||
<script src="view/topo/topoEvent.js"></script>
|
||||
<script src="view/topo/topoForce.js"></script>
|
||||
<script src="view/topo/topoPanel.js"></script>
|
||||
<script src="view/topo/topoInst.js"></script>
|
||||
<script src="view/device/device.js"></script>
|
||||
<!-- TODO: inject javascript refs server-side -->
|
||||
|
||||
|
@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
|
||||
/* --- Topo Panels --- */
|
||||
/* --- Topo Summary Panel --- */
|
||||
|
||||
#topo-p-summary {
|
||||
/* Base css from panel.css */
|
||||
@ -107,3 +107,78 @@
|
||||
background-color: #888;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
|
||||
/* --- Topo Detail Panel --- */
|
||||
|
||||
/* TODO: add CSS rules */
|
||||
|
||||
|
||||
/* --- Topo Instance Panel --- */
|
||||
|
||||
#topo-p-instance {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
#topo-p-instance div.onosInst {
|
||||
display: inline-block;
|
||||
width: 170px;
|
||||
height: 85px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#topo-p-instance svg rect {
|
||||
fill: #ccc;
|
||||
stroke: #aaa;
|
||||
stroke-width: 3.5;
|
||||
}
|
||||
#topo-p-instance .online svg rect {
|
||||
opacity: 1;
|
||||
fill: #9cf;
|
||||
stroke: #555;
|
||||
}
|
||||
|
||||
#topo-p-instance svg .glyph {
|
||||
fill: #888;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
#topo-p-instance .online svg .glyph {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
#topo-p-instance svg .badgeIcon {
|
||||
fill: #777;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
#topo-p-instance .online svg .badgeIcon {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
#topo-p-instance svg text {
|
||||
text-anchor: middle;
|
||||
fill: #777;
|
||||
}
|
||||
#topo-p-instance .online svg text {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
#topo-p-instance svg text.instTitle {
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
#topo-p-instance svg text.instLabel {
|
||||
font-size: 9pt;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#topo-p-instance .onosInst.mastership {
|
||||
opacity: 0.3;
|
||||
}
|
||||
#topo-p-instance .onosInst.mastership.affinity {
|
||||
opacity: 1.0;
|
||||
}
|
||||
#topo-p-instance .onosInst.mastership.affinity svg rect {
|
||||
/* TODO: add blue glow */
|
||||
/*filter: url(#blue-glow);*/
|
||||
}
|
||||
|
@ -143,12 +143,13 @@
|
||||
|
||||
.controller('OvTopoCtrl', [
|
||||
'$scope', '$log', '$location', '$timeout',
|
||||
'FnService', 'MastService',
|
||||
'KeyService', 'ZoomService', 'GlyphService', 'MapService',
|
||||
'FnService', 'MastService', 'KeyService', 'ZoomService',
|
||||
'GlyphService', 'MapService',
|
||||
'TopoEventService', 'TopoForceService', 'TopoPanelService',
|
||||
'TopoInstService',
|
||||
|
||||
function ($scope, _$log_, $loc, $timeout, _fs_, mast,
|
||||
_ks_, _zs_, _gs_, _ms_, tes, _tfs_, tps) {
|
||||
_ks_, _zs_, _gs_, _ms_, tes, _tfs_, tps, tis) {
|
||||
var self = this;
|
||||
$log = _$log_;
|
||||
fs = _fs_;
|
||||
@ -167,6 +168,7 @@
|
||||
$log.log('OvTopoCtrl is saying Buh-Bye!');
|
||||
tes.closeSock();
|
||||
tps.destroyPanels();
|
||||
tis.destroyInst();
|
||||
});
|
||||
|
||||
// svg layer and initialization of components
|
||||
@ -181,6 +183,7 @@
|
||||
setUpMap();
|
||||
setUpForce();
|
||||
|
||||
tis.initInst();
|
||||
tps.initPanels();
|
||||
tes.openSock();
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
'use strict';
|
||||
|
||||
// injected refs
|
||||
var $log, wss, wes, tps;
|
||||
var $log, wss, wes, tps, tis;
|
||||
|
||||
// internal state
|
||||
var wsock;
|
||||
@ -47,7 +47,8 @@
|
||||
}
|
||||
|
||||
function addInstance(ev) {
|
||||
$log.debug(' *** We got an ADD INSTANCE event: ', ev);
|
||||
$log.debug(' **** Add Instance **** ', ev.payload);
|
||||
tis.addInstance(ev.payload);
|
||||
}
|
||||
|
||||
// ==========================
|
||||
@ -87,13 +88,14 @@
|
||||
angular.module('ovTopo')
|
||||
.factory('TopoEventService',
|
||||
['$log', '$location', 'WebSocketService', 'WsEventService',
|
||||
'TopoPanelService',
|
||||
'TopoPanelService', 'TopoInstService',
|
||||
|
||||
function (_$log_, $loc, _wss_, _wes_, _tps_) {
|
||||
function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_) {
|
||||
$log = _$log_;
|
||||
wss = _wss_;
|
||||
wes = _wes_;
|
||||
tps = _tps_;
|
||||
tis = _tis_;
|
||||
|
||||
function bindDispatcher(TopoDomElementsPassedHere) {
|
||||
// TODO: store refs to topo DOM elements...
|
||||
|
304
web/gui/src/main/webapp/app/view/topo/topoInst.js
Normal file
304
web/gui/src/main/webapp/app/view/topo/topoInst.js
Normal file
@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright 2015 Open Networking Laboratory
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
ONOS GUI -- Topology Instances Module.
|
||||
Defines modeling of ONOS instances.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// injected refs
|
||||
var $log, ps, sus, gs;
|
||||
|
||||
// configuration
|
||||
var instCfg = {
|
||||
rectPad: 8,
|
||||
nodeOx: 9,
|
||||
nodeOy: 9,
|
||||
nodeDim: 40,
|
||||
birdOx: 19,
|
||||
birdOy: 21,
|
||||
birdDim: 21,
|
||||
uiDy: 45,
|
||||
titleDy: 30,
|
||||
textYOff: 20,
|
||||
textYSpc: 15
|
||||
},
|
||||
showLogicErrors = true,
|
||||
idIns = 'topo-p-instance',
|
||||
instOpts = {
|
||||
edge: 'left',
|
||||
width: 20
|
||||
};
|
||||
|
||||
// internal state
|
||||
var onosInstances,
|
||||
onosOrder,
|
||||
oiShowMaster,
|
||||
oiBox;
|
||||
|
||||
|
||||
// ==========================
|
||||
// *** ADD INSTANCE ***
|
||||
|
||||
function addInstance(data) {
|
||||
var id = data.id;
|
||||
|
||||
if (onosInstances[id]) {
|
||||
updateInstance(data);
|
||||
return;
|
||||
}
|
||||
onosInstances[id] = data;
|
||||
onosOrder.push(data);
|
||||
updateInstances();
|
||||
}
|
||||
|
||||
function updateInstance(data) {
|
||||
var id = data.id,
|
||||
d = onosInstances[id];
|
||||
if (d) {
|
||||
angular.extend(d, data);
|
||||
updateInstances();
|
||||
} else {
|
||||
logicError('updateInstance: lookup fail: ID = "' + id + '"');
|
||||
}
|
||||
}
|
||||
|
||||
function computeDim(self) {
|
||||
var css = window.getComputedStyle(self);
|
||||
return {
|
||||
w: sus.stripPx(css.width),
|
||||
h: sus.stripPx(css.height)
|
||||
};
|
||||
}
|
||||
|
||||
function clickInst(d) {
|
||||
var el = d3.select(this),
|
||||
aff = el.classed('affinity');
|
||||
if (!aff) {
|
||||
setAffinity(el, d);
|
||||
} else {
|
||||
cancelAffinity();
|
||||
}
|
||||
}
|
||||
|
||||
function setAffinity(el, d) {
|
||||
d3.selectAll('.onosInst')
|
||||
.classed('mastership', true)
|
||||
.classed('affinity', false);
|
||||
el.classed('affinity', true);
|
||||
|
||||
// TODO: suppress the layers and highlight only specific nodes...
|
||||
//suppressLayers(true);
|
||||
//node.each(function (n) {
|
||||
// if (n.master === d.id) {
|
||||
// n.el.classed('suppressed', false);
|
||||
// }
|
||||
//});
|
||||
oiShowMaster = true;
|
||||
}
|
||||
|
||||
function cancelAffinity() {
|
||||
d3.selectAll('.onosInst')
|
||||
.classed('mastership affinity', false);
|
||||
|
||||
// TODO: restore layer state
|
||||
//restoreLayerState();
|
||||
oiShowMaster = false;
|
||||
}
|
||||
|
||||
function instRectAttr(dim) {
|
||||
var pad = instCfg.rectPad;
|
||||
return {
|
||||
x: pad,
|
||||
y: pad,
|
||||
width: dim.w - pad*2,
|
||||
height: dim.h - pad*2,
|
||||
rx: 6
|
||||
};
|
||||
}
|
||||
|
||||
function viewBox(dim) {
|
||||
return '0 0 ' + dim.w + ' ' + dim.h;
|
||||
}
|
||||
|
||||
function attachUiBadge(svg) {
|
||||
gs.addGlyph(svg, 'uiAttached', 30, true, [12, instCfg.uiDy])
|
||||
.classed('badgeIcon uiBadge', true);
|
||||
}
|
||||
|
||||
function instColor(id, online) {
|
||||
// TODO: fix this..
|
||||
//return cat7.get(id, !online, network.view.getTheme());
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
// ==============================
|
||||
|
||||
function updateInstances() {
|
||||
var onoses = oiBox.el().selectAll('.onosInst')
|
||||
.data(onosOrder, function (d) { return d.id; }),
|
||||
instDim = {w:0,h:0},
|
||||
c = instCfg;
|
||||
|
||||
function nSw(n) {
|
||||
return '# Switches: ' + n;
|
||||
}
|
||||
|
||||
// operate on existing onos instances if necessary
|
||||
onoses.each(function (d) {
|
||||
var el = d3.select(this),
|
||||
svg = el.select('svg');
|
||||
instDim = computeDim(this);
|
||||
|
||||
// update online state
|
||||
el.classed('online', d.online);
|
||||
|
||||
// update ui-attached state
|
||||
svg.select('use.uiBadge').remove();
|
||||
if (d.uiAttached) {
|
||||
attachUiBadge(svg);
|
||||
}
|
||||
|
||||
function updAttr(id, value) {
|
||||
svg.select('text.instLabel.'+id).text(value);
|
||||
}
|
||||
|
||||
updAttr('ip', d.ip);
|
||||
updAttr('ns', nSw(d.switches));
|
||||
});
|
||||
|
||||
|
||||
// operate on new onos instances
|
||||
var entering = onoses.enter()
|
||||
.append('div')
|
||||
.attr('class', 'onosInst')
|
||||
.classed('online', function (d) { return d.online; })
|
||||
.on('click', clickInst);
|
||||
|
||||
entering.each(function (d) {
|
||||
var el = d3.select(this),
|
||||
rectAttr,
|
||||
svg;
|
||||
instDim = computeDim(this);
|
||||
rectAttr = instRectAttr(instDim);
|
||||
|
||||
svg = el.append('svg').attr({
|
||||
width: instDim.w,
|
||||
height: instDim.h,
|
||||
viewBox: viewBox(instDim)
|
||||
});
|
||||
|
||||
svg.append('rect').attr(rectAttr);
|
||||
|
||||
gs.addGlyph(svg, 'bird', 28, true, [14, 14])
|
||||
.classed('badgeIcon', true);
|
||||
|
||||
if (d.uiAttached) {
|
||||
attachUiBadge(svg);
|
||||
}
|
||||
|
||||
var left = c.nodeOx + c.nodeDim,
|
||||
len = rectAttr.width - left,
|
||||
hlen = len / 2,
|
||||
midline = hlen + left;
|
||||
|
||||
// title
|
||||
svg.append('text')
|
||||
.attr({
|
||||
class: 'instTitle',
|
||||
x: midline,
|
||||
y: c.titleDy
|
||||
})
|
||||
.text(d.id);
|
||||
|
||||
// a couple of attributes
|
||||
var ty = c.titleDy + c.textYOff;
|
||||
|
||||
function addAttr(id, label) {
|
||||
svg.append('text').attr({
|
||||
class: 'instLabel ' + id,
|
||||
x: midline,
|
||||
y: ty
|
||||
}).text(label);
|
||||
ty += c.textYSpc;
|
||||
}
|
||||
|
||||
addAttr('ip', d.ip);
|
||||
addAttr('ns', nSw(d.switches));
|
||||
});
|
||||
|
||||
// operate on existing + new onoses here
|
||||
// set the affinity colors...
|
||||
onoses.each(function (d) {
|
||||
var el = d3.select(this),
|
||||
rect = el.select('svg').select('rect'),
|
||||
col = instColor(d.id, d.online);
|
||||
rect.style('fill', col);
|
||||
});
|
||||
|
||||
// adjust the panel size appropriately...
|
||||
oiBox.width(instDim.w * onosOrder.length);
|
||||
oiBox.height(instDim.h);
|
||||
|
||||
// remove any outgoing instances
|
||||
onoses.exit().remove();
|
||||
}
|
||||
|
||||
|
||||
// ==========================
|
||||
|
||||
function logicError(msg) {
|
||||
if (showLogicErrors) {
|
||||
$log.warn('TopoInstService: ' + msg);
|
||||
}
|
||||
}
|
||||
|
||||
function initInst() {
|
||||
oiBox = ps.createPanel(idIns, instOpts);
|
||||
oiBox.show();
|
||||
|
||||
onosInstances = {};
|
||||
onosOrder = [];
|
||||
oiShowMaster = false;
|
||||
}
|
||||
|
||||
function destroyInst() {
|
||||
ps.destroyPanel(idIns);
|
||||
oiBox = null;
|
||||
}
|
||||
|
||||
// ==========================
|
||||
|
||||
angular.module('ovTopo')
|
||||
.factory('TopoInstService',
|
||||
['$log', 'PanelService', 'SvgUtilService', 'GlyphService',
|
||||
|
||||
function (_$log_, _ps_, _sus_, _gs_) {
|
||||
$log = _$log_;
|
||||
ps = _ps_;
|
||||
sus = _sus_;
|
||||
gs = _gs_;
|
||||
|
||||
return {
|
||||
initInst: initInst,
|
||||
destroyInst: destroyInst,
|
||||
addInstance: addInstance
|
||||
};
|
||||
}]);
|
||||
}());
|
@ -28,43 +28,20 @@
|
||||
// constants
|
||||
var idSum = 'topo-p-summary',
|
||||
idDet = 'topo-p-detail',
|
||||
idIns = 'topo-p-instance',
|
||||
panelOpts = {
|
||||
width: 260
|
||||
};
|
||||
|
||||
// internal state
|
||||
var settings;
|
||||
|
||||
|
||||
// SVG elements;
|
||||
var fooPane;
|
||||
|
||||
// D3 selections;
|
||||
// panels
|
||||
var summaryPanel,
|
||||
detailPanel,
|
||||
instancePanel;
|
||||
|
||||
// default settings for force layout
|
||||
var defaultSettings = {
|
||||
foo: 2
|
||||
};
|
||||
|
||||
detailPanel;
|
||||
|
||||
// ==========================
|
||||
// *** SHOW SUMMARY ***
|
||||
|
||||
function addSep(tbody) {
|
||||
tbody.append('tr').append('td').attr('colspan', 2).append('hr');
|
||||
}
|
||||
|
||||
function addProp(tbody, label, value) {
|
||||
var tr = tbody.append('tr');
|
||||
|
||||
function addCell(cls, txt) {
|
||||
tr.append('td').attr('class', cls).text(txt);
|
||||
}
|
||||
addCell('label', label + ' :');
|
||||
addCell('value', value);
|
||||
function showSummary(data) {
|
||||
populateSummary(data);
|
||||
showSummaryPanel();
|
||||
}
|
||||
|
||||
function populateSummary(data) {
|
||||
@ -89,9 +66,36 @@
|
||||
});
|
||||
}
|
||||
|
||||
function addSep(tbody) {
|
||||
tbody.append('tr').append('td').attr('colspan', 2).append('hr');
|
||||
}
|
||||
|
||||
function addProp(tbody, label, value) {
|
||||
var tr = tbody.append('tr');
|
||||
|
||||
function addCell(cls, txt) {
|
||||
tr.append('td').attr('class', cls).text(txt);
|
||||
}
|
||||
addCell('label', label + ' :');
|
||||
addCell('value', value);
|
||||
}
|
||||
|
||||
function showSummaryPanel() {
|
||||
summaryPanel.show();
|
||||
// TODO: augment, once we have the details pane also
|
||||
}
|
||||
|
||||
// ==========================
|
||||
|
||||
function initPanels() {
|
||||
summaryPanel = ps.createPanel(idSum, panelOpts);
|
||||
detailPanel = ps.createPanel(idDet, panelOpts);
|
||||
}
|
||||
|
||||
function destroyPanels() {
|
||||
ps.destroyPanel(idSum);
|
||||
ps.destroyPanel(idDet);
|
||||
summaryPanel = detailPanel = null;
|
||||
}
|
||||
|
||||
// ==========================
|
||||
@ -105,22 +109,6 @@
|
||||
ps = _ps_;
|
||||
gs = _gs_;
|
||||
|
||||
function initPanels() {
|
||||
summaryPanel = ps.createPanel(idSum, panelOpts);
|
||||
// TODO: set up detail and instance panels..
|
||||
}
|
||||
|
||||
function destroyPanels() {
|
||||
ps.destroyPanel(idSum);
|
||||
summaryPanel = null;
|
||||
// TODO: destroy detail and instance panels..
|
||||
}
|
||||
|
||||
function showSummary(payload) {
|
||||
populateSummary(payload);
|
||||
showSummaryPanel();
|
||||
}
|
||||
|
||||
return {
|
||||
initPanels: initPanels,
|
||||
destroyPanels: destroyPanels,
|
||||
|
@ -84,6 +84,13 @@ describe('factory: fw/layer/panel.js', function () {
|
||||
expect(el.style('width')).toEqual('200px');
|
||||
});
|
||||
|
||||
it('should provide an api of panel functions', function () {
|
||||
var p = ps.createPanel('foo');
|
||||
expect(fs.areFunctions(p, [
|
||||
'show', 'hide', 'empty', 'append', 'width', 'height', 'isVisible', 'el'
|
||||
])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should complain when a duplicate ID is used', function () {
|
||||
spyOn($log, 'warn');
|
||||
var p = ps.createPanel('foo');
|
||||
|
@ -252,7 +252,7 @@ describe('factory: fw/svg/glyph.js', function() {
|
||||
|
||||
it('should add a glyph with default size', function () {
|
||||
gs.init();
|
||||
gs.addGlyph(svg, 'crown');
|
||||
var retval = gs.addGlyph(svg, 'crown');
|
||||
var what = svg.selectAll('use');
|
||||
expect(what.size()).toEqual(1);
|
||||
expect(what.attr('width')).toEqual('40');
|
||||
@ -260,6 +260,10 @@ describe('factory: fw/svg/glyph.js', function() {
|
||||
expect(what.attr('xlink:href')).toEqual('#crown');
|
||||
expect(what.classed('glyph')).toBeTruthy();
|
||||
expect(what.classed('overlay')).toBeFalsy();
|
||||
|
||||
// check a couple on retval, which should be the same thing..
|
||||
expect(retval.attr('xlink:href')).toEqual('#crown');
|
||||
expect(retval.classed('glyph')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add a glyph with given size', function () {
|
||||
|
@ -43,7 +43,13 @@ describe('factory: fw/svg/svgUtil.js', function() {
|
||||
])).toBeTruthy();
|
||||
});
|
||||
|
||||
// TODO: add unit tests for drag behavior etc.
|
||||
|
||||
// TODO: add unit tests for drag behavior
|
||||
// TODO: add unit tests for loadGlow
|
||||
// TODO: add unit tests for cat7
|
||||
|
||||
|
||||
// === translate()
|
||||
|
||||
it('should translate from two args', function () {
|
||||
expect(sus.translate(1,2)).toEqual('translate(1,2)');
|
||||
@ -53,4 +59,14 @@ describe('factory: fw/svg/svgUtil.js', function() {
|
||||
expect(sus.translate([3,4])).toEqual('translate(3,4)');
|
||||
});
|
||||
|
||||
|
||||
// === stripPx()
|
||||
|
||||
it('should not affect a number', function () {
|
||||
expect(sus.stripPx('4')).toEqual('4');
|
||||
});
|
||||
|
||||
it('should remove trailing px', function () {
|
||||
expect(sus.stripPx('4px')).toEqual('4');
|
||||
});
|
||||
});
|
||||
|
14
web/gui/src/test/_karma/ev/migrate/ev_2_onos.json
Normal file
14
web/gui/src/test/_karma/ev/migrate/ev_2_onos.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"event": "addInstance",
|
||||
"payload": {
|
||||
"id": "local",
|
||||
"ip": "127.0.0.1",
|
||||
"online": true,
|
||||
"uiAttached": true,
|
||||
"switches": 25,
|
||||
"labels": [
|
||||
"local",
|
||||
"127.0.0.1"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user