feat: add hide IP toggle in sidebar
- Add hideIp / toggleHideIp to canvasStore (UI-only, not persisted) - Add maskIp util: 192.168.1.115 → 192.168.XX.XX (non-IPv4 passthrough) - BaseNode reads hideIp from store and masks last two octets when active - Sidebar shows Eye/EyeOff toggle below Scan Network, highlights when active
This commit is contained in:
@@ -6,6 +6,8 @@ import { resolveNodeColors } from '@/utils/nodeColors'
|
|||||||
import { resolveNodeIcon } from '@/utils/nodeIcons'
|
import { resolveNodeIcon } from '@/utils/nodeIcons'
|
||||||
import { useThemeStore } from '@/stores/themeStore'
|
import { useThemeStore } from '@/stores/themeStore'
|
||||||
import { THEMES } from '@/utils/themes'
|
import { THEMES } from '@/utils/themes'
|
||||||
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
|
import { maskIp } from '@/utils/maskIp'
|
||||||
|
|
||||||
interface BaseNodeProps extends NodeProps<Node<NodeData>> {
|
interface BaseNodeProps extends NodeProps<Node<NodeData>> {
|
||||||
icon: LucideIcon
|
icon: LucideIcon
|
||||||
@@ -13,6 +15,7 @@ interface BaseNodeProps extends NodeProps<Node<NodeData>> {
|
|||||||
|
|
||||||
export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||||
const activeTheme = useThemeStore((s) => s.activeTheme)
|
const activeTheme = useThemeStore((s) => s.activeTheme)
|
||||||
|
const hideIp = useCanvasStore((s) => s.hideIp)
|
||||||
const theme = THEMES[activeTheme]
|
const theme = THEMES[activeTheme]
|
||||||
|
|
||||||
const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon)
|
const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon)
|
||||||
@@ -70,7 +73,7 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
|||||||
style={{ color: theme.colors.nodeSubtextColor }}
|
style={{ color: theme.colors.nodeSubtextColor }}
|
||||||
title={data.ip}
|
title={data.ip}
|
||||||
>
|
>
|
||||||
{data.ip}
|
{hideIp ? maskIp(data.ip) : data.ip}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
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 { Logo } from '@/components/ui/Logo'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
@@ -40,7 +40,7 @@ interface SidebarProps {
|
|||||||
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved }: SidebarProps) {
|
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved }: SidebarProps) {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const [activeView, setActiveView] = useState<SidebarView>('canvas')
|
const [activeView, setActiveView] = useState<SidebarView>('canvas')
|
||||||
const { nodes, hasUnsavedChanges } = useCanvasStore()
|
const { nodes, hasUnsavedChanges, hideIp, toggleHideIp } = useCanvasStore()
|
||||||
|
|
||||||
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect')
|
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect')
|
||||||
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
||||||
@@ -126,6 +126,13 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro
|
|||||||
<SidebarItem icon={Plus} label="Add Node" collapsed={collapsed} onClick={onAddNode} />
|
<SidebarItem icon={Plus} label="Add Node" collapsed={collapsed} onClick={onAddNode} />
|
||||||
<SidebarItem icon={Square} label="Add Rectangle" collapsed={collapsed} onClick={onAddGroupRect} />
|
<SidebarItem icon={Square} label="Add Rectangle" collapsed={collapsed} onClick={onAddGroupRect} />
|
||||||
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
||||||
|
<SidebarItem
|
||||||
|
icon={hideIp ? EyeOff : Eye}
|
||||||
|
label={hideIp ? 'Show IPs' : 'Hide IPs'}
|
||||||
|
collapsed={collapsed}
|
||||||
|
onClick={toggleHideIp}
|
||||||
|
active={hideIp}
|
||||||
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={Save}
|
icon={Save}
|
||||||
label="Save Canvas"
|
label="Save Canvas"
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ interface CanvasState {
|
|||||||
markUnsaved: () => void
|
markUnsaved: () => void
|
||||||
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
loadCanvas: (nodes: Node<NodeData>[], edges: Edge<EdgeData>[]) => void
|
||||||
notifyScanDeviceFound: () => void
|
notifyScanDeviceFound: () => void
|
||||||
|
hideIp: boolean
|
||||||
|
toggleHideIp: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCanvasStore = create<CanvasState>((set) => ({
|
export const useCanvasStore = create<CanvasState>((set) => ({
|
||||||
@@ -43,6 +45,7 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
selectedNodeId: null,
|
selectedNodeId: null,
|
||||||
editingGroupRectId: null,
|
editingGroupRectId: null,
|
||||||
|
hideIp: false,
|
||||||
scanEventTs: 0,
|
scanEventTs: 0,
|
||||||
|
|
||||||
onNodesChange: (changes) =>
|
onNodesChange: (changes) =>
|
||||||
@@ -160,6 +163,9 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
|
|
||||||
notifyScanDeviceFound: () => set({ scanEventTs: Date.now() }),
|
notifyScanDeviceFound: () => set({ scanEventTs: Date.now() }),
|
||||||
|
|
||||||
|
hideIp: false,
|
||||||
|
toggleHideIp: () => set((s) => ({ hideIp: !s.hideIp })),
|
||||||
|
|
||||||
loadCanvas: (nodes, edges) => {
|
loadCanvas: (nodes, edges) => {
|
||||||
// React Flow requires parents before children in the array
|
// React Flow requires parents before children in the array
|
||||||
const parents = nodes.filter((n) => !n.parentId)
|
const parents = nodes.filter((n) => !n.parentId)
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user