From 015df1fe5fb104c436c5ea6f332c774f3cae6f0d Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 14 Apr 2026 04:49:25 +0000 Subject: [PATCH] fix(network): consolidate import buttons, redesign empty state, add shortcut overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import/Export button in editor header: removed standalone Import button, moved draw.io import into Export/Import dropdown with labelled sections; fixes conceptual trap where Import implied operating on the current diagram - List page: replaced two identical Upload-icon Import buttons with a single dropdown (Import JSON / Import draw.io) with format descriptions - Empty state: replaced icon-in-box with a horizontal card featuring a static SVG topology preview, MSP-specific value prop, and dual CTAs - Keyboard shortcuts: new KeyboardShortcutsOverlay component (4-group grid), triggered by ? key or the ? button pinned to the canvas bottom-right corner; wired into useCanvasShortcuts hook - Fixed Share2 → FileOutput icon for draw.io export (Share2 = send to someone, FileOutput = export file format) Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/network/DiagramHeader.tsx | 35 ++-- .../network/KeyboardShortcutsOverlay.tsx | 129 +++++++++++++ .../network/hooks/useCanvasShortcuts.ts | 7 +- .../pages/NetworkDiagrams/DiagramEditor.tsx | 14 ++ frontend/src/pages/NetworkDiagrams/index.tsx | 179 +++++++++++++++--- 5 files changed, 321 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/network/KeyboardShortcutsOverlay.tsx diff --git a/frontend/src/components/network/DiagramHeader.tsx b/frontend/src/components/network/DiagramHeader.tsx index 618fafe7..3372aa2f 100644 --- a/frontend/src/components/network/DiagramHeader.tsx +++ b/frontend/src/components/network/DiagramHeader.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useRef, useEffect } from 'react' import { useNavigate } from 'react-router-dom' -import { ChevronLeft, Save, Download, FileJson, FileCode, Image, FileText, Undo2, Redo2, MousePointer2, Hand, Share2, Upload, Cable } from 'lucide-react' +import { ChevronLeft, Save, Download, FileJson, FileCode, Image, FileText, Undo2, Redo2, MousePointer2, Hand, FileOutput, Upload, Cable } from 'lucide-react' import { cn } from '@/lib/utils' export type InteractionMode = 'select' | 'pan' | 'connect' @@ -19,7 +19,7 @@ interface DiagramHeaderProps { onExportPdf: () => void onExportJson: () => void onExportDrawio: () => void - onImportDrawio: () => void + onImportDrawio: () => void // draw.io import — triggered from Export menu onUndo: () => void onRedo: () => void canUndo: boolean @@ -216,55 +216,56 @@ export function DiagramHeader({ {isSaving ? 'Saving...' : 'Save'} - -
{showExportMenu && ( -
+
+
Export as
{diagramId && ( )} +
+
Import
+
)} diff --git a/frontend/src/components/network/KeyboardShortcutsOverlay.tsx b/frontend/src/components/network/KeyboardShortcutsOverlay.tsx new file mode 100644 index 00000000..de151d7c --- /dev/null +++ b/frontend/src/components/network/KeyboardShortcutsOverlay.tsx @@ -0,0 +1,129 @@ +import { useEffect } from 'react' +import { X } from 'lucide-react' + +interface ShortcutRow { + keys: string[] + label: string +} + +interface ShortcutGroup { + title: string + rows: ShortcutRow[] +} + +const GROUPS: ShortcutGroup[] = [ + { + title: 'Modes', + rows: [ + { keys: ['V'], label: 'Select mode' }, + { keys: ['H'], label: 'Pan mode' }, + { keys: ['C'], label: 'Connect mode' }, + { keys: ['Space'], label: 'Temporary pan (hold)' }, + ], + }, + { + title: 'Canvas', + rows: [ + { keys: ['Ctrl', 'Shift', 'F'], label: 'Fit view' }, + { keys: ['Ctrl', 'A'], label: 'Select all' }, + { keys: ['Ctrl', 'Z'], label: 'Undo' }, + { keys: ['Ctrl', 'Y'], label: 'Redo' }, + ], + }, + { + title: 'Nodes', + rows: [ + { keys: ['Ctrl', 'C'], label: 'Copy' }, + { keys: ['Ctrl', 'V'], label: 'Paste' }, + { keys: ['Ctrl', 'D'], label: 'Duplicate' }, + { keys: ['Del'], label: 'Delete selected' }, + { keys: [']'], label: 'Bring to front' }, + { keys: ['['], label: 'Send to back' }, + ], + }, + { + title: 'Nudge', + rows: [ + { keys: ['↑', '↓', '←', '→'], label: 'Move 1px' }, + { keys: ['Shift', '↑↓←→'], label: 'Move 10px' }, + ], + }, +] + +interface KeyboardShortcutsOverlayProps { + onClose: () => void +} + +function Kbd({ children }: { children: string }) { + return ( + + {children} + + ) +} + +export function KeyboardShortcutsOverlay({ onClose }: KeyboardShortcutsOverlayProps) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, [onClose]) + + return ( +
{ if (e.target === e.currentTarget) onClose() }} + > +
+ {/* Header */} +
+
+

