import { KeyRound } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import type { CapabilityCatalogDto } from '@shared/api/types' import { Chip } from '@shared/ui' /** 能力的中文名和说明。目录(分组、依赖、预设)来自后端,这里只管显示文字。 */ const NAMES: Record = { 'public.read': ['查看展示数据', '看隐藏线路;站点私有时看展示页'], 'fleet.read': ['查看节点', 'Agent、Probe、线路、加入代码'], 'fleet.write': ['修改节点', '改名、标签、可见性、地址、线路开关、断开会话'], 'fleet.enroll': ['添加节点', '生成加入代码、批准新节点'], 'fleet.revoke': ['吊销或删除节点', '破坏性操作'], 'tasks.read': ['查看任务', '任务模板、覆盖、测试记录'], 'tasks.write': ['修改任务', '创建、修改、删除任务;发起或取消测试'], 'data.read': ['读取原始数据', '原始块、导出'], 'data.write': ['维护数据', '重算分析、清理'], 'data.export': ['批量导出数据', '预留:按线路和时间批量拉取,带配额'], 'settings.read': ['查看设置', '站点设置、保留策略'], 'settings.write': ['修改设置', '站点设置、保留策略、重启计划'], 'update.read': ['查看更新', '版本、发布清单、Backend 更新状态'], 'update.confirm': ['确认更新', '确认 Backend 更新、请求节点更新'], 'keys.read': ['查看 API 令牌', ''], 'keys.manage': ['管理 API 令牌', '创建、吊销;只能授予自己拥有的能力'], 'accounts.manage': ['管理账户', '面板用户、密码'], 'events.read': ['查看事件', '节点上下线等事件流'], 'audit.read': ['查看审计日志', ''], 'ops.metrics': ['读取 /metrics', 'Prometheus 指标'], 'webhooks.read': ['查看 webhook', '预留:外部回调订阅'], 'webhooks.manage': ['管理 webhook', '预留:创建、修改、删除回调订阅,触发测试回调'], } const GROUPS: Record = { public: '展示', fleet: '节点', tasks: '任务', data: '数据', settings: '设置', update: '更新与发布', keys: 'API 令牌', accounts: '账户', ops: '运维与审计', integrations: '集成(预留)', } const PRESETS: Record = { 'read-only': '只读', operations: '运维', enroll: '接入', 'ci-release': 'CI 发布', all: '全部', } /** 复选框:支持半选(indeterminate)。 */ function Check({ checked, partial, disabled, onChange, label }: { checked: boolean; partial?: boolean; disabled?: boolean; onChange: (on: boolean) => void; label: React.ReactNode }) { const ref = useRef(null) useEffect(() => { if (ref.current) ref.current.indeterminate = !!partial && !checked }, [partial, checked]) return ( ) } /** * 能力选择器:分组复选框 + 预设 + 全选/组内全选(带半选)/反选 + 搜索。 * `mine` 是创建者自己拥有的能力:不在里面的灰色不可选(不能授予自己没有的);写蕴含读,勾选写时读会一起被勾上。 */ export function CapabilityPicker({ catalog, mine, value, onChange }: { catalog: CapabilityCatalogDto; mine: ReadonlySet; value: ReadonlySet; onChange: (next: Set) => void }) { const [query, setQuery] = useState('') const info = useMemo(() => new Map(catalog.groups.flatMap((g) => g.capabilities.map((c) => [c.id, c] as const))), [catalog]) const selectable = (id: string) => mine.has(id) // 勾选一项:补上它蕴含的;取消一项:同时取消依赖它的(写依赖读) const closure = (ids: Iterable): Set => { const out = new Set() const add = (id: string) => { if (out.has(id) || !selectable(id)) return out.add(id) info.get(id)?.implies.forEach(add) } for (const id of ids) add(id) return out } const set = (ids: Iterable) => onChange(closure(ids)) const toggle = (id: string, on: boolean) => { if (on) return set([...value, id]) const next = new Set(value) next.delete(id) // 依赖被取消的项一起取消 let changed = true while (changed) { changed = false for (const x of next) if (info.get(x)?.implies.some((i) => !next.has(i))) { next.delete(x); changed = true } } onChange(next) } const needle = query.trim().toLowerCase() const visible = (id: string) => !needle || id.includes(needle) || (NAMES[id]?.[0] ?? '').includes(needle) || (NAMES[id]?.[1] ?? '').includes(needle) const all = [...info.keys()].filter(selectable) const allOn = all.length > 0 && all.every((id) => value.has(id)) return (
预设: {catalog.presets.map((p) => ( ))} setQuery(e.target.value)} />
0} onChange={(on) => set(on ? all : [])} label={全选} /> 已选 {value.size} / {all.length} 项
{catalog.groups.map((g) => { const rows = g.capabilities.filter((c) => visible(c.id)) if (rows.length === 0) return null const own = g.capabilities.filter((c) => selectable(c.id)) const on = own.filter((c) => value.has(c.id)) return (
0 && on.length === own.length} partial={on.length > 0} disabled={own.length === 0} onChange={(next) => (next ? set([...value, ...own.map((c) => c.id)]) : onChange(new Set([...value].filter((id) => !own.some((c) => c.id === id)))))} label={{GROUPS[g.id] ?? g.id}} />
{rows.map((c) => (
toggle(c.id, next)} label={NAMES[c.id]?.[0] ?? c.id} /> {c.trust ? ( 信任类 ) : null} {c.reserved ? 预留 : null}
{c.id} {NAMES[c.id]?.[1] ?? ''}
))}
) })}
) } export function capabilityName(id: string): string { return NAMES[id]?.[0] ?? id }