feat(web): add backend switcher and connection status
Split the login page into backend list and credentials, show connection status in the sidebar and top bar, and render dialogs in a portal.
This commit is contained in:
1 parent
91c63d0cf2
commit
a9538eae4a
13 files changed
+522
-100
No files matched your search
+77
-33
@@ -1,7 +1,7 @@
|
||||
import { FlaskConical, ListChecks, LogOut, Radar, Server, Settings } from 'lucide-react'
|
||||
import { FlaskConical, ListChecks, LogOut, Plus, Radar, Server, Settings } from 'lucide-react'
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { NavLink, Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { isRemote } from '@shared/api/source'
|
||||
import { activateSource, getBase, listSources } from '@shared/api/source'
|
||||
import { describeError, Field, SourceButton, Spinner, ThemeToggle } from '@shared/ui'
|
||||
import { useAuth } from './auth'
|
||||
import { AgentDetail } from './pages/AgentDetail'
|
||||
@@ -11,22 +11,34 @@ import { System } from './pages/System'
|
||||
import { Tasks } from './pages/Tasks'
|
||||
import { Tests } from './pages/Tests'
|
||||
|
||||
/** 登录页:左边选后端(这个页面自己所在的,或保存过的,或添加一个),右边都是用户名 + 密码。 */
|
||||
function Login() {
|
||||
const { login, loginWithToken } = useAuth()
|
||||
const remote = isRemote()
|
||||
const [token, setTokenText] = useState('')
|
||||
const { login } = useAuth()
|
||||
const active = getBase()
|
||||
const saved = listSources()
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const choose = (base: string) => {
|
||||
setAdding(false)
|
||||
setError('')
|
||||
setPassword('')
|
||||
if (base !== active) {
|
||||
activateSource(base)
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setBusy(true)
|
||||
setError('')
|
||||
try {
|
||||
if (remote) await loginWithToken(token)
|
||||
else await login(username, password)
|
||||
await login(username, password, adding ? url : undefined)
|
||||
} catch (err) {
|
||||
setError(describeError(err))
|
||||
} finally {
|
||||
@@ -34,35 +46,67 @@ function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [{ base: '', label: location.host }, ...saved.map((x) => ({ base: x.base, label: x.base.replace(/^https?:\/\//i, '') }))]
|
||||
const title = adding ? '添加后端' : active ? active.replace(/^https?:\/\//i, '') : location.origin
|
||||
|
||||
return (
|
||||
<div className="login">
|
||||
<form className="card stack" onSubmit={submit}>
|
||||
<div>
|
||||
<h1>MyMonitor</h1>
|
||||
<p className="muted">管理端</p>
|
||||
</div>
|
||||
{remote ? (
|
||||
<Field label="管理令牌" hint="在管理端「系统 → API 令牌」里创建,范围选 admin">
|
||||
<input className="input" type="password" value={token} onChange={(e) => setTokenText(e.target.value)} autoComplete="off" autoFocus required />
|
||||
<form className="card login-split" onSubmit={submit}>
|
||||
<aside className="login-side">
|
||||
<div>
|
||||
<h1>MyMonitor</h1>
|
||||
<p className="muted">管理端</p>
|
||||
</div>
|
||||
<div className="src-list" role="listbox" aria-label="后端">
|
||||
{rows.map((row) => {
|
||||
const on = !adding && row.base === active
|
||||
return (
|
||||
<button key={row.base || 'page'} type="button" role="option" aria-selected={on} className={`src-pick${on ? ' on' : ''}`} onClick={() => choose(row.base)}>
|
||||
<span className="src-dot" />
|
||||
<span className="src-text">
|
||||
<b>{row.label}</b>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={adding}
|
||||
className={`src-pick${adding ? ' on' : ''}`}
|
||||
onClick={() => {
|
||||
setAdding(true)
|
||||
setUrl('')
|
||||
setPassword('')
|
||||
setError('')
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<span className="src-text">
|
||||
<b>添加后端</b>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<section className="login-main stack">
|
||||
<h2>{title}</h2>
|
||||
{adding ? (
|
||||
<Field label="后端地址">
|
||||
<input className="input" name="backend-url" type="text" inputMode="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="例如 https://你的域名" autoComplete="off" autoFocus required />
|
||||
</Field>
|
||||
) : null}
|
||||
<Field label="用户名">
|
||||
<input className="input" name="username" value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" autoFocus={!adding} required />
|
||||
</Field>
|
||||
) : (
|
||||
<>
|
||||
<Field label="用户名">
|
||||
<input className="input" value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" autoFocus required />
|
||||
</Field>
|
||||
<Field label="密码">
|
||||
<input className="input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" required />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
{error ? <div className="notice bad">{error}</div> : null}
|
||||
<button className="btn primary" type="submit" disabled={busy}>
|
||||
登录
|
||||
</button>
|
||||
<Field label="密码">
|
||||
<input className="input" name="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" required />
|
||||
</Field>
|
||||
{error ? <div className="notice bad">{error}</div> : null}
|
||||
<button className="btn primary" type="submit" disabled={busy}>
|
||||
{adding ? '保存并登录' : '登录'}
|
||||
</button>
|
||||
</section>
|
||||
</form>
|
||||
<div style={{ position: 'fixed', right: 16, bottom: 16 }}>
|
||||
<SourceButton tokenLabel="管理令牌" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -102,6 +146,7 @@ export function App() {
|
||||
</div>
|
||||
))}
|
||||
<div className="spacer" />
|
||||
<SourceButton variant="status" />
|
||||
<div className="me">
|
||||
<span className="avatar">{(me.username ?? 'A').slice(0, 1).toUpperCase()}</span>
|
||||
<span className="grow">
|
||||
@@ -110,7 +155,6 @@ export function App() {
|
||||
{me.scope}
|
||||
</div>
|
||||
</span>
|
||||
<SourceButton tokenLabel="管理令牌" />
|
||||
<ThemeToggle />
|
||||
<button type="button" className="btn icon" onClick={() => void logout()} aria-label="退出登录" title="退出登录">
|
||||
<LogOut size={18} />
|
||||
|
||||
Reference in new issue
Block a user