feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Database, RefreshCw, TriangleAlert } from "lucide-react";
|
||||
import { RefreshCw, TriangleAlert } from "lucide-react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
@@ -8,7 +8,10 @@ import { useAuth } from "~/context/AuthContext";
|
||||
import {
|
||||
getMonthlyMonitoringResult,
|
||||
getOperationsModel,
|
||||
getOperationsWorkbench,
|
||||
listOperationsModels,
|
||||
OperationsApiError,
|
||||
type OperationsWorkbenchDto,
|
||||
operationsApiMode,
|
||||
type OperationsApiMode,
|
||||
} from "~/services/operationsApi";
|
||||
@@ -16,6 +19,7 @@ import type { ModelRecord, MonitoringRow } from "./modelData";
|
||||
|
||||
type OperationsDataContextValue = {
|
||||
models: ModelRecord[];
|
||||
workbench: OperationsWorkbenchDto;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
source: OperationsApiMode;
|
||||
@@ -24,10 +28,20 @@ type OperationsDataContextValue = {
|
||||
|
||||
const OperationsDataContext = createContext<OperationsDataContextValue | null>(null);
|
||||
|
||||
const EMPTY_WORKBENCH: OperationsWorkbenchDto = {
|
||||
role: "model_team",
|
||||
alerts: [],
|
||||
kpis: [],
|
||||
todos: [],
|
||||
watches: [],
|
||||
activities: [],
|
||||
};
|
||||
|
||||
export function OperationsDataProvider({ children }: { children: ReactNode }) {
|
||||
const { currentWorkspace } = useAuth();
|
||||
const workspaceId = currentWorkspace?.workspace_id;
|
||||
const [models, setModels] = useState<ModelRecord[]>([]);
|
||||
const [workbench, setWorkbench] = useState<OperationsWorkbenchDto>(EMPTY_WORKBENCH);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -35,7 +49,13 @@ export function OperationsDataProvider({ children }: { children: ReactNode }) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
setModels(await listOperationsModels({ workspaceId, signal }));
|
||||
const [modelsResult, workbenchResult] = await Promise.allSettled([
|
||||
listOperationsModels({ workspaceId, signal }),
|
||||
getOperationsWorkbench(workspaceId, signal),
|
||||
]);
|
||||
if (modelsResult.status === "rejected") throw modelsResult.reason;
|
||||
setModels(modelsResult.value);
|
||||
setWorkbench(workbenchResult.status === "fulfilled" ? workbenchResult.value : EMPTY_WORKBENCH);
|
||||
} catch (cause) {
|
||||
if (cause instanceof DOMException && cause.name === "AbortError") return;
|
||||
setModels([]);
|
||||
@@ -53,11 +73,12 @@ export function OperationsDataProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const value = useMemo<OperationsDataContextValue>(() => ({
|
||||
models,
|
||||
workbench,
|
||||
loading,
|
||||
error,
|
||||
source: operationsApiMode,
|
||||
reload: () => load(),
|
||||
}), [error, load, loading, models]);
|
||||
}), [error, load, loading, models, workbench]);
|
||||
|
||||
return <OperationsDataContext.Provider value={value}>{children}</OperationsDataContext.Provider>;
|
||||
}
|
||||
@@ -83,7 +104,12 @@ export function useOperationsModelDetail(modelId: string, month: string) {
|
||||
setError(null);
|
||||
void Promise.all([
|
||||
getOperationsModel(modelId, workspaceId, controller.signal),
|
||||
getMonthlyMonitoringResult(modelId, month, workspaceId, controller.signal),
|
||||
getMonthlyMonitoringResult(modelId, month, workspaceId, controller.signal).catch((cause) => {
|
||||
if (cause instanceof OperationsApiError && cause.code === "MONITOR_RESULT_NOT_FOUND") {
|
||||
return null;
|
||||
}
|
||||
throw cause;
|
||||
}),
|
||||
]).then(([modelValue, resultValue]) => {
|
||||
setModel(modelValue);
|
||||
setMonitoringResult(resultValue);
|
||||
@@ -106,11 +132,11 @@ export function useOperationsModelDetail(modelId: string, month: string) {
|
||||
}
|
||||
|
||||
export function OperationsDataBoundary({ children }: { children: ReactNode }) {
|
||||
const { loading, error, models, reload, source } = useOperationsData();
|
||||
const { loading, error, reload } = useOperationsData();
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="h-full overflow-auto bg-bg p-6" aria-busy="true" aria-label="正在加载运维数据">
|
||||
<div className="mx-auto max-w-screen-2xl space-y-6">
|
||||
<div className="w-full space-y-6">
|
||||
<div className="space-y-3"><Skeleton className="h-3 w-48" /><Skeleton className="h-8 w-80" /><Skeleton className="h-4 w-[36rem]" /></div>
|
||||
<Skeleton className="h-40 w-full rounded-4xl" />
|
||||
<div className="grid grid-cols-4 gap-4">{Array.from({ length: 4 }, (_, index) => <Skeleton className="h-32 rounded-4xl" key={index} />)}</div>
|
||||
@@ -128,7 +154,7 @@ export function OperationsDataBoundary({ children }: { children: ReactNode }) {
|
||||
<span className="grid size-12 place-items-center rounded-2xl bg-danger-soft text-danger"><TriangleAlert className="size-6" /></span>
|
||||
<h2 className="mt-4 text-xl font-bold text-foreground">运维数据加载失败</h2>
|
||||
<p className="mt-2 max-w-md text-sm leading-6 text-muted-foreground">{error}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">当前数据源:{source === "api" ? "真实接口" : "前端 Mock"}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">当前数据源:真实接口</p>
|
||||
<Button className="mt-5" onClick={() => void reload()}><RefreshCw />重新加载</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -136,13 +162,5 @@ export function OperationsDataBoundary({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!models.length) {
|
||||
return (
|
||||
<section className="grid h-full place-items-center bg-bg p-6">
|
||||
<Card className="w-full max-w-xl"><CardContent className="flex flex-col items-center py-12 text-center"><span className="grid size-12 place-items-center rounded-2xl bg-brand-soft text-primary"><Database className="size-6" /></span><h2 className="mt-4 text-xl font-bold text-foreground">暂无模型数据</h2><p className="mt-2 text-sm text-muted-foreground">请确认模型平台已同步模型信息,或调整接口环境配置。</p><Button className="mt-5" variant="outline" onClick={() => void reload()}><RefreshCw />重新加载</Button></CardContent></Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user