diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index bf382d4..e614da5 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -1,8 +1,9 @@ import { useState, useCallback, useEffect, useRef } from 'react' -import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X } from 'lucide-react' +import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X, LogOut } from 'lucide-react' import { Logo } from '@/components/ui/Logo' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useCanvasStore } from '@/stores/canvasStore' +import { useAuthStore } from '@/stores/authStore' import { scanApi, settingsApi } from '@/api/client' import { toast } from 'sonner' import { useLatestRelease } from '@/hooks/useLatestRelease' @@ -43,6 +44,7 @@ interface SidebarProps { export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) { const [_collapsed, setCollapsed] = useState(false) const [_activeView, setActiveView] = useState('canvas') + const logout = useAuthStore((s) => s.logout) // When forceView is set, override local state without useEffect const collapsed = forceView ? false : _collapsed @@ -152,6 +154,14 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')} /> )} + {!STANDALONE && ( + + )} {!collapsed && } diff --git a/frontend/src/components/panels/__tests__/Sidebar.test.tsx b/frontend/src/components/panels/__tests__/Sidebar.test.tsx index a63d0ed..4853225 100644 --- a/frontend/src/components/panels/__tests__/Sidebar.test.tsx +++ b/frontend/src/components/panels/__tests__/Sidebar.test.tsx @@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { Sidebar } from '../Sidebar' import { useCanvasStore } from '@/stores/canvasStore' +import { useAuthStore } from '@/stores/authStore' import type { Node } from '@xyflow/react' import type { NodeData } from '@/types' // ── Mocks ──────────────────────────────────────────────────────────────────── vi.mock('@/stores/canvasStore') +vi.mock('@/stores/authStore') const mockBulkApprove = vi.fn() const mockBulkHide = vi.fn() @@ -60,6 +62,7 @@ const makeNode = (id: string, status: NodeData['status'], type: NodeData['type'] }) const mockToggleHideIp = vi.fn() +const mockLogout = vi.fn() function mockStore(overrides: Partial> = {}) { vi.mocked(useCanvasStore).mockReturnValue({ @@ -73,6 +76,12 @@ function mockStore(overrides: Partial> = {}) { } as ReturnType) } +function mockAuth() { + vi.mocked(useAuthStore).mockImplementation((selector: (s: { logout: () => void }) => unknown) => + selector({ logout: mockLogout }) as ReturnType + ) +} + const defaultProps = { onAddNode: vi.fn(), onAddGroupRect: vi.fn(), @@ -86,6 +95,7 @@ const defaultProps = { describe('Sidebar', () => { beforeEach(() => { mockStore() + mockAuth() vi.clearAllMocks() }) @@ -267,6 +277,19 @@ describe('Sidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Settings' })) expect(screen.queryByText('Status check interval (s)')).not.toBeInTheDocument() }) + + // ── Logout ───────────────────────────────────────────────────────────────── + + it('shows Logout button in normal mode', () => { + render() + expect(screen.getByText('Logout')).toBeInTheDocument() + }) + + it('calls logout when Logout is clicked', () => { + render() + fireEvent.click(screen.getByText('Logout')) + expect(mockLogout).toHaveBeenCalledOnce() + }) }) // ── PendingDevicesPanel — bulk select ───────────────────────────────────────── @@ -298,6 +321,7 @@ const DEVICE_B = { describe('PendingDevicesPanel — bulk select', () => { beforeEach(() => { mockStore() + mockAuth() vi.clearAllMocks() mockBulkApprove.mockResolvedValue({ data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 },