diff --git a/frontend/src/pages/ConcurrencySettings.tsx b/frontend/src/pages/ConcurrencySettings.tsx new file mode 100644 index 00000000..fbcfc18a --- /dev/null +++ b/frontend/src/pages/ConcurrencySettings.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from 'react' +import { api } from '../api/client' +import { Field, Notice, SectionBar, Spinner, confirmDialog } from '../components/ui' +import { errorMessage } from '../lib/errors' +import { useLeaveGuard } from '../lib/leave' +import { CONCURRENCY_CLASSES, CONCURRENCY_TEXT } from '../lib/termsConcurrency' +import { atLeast, usePrincipal } from '../store/auth' + +const T = CONCURRENCY_TEXT +type Klass = (typeof CONCURRENCY_CLASSES)[number] +interface Draft { total: string; limits: Record; priorities: Record } +type Now = Partial> + +const draftOf = (group: Record | undefined): Draft => ({ + total: group?.max_total == null ? '' : String(group.max_total), + limits: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, String(group?.classes?.[k]?.limit ?? '')])) as Record, + priorities: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, String(group?.classes?.[k]?.priority ?? '')])) as Record, +}) + +const changedCount = (a: Draft, b: Draft): number => + (a.total.trim() !== b.total ? 1 : 0) + + CONCURRENCY_CLASSES.filter((k) => a.limits[k].trim() !== b.limits[k]).length + + CONCURRENCY_CLASSES.filter((k) => a.priorities[k].trim() !== b.priorities[k]).length + +/** + * 设置页「偏好设置」里「运行并发」一节(docs/design/orchestration-round §5 W2-b):全站总上限,交互、应用、定时、系统四类各自的 + * 上限和优先级,旁边写各类现在在跑、在等几个(/system 的 cluster.runs)。 + * + * 自己读、自己存(PUT /settings 只带 concurrency 一组),带自己的离开前确认,同评测那一节的做法。搭建者只读、管理员写 + */ +export function ConcurrencySettingsSection() { + const me = usePrincipal() + const admin = atLeast(me, 'admin') + const [saved, setSaved] = useState(null) + const [draft, setDraft] = useState(null) + const [now, setNow] = useState(null) + const [loadError, setLoadError] = useState(null) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [flash, setFlash] = useState(false) + + const loadNow = () => api.system().then((s) => setNow(s?.cluster?.runs?.classes ?? null), () => setNow(null)) + useEffect(() => { + api.settings.get().then((v) => { + setSaved(draftOf(v?.concurrency)) + setLoadError(null) + }, setLoadError) + void loadNow() + }, []) + + const cur = draft ?? saved + const changed = !!(cur && saved) && changedCount(cur, saved) > 0 + useLeaveGuard(changed && !saving, () => confirmDialog({ + title: T.leaveTitle(cur && saved ? changedCount(cur, saved) : 0), + consequences: T.leaveConsequences, confirmLabel: T.discard, danger: true, + })) + + const save = async () => { + if (!cur || !changed) return + setSaving(true) + setError(null) + try { + const value = { + max_total: Number(cur.total.trim()), + classes: Object.fromEntries(CONCURRENCY_CLASSES.map((k) => [k, { + limit: Number(cur.limits[k].trim()), priority: Number(cur.priorities[k].trim()), + }])), + } + const out = await api.settings.put({ concurrency: value }) + setSaved(draftOf(out?.concurrency ?? value)) + setDraft(null) + setFlash(true) + setTimeout(() => setFlash(false), 1600) + void loadNow() + } catch (e) { + setError(errorMessage(e)) + } finally { + setSaving(false) + } + } + + if (loadError || !cur) return null + const edit = (patch: Partial) => { + setError(null) + setDraft({ ...cur, ...patch, limits: { ...cur.limits, ...(patch.limits ?? {}) }, + priorities: { ...cur.priorities, ...(patch.priorities ?? {}) } }) + } + return ( +
+ +
+ + {(p) => ( + edit({ total: e.target.value })} data-concurrency-setting="max_total" /> + )} + +
+ {CONCURRENCY_CLASSES.map((k) => { + const info = T.classes[k] + const live = now?.[k] + return ( +
+
+
{info.label}
+
{info.hint}
+ {live &&
{T.now(live.running ?? 0, live.waiting ?? 0)}
} +
+ + {(p) => ( + edit({ limits: { [k]: e.target.value } as Record })} + data-concurrency-setting={`${k}.limit`} /> + )} + + + {(p) => ( + edit({ priorities: { [k]: e.target.value } as Record })} + data-concurrency-setting={`${k}.priority`} /> + )} + +
+ ) + })} +
+
+ {!admin &&

{T.readOnly}

} + {error && {error}} + {admin && (changed || flash) && ( +
+ {flash && !changed && {T.saved}} + {changed && ( + <> + + + + )} +
+ )} +
+ ) +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 3d19eb5c..27e32f07 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -34,6 +34,7 @@ import { } from './VerifySettings' import { DraftModelSection } from './DraftModelSettings' import { EvalSettingsSection } from './EvalSettings' +import { ConcurrencySettingsSection } from './ConcurrencySettings' import { GeneralTab } from './settings/GeneralTab' import { IdentityTab } from './settings/IdentityTab' import { NotifyTab } from './settings/NotifyTab' @@ -1204,6 +1205,8 @@ function PrefsTab() { {values.eval && } + {values.concurrency && } + {values.ledger && } {values.catalog_draft && (