feat: improve agent management and speed up tests
This commit is contained in:
1 parent
134c06d605
commit
193fc3a95c
28 files changed
+419
-57
No files matched your search
@@ -442,7 +442,7 @@ export interface paths {
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
/** Revoke a join token */
|
||||
/** Revoke a join token; with purge=true delete the record for good */
|
||||
delete: operations["join_tokens_revoke"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
@@ -986,7 +986,7 @@ export interface components {
|
||||
CreatedJoinTokenDto: {
|
||||
token: components["schemas"]["JoinTokenDto"];
|
||||
plaintext: string;
|
||||
install_command: string;
|
||||
install_commands: components["schemas"]["InstallCommandsDto"];
|
||||
};
|
||||
DimensionDto: {
|
||||
id: string;
|
||||
@@ -1112,6 +1112,11 @@ export interface components {
|
||||
[key: string]: (null | number | string)[];
|
||||
};
|
||||
};
|
||||
InstallCommandsDto: {
|
||||
linux: string;
|
||||
macos: string;
|
||||
windows: string;
|
||||
};
|
||||
JoinTokenDto: {
|
||||
id: string;
|
||||
note: null | string;
|
||||
@@ -2902,7 +2907,9 @@ export interface operations {
|
||||
};
|
||||
join_tokens_revoke: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
query?: {
|
||||
purge?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
|
||||
@@ -700,3 +700,83 @@ pre.code {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 带标签切换的命令框 ── */
|
||||
.cmd-tabs {
|
||||
border-radius: var(--r-control);
|
||||
background: var(--app-card-inset);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cmd-tablist {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--app-outline);
|
||||
}
|
||||
|
||||
.cmd-tablist button {
|
||||
position: relative;
|
||||
padding: 10px 10px 9px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--app-ink-faint);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cmd-tablist button:hover {
|
||||
color: var(--app-ink);
|
||||
}
|
||||
|
||||
.cmd-tablist button.on {
|
||||
color: var(--app-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cmd-tablist button.on::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: var(--app-ink);
|
||||
}
|
||||
|
||||
.cmd-body {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px 12px 12px 14px;
|
||||
}
|
||||
|
||||
.cmd-body code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.cmd-copy {
|
||||
flex: none;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--app-ink-faint);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cmd-copy:hover {
|
||||
background: var(--app-card-hover);
|
||||
color: var(--app-ink);
|
||||
}
|
||||
+111
-1
@@ -1,5 +1,5 @@
|
||||
import { Check, Copy, Moon, Plug, Sun } from 'lucide-react'
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { Component, createContext, useCallback, useContext, useEffect, useRef, useState, type ErrorInfo, type ReactNode } from 'react'
|
||||
import { ApiError } from '../api/client'
|
||||
import { getBase, getToken, normalizeBase, setSource } from '../api/source'
|
||||
import { useTheme } from '../hooks'
|
||||
@@ -230,6 +230,116 @@ export function LoadingBar({ active }: { active: boolean }) {
|
||||
) : null
|
||||
}
|
||||
|
||||
export interface CommandTab {
|
||||
id: string
|
||||
label: string
|
||||
command: string
|
||||
}
|
||||
|
||||
/** 按浏览器所在的系统挑一个默认的标签(Windows / macOS / 其他当 Linux)。 */
|
||||
export function guessPlatform(): 'windows' | 'macos' | 'linux' {
|
||||
const text = `${navigator.platform} ${navigator.userAgent}`.toLowerCase()
|
||||
return text.includes('win') ? 'windows' : text.includes('mac') ? 'macos' : 'linux'
|
||||
}
|
||||
|
||||
/**
|
||||
* 带标签切换的命令框(curl / npm / bun 那样):上面一排下划线标签,下面是等宽的命令,右侧一个复制图标。
|
||||
* 记住上次选的标签(只是浏览器里的小方便)。
|
||||
*/
|
||||
export function CommandTabs({ tabs, initial, storageKey }: { tabs: CommandTab[]; initial?: string; storageKey?: string }) {
|
||||
const [active, setActive] = useState(() => {
|
||||
let saved: string | null = null
|
||||
try {
|
||||
saved = storageKey ? localStorage.getItem(storageKey) : null
|
||||
} catch {
|
||||
// 浏览器禁用存储时只是不记住
|
||||
}
|
||||
return [saved, initial].find((id) => id && tabs.some((t) => t.id === id)) ?? tabs[0]?.id ?? ''
|
||||
})
|
||||
const [done, setDone] = useState(false)
|
||||
const tab = tabs.find((t) => t.id === active) ?? tabs[0]
|
||||
if (!tab) return null
|
||||
const pick = (id: string) => {
|
||||
setActive(id)
|
||||
try {
|
||||
if (storageKey) localStorage.setItem(storageKey, id)
|
||||
} catch {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="cmd-tabs">
|
||||
<div className="cmd-tablist" role="tablist">
|
||||
{tabs.map((t) => (
|
||||
<button key={t.id} type="button" role="tab" aria-selected={t.id === tab.id} className={t.id === tab.id ? 'on' : ''} onClick={() => pick(t.id)}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="cmd-body">
|
||||
<code>{tab.command}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="cmd-copy"
|
||||
aria-label="复制命令"
|
||||
title="复制命令"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(tab.command)
|
||||
setDone(true)
|
||||
window.setTimeout(() => setDone(false), 1500)
|
||||
} catch {
|
||||
// 没有剪贴板权限时用户可以手动选中
|
||||
}
|
||||
}}
|
||||
>
|
||||
{done ? <Check size={16} /> : <Copy size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面出错时显示提示,而不是整页白屏:说明出了什么错,可以重试(重新渲染这一块)或刷新整个页面。
|
||||
* 常见原因是前端和后端版本对不上(后端还没升级到新接口)。
|
||||
*/
|
||||
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
|
||||
state: { error: Error | null } = { error: null }
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error(error, info.componentStack)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state
|
||||
if (!error) return this.props.children
|
||||
return (
|
||||
<div className="login">
|
||||
<div className="card stack" style={{ maxWidth: 520 }}>
|
||||
<div>
|
||||
<h1>页面出错了</h1>
|
||||
<p className="muted">这一页没能显示出来。如果刚升级过,多半是前端和后端的版本对不上:先把后端升级到对应的版本,再刷新。</p>
|
||||
</div>
|
||||
<pre className="code">{error.message}</pre>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<button type="button" className="btn primary" onClick={() => this.setState({ error: null })}>
|
||||
重试
|
||||
</button>
|
||||
<button type="button" className="btn" onClick={() => location.reload()}>
|
||||
刷新页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 数据源设置:默认连同源的后端;填了地址就连别的后端(跨域用 API 令牌,后端要放行这个前端的来源)。 */
|
||||
export function SourceButton({ tokenLabel = '只读令牌(站点私有时才需要)' }: { tokenLabel?: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
Reference in new issue
Block a user