"use client";
import { Activity, Database, History, KeyRound, PlayCircle, ShieldCheck, Zap } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
Badge,
Button,
Card,
EmptyState,
ErrorState,
LoadingBlock,
Select,
TableWrap,
Td,
Th,
TextInput,
cn,
formatTime,
statusTone,
useToast,
} from "@/components/admin/ui";
import { nl2sqlApi, type MaintenanceTask, type QueryHistoryItem } from "@/lib/nl2sql-api";
type Tab = "query" | "history" | "permission" | "ops";
const TABS: { key: Tab; label: string; icon: typeof Activity }[] = [
{ key: "query", label: "查询与健康", icon: Activity },
{ key: "history", label: "查询历史", icon: History },
{ key: "permission", label: "权限总览", icon: KeyRound },
{ key: "ops", label: "运维面板", icon: ShieldCheck },
];
const JOB_TASKS: { value: MaintenanceTask; label: string }[] = [
{ value: "metadata_sync", label: "元数据同步" },
{ value: "vector_cleanup", label: "向量清理" },
{ value: "consistency_check", label: "一致性校验" },
{ value: "history_cleanup", label: "历史清理" },
];
/** 通用 JSON 折叠查看:管理接口字段不固定,按原样展示避免前端臆造字段。 */
function JsonBlock({ data, empty = "暂无数据" }: { data: unknown; empty?: string }) {
if (data === null || data === undefined) return ;
if (Array.isArray(data) && data.length === 0) return ;
return (
{JSON.stringify(data, null, 2)}
);
}
export function Nl2SqlConsole() {
const toast = useToast();
const [tab, setTab] = useState("query");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [question, setQuestion] = useState("");
const [queryResult, setQueryResult] = useState | null>(null);
const [health, setHealth] = useState | null>(null);
const [sql, setSql] = useState("");
const [explain, setExplain] = useState[] | null>(null);
const [history, setHistory] = useState([]);
const [running, setRunning] = useState[]>([]);
const [diagnostics, setDiagnostics] = useState | null>(null);
const [roles, setRoles] = useState[]>([]);
const [tables, setTables] = useState[]>([]);
const [columns, setColumns] = useState[]>([]);
const [sensitive, setSensitive] = useState[]>([]);
const [selectedRole, setSelectedRole] = useState("");
const [metrics, setMetrics] = useState | null>(null);
const [runtimeConfig, setRuntimeConfig] = useState | null>(null);
const [semantics, setSemantics] = useState | null>(null);
const [jobHistory, setJobHistory] = useState[]>([]);
const [busy, setBusy] = useState(null);
const loadBase = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [healthData, runningData] = await Promise.all([
nl2sqlApi.health(),
nl2sqlApi.running(),
]);
setHealth(healthData);
setRunning(runningData ?? []);
} catch (err) {
setError(err instanceof Error ? err.message : "NL2SQL 状态加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void loadBase();
}, [loadBase]);
const loadHistory = useCallback(async () => {
setLoading(true);
try {
const page = await nl2sqlApi.history({ page: 1, page_size: 20 });
setHistory(page.items ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "查询历史加载失败");
} finally {
setLoading(false);
}
}, []);
const loadPermission = useCallback(async () => {
setLoading(true);
try {
const roleList = await nl2sqlApi.roles();
setRoles(roleList ?? []);
setSensitive((await nl2sqlApi.sensitiveFields()) ?? []);
if (!selectedRole && roleList?.length) {
const firstId = String((roleList[0] as { id?: number }).id ?? "");
setSelectedRole(firstId);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "权限数据加载失败");
} finally {
setLoading(false);
}
}, [selectedRole]);
const loadOps = useCallback(async () => {
setLoading(true);
try {
const [metricsData, configData, semanticsData, jobs] = await Promise.all([
nl2sqlApi.metrics(),
nl2sqlApi.runtimeConfig(),
nl2sqlApi.semantics(),
nl2sqlApi.jobHistory({ page: 1, page_size: 10 }),
]);
setMetrics(metricsData);
setRuntimeConfig(configData);
setSemantics(semanticsData);
setJobHistory(jobs.items ?? []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "运维数据加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (tab === "history") void loadHistory();
if (tab === "permission") void loadPermission();
if (tab === "ops") void loadOps();
}, [tab, loadHistory, loadPermission, loadOps]);
useEffect(() => {
if (!selectedRole) return;
void (async () => {
try {
const [tableList, columnList] = await Promise.all([
nl2sqlApi.roleTables(Number(selectedRole)),
nl2sqlApi.roleColumns(Number(selectedRole)),
]);
setTables(tableList ?? []);
setColumns(columnList ?? []);
} catch {
setTables([]);
setColumns([]);
}
})();
}, [selectedRole]);
const runQuery = useCallback(async () => {
if (!question.trim()) return;
setBusy("query");
try {
const result = await nl2sqlApi.query({ question, include_sql: true, page_size: 50 });
setQueryResult(result as unknown as Record);
toast("查询完成", "success");
} catch (err) {
toast(err instanceof Error ? err.message : "查询失败", "error");
} finally {
setBusy(null);
}
}, [question, toast]);
const runExplain = useCallback(async () => {
if (!sql.trim()) return;
setBusy("explain");
try {
setExplain(await nl2sqlApi.explain(sql));
} catch (err) {
toast(err instanceof Error ? err.message : "执行计划解析失败", "error");
} finally {
setBusy(null);
}
}, [sql, toast]);
const trigger = useCallback(
async (key: string, action: () => Promise, okText: string) => {
setBusy(key);
try {
await action();
toast(okText, "success");
} catch (err) {
toast(err instanceof Error ? err.message : "操作失败", "error");
} finally {
setBusy(null);
}
},
[toast]
);
return (
{TABS.map((item) => (
))}
}
>
{error && void loadBase()} />}
{loading && }
{!loading && !error && tab === "query" && (
setQuestion(event.target.value)}
placeholder="例如:查询近一个月申购金额最高的前 10 个客户"
/>
{queryResult && (
{typeof queryResult.summary === "string" && (
{queryResult.summary}
)}
{typeof queryResult.generated_sql === "string" && (
{queryResult.generated_sql}
)}
)}
setSql(event.target.value)}
placeholder="SELECT * FROM ..."
/>
{explain && {}
}
)}
{!loading && !error && tab === "history" && (
{history.length === 0 ? (
) : (
| 问题 |
状态 |
行数 |
耗时 |
时间 |
操作 |
{history.map((item) => (
|
{item.question}
|
{item.status ?? "--"}
|
{item.row_count ?? "--"} |
{item.elapsed_ms != null ? `${item.elapsed_ms} ms` : "--"} |
{formatTime(item.create_time)} |
|
))}
)}
)}
{!loading && !error && tab === "permission" && (
{roles.length === 0 ? (
) : (
| 角色 |
最大行数 |
状态 |
操作 |
{roles.map((role, index) => {
const id = String((role as { id?: number }).id ?? index);
return (
| {String(role.employee_role ?? "--")} |
{String(role.max_rows ?? "--")} |
{role.active === false ? "停用" : "启用"}
|
|
);
})}
)}
)}
{!loading && !error && tab === "ops" && (
{JOB_TASKS.map((job) => (
))}
}
>
void trigger(
"semantics",
async () => setSemantics(await nl2sqlApi.refreshSemantics()),
"语义目录已刷新"
)
}
>
刷新
}
>
void trigger(
"diagnostics",
async () => setDiagnostics(await nl2sqlApi.diagnostics()),
"诊断信息已刷新"
)
}
>
重新诊断
}
>
{
if (event.key !== "Enter") return;
const names = event.currentTarget.value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
if (!names.length) return;
void trigger("cache", () => nl2sqlApi.invalidateCache(names), "缓存已失效");
}}
/>
回车提交缓存失效;中止运行中查询请使用上方「运行中查询」返回的 query_id 调用 kill 接口。
)}
说明:本分区按约定只做只读展示与运维触发,权限的增删改表单不在本轮范围内。
);
}