From 4d8704aabc032aa172fa1811c153947e3ea32490 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 17 Mar 2025 11:11:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/mock/websocket.mock.js | 92 +++++++++++++++++++ react-ui/.storybook/preview.tsx | 5 + .../Experiment/components/LogGroup/index.tsx | 23 +++-- .../Experiment/components/LogList/index.tsx | 36 +++++--- .../pages/HyperParameter/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.tsx | 2 +- react-ui/src/stories/mockData.ts | 53 +++++++++++ .../src/stories/pages/LogList.stories.tsx | 90 ++++++++++++++++++ react-ui/src/utils/index.ts | 10 +- 9 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 react-ui/.storybook/mock/websocket.mock.js create mode 100644 react-ui/src/stories/pages/LogList.stories.tsx diff --git a/react-ui/.storybook/mock/websocket.mock.js b/react-ui/.storybook/mock/websocket.mock.js new file mode 100644 index 00000000..3661b99b --- /dev/null +++ b/react-ui/.storybook/mock/websocket.mock.js @@ -0,0 +1,92 @@ +export const createWebSocketMock = () => { + class WebSocketMock { + constructor(url) { + this.url = url; + this.readyState = WebSocket.OPEN; + this.listeners = {}; + this.count = 0; + + console.log("Mock WebSocket connected to:", url); + + // 模拟服务器推送消息 + this.intervalId = setInterval(() => { + this.count += 1; + if (this.count > 5) { + this.count = 0; + clearInterval(this.intervalId); + return; + } + this.sendMessage(JSON.stringify(logStreamData)); + }, 3000); + } + + sendMessage(data) { + if (this.listeners["message"]) { + this.listeners["message"].forEach((callback) => callback({ data })); + } + } + + addEventListener(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + removeEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); + } + } + + close() { + this.readyState = WebSocket.CLOSED; + console.log("Mock WebSocket closed"); + } + } + + return WebSocketMock; +}; + +export const logStreamData = { + streams: [ + { + stream: { + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + }, + values: [ + [ + '1742179591969785990', + 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', + ], + ], + }, + { + stream: { + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + }, + values: [ + [ + '1742179591973414064', + 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', + ], + ], + }, + ], +}; \ No newline at end of file diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index 61e82aaa..0ec22de0 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -5,6 +5,7 @@ import type { Preview } from '@storybook/react'; import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import { initialize, mswLoader } from 'msw-storybook-addon'; +import { createWebSocketMock } from './mock/websocket.mock'; import './storybook.css'; /* @@ -14,6 +15,10 @@ import './storybook.css'; */ initialize(); +// 替换全局 WebSocket 为 Mock 版本 +// @ts-ignore +global.WebSocket = createWebSocketMock(); + const preview: Preview = { parameters: { controls: { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 5123fae1..11d51aca 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -37,19 +37,20 @@ function LogGroup({ const [completed, setCompleted] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); - const preStatusRef = useRef(undefined); const socketRef = useRef(undefined); const retryRef = useRef(2); // 等待 2 秒,重试 3 次 - const elementRef = useRef(null); + const logElementRef = useRef(null); + // 如果是【运行中】状态,设置 hasRun 为 true,【运行中】或者从【运行中】切换到别的状态时,不显示【更多】按钮 + const [hasRun, setHasRun] = useState(false); + if (status === ExperimentStatus.Running && !hasRun) { + setHasRun(true); + } useEffect(() => { - scrollToBottom(false); if (status === ExperimentStatus.Running) { setupSockect(); - } else if (preStatusRef.current === ExperimentStatus.Running) { - setCompleted(true); } - preStatusRef.current = status; + scrollToBottom(false); }, [status]); // 鼠标拖到中不滚动到底部 @@ -208,15 +209,17 @@ function LogGroup({ // }; // element.scrollTo(optons); // } - elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' }); + logElementRef?.current?.scrollIntoView({ + block: 'end', + behavior: smooth ? 'smooth' : 'instant', + }); }; const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); - const showMoreBtn = - status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; + const showMoreBtn = !hasRun && !completed && showLog && logText !== ''; return ( -
+
{log_type === 'resource' && (
{pod_name}
diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 86c97d15..15ce0def 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -15,13 +15,21 @@ export type ExperimentLog = { }; type LogListProps = { - instanceName: string; // 实验实例 name - instanceNamespace: string; // 实验实例 namespace - pipelineNodeId: string; // 流水线节点 id - workflowId?: string; // 实验实例工作流 id - instanceNodeStartTime?: string; // 实验实例节点开始运行时间 + /** 实验实例 name */ + instanceName: string; + /** 实验实例 namespace */ + instanceNamespace: string; + /** 流水线节点 id */ + pipelineNodeId: string; + /** 实验实例工作流 id */ + workflowId?: string; + /** 实验实例节点开始运行时间 */ + instanceNodeStartTime?: string; + /** 实验实例节点运行状态 */ instanceNodeStatus?: ExperimentStatus; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; @@ -35,23 +43,21 @@ function LogList({ className, style, }: LogListProps) { - const [logList, setLogList] = useState([]); - const preStatusRef = useRef(undefined); + const [logGroups, setLogGroups] = useState([]); const retryRef = useRef(3); // 等待 2 秒,重试 3 次 - // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 + // 当实例节点运行状态不是 Pending,获取实验日志组 useEffect(() => { if ( instanceNodeStatus && instanceNodeStatus !== ExperimentStatus.Pending && - (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) + logGroups.length === 0 ) { getExperimentLog(); } - preStatusRef.current = instanceNodeStatus; }, [instanceNodeStatus]); - // 获取实验日志 + // 获取实验 Pods 组 const getExperimentLog = async () => { const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; const params = { @@ -71,7 +77,7 @@ function LogList({ log_type, }, ]; - setLogList(list); + setLogGroups(list); } else if (log_type === 'resource') { const list = pods.map((v: string) => ({ log_type, @@ -79,7 +85,7 @@ function LogList({ log_content: '', start_time, })); - setLogList(list); + setLogGroups(list); } } else { if (retryRef.current > 0) { @@ -93,8 +99,8 @@ function LogList({ return (
- {logList.length > 0 ? ( - logList.map((v) => ) + {logGroups.length > 0 ? ( + logGroups.map((v) => ) ) : (
暂无日志
)} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index df25cc18..aa206059 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -192,7 +192,7 @@ function HyperParameterInstance() { key: TabKeys.History, label: '寻优列表', icon: , - children: , + children: , }, ]; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index c4698464..d9091d26 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -35,7 +35,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }, []); // 计算 column - const first: HyperParameterTrial | undefined = trialList[0]; + const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined; const config: Record = first?.config ?? {}; const metricAnalysis: Record = first?.metric_analysis ?? {}; const paramsNames = Object.keys(config); diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts index 11b5ffa2..e5ea5bd1 100644 --- a/react-ui/src/stories/mockData.ts +++ b/react-ui/src/stories/mockData.ts @@ -1,3 +1,4 @@ +// 数据集列表 export const datasetListData = { msg: '操作成功', code: 200, @@ -76,6 +77,7 @@ export const datasetListData = { }, }; +// 数据集版本列表 export const datasetVersionData = { msg: '操作成功', code: 200, @@ -107,6 +109,7 @@ export const datasetVersionData = { ], }; +// 数据集详情 export const datasetDetailData = { msg: '操作成功', code: 200, @@ -137,6 +140,7 @@ export const datasetDetailData = { }, }; +// 模型列表 export const modelListData = { msg: '操作成功', code: 200, @@ -211,6 +215,7 @@ export const modelListData = { }, }; +// 模型版本列表 export const modelVersionData = { msg: '操作成功', code: 200, @@ -234,6 +239,7 @@ export const modelVersionData = { ], }; +// 模型详情 export const modelDetailData = { msg: '操作成功', code: 200, @@ -266,6 +272,7 @@ export const modelDetailData = { }, }; +// 镜像列表 export const mirrorListData = { code: 200, msg: '操作成功', @@ -384,6 +391,7 @@ export const mirrorListData = { }, }; +// 镜像版本列表 export const mirrorVerionData = { code: 200, msg: '操作成功', @@ -433,6 +441,7 @@ export const mirrorVerionData = { }, }; +// 代码配置列表 export const codeListData = { code: 200, msg: '操作成功', @@ -547,6 +556,7 @@ export const codeListData = { }, }; +// 服务列表 export const serviceListData = { code: 200, msg: '操作成功', @@ -620,6 +630,7 @@ export const serviceListData = { }, }; +// 计算资源列表 export const computeResourceData = { code: 200, msg: '操作成功', @@ -793,3 +804,45 @@ export const computeResourceData = { empty: false, }, }; + +// 日志组 +export const logGroupData = { + code: 200, + msg: '操作成功', + data: { + log_type: 'normal', + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: + '[2025-03-06 14:02:23] time="2025-03-06T14:02:23.068Z" level=info msg="capturing logs" argo=true\n[2025-03-06 14:02:23] Cloning into \'/tmp/traincode\'...\n[2025-03-06 14:02:23] Cloning public repository without authentication.\n[2025-03-06 14:02:23] Repository cloned successfully.\n[2025-03-06 14:02:24] time="2025-03-06T14:02:24.069Z" level=info msg="sub-process exited" argo=true error=""\n', + start_time: '1741240944069759628', + }, + pods: ['workflow-txpb5-git-clone-05955a53-2484323670'], + }, +}; + +// 日志 +export const logData = { + code: 200, + msg: '操作成功', + data: { + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: + '[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Main container completed" error=""\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No Script output reference in workflow. Capturing script output ignored"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output parameters"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output artifacts"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="S3 Save path: /tmp/argo/outputs/logs/main.log, key: workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Creating minio client using static credentials" endpoint="minio.argo.svc.cluster.local:9000"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Saving file to s3" bucket=my-bucket endpoint="minio.argo.svc.cluster.local:9000" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log path=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Save artifact" artifactName=main-logs duration=1.092064185s error="" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="not deleting local artifact" localArtPath=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Successfully saved file: /tmp/argo/outputs/logs/main.log"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.408Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot create resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.418Z" level=info msg="Alloc=8553 TotalAlloc=14914 Sys=24421 NumGC=4 Goroutines=10"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.419Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io \\"workflow-txpb5-2484323670\\" is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot patch resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.427Z" level=info msg="Deadline monitor stopped"\n', + start_time: '1741240945427542429', + }, + }, +}; + +export const logEmptyData = { + code: 200, + msg: '操作成功', + data: { + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: '', + start_time: '1741240945427542429', + }, + }, +}; diff --git a/react-ui/src/stories/pages/LogList.stories.tsx b/react-ui/src/stories/pages/LogList.stories.tsx new file mode 100644 index 00000000..5685b8fa --- /dev/null +++ b/react-ui/src/stories/pages/LogList.stories.tsx @@ -0,0 +1,90 @@ +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import type { Meta, StoryObj } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { logData, logEmptyData, logGroupData } from '../mockData'; + +let count = 0; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Pages/LogList 日志组', + component: LogList, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.post('/api/mmp/experimentIns/realTimeLog', () => { + return HttpResponse.json(logGroupData); + }), + http.get('/api/mmp/experimentIns/pods/log', () => { + if (count > 0) { + count = 0; + return HttpResponse.json(logEmptyData); + } + count += 1; + return HttpResponse.json(logData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + instanceNodeStatus: { control: 'select', options: Object.values(ExperimentStatus) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 运行完成时 */ +export const Succeeded: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Succeeded, + }, +}; + +/** 无状态,空数据 */ +export const NoStatus: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + }, +}; + +/** Pending */ +export const Pending: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Pending, + }, +}; + +export const Running: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Running, + }, +}; diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 3deb9832..4df48c1c 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -88,7 +88,10 @@ export function camelCaseToUnderscore(obj: Record) { } // null to undefined -export function nullToUndefined(obj: Record) { +export function nullToUndefined(obj: Record | null) { + if (obj === null) { + return undefined; + } if (!isPlainObject(obj)) { return obj; } @@ -111,7 +114,10 @@ export function nullToUndefined(obj: Record) { } // undefined to null -export function undefinedToNull(obj: Record) { +export function undefinedToNull(obj?: Record) { + if (obj === undefined) { + return null; + } if (!isPlainObject(obj)) { return obj; } From 944d03423f8f0c75531893f96573a3abd3179b96 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 18 Mar 2025 14:17:35 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0eslint-plugin?= =?UTF-8?q?-react-hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.eslintignore | 3 +- react-ui/.eslintrc.js | 8 +- react-ui/package.json | 1 + react-ui/src/app.tsx | 4 +- .../components/CodeSelectorModal/index.tsx | 28 +-- react-ui/src/components/IFramePage/index.tsx | 25 ++- .../src/components/ParameterSelect/index.tsx | 27 +-- .../src/components/ResourceSelect/index.tsx | 2 +- .../ResourceSelectorModal/index.tsx | 104 +++++----- react-ui/src/hooks/index.ts | 8 +- react-ui/src/hooks/resource.ts | 37 ++-- react-ui/src/hooks/sessionStorage.ts | 25 --- react-ui/src/pages/AutoML/Create/index.tsx | 100 +++++----- react-ui/src/pages/AutoML/Info/index.tsx | 18 +- .../components/ExperimentHistory/index.tsx | 54 +++--- .../components/ExperimentInstance/index.tsx | 2 +- .../components/ExperimentList/index.tsx | 14 +- .../components/ExperimentResult/index.tsx | 16 +- react-ui/src/pages/CodeConfig/List/index.tsx | 14 +- .../Dataset/components/ResourceInfo/index.tsx | 101 +++++----- .../Dataset/components/ResourceList/index.tsx | 46 ++--- .../Dataset/components/ResourcePage/index.tsx | 45 ++--- .../components/VersionCompareModal/index.tsx | 36 ++-- .../DevelopmentEnvironment/List/index.tsx | 14 +- .../src/pages/Experiment/Comparison/index.tsx | 38 ++-- react-ui/src/pages/Experiment/Info/index.jsx | 4 - .../components/ExperimentDrawer/index.tsx | 3 + .../components/ExperimentInstance/index.tsx | 2 +- .../components/ExperimentResult/index.tsx | 23 ++- .../components/ExportModelModal/index.tsx | 28 +-- .../Experiment/components/LogGroup/index.tsx | 2 +- .../Experiment/components/LogList/index.tsx | 28 +-- react-ui/src/pages/Experiment/index.jsx | 40 ++-- .../src/pages/HyperParameter/Create/index.tsx | 54 +++--- .../src/pages/HyperParameter/Info/index.tsx | 18 +- .../components/ExperimentHistory/index.tsx | 2 +- .../components/ExperimentResult/index.tsx | 16 +- .../components/HyperParameterBasic/index.tsx | 13 +- react-ui/src/pages/Mirror/Create/index.tsx | 2 +- react-ui/src/pages/Mirror/Info/index.tsx | 42 +++-- react-ui/src/pages/Mirror/List/index.tsx | 36 ++-- .../Model/components/MetricsChart/index.tsx | 133 ++++++------- .../Model/components/ModelEvolution/index.tsx | 2 +- .../Model/components/ModelMetrics/index.tsx | 96 +++++----- .../ModelDeployment/CreateService/index.tsx | 34 ++-- .../ModelDeployment/CreateVersion/index.tsx | 27 ++- .../src/pages/ModelDeployment/List/index.tsx | 91 ++++----- .../ModelDeployment/ServiceInfo/index.tsx | 26 +-- .../ModelDeployment/VersionInfo/index.tsx | 18 +- .../components/ServerLog/index.tsx | 37 ++-- .../components/UserGuide/index.tsx | 20 +- .../components/VersionBasicInfo/index.tsx | 11 +- .../components/VersionCompareModal/index.tsx | 26 +-- react-ui/src/pages/Monitor/Job/edit.tsx | 2 +- react-ui/src/pages/Pipeline/Info/index.jsx | 4 +- .../components/GlobalParamsDrawer/index.tsx | 30 +-- .../Pipeline/components/ModelMenu/index.tsx | 76 ++++---- .../components/PipelineNodeDrawer/index.tsx | 98 +++++----- react-ui/src/pages/Pipeline/index.jsx | 15 +- react-ui/src/pages/System/Config/edit.tsx | 2 +- react-ui/src/pages/System/Dept/edit.tsx | 2 +- react-ui/src/pages/System/Dict/edit.tsx | 2 +- react-ui/src/pages/System/DictData/edit.tsx | 2 +- react-ui/src/pages/System/DictData/index.tsx | 2 +- react-ui/src/pages/System/Menu/edit.tsx | 2 +- react-ui/src/pages/System/Notice/edit.tsx | 2 +- react-ui/src/pages/System/Post/edit.tsx | 2 +- .../System/Role/components/DataScope.tsx | 2 +- react-ui/src/pages/System/Role/edit.tsx | 2 +- react-ui/src/pages/System/User/edit.tsx | 2 +- react-ui/src/pages/User/Login/login.tsx | 3 +- .../components/AssetsManagement/index.tsx | 76 ++++---- .../components/ExperimentChart/index.tsx | 178 +++++++++--------- react-ui/src/state/computingResourceStore.ts | 16 -- react-ui/src/utils/table.tsx | 1 + 75 files changed, 1054 insertions(+), 1071 deletions(-) delete mode 100644 react-ui/src/hooks/sessionStorage.ts delete mode 100644 react-ui/src/state/computingResourceStore.ts diff --git a/react-ui/.eslintignore b/react-ui/.eslintignore index 8336e935..3bc705a6 100644 --- a/react-ui/.eslintignore +++ b/react-ui/.eslintignore @@ -5,4 +5,5 @@ public dist .umi -mock \ No newline at end of file +mock +/src/iconfont/ \ No newline at end of file diff --git a/react-ui/.eslintrc.js b/react-ui/.eslintrc.js index 564a28d2..85537dd8 100644 --- a/react-ui/.eslintrc.js +++ b/react-ui/.eslintrc.js @@ -1,10 +1,16 @@ module.exports = { - extends: [require.resolve('@umijs/lint/dist/config/eslint')], + extends: [ + require.resolve('@umijs/lint/dist/config/eslint'), + 'plugin:react/recommended', + "plugin:react-hooks/recommended" + ], globals: { page: true, REACT_APP_ENV: true, }, rules: { '@typescript-eslint/no-use-before-define': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off' }, }; diff --git a/react-ui/package.json b/react-ui/package.json index 1a8d7ebc..fbc014d1 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -114,6 +114,7 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-react-hooks": "~5.2.0", "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 26dfa334..7c026d3d 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -168,7 +168,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { } }; -export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { +export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { //console.log('patchRoutes', e); }; @@ -232,7 +232,7 @@ export const antd: RuntimeAntdConfig = (memo) => { memo.theme.components.Table = { headerBg: 'rgba(242, 244, 247, 0.36)', headerBorderRadius: 4, - rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + // rowSelectedBg: 'rgba(22, 100, 255, 0.05)', 固定列时,横向滑动导致重叠 }; memo.theme.components.Tabs = { titleFontSize: 16, diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index c983093e..430971e6 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -33,23 +33,23 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [inputText, setInputText] = useState(undefined); useEffect(() => { + // 获取数据请求 + const getDataList = async () => { + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + code_repo_name: searchText || undefined, + }; + const [res] = await to(getCodeConfigListReq(params)); + if (res && res.data && res.data.content) { + setDataList(res.data.content); + setTotal(res.data.totalElements); + } + }; + getDataList(); }, [pagination, searchText]); - // 获取数据请求 - const getDataList = async () => { - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, - code_repo_name: searchText || undefined, - }; - const [res] = await to(getCodeConfigListReq(params)); - if (res && res.data && res.data.content) { - setDataList(res.data.content); - setTotal(res.data.totalElements); - } - }; - // 搜索 const handleSearch = (value: string) => { setSearchText(value); diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index ef96c914..aa292d47 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -45,23 +45,20 @@ type IframePageProps = { function IframePage({ type, className, style }: IframePageProps) { const [iframeUrl, setIframeUrl] = useState(''); const [loading, setLoading] = useState(false); + useEffect(() => { - requestIframeUrl(); - return () => { - if (type === IframePageType.DevEnv) { - SessionStorage.removeItem(SessionStorage.editorUrlKey); + const requestIframeUrl = async () => { + setLoading(true); + const [res] = await to(getRequestAPI(type)()); + if (res && res.data) { + setIframeUrl(res.data); + } else { + setLoading(false); } }; - }, []); - const requestIframeUrl = async () => { - setLoading(true); - const [res] = await to(getRequestAPI(type)()); - if (res && res.data) { - setIframeUrl(res.data); - } else { - setLoading(false); - } - }; + + requestIframeUrl(); + }, [type]); const hideLoading = () => { setLoading(false); diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 182db352..f1902e5c 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -39,8 +39,20 @@ function ParameterSelect({ const valueText = typeof value === 'object' && value !== null ? value.value : value; useEffect(() => { + // 获取下拉数据 + const getSelectOptions = async () => { + if (!propsConfig) { + return; + } + const getOptions = propsConfig.getOptions; + const [res] = await to(getOptions()); + if (res) { + setOptions(res); + } + }; + getSelectOptions(); - }, []); + }, [propsConfig]); const handleChange = (text: string) => { if (typeof value === 'object' && value !== null) { @@ -53,18 +65,7 @@ function ParameterSelect({ } }; - // 获取下拉数据 - const getSelectOptions = async () => { - if (!propsConfig) { - return; - } - const getOptions = propsConfig.getOptions; - const [res] = await to(getOptions()); - if (res) { - setOptions(res); - } - }; - + // 只用于展示,FormInfo 组件带有 Tooltip if (display) { return ( { const resource = selectedResource; diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index 24e97a4d..bbc78db3 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -91,16 +91,7 @@ function ResourceSelectorModal({ const treeRef = useRef(null); const config = selectorTypeConfig[type]; - useEffect(() => { - setExpandedKeys([]); - setCheckedKeys([]); - setLoadedKeys([]); - setFiles([]); - setVersionPath(''); - setSearchText(''); - getTreeData(); - }, [activeTab, type]); - + // 搜索 const treeData = useMemo( () => originTreeData.filter((v) => @@ -109,19 +100,45 @@ function ResourceSelectorModal({ [originTreeData, searchText], ); - // 获取数据集\模型\镜像列表 - const getTreeData = async () => { - const isPublic = activeTab === CommonTabKeys.Private ? false : true; - const [res] = await to(config.getList(isPublic)); - if (res) { - setOriginTreeData(res); + useEffect(() => { + // 获取数据集\模型\镜像列表 + const getTreeData = async () => { + const isPublic = activeTab === CommonTabKeys.Private ? false : true; + const [res] = await to(config.getList(isPublic)); + if (res) { + setOriginTreeData(res); - // 恢复上一次的 Expand 操作 - restoreLastExpand(); - } else { - setOriginTreeData([]); - } - }; + // 恢复上一次的 Expand 操作 + setFirstLoadList(true); + } else { + setOriginTreeData([]); + } + }; + + setExpandedKeys([]); + setCheckedKeys([]); + setLoadedKeys([]); + setFiles([]); + setVersionPath(''); + setSearchText(''); + getTreeData(); + }, [activeTab, config]); + + useEffect(() => { + // 恢复上一次的 Expand 操作 + // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys + // fisrtLoadList 标志位 + const restoreLastExpand = () => { + if (firstLoadList && Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length > 0) { + setExpandedKeys(defaultExpandedKeys); + // 延时滑动到 defaultExpandedKeys,不然不会加载 defaultExpandedKeys,不然不会加载版本 + setTimeout(() => { + treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); + }, 100); + } + }; + restoreLastExpand(); + }, [firstLoadList, defaultExpandedKeys]); // 获取数据集\模型\镜像版本列表 const getVersions = async (parentId: string, parentNode: any) => { @@ -136,10 +153,10 @@ function ResourceSelectorModal({ setLoadedKeys((prev) => prev.concat(parentId)); } - // 恢复上一次的 Check 操作 + // 恢复上一次的 Check 操作,需要延时以便 TreeData 更新完 setTimeout(() => { restoreLastCheck(parentId, res); - }, 300); + }, 100); } else { setExpandedKeys([]); return Promise.reject(error); @@ -158,7 +175,7 @@ function ResourceSelectorModal({ } }; - // 动态加载 tree children + // 展开时,动态加载 tree children const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { if (children) { return Promise.resolve(); @@ -187,42 +204,25 @@ function ResourceSelectorModal({ } }; - // 恢复上一次的 Expand 操作 - // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys - // fisrtLoadList 标志位 - const restoreLastExpand = () => { - if (!firstLoadList && defaultExpandedKeys.length > 0) { - setTimeout(() => { - setExpandedKeys(defaultExpandedKeys); - setFirstLoadList(true); - setTimeout(() => { - treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); - }, 100); - }, 0); - } - }; - // 恢复上一次的 Check 操作 // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 // fisrtLoadVersions 标志位 const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { - if (!firstLoadVersions && defaultCheckedKeys.length > 0) { + if (!firstLoadVersions && Array.isArray(defaultCheckedKeys) && defaultCheckedKeys.length > 0) { const last = defaultCheckedKeys[0] as string; const { id } = getIdAndVersion(last); // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 if (id === parentId) { + setCheckedKeys(defaultCheckedKeys); + const parentNode = versions.find((v) => v.key === last); + getFiles(last, parentNode); + setFirstLoadVersions(true); setTimeout(() => { - setCheckedKeys(defaultCheckedKeys); - const parentNode = versions.find((v) => v.key === last); - getFiles(last, parentNode); - setFirstLoadVersions(true); - setTimeout(() => { - treeRef?.current?.scrollTo({ - key: defaultCheckedKeys[0], - align: 'bottom', - }); - }, 100); - }, 0); + treeRef?.current?.scrollTo({ + key: defaultCheckedKeys[0], + align: 'bottom', + }); + }, 100); } } }; diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index f5ef64af..59ba1d6e 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -105,6 +105,7 @@ export function useDomSize( return () => { window.removeEventListener('resize', debounceFunc); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [domRef, ...deps]); return [domRef, { width, height }] as const; @@ -136,10 +137,10 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => { * Executes the effect function when the specified condition is true. * * @param effect - The effect function to execute. - * @param deps - The dependencies for the effect. * @param when - The condition to trigger the effect. + * @param deps - The dependencies for the effect. */ -export const useEffectWhen = (effect: () => void, deps: React.DependencyList, when: boolean) => { +export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => { const requestFns = useRef<(() => void)[]>([]); useEffect(() => { if (when) { @@ -147,6 +148,7 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh } else { requestFns.current.splice(0, 1, effect); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); useEffect(() => { @@ -185,7 +187,7 @@ export const useCheck = (list: T[]) => { } }); }, - [selected, isSingleChecked], + [isSingleChecked], ); return [ diff --git a/react-ui/src/hooks/resource.ts b/react-ui/src/hooks/resource.ts index 82a3ee78..cb4434ab 100644 --- a/react-ui/src/hooks/resource.ts +++ b/react-ui/src/hooks/resource.ts @@ -5,40 +5,39 @@ */ import { getComputingResourceReq } from '@/services/pipeline'; -import computingResourceState, { setComputingResource } from '@/state/computingResourceStore'; import { ComputingResource } from '@/types'; import { to } from '@/utils/promise'; import { type SelectProps } from 'antd'; import { useCallback, useEffect, useState } from 'react'; -import { useSnapshot } from 'umi'; + +const computingResource: ComputingResource[] = []; // 获取资源规格 export function useComputingResource() { const [resourceStandardList, setResourceStandardList] = useState([]); - const snap = useSnapshot(computingResourceState); useEffect(() => { - if (snap.computingResource.length > 0) { - setResourceStandardList(snap.computingResource as ComputingResource[]); + // 获取资源规格列表数据 + const getComputingResource = async () => { + const params = { + page: 0, + size: 1000, + resource_type: '', + }; + const [res] = await to(getComputingResourceReq(params)); + if (res && res.data && res.data.content) { + setResourceStandardList(res.data.content); + computingResource.splice(0, computingResource.length, ...res.data.content); + } + }; + + if (computingResource.length > 0) { + setResourceStandardList(computingResource); } else { getComputingResource(); } }, []); - // 获取资源规格列表数据 - const getComputingResource = useCallback(async () => { - const params = { - page: 0, - size: 1000, - resource_type: '', - }; - const [res] = await to(getComputingResourceReq(params)); - if (res && res.data && res.data.content) { - setResourceStandardList(res.data.content); - setComputingResource(res.data.content); - } - }, []); - // 过滤资源规格 const filterResourceStandard: SelectProps['filterOption'] = useCallback((input: string, option?: ComputingResource) => { diff --git a/react-ui/src/hooks/sessionStorage.ts b/react-ui/src/hooks/sessionStorage.ts deleted file mode 100644 index 8cf3c921..00000000 --- a/react-ui/src/hooks/sessionStorage.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * @Author: 赵伟 - * @Date: 2024-11-06 14:53:37 - * @Description: SessionStorage hook - */ - -import SessionStorage from '@/utils/sessionStorage'; -import { useEffect, useState } from 'react'; - -// 读取缓存数据,组件卸载时清除缓存 -export function useSessionStorage(key: string, isObject: boolean, initialValue: T) { - const [storage, setStorage] = useState(initialValue); - - useEffect(() => { - const res = SessionStorage.getItem(key, isObject); - if (res) { - setStorage(res); - } - return () => { - SessionStorage.removeItem(key); - }; - }, []); - - return [storage]; -} diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index ec016c3c..b5ceae7c 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -29,60 +29,60 @@ function CreateAutoML() { const isCopy = pathname.includes('copy'); useEffect(() => { + // 获取服务详情 + const getAutoMLInfo = async (id: number) => { + const [res] = await to(getAutoMLInfoReq({ id })); + if (res && res.data) { + const autoMLInfo: AutoMLData = res.data; + const { + include_classifier: include_classifier_str, + include_feature_preprocessor: include_feature_preprocessor_str, + include_regressor: include_regressor_str, + exclude_classifier: exclude_classifier_str, + exclude_feature_preprocessor: exclude_feature_preprocessor_str, + exclude_regressor: exclude_regressor_str, + metrics: metrics_str, + ml_name: ml_name_str, + ...rest + } = autoMLInfo; + const include_classifier = include_classifier_str?.split(',').filter(Boolean); + const include_feature_preprocessor = include_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const include_regressor = include_regressor_str?.split(',').filter(Boolean); + const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); + const exclude_feature_preprocessor = exclude_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); + const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; + const metrics = Object.entries(metricsObj).map(([key, value]) => ({ + name: key, + value, + })); + const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; + + const formData = { + ...rest, + include_classifier, + include_feature_preprocessor, + include_regressor, + exclude_classifier, + exclude_feature_preprocessor, + exclude_regressor, + metrics, + ml_name, + }; + + form.setFieldsValue(formData); + } + }; + // 编辑,复制 if (id && !Number.isNaN(id)) { getAutoMLInfo(id); } - }, [id]); - - // 获取服务详情 - const getAutoMLInfo = async (id: number) => { - const [res] = await to(getAutoMLInfoReq({ id })); - if (res && res.data) { - const autoMLInfo: AutoMLData = res.data; - const { - include_classifier: include_classifier_str, - include_feature_preprocessor: include_feature_preprocessor_str, - include_regressor: include_regressor_str, - exclude_classifier: exclude_classifier_str, - exclude_feature_preprocessor: exclude_feature_preprocessor_str, - exclude_regressor: exclude_regressor_str, - metrics: metrics_str, - ml_name: ml_name_str, - ...rest - } = autoMLInfo; - const include_classifier = include_classifier_str?.split(',').filter(Boolean); - const include_feature_preprocessor = include_feature_preprocessor_str - ?.split(',') - .filter(Boolean); - const include_regressor = include_regressor_str?.split(',').filter(Boolean); - const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); - const exclude_feature_preprocessor = exclude_feature_preprocessor_str - ?.split(',') - .filter(Boolean); - const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); - const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; - const metrics = Object.entries(metricsObj).map(([key, value]) => ({ - name: key, - value, - })); - const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; - - const formData = { - ...rest, - include_classifier, - include_feature_preprocessor, - include_regressor, - exclude_classifier, - exclude_feature_preprocessor, - exclude_regressor, - metrics, - ml_name, - }; - - form.setFieldsValue(formData); - } - }; + }, [id, form, isCopy]); // 创建、更新、复制实验 const createExperiment = async (formData: FormData) => { diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx index 0d0ec460..83f7dea0 100644 --- a/react-ui/src/pages/AutoML/Info/index.tsx +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -19,18 +19,18 @@ function AutoMLInfo() { const [autoMLInfo, setAutoMLInfo] = useState(undefined); useEffect(() => { + // 获取自动机器学习详情 + const getAutoMLInfo = async () => { + const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + if (res && res.data) { + setAutoMLInfo(res.data); + } + }; + if (autoMLId) { getAutoMLInfo(); } - }, []); - - // 获取自动机器学习详情 - const getAutoMLInfo = async () => { - const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); - if (res && res.data) { - setAutoMLInfo(res.data); - } - }; + }, [autoMLId]); return (
diff --git a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx index fa694b27..09d0cd6e 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx @@ -25,36 +25,36 @@ type TableData = { function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { const [tableData, setTableData] = useState([]); useEffect(() => { + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: item[1]?.[5]?.accuracy, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + if (fileUrl) { getHistoryFile(); } - }, [fileUrl]); - - // 获取实验运行历史记录 - const getHistoryFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - const data: any[] = res.data; - const list: TableData[] = data.map((item) => { - return { - id: item[0]?.[0], - accuracy: item[1]?.[5]?.accuracy, - duration: item[1]?.[5]?.duration, - train_loss: item[1]?.[5]?.train_loss, - status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], - }; - }); - list.forEach((item) => { - if (!item.id) return; - const config = (res as any).configs?.[item.id]; - item.feature = config?.['feature_preprocessor:__choice__']; - item.althorithm = isClassification - ? config?.['classifier:__choice__'] - : config?.['regressor:__choice__']; - }); - setTableData(list); - } - }; + }, [fileUrl, isClassification]); const columns: TableProps['columns'] = [ { diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx index 9c8ea687..aded5f2a 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -53,7 +53,7 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInsList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index bcc85a2f..b4e7f24b 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -28,7 +28,7 @@ import { } from 'antd'; import { type SearchProps } from 'antd/es/input'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import ExperimentInstance from '../ExperimentInstance'; import { ExperimentListType, experimentListConfig } from './config'; import styles from './index.less'; @@ -58,12 +58,8 @@ function ExperimentList({ type }: ExperimentListProps) { ); const config = experimentListConfig[type]; - useEffect(() => { - getAutoMLList(); - }, [pagination, searchText]); - // 获取自主机器学习或超参数自动优化列表 - const getAutoMLList = async () => { + const getAutoMLList = useCallback(async () => { const params: Record = { page: pagination.current! - 1, size: pagination.pageSize, @@ -76,7 +72,11 @@ function ExperimentList({ type }: ExperimentListProps) { setTableData(content); setTotal(totalElements); } - }; + }, [pagination, searchText, config]); + + useEffect(() => { + getAutoMLList(); + }, [getAutoMLList]); // 搜索 const onSearch: SearchProps['onSearch'] = (value) => { diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx index a826155d..8680bf60 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx @@ -22,19 +22,19 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp }, [imageUrl]); useEffect(() => { + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + if (fileUrl) { getResultFile(); } }, [fileUrl]); - // 获取实验运行历史记录 - const getResultFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - setResult(res as any as string); - } - }; - return (
diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 0c484e54..3f567465 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -13,7 +13,7 @@ import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { App, Button, Input, Pagination, PaginationProps } from 'antd'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; import CodeConfigItem from '../components/CodeConfigItem'; import styles from './index.less'; @@ -50,12 +50,8 @@ function CodeConfigList() { const [inputText, setInputText] = useState(undefined); const { message } = App.useApp(); - useEffect(() => { - getDataList(); - }, [pagination, searchText]); - // 获取数据请求 - const getDataList = async () => { + const getDataList = useCallback(async () => { const params = { page: pagination.current! - 1, size: pagination.pageSize, @@ -69,7 +65,11 @@ function CodeConfigList() { setDataList([]); setTotal(0); } - }; + }, [pagination, searchText]); + + useEffect(() => { + getDataList(); + }, [getDataList]); // 删除请求 const deleteRecord = async (id: number) => { diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx index c0bfdb33..c74a3cc8 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -18,7 +18,7 @@ import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { useParams, useSearchParams } from '@umijs/max'; import { App, Button, Flex, Select, Tabs } from 'antd'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import AddVersionModal from '../AddVersionModal'; import ResourceIntro from '../ResourceIntro'; import ResourceVersion from '../ResourceVersion'; @@ -45,7 +45,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { // 模型演化传入的 tab const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; // 模型演化传入的版本 - let versionParam = searchParams.get('version'); + const versionParam = searchParams.get('version'); const name = searchParams.get('name') || ''; const owner = searchParams.get('owner') || ''; const identifier = searchParams.get('identifier') || ''; @@ -57,63 +57,60 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const typeName = config.name; // 数据集/模型 const { message } = App.useApp(); - useEffect(() => { - getVersionList(); - }, [resourceId, owner, identifier]); - - useEffect(() => { - if (version) { - getResourceDetail({ - id: resourceId, - owner, - name, - identifier, - version, - is_public: is_public, - }); - } - }, [version]); - // 获取详情 - const getResourceDetail = async (params: { - owner: string; - name: string; - id: number; - identifier: string; - version?: string; - is_public: boolean; - }) => { + const getResourceDetail = useCallback(async () => { + const params = { + id: resourceId, + owner, + name, + identifier, + version, + is_public, + }; const request = config.getInfo; const [res] = await to(request(params)); if (res && res.data) { setInfo(res.data); } - }; + }, [config, resourceId, owner, name, identifier, version, is_public]); // 获取版本列表 - const getVersionList = async () => { - const request = config.getVersions; - const [res] = await to( - request({ - owner, - identifier, - }), - ); - if (res && res.data && res.data.length > 0) { - setVersionList(res.data); - if ( - versionParam && - res.data.find((item: ResourceVersionData) => item.name === versionParam) - ) { - setVersion(versionParam); - versionParam = null; + const getVersionList = useCallback( + async (refresh: boolean) => { + const request = config.getVersions; + const [res] = await to( + request({ + owner, + identifier, + }), + ); + if (res && res.data && res.data.length > 0) { + setVersionList(res.data); + if ( + !refresh && + versionParam && + res.data.find((item: ResourceVersionData) => item.name === versionParam) + ) { + setVersion(versionParam); + } else { + setVersion(res.data[0].name); + } } else { - setVersion(res.data[0].name); + setVersion(undefined); } - } else { - setVersion(undefined); + }, + [config, owner, identifier, versionParam], + ); + + useEffect(() => { + if (version) { + getResourceDetail(); } - }; + }, [version, getResourceDetail]); + + useEffect(() => { + getVersionList(false); + }, [getVersionList]); // 新建版本 const showModal = () => { @@ -125,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { identifier: info.identifier, is_public: is_public, onOk: () => { - getVersionList(); + getVersionList(true); close(); }, }); @@ -172,12 +169,12 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const [res] = await to(request(params)); if (res) { message.success('删除成功'); - getVersionList(); + getVersionList(true); } }; // 处理删除 - const hanldeDelete = () => { + const handleDelete = () => { modalConfirm({ title: '删除后,该版本将不可恢复', content: '是否确认删除?', @@ -268,7 +265,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {