mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 00:00:50 +00:00
feat(admin-ui): add app shell (main, App, LoginPage, Nav) and tab stubs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
49a1292c0f
commit
acda35f063
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useAuthStore } from './store/auth'
|
||||
import { useWsStore } from './store/ws'
|
||||
import { connectWs, disconnectWs } from './api/websocket'
|
||||
import LoginPage from './components/layout/LoginPage'
|
||||
import Nav from './components/layout/Nav'
|
||||
import Dashboard from './tabs/dashboard/Dashboard'
|
||||
import RequestLog from './tabs/requests/RequestLog'
|
||||
import Settings from './tabs/settings/Settings'
|
||||
import Backends from './tabs/backends/Backends'
|
||||
import Keys from './tabs/keys/Keys'
|
||||
import Models from './tabs/models/Models'
|
||||
import Audit from './tabs/audit/Audit'
|
||||
import TrafficView from './tabs/traffic/TrafficView'
|
||||
import UptimeView from './tabs/uptime/UptimeView'
|
||||
|
||||
type Tab = 'dashboard' | 'requests' | 'settings' | 'backends' | 'keys' | 'models' | 'audit' | 'traffic' | 'uptime'
|
||||
|
||||
export default function App() {
|
||||
const token = useAuthStore((s) => s.token)
|
||||
const lastEvent = useWsStore((s) => s.lastEvent)
|
||||
const qc = useQueryClient()
|
||||
const [activeTab, setActiveTab] = useState<Tab>('dashboard')
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
connectWs()
|
||||
} else {
|
||||
disconnectWs()
|
||||
}
|
||||
}, [token])
|
||||
|
||||
// Invalidate query cache on relevant WS events.
|
||||
useEffect(() => {
|
||||
if (!lastEvent) return
|
||||
if (lastEvent.type === 'metrics_snapshot') {
|
||||
qc.setQueryData(['metrics'], lastEvent.data)
|
||||
} else if (lastEvent.type === 'backend_health_changed') {
|
||||
qc.invalidateQueries({ queryKey: ['uptime'] })
|
||||
}
|
||||
}, [lastEvent, qc])
|
||||
|
||||
if (!token) return <LoginPage />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Nav activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
<div className="tab-content">
|
||||
{activeTab === 'dashboard' && <Dashboard />}
|
||||
{activeTab === 'requests' && <RequestLog />}
|
||||
{activeTab === 'settings' && <Settings />}
|
||||
{activeTab === 'backends' && <Backends />}
|
||||
{activeTab === 'keys' && <Keys />}
|
||||
{activeTab === 'models' && <Models />}
|
||||
{activeTab === 'audit' && <Audit />}
|
||||
{activeTab === 'traffic' && <TrafficView />}
|
||||
{activeTab === 'uptime' && <UptimeView />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { useAuthStore } from '../../store/auth'
|
||||
|
||||
export default function LoginPage() {
|
||||
const login = useAuthStore((s) => s.login)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
const token = (e.currentTarget.elements.namedItem('token') as HTMLInputElement).value.trim()
|
||||
if (!token) return
|
||||
setLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
// Validate by hitting a lightweight endpoint with the candidate token.
|
||||
const res = await fetch('/admin/api/metrics', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) throw new Error('Invalid token')
|
||||
login(token)
|
||||
} catch {
|
||||
setError('Invalid token')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-overlay">
|
||||
<div className="login-card">
|
||||
<div className="login-title">
|
||||
<span className="prompt">> </span>proxy admin
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="password"
|
||||
name="token"
|
||||
placeholder="Admin token"
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? 'Signing in\u2026' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="login-error">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useAuthStore } from '../../store/auth'
|
||||
import { useWsStore } from '../../store/ws'
|
||||
|
||||
type Tab = 'dashboard' | 'requests' | 'settings' | 'backends' | 'keys' | 'models' | 'audit' | 'traffic' | 'uptime'
|
||||
|
||||
const TABS: { id: Tab; label: string }[] = [
|
||||
{ id: 'dashboard', label: 'Dashboard' },
|
||||
{ id: 'requests', label: 'Request Log' },
|
||||
{ id: 'settings', label: 'Settings' },
|
||||
{ id: 'backends', label: 'Backends' },
|
||||
{ id: 'keys', label: 'Access Control' },
|
||||
{ id: 'models', label: 'Models' },
|
||||
{ id: 'audit', label: 'Audit' },
|
||||
{ id: 'traffic', label: 'Traffic' },
|
||||
{ id: 'uptime', label: 'Uptime' },
|
||||
]
|
||||
|
||||
interface NavProps {
|
||||
activeTab: Tab
|
||||
onTabChange: (tab: Tab) => void
|
||||
}
|
||||
|
||||
export default function Nav({ activeTab, onTabChange }: NavProps) {
|
||||
const logout = useAuthStore((s) => s.logout)
|
||||
const wsStatus = useWsStore((s) => s.status)
|
||||
|
||||
return (
|
||||
<nav className="nav">
|
||||
<div className="nav-brand">anyllm</div>
|
||||
{TABS.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`nav-item${activeTab === t.id ? ' active' : ''}`}
|
||||
onClick={() => onTabChange(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</div>
|
||||
))}
|
||||
<div className="nav-right">
|
||||
<span
|
||||
className={`ws-status ${wsStatus === 'connected' ? 'connected' : 'disconnected'}`}
|
||||
>
|
||||
{wsStatus === 'connected' ? 'Live' : 'Offline'}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
style={{ marginLeft: 12 }}
|
||||
onClick={logout}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import App from './App'
|
||||
import './styles/globals.css'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export default function Audit() { return <div>Audit</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function Backends() { return <div>Backends</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function Dashboard() { return <div>Dashboard</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function Keys() { return <div>Keys</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function Models() { return <div>Models</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function RequestLog() { return <div>Request Log</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function Settings() { return <div>Settings</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function TrafficView() { return <div>Traffic</div> }
|
||||
@@ -0,0 +1 @@
|
||||
export default function UptimeView() { return <div>Uptime</div> }
|
||||
Reference in New Issue
Block a user