feat(network): draw.io XML import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import { CanvasEmptyPrompt } from '@/components/network/CanvasEmptyPrompt'
|
||||
import { networkDiagramsApi, deviceTypesApi } from '@/api'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { exportToDrawio } from '@/lib/drawio-export'
|
||||
import { parseDrawioXml } from '@/lib/drawio-import'
|
||||
import type { DeviceTypeResponse, DeviceProperties, AIGenerateResponse, DiagramEdge, DiagramNode } from '@/types'
|
||||
import type { DeviceNodeData } from '@/components/network/nodes/DeviceNode'
|
||||
|
||||
@@ -73,6 +74,7 @@ function DiagramEditorInner() {
|
||||
const [interactionMode, setInteractionMode] = useState<InteractionMode>('select')
|
||||
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null)
|
||||
const drawioImportRef = useRef<HTMLInputElement>(null)
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
|
||||
const [pendingDeleteNodeId, setPendingDeleteNodeId] = useState<string | null>(null)
|
||||
|
||||
@@ -745,6 +747,39 @@ function DiagramEditorInner() {
|
||||
URL.revokeObjectURL(url)
|
||||
}, [nodes, edges, getNodes, name])
|
||||
|
||||
const handleImportDrawio = useCallback(() => {
|
||||
drawioImportRef.current?.click()
|
||||
}, [])
|
||||
|
||||
const handleDrawioFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
e.target.value = ''
|
||||
try {
|
||||
const text = await file.text()
|
||||
const { nodes: importedNodes, edges: importedEdges, warnings } = parseDrawioXml(text)
|
||||
const importPayload = {
|
||||
schemaVersion: 1 as const,
|
||||
name: file.name.replace(/\.drawio$/i, '') || 'Imported Diagram',
|
||||
client_name: null,
|
||||
description: null,
|
||||
nodes: importedNodes,
|
||||
edges: importedEdges,
|
||||
}
|
||||
const result = await networkDiagramsApi.importJson(importPayload)
|
||||
const allWarnings = [...warnings, ...result.warnings]
|
||||
if (allWarnings.length > 0) {
|
||||
toast.warning(`Imported with ${allWarnings.length} warning(s): ${allWarnings[0]}`)
|
||||
} else {
|
||||
toast.success('draw.io file imported successfully')
|
||||
}
|
||||
navigate(`/network-diagrams/${result.diagram.id}`)
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error'
|
||||
toast.error(`Import failed: ${msg}`)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -769,6 +804,7 @@ function DiagramEditorInner() {
|
||||
onExportPdf={handleExportPdf}
|
||||
onExportJson={handleExportJson}
|
||||
onExportDrawio={handleExportDrawio}
|
||||
onImportDrawio={handleImportDrawio}
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
canUndo={canUndo}
|
||||
@@ -896,6 +932,13 @@ function DiagramEditorInner() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={drawioImportRef}
|
||||
type="file"
|
||||
accept=".drawio,.xml"
|
||||
className="hidden"
|
||||
onChange={handleDrawioFileChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export default function NetworkDiagramsPage() {
|
||||
const [menuOpenId, setMenuOpenId] = useState<string | null>(null)
|
||||
const [confirmArchiveId, setConfirmArchiveId] = useState<string | null>(null)
|
||||
const clientDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const drawioListImportRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientDropdownOpen) return
|
||||
@@ -129,6 +130,35 @@ export default function NetworkDiagramsPage() {
|
||||
input.click()
|
||||
}, [navigate])
|
||||
|
||||
const handleListDrawioImport = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
e.target.value = ''
|
||||
try {
|
||||
const { parseDrawioXml } = await import('@/lib/drawio-import')
|
||||
const text = await file.text()
|
||||
const { nodes: importedNodes, edges: importedEdges, warnings } = parseDrawioXml(text)
|
||||
const result = await networkDiagramsApi.importJson({
|
||||
schemaVersion: 1,
|
||||
name: file.name.replace(/\.drawio$/i, '') || 'Imported Diagram',
|
||||
client_name: null,
|
||||
description: null,
|
||||
nodes: importedNodes,
|
||||
edges: importedEdges,
|
||||
})
|
||||
const allWarnings = [...warnings, ...result.warnings]
|
||||
if (allWarnings.length > 0) {
|
||||
toast.warning(`Imported with ${allWarnings.length} warning(s)`)
|
||||
} else {
|
||||
toast.success('Imported successfully')
|
||||
}
|
||||
navigate(`/network-diagrams/${result.diagram.id}`)
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error'
|
||||
toast.error(`Import failed: ${msg}`)
|
||||
}
|
||||
}, [navigate])
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
@@ -148,6 +178,20 @@ export default function NetworkDiagramsPage() {
|
||||
<Upload size={14} />
|
||||
Import
|
||||
</button>
|
||||
<input
|
||||
ref={drawioListImportRef}
|
||||
type="file"
|
||||
accept=".drawio,.xml"
|
||||
className="hidden"
|
||||
onChange={handleListDrawioImport}
|
||||
/>
|
||||
<button
|
||||
onClick={() => drawioListImportRef.current?.click()}
|
||||
className="flex items-center gap-1.5 rounded border border-default px-3 py-2 text-sm text-primary hover:border-hover"
|
||||
>
|
||||
<Upload size={14} />
|
||||
Import draw.io
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/network-diagrams/new')}
|
||||
className="flex items-center gap-1.5 rounded bg-accent px-4 py-2 text-sm font-medium text-white hover:bg-accent/90"
|
||||
|
||||
Reference in New Issue
Block a user