= {
+ 'router': 'shape=mxgraph.cisco.routers.router;',
+ 'switch': 'shape=mxgraph.cisco.switches.layer_3_switch;',
+ 'access-point': 'shape=mxgraph.cisco.misc.access_point;',
+ 'load-balancer': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'firewall': 'shape=mxgraph.cisco.firewalls.firewall;',
+ 'badge-reader': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'server': 'shape=mxgraph.cisco.servers.standard_server;',
+ 'vm': 'shape=mxgraph.cisco.servers.standard_server;',
+ 'container': 'shape=mxgraph.cisco.servers.standard_server;',
+ 'nas': 'shape=mxgraph.cisco.storage.tape_storage_library;',
+ 'san': 'shape=mxgraph.cisco.storage.tape_storage_library;',
+ 'cloud-storage': 'shape=mxgraph.cisco.misc.cloud;',
+ 'cloud': 'shape=mxgraph.cisco.misc.cloud;',
+ 'aws': 'shape=mxgraph.cisco.misc.cloud;',
+ 'azure': 'shape=mxgraph.cisco.misc.cloud;',
+ 'gcp': 'shape=mxgraph.cisco.misc.cloud;',
+ 'isp': 'shape=mxgraph.cisco.misc.cloud;',
+ 'workstation': 'shape=mxgraph.cisco.computers_and_peripherals.pc;',
+ 'laptop': 'shape=mxgraph.cisco.computers_and_peripherals.laptop;',
+ 'tablet': 'shape=mxgraph.cisco.computers_and_peripherals.laptop;',
+ 'phone': 'shape=mxgraph.cisco.computers_and_peripherals.ip_phone;',
+ 'printer': 'shape=mxgraph.cisco.computers_and_peripherals.printer;',
+ 'ups': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'pdu': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'rack': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'patch-panel': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'camera': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'nvr': 'shape=mxgraph.cisco.misc.generic_building;',
+ 'iot': 'shape=mxgraph.cisco.misc.generic_building;',
+}
+
+const BASE_NODE_STYLE =
+ 'sketch=0;html=1;pointerEvents=1;dashed=0;fillColor=#036897;strokeColor=#ffffff;strokeWidth=2;verticalLabelPosition=bottom;verticalAlign=top;align=center;outlineConnect=0;'
+const GROUP_STYLE =
+ 'swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;collapsible=0;marginBottom=0;swimlaneHead=0;fillColor=none;'
+
+function esc(s: string): string {
+ return s
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+}
+
+export function exportToDrawio(nodes: Node[], edges: Edge[]): string {
+ const cells: string[] = [
+ '',
+ '',
+ ]
+
+ for (const node of nodes) {
+ const w = typeof node.style?.width === 'number' ? node.style.width : (node.measured?.width ?? 120)
+ const h = typeof node.style?.height === 'number' ? node.style.height : (node.measured?.height ?? 120)
+ const x = node.position.x
+ const y = node.position.y
+ const parentId = node.parentId ?? '1'
+
+ if (node.type === 'group') {
+ const gd = node.data as GroupNodeData
+ cells.push(
+ `` +
+ `` +
+ ``,
+ )
+ } else {
+ const dd = node.data as DeviceNodeData
+ const slug = dd.deviceType ?? 'server'
+ const shapeStyle = SLUG_TO_DRAWIO_STYLE[slug] ?? 'rounded=1;whiteSpace=wrap;html=1;'
+ cells.push(
+ `` +
+ `` +
+ ``,
+ )
+ }
+ }
+
+ for (const edge of edges) {
+ const label = typeof edge.label === 'string' ? edge.label : ''
+ cells.push(
+ `` +
+ `` +
+ ``,
+ )
+ }
+
+ const xml =
+ `\n` +
+ `\n` +
+ cells.join('\n') +
+ `\n`
+
+ return xml
+}
diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
index 0d000f98..abe302f8 100644
--- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
+++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx
@@ -25,6 +25,7 @@ import { AIAssistPanel } from '@/components/network/panels/AIAssistPanel'
import { CanvasEmptyPrompt } from '@/components/network/CanvasEmptyPrompt'
import { networkDiagramsApi, deviceTypesApi } from '@/api'
import { toast } from '@/lib/toast'
+import { exportToDrawio } from '@/lib/drawio-export'
import type { DeviceTypeResponse, DeviceProperties, AIGenerateResponse, DiagramEdge, DiagramNode } from '@/types'
import type { DeviceNodeData } from '@/components/network/nodes/DeviceNode'
@@ -729,6 +730,21 @@ function DiagramEditorInner() {
}
}, [diagramId, name])
+ const handleExportDrawio = useCallback(() => {
+ if (nodes.length === 0) {
+ toast.warning('Add some devices to the diagram before exporting')
+ return
+ }
+ const xml = exportToDrawio(getNodes(), edges)
+ const blob = new Blob([xml], { type: 'application/xml' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `${name.replace(/[^a-zA-Z0-9-_ ]/g, '') || 'diagram'}.drawio`
+ a.click()
+ URL.revokeObjectURL(url)
+ }, [nodes, edges, getNodes, name])
+
if (loading) {
return (
@@ -752,6 +768,7 @@ function DiagramEditorInner() {
onExportSvg={handleExportSvg}
onExportPdf={handleExportPdf}
onExportJson={handleExportJson}
+ onExportDrawio={handleExportDrawio}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}