Skip to content

Instantly share code, notes, and snippets.

@syuchan1005
Last active August 30, 2023 13:47
Show Gist options
  • Save syuchan1005/75186d9f4f3835c331bf1021f35043c2 to your computer and use it in GitHub Desktop.
Save syuchan1005/75186d9f4f3835c331bf1021f35043c2 to your computer and use it in GitHub Desktop.
Script to convert Unifi device topology map to drawio

convert-to-drawio.js

Script to convert Unifi device topology map to drawio

Example

https://twitter.com/syu_chan_1005/status/1694661374608281770

How to use

  1. log in to unifi os and display the topology map
  2. open the developer tools and go to the console tab
  3. paste convert-to-drawio.js into the console and run it
  4. 🎉
(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}/grid@2x.png`
: 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/core@0.3.0/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');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment