diff --git a/frontend/src/components/canvas/nodes/BaseNode.tsx b/frontend/src/components/canvas/nodes/BaseNode.tsx index 7531319..f465651 100644 --- a/frontend/src/components/canvas/nodes/BaseNode.tsx +++ b/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -6,6 +6,8 @@ import { resolveNodeColors } from '@/utils/nodeColors' import { resolveNodeIcon } from '@/utils/nodeIcons' import { useThemeStore } from '@/stores/themeStore' import { THEMES } from '@/utils/themes' +import { useCanvasStore } from '@/stores/canvasStore' +import { maskIp } from '@/utils/maskIp' interface BaseNodeProps extends NodeProps> { icon: LucideIcon @@ -13,6 +15,7 @@ interface BaseNodeProps extends NodeProps> { export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) { const activeTheme = useThemeStore((s) => s.activeTheme) + const hideIp = useCanvasStore((s) => s.hideIp) const theme = THEMES[activeTheme] const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon) @@ -70,7 +73,7 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) { style={{ color: theme.colors.nodeSubtextColor }} title={data.ip} > - {data.ip} + {hideIp ? maskIp(data.ip) : data.ip} )} diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index c2e0098..90690c3 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef } from 'react' -import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square } from 'lucide-react' +import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye } from 'lucide-react' import { Logo } from '@/components/ui/Logo' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useCanvasStore } from '@/stores/canvasStore' @@ -40,7 +40,7 @@ interface SidebarProps { export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved }: SidebarProps) { const [collapsed, setCollapsed] = useState(false) const [activeView, setActiveView] = useState('canvas') - const { nodes, hasUnsavedChanges } = useCanvasStore() + const { nodes, hasUnsavedChanges, hideIp, toggleHideIp } = useCanvasStore() const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect') const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length @@ -126,6 +126,13 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro {!STANDALONE && } + void loadCanvas: (nodes: Node[], edges: Edge[]) => void notifyScanDeviceFound: () => void + hideIp: boolean + toggleHideIp: () => void } export const useCanvasStore = create((set) => ({ @@ -43,6 +45,7 @@ export const useCanvasStore = create((set) => ({ hasUnsavedChanges: false, selectedNodeId: null, editingGroupRectId: null, + hideIp: false, scanEventTs: 0, onNodesChange: (changes) => @@ -160,6 +163,9 @@ export const useCanvasStore = create((set) => ({ notifyScanDeviceFound: () => set({ scanEventTs: Date.now() }), + hideIp: false, + toggleHideIp: () => set((s) => ({ hideIp: !s.hideIp })), + loadCanvas: (nodes, edges) => { // React Flow requires parents before children in the array const parents = nodes.filter((n) => !n.parentId) diff --git a/frontend/src/utils/__tests__/maskIp.test.ts b/frontend/src/utils/__tests__/maskIp.test.ts new file mode 100644 index 0000000..1f18cb5 --- /dev/null +++ b/frontend/src/utils/__tests__/maskIp.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest' +import { maskIp } from '../maskIp' + +describe('maskIp', () => { + it('masks last two octets of a standard IPv4', () => { + expect(maskIp('192.168.1.115')).toBe('192.168.XX.XX') + }) + + it('masks any IPv4', () => { + expect(maskIp('10.0.0.1')).toBe('10.0.XX.XX') + expect(maskIp('172.16.254.1')).toBe('172.16.XX.XX') + }) + + it('passes through non-IPv4 strings unchanged', () => { + expect(maskIp('hostname')).toBe('hostname') + expect(maskIp('fe80::1')).toBe('fe80::1') + expect(maskIp('')).toBe('') + }) +}) diff --git a/frontend/src/utils/maskIp.ts b/frontend/src/utils/maskIp.ts new file mode 100644 index 0000000..7c0e8c9 --- /dev/null +++ b/frontend/src/utils/maskIp.ts @@ -0,0 +1,10 @@ +/** + * Mask the last two octets of an IPv4 address. + * e.g. "192.168.1.115" → "192.168.XX.XX" + * Non-IPv4 strings are returned unchanged. + */ +export function maskIp(ip: string): string { + const parts = ip.split('.') + if (parts.length === 4) return `${parts[0]}.${parts[1]}.XX.XX` + return ip +}