feat: add logout button to sidebar
This commit is contained in:
@@ -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<SidebarView>('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 && (
|
||||
<SidebarItem
|
||||
icon={LogOut}
|
||||
label="Logout"
|
||||
collapsed={collapsed}
|
||||
onClick={logout}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!collapsed && <VersionBadge />}
|
||||
|
||||
@@ -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<ReturnType<typeof useCanvasStore>> = {}) {
|
||||
vi.mocked(useCanvasStore).mockReturnValue({
|
||||
@@ -73,6 +76,12 @@ function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
||||
} as ReturnType<typeof useCanvasStore>)
|
||||
}
|
||||
|
||||
function mockAuth() {
|
||||
vi.mocked(useAuthStore).mockImplementation((selector: (s: { logout: () => void }) => unknown) =>
|
||||
selector({ logout: mockLogout }) as ReturnType<typeof useAuthStore>
|
||||
)
|
||||
}
|
||||
|
||||
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(<Sidebar {...defaultProps} />)
|
||||
expect(screen.getByText('Logout')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls logout when Logout is clicked', () => {
|
||||
render(<Sidebar {...defaultProps} />)
|
||||
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 },
|
||||
|
||||
Reference in New Issue
Block a user