chore: remove deprecated Header component (replaced by ContextStrip)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,226 +0,0 @@
|
||||
// DEPRECATED: Replaced by ContextStrip.tsx — keeping for reference during transition
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, Link } from '@tanstack/react-router'
|
||||
import { LogOut, ChevronDown, User, Search, Sun, Moon, RefreshCw, Menu, Settings } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useCommandPalette } from '@/components/command-palette/useCommandPalette'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { useAuth, isSuperAdmin } from '@/lib/auth'
|
||||
import { useUIStore } from '@/lib/store'
|
||||
import { tenantsApi } from '@/lib/api'
|
||||
import { useEventStreamContext } from '@/contexts/EventStreamContext'
|
||||
import type { ConnectionState } from '@/hooks/useEventStream'
|
||||
|
||||
// ─── Connection State Indicator ──────────────────────────────────────────────
|
||||
|
||||
const CONNECTION_CONFIG: Record<
|
||||
ConnectionState,
|
||||
{ colorClass: string; label: string; pulse: boolean }
|
||||
> = {
|
||||
connected: { colorClass: 'bg-success', label: 'Connected', pulse: false },
|
||||
connecting: { colorClass: 'bg-warning', label: 'Connecting...', pulse: true },
|
||||
reconnecting: { colorClass: 'bg-warning', label: 'Reconnecting...', pulse: true },
|
||||
disconnected: { colorClass: 'bg-error', label: 'Disconnected', pulse: false },
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function ConnectionIndicator({
|
||||
state,
|
||||
lastConnectedAt,
|
||||
onReconnect,
|
||||
}: {
|
||||
state: ConnectionState
|
||||
lastConnectedAt: Date | null
|
||||
onReconnect: () => void
|
||||
}) {
|
||||
const { colorClass, label, pulse } = CONNECTION_CONFIG[state]
|
||||
|
||||
return (
|
||||
<div className="relative group flex items-center">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${colorClass} ${pulse ? 'animate-pulse' : ''}`}
|
||||
role="status"
|
||||
aria-label={`Real-time connection: ${label}`}
|
||||
/>
|
||||
{/* Tooltip on hover */}
|
||||
<div className="absolute right-0 top-full mt-2 hidden group-hover:block z-50">
|
||||
<div className="bg-surface border border-border rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap">
|
||||
<p className="font-medium text-text-primary">{label}</p>
|
||||
{lastConnectedAt && (
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Last connected: {formatTime(lastConnectedAt)}
|
||||
</p>
|
||||
)}
|
||||
{state === 'disconnected' && (
|
||||
<button
|
||||
onClick={onReconnect}
|
||||
className="flex items-center gap-1 mt-1.5 text-accent hover:text-accent-hover transition-colors"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Reconnect
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Header ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function Header() {
|
||||
const { user, logout } = useAuth()
|
||||
const { selectedTenantId, setSelectedTenantId, theme, setTheme } = useUIStore()
|
||||
const { connectionState, lastConnectedAt, reconnect } = useEventStreamContext()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const SYSTEM_TENANT_ID = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
const { data: tenants } = useQuery({
|
||||
queryKey: ['tenants'],
|
||||
queryFn: tenantsApi.list,
|
||||
enabled: isSuperAdmin(user),
|
||||
select: (data) => data.filter((t) => t.id !== SYSTEM_TENANT_ID),
|
||||
})
|
||||
|
||||
const selectedTenant = tenants?.find((t) => t.id === selectedTenantId)
|
||||
|
||||
// Auto-select when there's exactly one tenant and nothing selected
|
||||
useEffect(() => {
|
||||
if (isSuperAdmin(user) && tenants && tenants.length === 1 && !selectedTenantId) {
|
||||
setSelectedTenantId(tenants[0].id)
|
||||
}
|
||||
}, [tenants, selectedTenantId, user, setSelectedTenantId])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout()
|
||||
void navigate({ to: '/login' })
|
||||
}
|
||||
|
||||
const roleLabel: Record<string, string> = {
|
||||
super_admin: 'Super Admin',
|
||||
tenant_admin: 'Admin',
|
||||
operator: 'Operator',
|
||||
viewer: 'Viewer',
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between h-12 px-4 border-b border-border bg-sidebar flex-shrink-0" data-testid="header">
|
||||
{/* Left: hamburger + tenant context */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => useUIStore.getState().setMobileSidebarOpen(true)}
|
||||
className="lg:hidden p-1.5 rounded-md text-text-muted hover:text-text-primary hover:bg-elevated transition-colors"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
{isSuperAdmin(user) && tenants && tenants.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 text-sm text-text-secondary hover:text-text-primary transition-colors" data-testid="header-org-selector">
|
||||
<span>{selectedTenant ? selectedTenant.name : 'All Organizations'}</span>
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuLabel>Organization Context</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setSelectedTenantId(null)}>
|
||||
All Organizations
|
||||
</DropdownMenuItem>
|
||||
{tenants.map((tenant) => (
|
||||
<DropdownMenuItem
|
||||
key={tenant.id}
|
||||
onClick={() => setSelectedTenantId(tenant.id)}
|
||||
>
|
||||
{tenant.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: actions + user menu */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Search trigger */}
|
||||
<button
|
||||
onClick={() => useCommandPalette.getState().setOpen(true)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-border text-text-muted text-sm hover:text-text-secondary hover:border-border-bright transition-colors"
|
||||
aria-label="Search (Cmd+K)"
|
||||
data-testid="header-search"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<span className="hidden lg:inline">Search...</span>
|
||||
<kbd className="hidden lg:inline-flex items-center gap-0.5 rounded border border-border px-1.5 py-0.5 text-[10px] font-mono text-text-muted">
|
||||
Cmd+K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
{/* Dark/light mode toggle */}
|
||||
<button
|
||||
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
||||
className="p-2 rounded-lg text-text-muted hover:text-text-primary hover:bg-elevated transition-colors"
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
data-testid="header-theme-toggle"
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
{/* SSE connection state indicator */}
|
||||
<ConnectionIndicator
|
||||
state={connectionState}
|
||||
lastConnectedAt={lastConnectedAt}
|
||||
onReconnect={reconnect}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary transition-colors" aria-label="User menu" data-testid="header-user-menu">
|
||||
<div className="w-6 h-6 rounded-full bg-elevated flex items-center justify-center">
|
||||
<User className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span className="hidden lg:block">{user?.name ?? user?.email}</span>
|
||||
{/* Only show role if it differs from the display name */}
|
||||
{(user?.name ?? user?.email) !== (roleLabel[user?.role ?? ''] ?? user?.role) && (
|
||||
<span className="hidden lg:block text-xs text-text-muted">
|
||||
{roleLabel[user?.role ?? ''] ?? user?.role}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>
|
||||
<div className="font-normal text-text-secondary text-xs">{user?.email}</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/settings" className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => void handleLogout()} className="text-error" data-testid="button-sign-out">
|
||||
<LogOut className="h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user