Keyboard Shortcuts

+

Press ? anytime to open this

+
+ +
+ + {/* Shortcut grid */} +
+ {GROUPS.map((group, gi) => ( +
= 2 ? 'border-t border-default' : ''}> +
+
+ {group.title} +
+
+ {group.rows.map(row => ( +
+ {row.label} +
+ {row.keys.map((k, i) => ( + + {i > 0 && +} + {k} + + ))} +
+
+ ))} +
+
+
+ ))} +
+ + {/* Footer hint */} +
+ On Mac, Ctrl = ⌘ Cmd +
+
+
+ ) +} diff --git a/frontend/src/components/network/hooks/useCanvasShortcuts.ts b/frontend/src/components/network/hooks/useCanvasShortcuts.ts index da33bfb8..ff997473 100644 --- a/frontend/src/components/network/hooks/useCanvasShortcuts.ts +++ b/frontend/src/components/network/hooks/useCanvasShortcuts.ts @@ -37,6 +37,7 @@ export function useCanvasShortcuts({ onRedo, onNudge, onSetMode, + onToggleShortcuts, }: { nodes: Node[] edges: Edge[] @@ -48,6 +49,7 @@ export function useCanvasShortcuts({ onRedo: () => void onNudge: (dx: number, dy: number) => void onSetMode: (mode: 'select' | 'pan' | 'connect') => void + onToggleShortcuts: () => void }) { const { getNodes, fitView, screenToFlowPosition, setNodes: rfSetNodes } = useReactFlow() const clipboardRef = useRef(null) @@ -279,12 +281,15 @@ export function useCanvasShortcuts({ } else if (e.key === '[' && !ctrl) { e.preventDefault() sendSelectedToBack() + } else if (e.key === '?' && !ctrl) { + e.preventDefault() + onToggleShortcuts() } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) - }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge, onSetMode]) + }, [copyNodes, pasteNodes, duplicateNodes, selectAll, fitView, bringSelectedToFront, sendSelectedToBack, onUndo, onRedo, onNudge, onSetMode, onToggleShortcuts]) return { copyNodes, diff --git a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx index 689a9039..ed3e1661 100644 --- a/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx +++ b/frontend/src/pages/NetworkDiagrams/DiagramEditor.tsx @@ -24,6 +24,7 @@ import { DeviceToolbar } from '@/components/network/panels/DeviceToolbar' import { PropertiesPanel } from '@/components/network/panels/PropertiesPanel' import { AIAssistPanel } from '@/components/network/panels/AIAssistPanel' import { CanvasEmptyPrompt } from '@/components/network/CanvasEmptyPrompt' +import { KeyboardShortcutsOverlay } from '@/components/network/KeyboardShortcutsOverlay' import { networkDiagramsApi, deviceTypesApi } from '@/api' import { toast } from '@/lib/toast' import { exportToDrawio } from '@/lib/drawio-export' @@ -73,6 +74,7 @@ function DiagramEditorInner() { const [isDragOver, setIsDragOver] = useState(false) const [interactionMode, setInteractionMode] = useState('select') + const [showShortcuts, setShowShortcuts] = useState(false) const canvasRef = useRef(null) const drawioImportRef = useRef(null) @@ -161,6 +163,7 @@ function DiagramEditorInner() { onRedo: redo, onNudge, onSetMode: setInteractionMode, + onToggleShortcuts: () => setShowShortcuts(v => !v), }) const handleNodesChange: typeof onNodesChange = useCallback((changes) => { @@ -850,6 +853,14 @@ function DiagramEditorInner() { {nodes.length === 0 && !loading && ( )} + {/* Keyboard shortcut hint button — bottom-right corner */} +
{nodes.length > 0 && ( + {showShortcuts && ( + setShowShortcuts(false)} /> + )}
) } diff --git a/frontend/src/pages/NetworkDiagrams/index.tsx b/frontend/src/pages/NetworkDiagrams/index.tsx index 4ef87e27..643a492d 100644 --- a/frontend/src/pages/NetworkDiagrams/index.tsx +++ b/frontend/src/pages/NetworkDiagrams/index.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { useNavigate } from 'react-router-dom' -import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown } from 'lucide-react' +import { Plus, Search, Network, MoreHorizontal, Upload, ChevronDown, FileJson, FileOutput } from 'lucide-react' import { cn } from '@/lib/utils' import { networkDiagramsApi } from '@/api' import { toast } from '@/lib/toast' @@ -39,7 +39,9 @@ export default function NetworkDiagramsPage() { const [clientSearch, setClientSearch] = useState('') const [menuOpenId, setMenuOpenId] = useState(null) const [confirmArchiveId, setConfirmArchiveId] = useState(null) + const [importMenuOpen, setImportMenuOpen] = useState(false) const clientDropdownRef = useRef(null) + const importMenuRef = useRef(null) const drawioListImportRef = useRef(null) useEffect(() => { @@ -53,6 +55,17 @@ export default function NetworkDiagramsPage() { return () => document.removeEventListener('mousedown', handleClick) }, [clientDropdownOpen]) + useEffect(() => { + if (!importMenuOpen) return + const handleClick = (e: MouseEvent) => { + if (importMenuRef.current && !importMenuRef.current.contains(e.target as Node)) { + setImportMenuOpen(false) + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, [importMenuOpen]) + const loadDiagrams = useCallback(async () => { try { const params: Record = {} @@ -171,13 +184,41 @@ export default function NetworkDiagramsPage() {

Visual network topology documentation for your clients

- + {/* Single "Import" dropdown replacing two separate buttons */} +
+ + {importMenuOpen && ( +
+ + +
+ )} +
- +
+
+ {/* Left: mini topology preview */} +
+ {/* Dot grid background */} + + + + + + + + + {/* Static topology SVG */} + + {/* Edges */} + + + + + + + + {/* Firewall node */} + + + + + Firewall + {/* Switch node */} + + + + + + Core Switch + {/* Router node */} + + + + + Router + {/* Server farm */} + + + + + + Servers + {/* Leaf nodes */} + + PC × 12 + + AP × 4 + + NAS + + VM × 6 + +
+ + {/* Right: value prop + CTA */} +
+
+ + Network Maps +
+

+ Document every client's infrastructure — once +

+

+ Drag-and-drop topology diagrams that live next to your tickets. Generate a first draft from a plain-text description, then keep it up to date as networks change. +

+
    +
  • +
    + AI topology generation from natural language +
  • +
  • +
    + Export to PNG, SVG, PDF, or draw.io +
  • +
  • +
    + Shared across your whole team instantly +
  • +
+
+ + +
+
+
)}