|
(async () => { |
|
console.log('Start'); |
|
const consoleId = (location.href.match(/consoles\/([^:]+)/) || [])[1].replace(/0+/g, '0'); |
|
|
|
const clients = await fetch(`https://${consoleId}.id.ui.direct/proxy/network/v2/api/site/default/clients/active?includeTrafficUsage=true`, { credentials: 'include' }).then((res) => res.json()); |
|
const topology = await fetch(`https://${consoleId}.id.ui.direct/proxy/network/v2/api/site/default/topology`, { credentials: 'include' }).then((res) => res.json()); |
|
const udmDevice = await fetch(`https://${consoleId}.id.ui.direct/proxy/network/api/s/default/stat/device`, { credentials: 'include' }).then((res) => res.json()); |
|
console.log('data fetched'); |
|
|
|
const macToClientDeviceImageUrl = clients.reduce((acc, client) => ({ |
|
...acc, |
|
[client.mac]: ( |
|
(client.fingerprint.computed_engine !== undefined && client.fingerprint.computed_dev_id !== undefined) |
|
? `https://static.ui.com/fingerprint/${client.fingerprint.computed_engine}/${client.fingerprint.computed_dev_id}_257x257.png` |
|
: undefined |
|
), |
|
}), {}); |
|
const macToUnifiDeviceImageUrl = udmDevice.data.reduce((acc, device) => ({ |
|
...acc, |
|
[device.mac]: ( |
|
(device.type !== undefined && device.model !== undefined) |
|
? `https://net-fe-static-assets.network-controller.svc.ui.com/release${window.unifiConstant.BASE_HREF}react/images/device/${device.type}/${device.model}/[email protected]` |
|
: undefined |
|
), |
|
}), {}); |
|
const macToImageUrl = { ...macToClientDeviceImageUrl, ...macToUnifiDeviceImageUrl }; |
|
|
|
const data = { |
|
edges: topology.edges.map(({ uplinkMac, downlinkMac, type, essid }) => ({ uplinkMac, downlinkMac, type, essid })), |
|
vertices: topology.vertices.map(({ mac, name }) => ({ mac, name })), |
|
}; |
|
|
|
const imageCache = {}; |
|
const getImageAsUrl = async (mac) => { |
|
if (imageCache[mac]) return imageCache[mac]; |
|
|
|
const imageUrl = macToImageUrl[mac]; |
|
if (!imageUrl) return null; |
|
const blob = await fetch(imageUrl).then((res) => res.blob()).catch(() => null); |
|
if (!blob || blob.size < 1 || !blob.type.startsWith('image/')) return imageUrl; |
|
|
|
const imageDataUrl = await new Promise((resolve) => { |
|
const reader = new FileReader(); |
|
reader.onloadend = () => { |
|
resolve(reader.result); |
|
}; |
|
reader.readAsDataURL(blob); |
|
}); |
|
imageCache[mac] = imageDataUrl; |
|
return imageDataUrl; |
|
}; |
|
|
|
await Promise.allSettled(data.vertices.map(({ mac }) => getImageAsUrl(mac))); |
|
console.log('images fetched'); |
|
|
|
// create graph |
|
const { |
|
Graph, |
|
HierarchicalLayout, |
|
Codec, |
|
constants, |
|
xmlUtils, |
|
} = await import('https://www.unpkg.com/@maxgraph/[email protected]/dist/esm/index.js'); |
|
console.log('library loaded'); |
|
|
|
const graph = new Graph(); |
|
const graphParent = graph.getDefaultParent(); |
|
const layout = new HierarchicalLayout(graph, { direction: constants.DIRECTION.WEST }); |
|
|
|
graph.batchUpdate(() => { |
|
const vertexMap = data.vertices.reduce((acc, { mac, name }) => { |
|
const image = imageCache[mac]; |
|
const vertex = graph.insertVertex({ |
|
parent: graphParent, |
|
position: [0, 0], |
|
size: [100, 100], |
|
value: name, |
|
style: { |
|
verticalLabelPosition: 'bottom', |
|
verticalAlign: 'top', |
|
shape: image ? constants.SHAPE.IMAGE : constants.SHAPE.RECTANGLE, |
|
image, |
|
}, |
|
}); |
|
acc[mac] = vertex; |
|
return acc; |
|
}, {}); |
|
data.edges.forEach(({ uplinkMac, downlinkMac, type, essid }) => { |
|
const source = vertexMap[uplinkMac]; |
|
const target = vertexMap[downlinkMac]; |
|
graph.insertEdge({ |
|
parent: graphParent, |
|
source, |
|
target, |
|
value: `${type}${essid ? `: ${essid}` : ''}`, |
|
}); |
|
}); |
|
}); |
|
|
|
layout.execute(graphParent); |
|
console.log('graph created'); |
|
|
|
const node = (new Codec()).encode(graph.getDataModel()); |
|
const xmlDocument = (new DOMParser()).parseFromString(`<mxfile><diagram>${xmlUtils.getXml(node)}</diagram></mxfile>`, 'application/xml'); |
|
xmlDocument.querySelectorAll('Cell > Object[as=style]').forEach((object) => { |
|
if (object.getAttributeNames().length > 1) { |
|
let styleText = ''; |
|
object.getAttributeNames().filter((name) => name !== 'as').forEach((name) => { |
|
styleText += `${name}=${object.getAttribute(name)};`; |
|
}); |
|
|
|
if (object.parentElement) { |
|
object.parentElement.setAttribute('style', styleText); |
|
} |
|
} |
|
object.remove(); |
|
}); |
|
const xmlDocumentString = (new XMLSerializer()).serializeToString(xmlDocument); |
|
|
|
const drawioString = xmlDocumentString |
|
.replace(/GraphDataModel>/g, 'mxGraphModel>') |
|
.replace(/<(\/?)([A-Z])/g, '<$1mx$2') |
|
.replace(/_(x|y|width|height)/g, '$1') |
|
.replace(/;base64/g, ''); |
|
console.log('drawio string created'); |
|
|
|
|
|
const drawioBlob = new Blob([drawioString], { type: 'application/octet-stream' }); |
|
const drawioUrl = URL.createObjectURL(drawioBlob); |
|
const link = document.createElement('a'); |
|
link.href = drawioUrl; |
|
link.download = 'topology.drawio'; |
|
link.click(); |
|
setTimeout(() => { |
|
URL.revokeObjectURL(drawioUrl); |
|
}, 1000); |
|
|
|
console.log('Done'); |
|
})(); |