feat: add logout button to sidebar
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
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, 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 { 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'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { scanApi, settingsApi } from '@/api/client'
|
import { scanApi, settingsApi } from '@/api/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useLatestRelease } from '@/hooks/useLatestRelease'
|
import { useLatestRelease } from '@/hooks/useLatestRelease'
|
||||||
@@ -43,6 +44,7 @@ interface SidebarProps {
|
|||||||
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) {
|
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) {
|
||||||
const [_collapsed, setCollapsed] = useState(false)
|
const [_collapsed, setCollapsed] = useState(false)
|
||||||
const [_activeView, setActiveView] = useState<SidebarView>('canvas')
|
const [_activeView, setActiveView] = useState<SidebarView>('canvas')
|
||||||
|
const logout = useAuthStore((s) => s.logout)
|
||||||
|
|
||||||
// When forceView is set, override local state without useEffect
|
// When forceView is set, override local state without useEffect
|
||||||
const collapsed = forceView ? false : _collapsed
|
const collapsed = forceView ? false : _collapsed
|
||||||
@@ -152,6 +154,14 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro
|
|||||||
onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')}
|
onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!STANDALONE && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={LogOut}
|
||||||
|
label="Logout"
|
||||||
|
collapsed={collapsed}
|
||||||
|
onClick={logout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && <VersionBadge />}
|
{!collapsed && <VersionBadge />}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import { Sidebar } from '../Sidebar'
|
import { Sidebar } from '../Sidebar'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import type { Node } from '@xyflow/react'
|
import type { Node } from '@xyflow/react'
|
||||||
import type { NodeData } from '@/types'
|
import type { NodeData } from '@/types'
|
||||||
|
|
||||||
// ── Mocks ────────────────────────────────────────────────────────────────────
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
vi.mock('@/stores/canvasStore')
|
vi.mock('@/stores/canvasStore')
|
||||||
|
vi.mock('@/stores/authStore')
|
||||||
|
|
||||||
const mockBulkApprove = vi.fn()
|
const mockBulkApprove = vi.fn()
|
||||||
const mockBulkHide = 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 mockToggleHideIp = vi.fn()
|
||||||
|
const mockLogout = vi.fn()
|
||||||
|
|
||||||
function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
||||||
vi.mocked(useCanvasStore).mockReturnValue({
|
vi.mocked(useCanvasStore).mockReturnValue({
|
||||||
@@ -73,6 +76,12 @@ function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
|||||||
} as 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 = {
|
const defaultProps = {
|
||||||
onAddNode: vi.fn(),
|
onAddNode: vi.fn(),
|
||||||
onAddGroupRect: vi.fn(),
|
onAddGroupRect: vi.fn(),
|
||||||
@@ -86,6 +95,7 @@ const defaultProps = {
|
|||||||
describe('Sidebar', () => {
|
describe('Sidebar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockStore()
|
mockStore()
|
||||||
|
mockAuth()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,6 +277,19 @@ describe('Sidebar', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
||||||
expect(screen.queryByText('Status check interval (s)')).not.toBeInTheDocument()
|
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 ─────────────────────────────────────────
|
// ── PendingDevicesPanel — bulk select ─────────────────────────────────────────
|
||||||
@@ -298,6 +321,7 @@ const DEVICE_B = {
|
|||||||
describe('PendingDevicesPanel — bulk select', () => {
|
describe('PendingDevicesPanel — bulk select', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockStore()
|
mockStore()
|
||||||
|
mockAuth()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockBulkApprove.mockResolvedValue({
|
mockBulkApprove.mockResolvedValue({
|
||||||
data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 },
|
data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 },
|
||||||
|
|||||||
Reference in New Issue
Block a user