feat(settings): move Hide IP toggle into Settings modal, persist it
Hide-IP was a sidebar button held only in memory, so it reset on reload. Moved it into the Settings modal Canvas section and persist it to localStorage (new ipDisplay util); the canvas store now seeds hideIp from storage and writes through on toggleHideIp/setHideIp. Settings is now also reachable in standalone (no-backend) builds, with the backend-only status interval guarded so the modal still works there. ha-relevant: yes
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { settingsApi } from '@/api/client'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
type AlignmentSettings,
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
subscribeAlignmentSettings,
|
||||
} from '@/utils/alignmentSettings'
|
||||
|
||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
@@ -19,9 +22,11 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
const [interval, setIntervalValue] = useState(60)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [alignment, setAlignment] = useState<AlignmentSettings>(readAlignmentSettings)
|
||||
const hideIp = useCanvasStore((s) => s.hideIp)
|
||||
const setHideIp = useCanvasStore((s) => s.setHideIp)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
if (!open || STANDALONE) return
|
||||
settingsApi.get()
|
||||
.then((res) => setIntervalValue(res.data.interval_seconds))
|
||||
.catch(() => {/* use default */})
|
||||
@@ -36,6 +41,12 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
// Canvas prefs (alignment, hide-IP) persist on change; only the backend
|
||||
// status-check interval needs an API round-trip.
|
||||
if (STANDALONE) {
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
await settingsApi.save({ interval_seconds: interval })
|
||||
@@ -57,6 +68,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
|
||||
<div className="space-y-5 py-2">
|
||||
{/* Status checker */}
|
||||
{!STANDALONE && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">Status check interval (s)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -74,6 +86,7 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
How often node health is polled (ping, HTTP, SSH…)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="pt-3 border-t border-border space-y-3">
|
||||
@@ -90,6 +103,17 @@ export function SettingsModal({ open, onClose }: SettingsModalProps) {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-2 cursor-pointer">
|
||||
<span className="text-xs text-foreground">Hide IP addresses</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hideIp}
|
||||
onChange={(e) => setHideIp(e.target.checked)}
|
||||
className="cursor-pointer accent-[#00d4ff]"
|
||||
aria-label="Toggle IP address masking"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className={alignment.enabled ? 'space-y-1.5' : 'space-y-1.5 opacity-50 pointer-events-none'}>
|
||||
<label className="text-xs text-muted-foreground">Snap distance</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock('@/api/client', () => ({
|
||||
|
||||
import { settingsApi } from '@/api/client'
|
||||
import { toast } from 'sonner'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
|
||||
describe('SettingsModal', () => {
|
||||
beforeEach(() => {
|
||||
@@ -64,6 +65,17 @@ describe('SettingsModal', () => {
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reflects and persists the hide-IP preference', async () => {
|
||||
useCanvasStore.setState({ hideIp: false })
|
||||
localStorage.removeItem('homelable.hideIp')
|
||||
render(<SettingsModal open onClose={vi.fn()} />)
|
||||
const checkbox = screen.getByLabelText('Toggle IP address masking') as HTMLInputElement
|
||||
expect(checkbox.checked).toBe(false)
|
||||
fireEvent.click(checkbox)
|
||||
expect(useCanvasStore.getState().hideIp).toBe(true)
|
||||
expect(localStorage.getItem('homelable.hideIp')).toBe('true')
|
||||
})
|
||||
|
||||
it('calls onClose on Cancel', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(<SettingsModal open onClose={onClose} />)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, LogOut, Network, Type, PlusCircle, Pencil, Trash2 } from 'lucide-react'
|
||||
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, RefreshCw, Loader2, Square, Settings, StopCircle, LogOut, Network, Type, PlusCircle, Pencil, Trash2 } from 'lucide-react'
|
||||
import { Logo } from '@/components/ui/Logo'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
@@ -90,7 +90,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
||||
}
|
||||
}
|
||||
|
||||
const { nodes, hasUnsavedChanges, hideIp, toggleHideIp } = useCanvasStore()
|
||||
const { nodes, hasUnsavedChanges } = useCanvasStore()
|
||||
|
||||
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect' && n.data.type !== 'text')
|
||||
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
||||
@@ -253,13 +253,6 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
||||
<SidebarItem icon={Type} label="Add Text" collapsed={collapsed} onClick={onAddText} />
|
||||
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
||||
{!STANDALONE && <SidebarItem icon={Network} label="Zigbee Import" collapsed={collapsed} onClick={onZigbeeImport} />}
|
||||
<SidebarItem
|
||||
icon={hideIp ? EyeOff : Eye}
|
||||
label={hideIp ? 'Show IPs' : 'Hide IPs'}
|
||||
collapsed={collapsed}
|
||||
onClick={toggleHideIp}
|
||||
active={hideIp}
|
||||
/>
|
||||
<SidebarItem
|
||||
icon={Save}
|
||||
label="Save Canvas"
|
||||
@@ -268,14 +261,12 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
||||
badge={hasUnsavedChanges}
|
||||
accent
|
||||
/>
|
||||
{!STANDALONE && (
|
||||
<SidebarItem
|
||||
icon={Settings}
|
||||
label="Settings"
|
||||
collapsed={collapsed}
|
||||
onClick={onOpenSettings}
|
||||
/>
|
||||
)}
|
||||
<SidebarItem
|
||||
icon={Settings}
|
||||
label="Settings"
|
||||
collapsed={collapsed}
|
||||
onClick={onOpenSettings}
|
||||
/>
|
||||
{!STANDALONE && (
|
||||
<SidebarItem
|
||||
icon={LogOut}
|
||||
|
||||
@@ -46,15 +46,12 @@ const makeNode = (id: string, status: NodeData['status'], type: NodeData['type']
|
||||
data: { label: id, type, status, services: [] },
|
||||
})
|
||||
|
||||
const mockToggleHideIp = vi.fn()
|
||||
const mockLogout = vi.fn()
|
||||
|
||||
function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
||||
vi.mocked(useCanvasStore).mockReturnValue({
|
||||
nodes: [],
|
||||
hasUnsavedChanges: false,
|
||||
hideIp: false,
|
||||
toggleHideIp: mockToggleHideIp,
|
||||
addNode: vi.fn(),
|
||||
scanEventTs: 0,
|
||||
...overrides,
|
||||
@@ -191,16 +188,10 @@ describe('Sidebar', () => {
|
||||
expect(defaultProps.onSave).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('calls toggleHideIp when Hide IPs is clicked', () => {
|
||||
it('calls onOpenSettings when Settings is clicked', () => {
|
||||
render(<Sidebar {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('Hide IPs'))
|
||||
expect(mockToggleHideIp).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows Show IPs label when hideIp is true', () => {
|
||||
mockStore({ hideIp: true })
|
||||
render(<Sidebar {...defaultProps} />)
|
||||
expect(screen.getByText('Show IPs')).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByText('Settings'))
|
||||
expect(defaultProps.onOpenSettings).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
// ── Unsaved changes badge ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user