feat(admin): 补全平台任务观测 + 空间管理 + 基建加 MinIO/实时探针
对照已实现系统功能补 admin 缺失模块(feat/site): - 全平台任务/运行观测:GET /admin/tasks(跨租户查 sundynix_task,join 租户名/提交人邮箱/评测, 按状态/租户筛 + 状态计数;含 HITL 待审批=筛 waiting)+ TasksPage(状态卡+筛+表)。 - 空间(Space)管理:GET /admin/spaces(跨租户列 Space + 成员数子查询 + kind/归档态)+ SpacesPage。 - 基建观测:infra 加 MinIO(126 对象存储);postgres/redis/minio 从启动标志升级为实时 ping (blob.Ping/Postgres.Ping/Redis.Ping),能反映中途掉线。StatusPage 加 minio 标签、6 个依赖。 - 模型健康/熔断:确认 DashboardPage 已有「运行时链路态」渲染 m.health,无需重做。 验证:admin tsc + vitest 41 过、gateway build/vet/test 过;两新页浏览器实测渲染+优雅错误处理; 两新端点起临时 gateway 打真 PG 实测——tasks(counts+3路join)、spaces(成员数子查询)均返真数据。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { adminSpaces, type AdminSpace } from "../api";
|
||||
|
||||
// 全平台空间(Space)观测:跨租户看所有空间(类型/租户/建者/成员数/归档态)。
|
||||
// Space 是租户与成员之间的中间容器(Tenant > Space > 成员),数据来自 sundynix_space。
|
||||
|
||||
const KIND: Record<string, { label: string; badge: string }> = {
|
||||
personal: { label: "个人", badge: "bg-gray-100 text-gray-500" },
|
||||
project: { label: "项目", badge: "bg-cyan-50 text-cyan-600" },
|
||||
tenant: { label: "全员", badge: "bg-violet-50 text-violet-600" },
|
||||
};
|
||||
const kind = (k: string) => KIND[k] ?? { label: k, badge: "bg-gray-100 text-gray-500" };
|
||||
|
||||
export function SpacesPage() {
|
||||
const [spaces, setSpaces] = useState<AdminSpace[]>([]);
|
||||
const [err, setErr] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hideArchived, setHideArchived] = useState(true);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
adminSpaces(300)
|
||||
.then((s) => {
|
||||
setSpaces(s);
|
||||
setErr("");
|
||||
})
|
||||
.catch((e) => setErr((e as Error).message))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
useEffect(load, [load]);
|
||||
|
||||
const shown = useMemo(() => (hideArchived ? spaces.filter((s) => !s.archived) : spaces), [spaces, hideArchived]);
|
||||
const stats = useMemo(() => {
|
||||
const byKind: Record<string, number> = {};
|
||||
let archived = 0;
|
||||
for (const s of spaces) {
|
||||
byKind[s.kind] = (byKind[s.kind] ?? 0) + 1;
|
||||
if (s.archived) archived++;
|
||||
}
|
||||
return { total: spaces.length, byKind, archived };
|
||||
}, [spaces]);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<h3 className="text-sm font-semibold text-gray-700">全平台空间(Space)</h3>
|
||||
<span className="text-[11px] text-gray-400">租户与成员之间的中间容器:Agent / 知识库按空间共享</span>
|
||||
</div>
|
||||
|
||||
{/* 计数卡片 */}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<Stat label="空间总数" value={stats.total} tone="text-gray-800" />
|
||||
<Stat label="项目空间" value={stats.byKind.project ?? 0} tone="text-cyan-600" />
|
||||
<Stat label="全员空间" value={stats.byKind.tenant ?? 0} tone="text-violet-600" />
|
||||
<Stat label="已归档" value={stats.archived} tone="text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">{loading ? "加载中…" : `共 ${shown.length} 个空间`}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<input type="checkbox" checked={hideArchived} onChange={(e) => setHideArchived(e.target.checked)} />
|
||||
隐藏已归档
|
||||
</label>
|
||||
<button onClick={load} className="rounded-lg border border-gray-200 px-3 py-1.5 text-xs text-gray-500 hover:bg-gray-50">
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{err && <p className="mb-2 text-xs text-rose-500">{err}</p>}
|
||||
|
||||
<div className="max-h-[32rem] overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-white">
|
||||
<tr className="border-b border-gray-100 text-left text-[11px] uppercase tracking-wide text-gray-400">
|
||||
<th className="py-2 pr-3 font-medium">空间</th>
|
||||
<th className="py-2 pr-3 font-medium">类型</th>
|
||||
<th className="py-2 pr-3 font-medium">租户</th>
|
||||
<th className="py-2 pr-3 font-medium">建者</th>
|
||||
<th className="py-2 pr-3 text-right font-medium">成员</th>
|
||||
<th className="py-2 pr-3 font-medium">创建时间</th>
|
||||
<th className="py-2 font-medium">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shown.map((s) => (
|
||||
<tr key={s.id} className="border-b border-gray-50 last:border-0">
|
||||
<td className="py-2 pr-3 text-gray-800">{s.name}</td>
|
||||
<td className="py-2 pr-3">
|
||||
<span className={`rounded px-1.5 py-0.5 text-[10px] ${kind(s.kind).badge}`}>{kind(s.kind).label}</span>
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-gray-600">{s.tenant_name || <span className="text-gray-300">{s.tenant_id || "—"}</span>}</td>
|
||||
<td className="py-2 pr-3 text-xs text-gray-500">{s.creator_email || s.creator || "—"}</td>
|
||||
<td className="py-2 pr-3 text-right tabular-nums text-gray-700">{s.members}</td>
|
||||
<td className="py-2 pr-3 text-xs text-gray-500 whitespace-nowrap">{new Date(s.created_at).toLocaleDateString("zh-CN")}</td>
|
||||
<td className="py-2 text-xs">
|
||||
{s.archived ? <span className="text-gray-400">已归档</span> : <span className="text-emerald-600">活跃</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{shown.length === 0 && !loading && (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-8 text-center text-xs text-gray-400">暂无空间</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ label, value, tone }: { label: string; value: number; tone: string }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm">
|
||||
<div className="text-xs text-gray-400">{label}</div>
|
||||
<div className={`mt-1 text-2xl font-semibold tabular-nums ${tone}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user