| @@ -60,3 +60,5 @@ mvnw | |||
| **/node_modules | |||
| *storybook.log | |||
| /react-ui/docs | |||
| @@ -5,4 +5,5 @@ | |||
| public | |||
| dist | |||
| .umi | |||
| mock | |||
| mock | |||
| /src/iconfont/ | |||
| @@ -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' | |||
| }, | |||
| }; | |||
| @@ -1 +1 @@ | |||
| v18.16.0 | |||
| v18.20.7 | |||
| @@ -0,0 +1,19 @@ | |||
| import { Of, useOf } from '@storybook/blocks'; | |||
| /** | |||
| * A block that displays the story name or title from the of prop | |||
| * - if a story reference is passed, it renders the story name | |||
| * - if a meta reference is passed, it renders the stories' title | |||
| * - if nothing is passed, it defaults to the primary story | |||
| */ | |||
| export const StoryName = ({ of }: { of?: Of }) => { | |||
| const resolvedOf = useOf(of || 'story', ['story', 'meta']); | |||
| switch (resolvedOf.type) { | |||
| case 'story': { | |||
| return <h3 className="css-wzniqs">{resolvedOf.story.name}</h3>; | |||
| } | |||
| case 'meta': { | |||
| return <h3 className="css-wzniqs">{resolvedOf.preparedMeta.title}</h3>; | |||
| } | |||
| } | |||
| }; | |||
| @@ -16,7 +16,7 @@ const config: StorybookConfig = { | |||
| name: '@storybook/react-webpack5', | |||
| options: {}, | |||
| }, | |||
| staticDirs: ['../static'], | |||
| staticDirs: ['../public', { from: '../docs', to: '/docs' }], | |||
| docs: { | |||
| defaultName: 'Documentation', | |||
| }, | |||
| @@ -0,0 +1,6 @@ | |||
| import { addons } from '@storybook/manager-api'; | |||
| import theme from './theme'; | |||
| addons.setConfig({ | |||
| theme: theme, | |||
| }); | |||
| @@ -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', | |||
| ], | |||
| ], | |||
| }, | |||
| ], | |||
| }; | |||
| @@ -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: { | |||
| @@ -0,0 +1,7 @@ | |||
| import { create } from '@storybook/theming'; | |||
| export default create({ | |||
| base: 'light', | |||
| brandTitle: '组件库文档', | |||
| brandUrl: 'https://storybook.js.org/docs', | |||
| brandTarget: '_blank', | |||
| }); | |||
| @@ -1,133 +1 @@ | |||
| Language : 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md) | |||
| <h1 align="center">Ant Design Pro</h1> | |||
| <div align="center"> | |||
| An out-of-box UI solution for enterprise applications as a React boilerplate. | |||
| [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master)   | |||
| [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](http://umijs.org/)  | |||
|  | |||
| </div> | |||
| - Preview: http://preview.pro.ant.design | |||
| - Home Page: http://pro.ant.design | |||
| - Documentation: http://pro.ant.design/docs/getting-started | |||
| - ChangeLog: http://pro.ant.design/docs/changelog | |||
| - FAQ: http://pro.ant.design/docs/faq | |||
| - Mirror Site in China: http://ant-design-pro.gitee.io | |||
| ## 5.0 is out! 🎉🎉🎉 | |||
| [Ant Design Pro 5.0.0](https://github.com/ant-design/ant-design-pro/issues/8656) | |||
| ## Translation Recruitment :loudspeaker: | |||
| We need your help: https://github.com/ant-design/ant-design-pro/issues/120 | |||
| ## Features | |||
| - :bulb: **TypeScript**: A language for application-scale JavaScript | |||
| - :scroll: **Blocks**: Build page with block template | |||
| - :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/) | |||
| - :triangular_ruler: **Common Templates**: Typical templates for enterprise applications | |||
| - :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd | |||
| - :iphone: **Responsive**: Designed for variable screen sizes | |||
| - :art: **Theming**: Customizable theme with simple config | |||
| - :globe_with_meridians: **International**: Built-in i18n solution | |||
| - :gear: **Best Practices**: Solid workflow to make your code healthy | |||
| - :1234: **Mock development**: Easy to use mock development solution | |||
| - :white_check_mark: **UI Test**: Fly safely with unit and e2e tests | |||
| ## Templates | |||
| ``` | |||
| - Dashboard | |||
| - Analytic | |||
| - Monitor | |||
| - Workspace | |||
| - Form | |||
| - Basic Form | |||
| - Step Form | |||
| - Advanced From | |||
| - List | |||
| - Standard Table | |||
| - Standard List | |||
| - Card List | |||
| - Search List (Project/Applications/Article) | |||
| - Profile | |||
| - Simple Profile | |||
| - Advanced Profile | |||
| - Account | |||
| - Account Center | |||
| - Account Settings | |||
| - Result | |||
| - Success | |||
| - Failed | |||
| - Exception | |||
| - 403 | |||
| - 404 | |||
| - 500 | |||
| - User | |||
| - Login | |||
| - Register | |||
| - Register Result | |||
| ``` | |||
| ## Usage | |||
| ### Use bash | |||
| We provide pro-cli to quickly initialize scaffolding. | |||
| ```bash | |||
| # use npm | |||
| npm i @ant-design/pro-cli -g | |||
| pro create myapp | |||
| ``` | |||
| select umi version | |||
| ```shell | |||
| 🐂 Use umi@4 or umi@3 ? (Use arrow keys) | |||
| ❯ umi@4 | |||
| umi@3 | |||
| ``` | |||
| > If the umi@4 version is selected, full blocks are not yet supported. | |||
| If you choose umi@3, you can also choose the pro template. Pro is the basic template, which only provides the basic content of the framework operation. Complete contains all blocks, which is not suitable for secondary development as a basic template. | |||
| ```shell | |||
| ? 🚀 Full or a simple scaffold? (Use arrow keys) | |||
| ❯ simple | |||
| complete | |||
| ``` | |||
| Install dependencies: | |||
| ```shell | |||
| $ cd myapp && tyarn | |||
| // or | |||
| $ cd myapp && npm install | |||
| ``` | |||
| ## Browsers support | |||
| Modern browsers. | |||
| | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera | | |||
| | --- | --- | --- | --- | --- | | |||
| | Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | | |||
| ## Contributing | |||
| Any type of contribution is welcome, here are some examples of how you may contribute to this project: | |||
| - Use Ant Design Pro in your daily work. | |||
| - Submit [issues](http://github.com/ant-design/ant-design-pro/issues) to report bugs or ask questions. | |||
| - Propose [pull requests](http://github.com/ant-design/ant-design-pro/pulls) to improve our code. | |||
| # Documentation | |||
| @@ -327,55 +327,55 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '模型部署', | |||
| path: '/modelDeployment', | |||
| routes: [ | |||
| { | |||
| name: '模型部署', | |||
| path: '', | |||
| component: './ModelDeployment/List', | |||
| }, | |||
| { | |||
| name: '创建推理服务', | |||
| path: 'createService', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '编辑推理服务', | |||
| path: 'editService/:serviceId', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:serviceId', | |||
| path: 'modelDeployment', | |||
| routes: [ | |||
| { | |||
| name: '服务详情', | |||
| name: '模型部署', | |||
| path: '', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'createVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| component: './ModelDeployment/List', | |||
| }, | |||
| { | |||
| name: '更新服务版本', | |||
| path: 'updateVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| name: '创建推理服务', | |||
| path: 'createService', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '重启服务版本', | |||
| path: 'restartVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| name: '编辑推理服务', | |||
| path: 'editService/:serviceId', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:serviceId', | |||
| routes: [ | |||
| { | |||
| name: '服务详情', | |||
| path: '', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'createVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '更新服务版本', | |||
| path: 'updateVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '重启服务版本', | |||
| path: 'restartVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -533,6 +533,18 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '算力积分', | |||
| path: '/points', | |||
| routes: [ | |||
| { | |||
| name: '算力积分', | |||
| path: '', | |||
| key: 'points', | |||
| component: './Points/index', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: '*', | |||
| layout: false, | |||
| @@ -1,14 +1,14 @@ | |||
| { | |||
| "name": "ant-design-pro", | |||
| "version": "6.0.0", | |||
| "name": "cl-model", | |||
| "version": "1.0.0", | |||
| "private": true, | |||
| "description": "An out-of-box UI solution for enterprise applications", | |||
| "description": "", | |||
| "scripts": { | |||
| "analyze": "cross-env ANALYZE=1 max build", | |||
| "build": "max build", | |||
| "deploy": "npm run build && npm run gh-pages", | |||
| "dev": "npm run start:dev", | |||
| "dev-no-sso": "NO_SSO=true npm run start:dev", | |||
| "dev-no-sso": "cross-env NO_SSO=true npm run start:dev", | |||
| "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | |||
| "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | |||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | |||
| @@ -16,6 +16,7 @@ | |||
| "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", | |||
| "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", | |||
| "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", | |||
| "docs": "typedoc --entryPointStrategy expand --entryPoints 'src/utils' --skipErrorChecking --out docs", | |||
| "gh-pages": "gh-pages -d dist", | |||
| "i18n-remove": "pro i18n-remove --locale=zh-CN --write", | |||
| "postinstall": "max setup", | |||
| @@ -66,6 +67,7 @@ | |||
| "@types/crypto-js": "^4.2.2", | |||
| "@umijs/route-utils": "^4.0.1", | |||
| "antd": "~5.21.4", | |||
| "caniuse-lite": "~1.0.30001707", | |||
| "classnames": "^2.3.2", | |||
| "crypto-js": "^4.2.0", | |||
| "echarts": "^5.5.0", | |||
| @@ -96,9 +98,11 @@ | |||
| "@storybook/addon-webpack5-compiler-babel": "~3.0.5", | |||
| "@storybook/addon-webpack5-compiler-swc": "~2.0.0", | |||
| "@storybook/blocks": "~8.5.3", | |||
| "@storybook/manager-api": "~8.6.0", | |||
| "@storybook/react": "~8.5.3", | |||
| "@storybook/react-webpack5": "~8.5.3", | |||
| "@storybook/test": "~8.5.3", | |||
| "@storybook/theming": "~8.6.0", | |||
| "@testing-library/react": "^14.0.0", | |||
| "@types/antd": "^1.0.0", | |||
| "@types/express": "^4.17.14", | |||
| @@ -112,6 +116,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", | |||
| @@ -129,6 +134,7 @@ | |||
| "swagger-ui-dist": "^4.18.2", | |||
| "ts-loader": "~9.5.2", | |||
| "ts-node": "^10.9.1", | |||
| "typedoc": "~0.28.1", | |||
| "typescript": "^5.0.4", | |||
| "umi-presets-pro": "^2.0.0" | |||
| }, | |||
| @@ -166,7 +172,7 @@ | |||
| }, | |||
| "msw": { | |||
| "workerDirectory": [ | |||
| "static" | |||
| "public" | |||
| ] | |||
| } | |||
| } | |||
| @@ -1,5 +1,23 @@ | |||
| @font-face { | |||
| font-family: Alibaba; | |||
| src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); | |||
| font-display: swap; | |||
| } | |||
| font-family: Alibaba; | |||
| src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); | |||
| font-display: swap; | |||
| } | |||
| @font-face { | |||
| font-family: 'TaoBaoMaiCaiTi'; | |||
| src: url('./TaoBaoMaiCaiTi-Regular.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.woff') format('woff'), /* 兼容性较好的 woff */ | |||
| url('./TaoBaoMaiCaiTi-Regular.ttf') format('truetype'), /* ttf 作为备选 */ | |||
| url('./TaoBaoMaiCaiTi-Regular.otf') format('opentype'); /* otf 作为最后选项 */ | |||
| font-display: swap; /* 优化页面加载时的字体显示 */ | |||
| } | |||
| @font-face { | |||
| font-family: 'DingTalk-JinBuTi'; | |||
| src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */ | |||
| url('./DingTalk-JinBuTi.woff') format('woff'), /* 兼容性较好的 woff */ | |||
| url('./DingTalk-JinBuTi.ttf') format('truetype'); /* ttf 作为备选 */ | |||
| font-display: swap; /* 优化页面加载时的字体显示 */ | |||
| } | |||
| @@ -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, | |||
| @@ -245,9 +245,12 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||
| }; | |||
| memo.theme.components.Tree = { | |||
| directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', | |||
| }; | |||
| memo.theme.cssVar = true; | |||
| // memo.theme.hashed = false; | |||
| memo.theme.hashed = false; | |||
| memo.appConfig = { | |||
| message: { | |||
| @@ -27,12 +27,12 @@ export type BasicInfoProps = { | |||
| */ | |||
| export default function BasicInfo({ | |||
| datas, | |||
| className, | |||
| style, | |||
| labelWidth, | |||
| labelEllipsis = true, | |||
| threeColumns = false, | |||
| labelAlign = 'start', | |||
| threeColumns = false, | |||
| className, | |||
| style, | |||
| }: BasicInfoProps) { | |||
| return ( | |||
| <div | |||
| @@ -5,16 +5,18 @@ import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | |||
| import './index.less'; | |||
| export type { BasicInfoData, BasicInfoLink }; | |||
| export type BasicTableInfoProps = Omit<BasicInfoProps, 'labelAlign' | 'threeColumns'>; | |||
| /** | |||
| * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 | |||
| */ | |||
| export default function BasicTableInfo({ | |||
| datas, | |||
| className, | |||
| style, | |||
| labelWidth, | |||
| labelEllipsis, | |||
| }: BasicInfoProps) { | |||
| className, | |||
| style, | |||
| }: BasicTableInfoProps) { | |||
| const remainder = datas.length % 4; | |||
| const array = []; | |||
| if (remainder > 0) { | |||
| @@ -30,35 +30,42 @@ function CodeSelect({ | |||
| onChange, | |||
| ...rest | |||
| }: CodeSelectProps) { | |||
| // 选择代码配置 | |||
| const selectResource = () => { | |||
| const { close } = openAntdModal(CodeSelectorModal, { | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const { git_url, git_branch, code_repo_name } = res; | |||
| const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = | |||
| res; | |||
| const jsonObj = { | |||
| id, | |||
| name: code_repo_name, | |||
| code_path: git_url, | |||
| branch: git_branch, | |||
| username: git_user_name, | |||
| password: git_password, | |||
| ssh_private_key: ssh_key, | |||
| }; | |||
| const jsonObjStr = JSON.stringify(jsonObj); | |||
| const showValue = code_repo_name; | |||
| onChange?.({ | |||
| value: jsonObjStr, | |||
| showValue, | |||
| showValue: code_repo_name, | |||
| fromSelect: true, | |||
| ...jsonObj, | |||
| }); | |||
| } else { | |||
| onChange?.({ | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| }); | |||
| onChange?.(undefined); | |||
| } | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = () => { | |||
| onChange?.(undefined); | |||
| }; | |||
| return ( | |||
| <div className={classNames('kf-code-select', className)} style={style}> | |||
| <ParameterInput | |||
| @@ -68,6 +75,7 @@ function CodeSelect({ | |||
| value={value} | |||
| onChange={onChange} | |||
| onClick={selectResource} | |||
| onRemove={handleRemove} | |||
| ></ParameterInput> | |||
| <Button | |||
| className="kf-code-select__button" | |||
| @@ -33,23 +33,23 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| const [inputText, setInputText] = useState<string | undefined>(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); | |||
| @@ -6,7 +6,6 @@ | |||
| background-color: rgba(0, 0, 0, 0.04); | |||
| border: 1px solid #d9d9d9; | |||
| border-radius: 6px; | |||
| cursor: not-allowed; | |||
| .ant-typography { | |||
| margin: 0 !important; | |||
| @@ -1,14 +1,21 @@ | |||
| import { Typography } from 'antd'; | |||
| import { formatEnum } from '@/utils/format'; | |||
| import { Typography, type SelectProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type FormInfoProps = { | |||
| /** 自定义类名 */ | |||
| /** 值 */ | |||
| value?: any; | |||
| /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ | |||
| /** 如果 `value` 是对象,取对象的哪个属性作为值 */ | |||
| valuePropName?: string; | |||
| /** 是否是多行 */ | |||
| multiline?: boolean; | |||
| /** 是否是多行文本 */ | |||
| textArea?: boolean; | |||
| /** 是否是下拉框 */ | |||
| select?: boolean; | |||
| /** 下拉框数据 */ | |||
| options?: SelectProps['options']; | |||
| /** 自定义节点 label、value 的字段 */ | |||
| fieldNames?: SelectProps['fieldNames']; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| @@ -18,21 +25,47 @@ type FormInfoProps = { | |||
| /** | |||
| * 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容 | |||
| */ | |||
| function FormInfo({ value, valuePropName, className, style, multiline = false }: FormInfoProps) { | |||
| const data = value && typeof value === 'object' && valuePropName ? value[valuePropName] : value; | |||
| function FormInfo({ | |||
| value, | |||
| valuePropName, | |||
| textArea = false, | |||
| select = false, | |||
| options, | |||
| fieldNames, | |||
| className, | |||
| style, | |||
| }: FormInfoProps) { | |||
| let showValue = value; | |||
| if (value && typeof value === 'object' && valuePropName) { | |||
| showValue = value[valuePropName]; | |||
| } else if (select === true && options) { | |||
| let _options: SelectProps['options'] = options; | |||
| if (fieldNames) { | |||
| _options = options.map((v) => { | |||
| return { | |||
| ...v, | |||
| label: fieldNames.label && v[fieldNames.label], | |||
| value: fieldNames.value && v[fieldNames.value], | |||
| options: fieldNames.options && v[fieldNames.options], | |||
| }; | |||
| }); | |||
| } | |||
| showValue = formatEnum(_options)(value); | |||
| } | |||
| return ( | |||
| <div | |||
| className={classNames( | |||
| 'form-info', | |||
| { | |||
| 'form-info--multiline': multiline, | |||
| 'form-info--multiline': textArea, | |||
| }, | |||
| className, | |||
| )} | |||
| style={style} | |||
| > | |||
| <Typography.Paragraph ellipsis={multiline ? false : { tooltip: data }}> | |||
| {data} | |||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}> | |||
| {showValue} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| ); | |||
| @@ -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); | |||
| @@ -9,7 +9,7 @@ import './index.less'; | |||
| interface KFSpinProps extends SpinProps { | |||
| /** 加载文本 */ | |||
| label: string; | |||
| label?: string; | |||
| } | |||
| /** 自定义 Spin */ | |||
| @@ -6,7 +6,7 @@ | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Form, Input } from 'antd'; | |||
| import { ConfigProvider, Form, Input } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| @@ -67,7 +67,7 @@ function ParameterInput({ | |||
| allowClear, | |||
| className, | |||
| style, | |||
| size = 'middle', | |||
| size, | |||
| disabled = false, | |||
| id, | |||
| ...rest | |||
| @@ -81,10 +81,17 @@ function ParameterInput({ | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| const InputComponent = textArea ? Input.TextArea : Input; | |||
| const { status } = Form.Item.useStatus(); | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| const mySize = size || componentSize; | |||
| // 删除 | |||
| const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { | |||
| e.stopPropagation(); | |||
| if (onRemove) { | |||
| onRemove(); | |||
| return; | |||
| } | |||
| onChange?.({ | |||
| ...valueObj, | |||
| value: undefined, | |||
| @@ -94,7 +101,6 @@ function ParameterInput({ | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| onRemove?.(); | |||
| }; | |||
| return ( | |||
| @@ -104,8 +110,8 @@ function ParameterInput({ | |||
| id={id} | |||
| className={classNames( | |||
| 'parameter-input', | |||
| { 'parameter-input--large': size === 'large' }, | |||
| { 'parameter-input--small': size === 'small' }, | |||
| { 'parameter-input--large': mySize === 'large' }, | |||
| { 'parameter-input--small': mySize === 'small' }, | |||
| { [`parameter-input--${status}`]: status }, | |||
| className, | |||
| )} | |||
| @@ -128,7 +134,7 @@ function ParameterInput({ | |||
| <InputComponent | |||
| {...rest} | |||
| id={id} | |||
| size={size} | |||
| size={mySize} | |||
| className={className} | |||
| style={style} | |||
| placeholder={placeholder} | |||
| @@ -1,21 +1,10 @@ | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource'; | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||
| import { getComputingResourceReq } from '@/services/pipeline'; | |||
| import { ComputingResource } from '@/types'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| // 过滤资源规格 | |||
| const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| input: string, | |||
| option?: ComputingResource, | |||
| ) => { | |||
| return ( | |||
| option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false | |||
| ); | |||
| }; | |||
| // id 从 number 转换为 string | |||
| const convertId = (item: any) => ({ | |||
| ...item, | |||
| @@ -86,17 +75,10 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| }, | |||
| resource: { | |||
| getOptions: async () => { | |||
| const res = await getComputingResourceReq({ | |||
| page: 0, | |||
| size: 1000, | |||
| resource_type: '', | |||
| }); | |||
| return res?.data?.content ?? []; | |||
| }, | |||
| fieldNames: { | |||
| label: 'description', | |||
| value: 'standard', | |||
| // 不需要这个函数 | |||
| return []; | |||
| }, | |||
| fieldNames: resourceFieldNames, | |||
| filterOption: filterResourceStandard as SelectProps['filterOption'], | |||
| }, | |||
| }; | |||
| @@ -4,57 +4,92 @@ | |||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||
| */ | |||
| import { PipelineNodeModelParameter } from '@/types'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Select } from 'antd'; | |||
| import { Select, type SelectProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import FormInfo from '../FormInfo'; | |||
| import { paramSelectConfig } from './config'; | |||
| type ParameterSelectProps = { | |||
| value?: PipelineNodeModelParameter; | |||
| onChange?: (value: PipelineNodeModelParameter) => void; | |||
| disabled?: boolean; | |||
| export type ParameterSelectObject = { | |||
| value: any; | |||
| [key: string]: any; | |||
| }; | |||
| function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState([]); | |||
| const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); | |||
| const { item_type } = valueNonNullable; | |||
| const propsConfig = paramSelectConfig[item_type]; | |||
| export interface ParameterSelectProps extends SelectProps { | |||
| /** 类型 */ | |||
| dataType: 'dataset' | 'model' | 'service' | 'resource'; | |||
| /** 是否只是展示信息 */ | |||
| display?: boolean; | |||
| /** 值,支持对象,对象必须包含 value */ | |||
| value?: string | ParameterSelectObject; | |||
| /** 修改后回调 */ | |||
| onChange?: (value: string | ParameterSelectObject) => void; | |||
| } | |||
| /** 参数选择器,支持资源规格、数据集、模型、服务 */ | |||
| function ParameterSelect({ | |||
| dataType, | |||
| display = false, | |||
| value, | |||
| onChange, | |||
| ...rest | |||
| }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState<SelectProps['options']>([]); | |||
| const propsConfig = paramSelectConfig[dataType]; | |||
| const valueText = typeof value === 'object' && value !== null ? value.value : value; | |||
| const [resourceStandardList] = useComputingResource(); | |||
| useEffect(() => { | |||
| // 获取下拉数据 | |||
| const getSelectOptions = async () => { | |||
| if (!propsConfig) { | |||
| return; | |||
| } | |||
| const getOptions = propsConfig.getOptions; | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| } | |||
| }; | |||
| getSelectOptions(); | |||
| }, []); | |||
| }, [propsConfig]); | |||
| const hangleChange = (e: string) => { | |||
| onChange?.({ | |||
| ...valueNonNullable, | |||
| value: e, | |||
| }); | |||
| }; | |||
| const selectOptions = dataType === 'resource' ? resourceStandardList : options; | |||
| // 获取下拉数据 | |||
| const getSelectOptions = async () => { | |||
| if (!propsConfig) { | |||
| return; | |||
| } | |||
| const getOptions = propsConfig.getOptions; | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| const handleChange = (text: string) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: text, | |||
| }); | |||
| } else { | |||
| onChange?.(text); | |||
| } | |||
| }; | |||
| // 只用于展示,FormInfo 组件带有 Tooltip | |||
| if (display) { | |||
| return ( | |||
| <FormInfo | |||
| select | |||
| value={valueText} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| ></FormInfo> | |||
| ); | |||
| } | |||
| return ( | |||
| <Select | |||
| placeholder={valueNonNullable.placeholder} | |||
| {...rest} | |||
| filterOption={propsConfig?.filterOption} | |||
| options={options} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| value={valueNonNullable.value} | |||
| optionFilterProp={propsConfig.optionFilterProp} | |||
| onChange={hangleChange} | |||
| disabled={disabled} | |||
| optionFilterProp={propsConfig?.optionFilterProp} | |||
| value={valueText} | |||
| onChange={handleChange} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| @@ -6,15 +6,14 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorResponse, | |||
| type ResourceSelectorResponse, | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/components/ResourceSelectorModal'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button } from 'antd'; | |||
| import { Button, ConfigProvider } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | |||
| import './index.less'; | |||
| @@ -46,43 +45,40 @@ function ResourceSelect({ | |||
| onChange, | |||
| ...rest | |||
| }: ResourceSelectProps) { | |||
| const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | |||
| undefined, | |||
| ); | |||
| useEffect(() => { | |||
| if ( | |||
| value && | |||
| typeof value === 'object' && | |||
| value.activeTab && | |||
| value.id && | |||
| value.name && | |||
| value.version && | |||
| value.path && | |||
| (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) | |||
| ) { | |||
| const originResource = pick(value, [ | |||
| 'activeTab', | |||
| 'id', | |||
| 'identifier', | |||
| 'name', | |||
| 'owner', | |||
| 'version', | |||
| 'path', | |||
| ]) as ResourceSelectorResponse; | |||
| setSelectedResource(originResource); | |||
| } | |||
| }, [value]); | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| const mySize = size || componentSize; | |||
| let selectedResource: ResourceSelectorResponse | undefined = undefined; | |||
| if ( | |||
| value && | |||
| typeof value === 'object' && | |||
| value.activeTab && | |||
| value.id && | |||
| value.name && | |||
| value.version && | |||
| value.path && | |||
| (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) | |||
| ) { | |||
| selectedResource = pick(value, [ | |||
| 'activeTab', | |||
| 'id', | |||
| 'identifier', | |||
| 'name', | |||
| 'owner', | |||
| 'version', | |||
| 'path', | |||
| ]) as ResourceSelectorResponse; | |||
| } | |||
| // 选择数据集、模型、镜像 | |||
| const selectResource = () => { | |||
| const resource = selectedResource; | |||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||
| type, | |||
| defaultExpandedKeys: resource ? [resource.id] : [], | |||
| defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], | |||
| defaultActiveTab: resource?.activeTab, | |||
| defaultExpandedKeys: selectedResource ? [selectedResource.id] : [], | |||
| defaultCheckedKeys: selectedResource | |||
| ? [`${selectedResource.id}-${selectedResource.version}`] | |||
| : [], | |||
| defaultActiveTab: selectedResource?.activeTab, | |||
| onOk: (res) => { | |||
| setSelectedResource(res); | |||
| if (res) { | |||
| const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| @@ -116,32 +112,32 @@ function ResourceSelect({ | |||
| }); | |||
| } | |||
| } else { | |||
| onChange?.({ | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| }); | |||
| onChange?.(undefined); | |||
| } | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = () => { | |||
| onChange?.(undefined); | |||
| }; | |||
| return ( | |||
| <div className={classNames('kf-resource-select', className)} style={style}> | |||
| <ParameterInput | |||
| {...rest} | |||
| disabled={disabled} | |||
| value={value} | |||
| size={size} | |||
| size={mySize} | |||
| onChange={onChange} | |||
| onRemove={() => setSelectedResource(undefined)} | |||
| onRemove={handleRemove} | |||
| onClick={selectResource} | |||
| ></ParameterInput> | |||
| <Button | |||
| className="kf-resource-select__button" | |||
| size={size} | |||
| size={mySize} | |||
| type="link" | |||
| icon={getSelectBtnIcon(type)} | |||
| disabled={disabled} | |||
| @@ -91,16 +91,7 @@ function ResourceSelectorModal({ | |||
| const treeRef = useRef<TreeRef>(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); | |||
| } | |||
| } | |||
| }; | |||
| @@ -0,0 +1,3 @@ | |||
| .ant-table .ant-table-cell .kf-table-col-title { | |||
| margin-bottom: 0; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-11 10:52:23 | |||
| * @Description: 用于内容可变的表格类标题 | |||
| */ | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type TableColTitleProps = { | |||
| /** 标题 */ | |||
| title: string; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function TableColTitle({ title, className, style }: TableColTitleProps) { | |||
| return ( | |||
| <Typography.Paragraph | |||
| ellipsis={{ tooltip: title }} | |||
| className={classNames('kf-table-col-title', className)} | |||
| style={style} | |||
| > | |||
| {title} | |||
| </Typography.Paragraph> | |||
| ); | |||
| } | |||
| export default TableColTitle; | |||
| @@ -129,3 +129,23 @@ export const hyperParameterOptimizedModeOptions = [ | |||
| { label: '越大越好', value: hyperParameterOptimizedMode.Max }, | |||
| { label: '越小越好', value: hyperParameterOptimizedMode.Min }, | |||
| ]; | |||
| // 超参数 Trail 运行状态 | |||
| export enum HyperParameterTrailStatus { | |||
| PENDING = 'PENDING', // 挂起 | |||
| RUNNING = 'RUNNING', // 运行中 | |||
| TERMINATED = 'TERMINATED', // 成功 | |||
| ERROR = 'ERROR', // 错误 | |||
| PAUSED = 'PAUSED', // 暂停 | |||
| RESTORING = 'RESTORING', // 恢复中 | |||
| } | |||
| // 自动 Trail 运行状态 | |||
| export enum AutoMLTrailStatus { | |||
| TIMEOUT = 'TIMEOUT', // 超时 | |||
| SUCCESS = 'SUCCESS', // 成功 | |||
| FAILURE = 'FAILURE', // 失败 | |||
| CRASHED = 'CRASHED', // 崩溃 | |||
| STOP = 'STOP', // 停止 | |||
| CANCELLED = 'CANCELLED', // 取消 | |||
| } | |||
| @@ -105,6 +105,7 @@ export function useDomSize<T extends HTMLElement>( | |||
| 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 = <T>(list: T[]) => { | |||
| } | |||
| }); | |||
| }, | |||
| [selected, isSingleChecked], | |||
| [isSingleChecked], | |||
| ); | |||
| return [ | |||
| @@ -5,56 +5,65 @@ | |||
| */ | |||
| 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 const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| input: string, | |||
| option?: ComputingResource, | |||
| ) => { | |||
| return ( | |||
| option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false | |||
| ); | |||
| }; | |||
| // 资源规格字段 | |||
| export const resourceFieldNames = { | |||
| label: 'description', | |||
| value: 'id', | |||
| }; | |||
| // 获取资源规格 | |||
| export function useComputingResource() { | |||
| const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | |||
| const computingResourceSnap = useSnapshot(computingResourceState); | |||
| useEffect(() => { | |||
| if (computingResourceSnap.computingResource.length > 0) { | |||
| setResourceStandardList(computingResourceSnap.computingResource as ComputingResource[]); | |||
| // 获取资源规格列表数据 | |||
| const getComputingResource = async () => { | |||
| const params = { | |||
| page: 0, | |||
| size: 1000, | |||
| resource_type: '', | |||
| }; | |||
| const [res] = await to(getComputingResourceReq(params)); | |||
| if (res && res.data && Array.isArray(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<string, ComputingResource>['filterOption'] = | |||
| useCallback((input: string, option?: ComputingResource) => { | |||
| return ( | |||
| option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? | |||
| false | |||
| ); | |||
| }, []); | |||
| // 根据 standard 获取 description | |||
| const getDescription = useCallback( | |||
| (standard: string) => { | |||
| return resourceStandardList.find((item) => item.standard === standard)?.description; | |||
| (id?: string | number) => { | |||
| if (!id) { | |||
| return undefined; | |||
| } | |||
| return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; | |||
| }, | |||
| [resourceStandardList], | |||
| ); | |||
| return [resourceStandardList, filterResourceStandard, getDescription] as const; | |||
| return [resourceStandardList, getDescription] as const; | |||
| } | |||
| @@ -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<T>(key: string, isObject: boolean, initialValue: T) { | |||
| const [storage, setStorage] = useState<T>(initialValue); | |||
| useEffect(() => { | |||
| const res = SessionStorage.getItem(key, isObject); | |||
| if (res) { | |||
| setStorage(res); | |||
| } | |||
| return () => { | |||
| SessionStorage.removeItem(key); | |||
| }; | |||
| }, []); | |||
| return [storage]; | |||
| } | |||
| @@ -3,7 +3,7 @@ import { loginByOauth2Req } from '@/services/auth'; | |||
| import { to } from '@/utils/promise'; | |||
| import { history, useModel, useSearchParams } from '@umijs/max'; | |||
| import { message } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import { useCallback, useEffect } from 'react'; | |||
| import { flushSync } from 'react-dom'; | |||
| import styles from './index.less'; | |||
| @@ -12,12 +12,21 @@ function Authorize() { | |||
| const [searchParams] = useSearchParams(); | |||
| const code = searchParams.get('code'); | |||
| const redirect = searchParams.get('redirect'); | |||
| useEffect(() => { | |||
| loginByOauth2(); | |||
| }, []); | |||
| const fetchUserInfo = useCallback(async () => { | |||
| const userInfo = await initialState?.fetchUserInfo?.(); | |||
| if (userInfo) { | |||
| flushSync(() => { | |||
| setInitialState((s) => ({ | |||
| ...s, | |||
| currentUser: userInfo, | |||
| })); | |||
| }); | |||
| } | |||
| }, [initialState, setInitialState]); | |||
| // 登录 | |||
| const loginByOauth2 = async () => { | |||
| const loginByOauth2 = useCallback(async () => { | |||
| const params = { | |||
| code, | |||
| }; | |||
| @@ -29,19 +38,11 @@ function Authorize() { | |||
| await fetchUserInfo(); | |||
| history.push(redirect || '/'); | |||
| } | |||
| }; | |||
| }, [fetchUserInfo, redirect, code]); | |||
| const fetchUserInfo = async () => { | |||
| const userInfo = await initialState?.fetchUserInfo?.(); | |||
| if (userInfo) { | |||
| flushSync(() => { | |||
| setInitialState((s) => ({ | |||
| ...s, | |||
| currentUser: userInfo, | |||
| })); | |||
| }); | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| loginByOauth2(); | |||
| }, [loginByOauth2]); | |||
| return <div className={styles.container}></div>; | |||
| } | |||
| @@ -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) => { | |||
| @@ -19,18 +19,18 @@ function AutoMLInfo() { | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(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 ( | |||
| <div className={styles['auto-ml-info']}> | |||
| @@ -22,8 +22,9 @@ enum TabKeys { | |||
| History = 'history', | |||
| } | |||
| const NodePrefix = 'auto-ml'; | |||
| function AutoMLInstance() { | |||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const params = useParams(); | |||
| @@ -37,7 +38,8 @@ function AutoMLInstance() { | |||
| return () => { | |||
| closeSSE(); | |||
| }; | |||
| }, []); | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [instanceId]); | |||
| // 获取实验实例详情 | |||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | |||
| @@ -65,7 +67,7 @@ function AutoMLInstance() { | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| Object.keys(nodeStatusJson).forEach((key) => { | |||
| if (key.startsWith('auto-ml')) { | |||
| if (key.startsWith(NodePrefix)) { | |||
| const value = nodeStatusJson[key]; | |||
| info.nodeStatus = value; | |||
| } | |||
| @@ -80,10 +82,7 @@ function AutoMLInstance() { | |||
| }; | |||
| const setupSSE = (name: string, namespace: string) => { | |||
| let { origin } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| origin = 'http://172.20.32.181:31213'; | |||
| } | |||
| const { origin } = location; | |||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||
| const evtSource = new EventSource( | |||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||
| @@ -99,7 +98,7 @@ function AutoMLInstance() { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| const statusData = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith('auto-ml'), | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| if (statusData) { | |||
| setInstanceInfo((prev) => ({ | |||
| @@ -183,7 +182,7 @@ function AutoMLInstance() { | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| label: '试验列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| @@ -201,12 +200,7 @@ function AutoMLInstance() { | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| /> | |||
| <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -30,7 +30,6 @@ function DatasetConfig() { | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -431,7 +431,12 @@ function ExecuteConfig() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="是否打乱" name="shuffle" tooltip="拆分数据前是否打乱顺序"> | |||
| <Form.Item | |||
| label="是否打乱" | |||
| name="shuffle" | |||
| tooltip="拆分数据前是否打乱顺序" | |||
| valuePropName="checked" | |||
| > | |||
| <Switch /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -1,6 +1,6 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { AutoMLTaskType } from '@/enums'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { removeFormListItem } from '@/utils/ui'; | |||
| import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; | |||
| import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; | |||
| import { classificationMetrics, regressionMetrics } from './ExecuteConfig'; | |||
| @@ -72,12 +72,14 @@ function TrialConfig() { | |||
| type="text" | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '确定要删除该指标权重吗?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| }); | |||
| removeFormListItem( | |||
| form, | |||
| 'metrics', | |||
| name, | |||
| remove, | |||
| ['name', 'value'], | |||
| '删除后,该该指标权重将不可恢复', | |||
| ); | |||
| }} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| @@ -4,6 +4,7 @@ import tableCellRender from '@/utils/table'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import TrialStatusCell from '../TrialStatusCell'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| @@ -24,36 +25,36 @@ type TableData = { | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| 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<TableData>['columns'] = [ | |||
| { | |||
| @@ -68,42 +69,37 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| key: 'duration', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '训练损失', | |||
| dataIndex: 'train_loss', | |||
| key: 'train_loss', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '特征处理', | |||
| dataIndex: 'feature', | |||
| key: 'feature', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '算法', | |||
| dataIndex: 'althorithm', | |||
| key: 'althorithm', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| render: TrialStatusCell, | |||
| }, | |||
| ]; | |||
| @@ -26,7 +26,7 @@ | |||
| .startTime { | |||
| .singleLine(); | |||
| width: calc(20% + 10px); | |||
| width: 200px; | |||
| } | |||
| .status { | |||
| @@ -8,7 +8,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | |||
| @@ -53,7 +53,7 @@ function ExperimentInstanceComponent({ | |||
| if (allIntanceIds.length === 0) { | |||
| setSelectedIns([]); | |||
| } | |||
| }, [experimentInsList]); | |||
| }, [allIntanceIds, setSelectedIns]); | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| @@ -107,7 +107,7 @@ function ExperimentInstanceComponent({ | |||
| }; | |||
| if (!experimentInsList || experimentInsList.length === 0) { | |||
| return <div style={{ textAlign: 'center' }}>暂无实验实例</div>; | |||
| return <div style={{ textAlign: 'center' }}>暂无数据</div>; | |||
| } | |||
| return ( | |||
| @@ -159,9 +159,9 @@ function ExperimentInstanceComponent({ | |||
| {elapsedTime(item.create_time, item.finish_time)} | |||
| </div> | |||
| <div className={styles.startTime}> | |||
| <Tooltip title={formatDate(item.create_time)}> | |||
| <span>{formatDate(item.create_time)}</span> | |||
| </Tooltip> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||
| {formatDate(item.create_time)} | |||
| </Typography.Text> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| @@ -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<string, any> = { | |||
| 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) => { | |||
| @@ -261,16 +261,13 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| dataIndex: config.descProperty, | |||
| key: 'ml_description', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: '20%', | |||
| render: tableCellRender(true, TableCellValueType.Date), | |||
| ellipsis: { showTitle: false }, | |||
| width: 200, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '最近五次运行状态', | |||
| @@ -412,11 +409,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| ), | |||
| onExpand: (e, a) => { | |||
| handleExpandChange(e, a); | |||
| }, | |||
| onExpand: handleExpandChange, | |||
| expandedRowKeys: expandedRowKeys, | |||
| rowExpandable: () => true, | |||
| }} | |||
| rowKey="id" | |||
| /> | |||
| @@ -25,7 +25,8 @@ | |||
| } | |||
| &__text { | |||
| white-space: pre-wrap; | |||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||
| white-space: pre; | |||
| } | |||
| &__images { | |||
| @@ -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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| @@ -0,0 +1,3 @@ | |||
| .trial-status-cell { | |||
| height: 100%; | |||
| } | |||
| @@ -0,0 +1,67 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-18 18:35:41 | |||
| * @Description: 实验状态 | |||
| */ | |||
| import { AutoMLTrailStatus } from '@/enums'; | |||
| import { ExperimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import themes from '@/styles/theme.less'; | |||
| import styles from './index.less'; | |||
| export const statusInfo: Record<AutoMLTrailStatus, ExperimentStatusInfo> = { | |||
| [AutoMLTrailStatus.SUCCESS]: { | |||
| label: '成功', | |||
| color: themes.successColor, | |||
| icon: '/assets/images/experiment-status/success-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.TIMEOUT]: { | |||
| label: '超时', | |||
| color: themes.pendingColor, | |||
| icon: '/assets/images/experiment-status/pending-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.FAILURE]: { | |||
| label: '失败', | |||
| color: themes.errorColor, | |||
| icon: '/assets/images/experiment-status/fail-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.CRASHED]: { | |||
| label: '崩溃', | |||
| color: themes.errorColor, | |||
| icon: '/assets/images/experiment-status/fail-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.CANCELLED]: { | |||
| label: '取消', | |||
| color: themes.abortColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| [AutoMLTrailStatus.STOP]: { | |||
| label: '停止', | |||
| color: themes.textColor, | |||
| icon: '/assets/images/experiment-status/omitted-icon.png', | |||
| }, | |||
| }; | |||
| function TrialStatusCell(status?: AutoMLTrailStatus | null) { | |||
| if (status === null || status === undefined) { | |||
| return <span>--</span>; | |||
| } | |||
| return ( | |||
| <div className={styles['trial-status-cell']}> | |||
| {/* <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> */} | |||
| <span | |||
| style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }} | |||
| className={styles['trial-status-cell__label']} | |||
| > | |||
| {statusInfo[status] ? statusInfo[status].label : status} | |||
| </span> | |||
| </div> | |||
| ); | |||
| } | |||
| export default TrialStatusCell; | |||
| @@ -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<string | undefined>(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) => { | |||
| @@ -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) => { | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: 'auto', marginRight: 0 }} | |||
| onClick={hanldeDelete} | |||
| onClick={handleDelete} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| disabled={!version} | |||
| danger | |||
| @@ -8,7 +8,7 @@ import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { App, Button, Input, Pagination, PaginationProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | |||
| import { Ref, forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; | |||
| import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; | |||
| import AddDatasetModal from '../AddDatasetModal'; | |||
| import ResourceItem from '../ResourceItem'; | |||
| @@ -58,9 +58,30 @@ function ResourceList( | |||
| const { message } = App.useApp(); | |||
| const config = resourceConfig[resourceType]; | |||
| // 获取数据请求 | |||
| const getDataList = useCallback(async () => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| is_public: isPublic, | |||
| [config.typeParamKey]: dataType, | |||
| [config.tagParamKey]: dataTag, | |||
| name: searchText || undefined, | |||
| }; | |||
| const request = config.getList; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } else { | |||
| setDataList([]); | |||
| setTotal(0); | |||
| } | |||
| }, [dataType, dataTag, pagination, searchText, isPublic, config]); | |||
| useEffect(() => { | |||
| getDataList(); | |||
| }, [resourceType, dataType, dataTag, pagination, searchText, isPublic]); | |||
| }, [getDataList]); | |||
| useImperativeHandle( | |||
| ref, | |||
| @@ -81,27 +102,6 @@ function ResourceList( | |||
| [], | |||
| ); | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| is_public: isPublic, | |||
| [config.typeParamKey]: dataType, | |||
| [config.tagParamKey]: dataTag, | |||
| name: searchText || undefined, | |||
| }; | |||
| const request = config.getList; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } else { | |||
| setDataList([]); | |||
| setTotal(0); | |||
| } | |||
| }; | |||
| // 删除请求 | |||
| const deleteRecord = async (params: { owner: string; identifier: string; repo_id?: number }) => { | |||
| const request = config.deleteRecord; | |||
| @@ -3,7 +3,7 @@ import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { getAssetIcon } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Flex, Tabs, type TabsProps } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { CategoryData, ResourceType, resourceConfig } from '../../config'; | |||
| import CategoryList from '../CategoryList'; | |||
| import ResourceList, { ResourceListRef } from '../ResourceList'; | |||
| @@ -23,9 +23,31 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||
| const dataListRef = useRef<ResourceListRef>(null); | |||
| const config = resourceConfig[resourceType]; | |||
| // 获取分类 | |||
| const getAssetIconList = useCallback( | |||
| async (name: string = '') => { | |||
| const params = { | |||
| name: name, | |||
| page: 0, | |||
| size: 10000, | |||
| }; | |||
| const [res] = await to(getAssetIcon(params)); | |||
| if (res && res.data && res.data.content) { | |||
| const { content } = res.data; | |||
| setTypeList( | |||
| content.filter((item: CategoryData) => Number(item.category_id) === config.typeValue), | |||
| ); | |||
| setTagList( | |||
| content.filter((item: CategoryData) => Number(item.category_id) === config.tagValue), | |||
| ); | |||
| } | |||
| }, | |||
| [config], | |||
| ); | |||
| useEffect(() => { | |||
| getAssetIconList(); | |||
| }, []); | |||
| }, [getAssetIconList]); | |||
| // 分类搜索 | |||
| const handleCategorySearch = (value: string) => { | |||
| @@ -42,25 +64,6 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||
| setActiveTag((prev) => (prev === record.name ? undefined : record.name)); | |||
| }; | |||
| // 获取分类 | |||
| const getAssetIconList = async (name: string = '') => { | |||
| const params = { | |||
| name: name, | |||
| page: 0, | |||
| size: 10000, | |||
| }; | |||
| const [res] = await to(getAssetIcon(params)); | |||
| if (res && res.data && res.data.content) { | |||
| const { content } = res.data; | |||
| setTypeList( | |||
| content.filter((item: CategoryData) => Number(item.category_id) === config.typeValue), | |||
| ); | |||
| setTagList( | |||
| content.filter((item: CategoryData) => Number(item.category_id) === config.tagValue), | |||
| ); | |||
| } | |||
| }; | |||
| // 切换 Tab,重置数据 | |||
| const hanleTabChange: TabsProps['onChange'] = (value) => { | |||
| dataListRef.current?.reset(); | |||
| @@ -58,7 +58,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| title: '文件大小', | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| render: tableCellRender(), | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| @@ -99,7 +99,13 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| </Button> | |||
| </Flex> | |||
| </Flex> | |||
| <Table columns={columns} dataSource={fileList} pagination={false} rowKey="url" /> | |||
| <Table | |||
| columns={columns} | |||
| dataSource={fileList} | |||
| pagination={false} | |||
| rowKey="url" | |||
| tableLayout="fixed" | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -127,28 +127,28 @@ function VersionCompareModal({ | |||
| text: '版本描述', | |||
| }, | |||
| ], | |||
| [], | |||
| [resourceType], | |||
| ); | |||
| useEffect(() => { | |||
| getServiceVersionCompare(); | |||
| }, []); | |||
| // 获取对比数据 | |||
| const getServiceVersionCompare = async () => { | |||
| const params = { | |||
| versions, | |||
| identifier, | |||
| is_public, | |||
| owner, | |||
| repo_id, | |||
| // 获取对比数据 | |||
| const getServiceVersionCompare = async () => { | |||
| const params = { | |||
| versions, | |||
| identifier, | |||
| is_public, | |||
| owner, | |||
| repo_id, | |||
| }; | |||
| const request = config.compareVersion; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| setCompareData(res.data); | |||
| } | |||
| }; | |||
| const request = config.compareVersion; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| setCompareData(res.data); | |||
| } | |||
| }; | |||
| getServiceVersionCompare(); | |||
| }, [versions, identifier, is_public, owner, repo_id, config]); | |||
| // 获取值 | |||
| function getValue<T extends DatasetData | ModelData>( | |||
| @@ -25,9 +25,10 @@ export enum ResourceType { | |||
| } | |||
| export enum DataSource { | |||
| AtuoExport = 'auto_export', // 自动导出 | |||
| AutoExport = 'auto_export', // 自动导出 | |||
| HandExport = 'hand_export', // 手动导出 | |||
| Create = 'add', // 新增 | |||
| LabelStudioExport = 'label_studio_export', // LabelStudio 导出 | |||
| } | |||
| type ResourceTypeInfo = { | |||
| @@ -6,17 +6,17 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import ResourceSelect, { | |||
| requiredValidator, | |||
| ResourceSelectorType, | |||
| type ParameterInputObject, | |||
| } from '@/components/ResourceSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { createEditorReq } from '@/services/developmentEnvironment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { App, Button, Col, Form, Input, Row, Select } from 'antd'; | |||
| import { App, Button, Col, Form, Input, Row } from 'antd'; | |||
| import { omit, pick } from 'lodash'; | |||
| import styles from './index.less'; | |||
| @@ -51,7 +51,6 @@ function EditorCreate() { | |||
| const navigate = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| const { message } = App.useApp(); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | |||
| // 创建编辑器 | |||
| const createEditor = async (formData: FormData) => { | |||
| @@ -62,8 +61,8 @@ function EditorCreate() { | |||
| const params = { | |||
| ...omit(formData, ['image', 'model', 'dataset']), | |||
| image: image.value, | |||
| model: pick(model, ['id', 'version', 'path', 'showValue']), | |||
| dataset: pick(dataset, ['id', 'version', 'path', 'showValue']), | |||
| model: model && pick(model, ['id', 'version', 'path', 'showValue']), | |||
| dataset: dataset && pick(dataset, ['id', 'version', 'path', 'showValue']), | |||
| }; | |||
| const [res] = await to(createEditorReq(params)); | |||
| if (res) { | |||
| @@ -138,7 +137,7 @@ function EditorCreate() { | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="资源规格" | |||
| name="standard" | |||
| name="computing_resource_id" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| @@ -146,16 +145,7 @@ function EditorCreate() { | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| showSearch | |||
| placeholder="请选择资源规格" | |||
| filterOption={filterResourceStandard} | |||
| options={resourceStandardList} | |||
| fieldNames={{ | |||
| label: 'description', | |||
| value: 'standard', | |||
| }} | |||
| /> | |||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -181,7 +171,6 @@ function EditorCreate() { | |||
| type={ResourceSelectorType.Mirror} | |||
| placeholder="请选择镜像" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -193,7 +182,6 @@ function EditorCreate() { | |||
| type={ResourceSelectorType.Model} | |||
| placeholder="请选择模型" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -205,7 +193,6 @@ function EditorCreate() { | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -29,7 +29,7 @@ import { | |||
| type TableProps, | |||
| } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import CreateMirrorModal from '../components/CreateMirrorModal'; | |||
| import EditorStatusCell from '../components/EditorStatusCell'; | |||
| import styles from './index.less'; | |||
| @@ -57,12 +57,8 @@ function EditorList() { | |||
| }, | |||
| ); | |||
| useEffect(() => { | |||
| getEditorList(); | |||
| }, [pagination]); | |||
| // 获取编辑器列表 | |||
| const getEditorList = async () => { | |||
| const getEditorList = useCallback(async () => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| @@ -73,7 +69,11 @@ function EditorList() { | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }; | |||
| }, [pagination]); | |||
| useEffect(() => { | |||
| getEditorList(); | |||
| }, [getEditorList]); | |||
| // 删除编辑器 | |||
| const deleteEditor = async (id: number) => { | |||
| @@ -67,8 +67,8 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { | |||
| message: '请输入镜像Tag', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9_-]*$/, | |||
| message: '只支持字母、数字、下划线(_)、中横线(-)', | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| ]} | |||
| > | |||
| @@ -18,26 +18,6 @@ | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__footer { | |||
| display: flex; | |||
| align-items: center; | |||
| padding-top: 20px; | |||
| color: @text-color-secondary; | |||
| font-size: 12px; | |||
| background-color: white; | |||
| div { | |||
| flex: 1; | |||
| height: 1px; | |||
| background-color: @border-color; | |||
| } | |||
| p { | |||
| flex: none; | |||
| margin: 0 8px; | |||
| } | |||
| } | |||
| :global { | |||
| .ant-table-container { | |||
| border: none !important; | |||
| @@ -54,13 +34,13 @@ | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-tbody-virtual::after { | |||
| border-bottom: none !important; | |||
| } | |||
| .ant-table-footer { | |||
| padding: 0; | |||
| border: none !important; | |||
| } | |||
| .ant-table-column-title { | |||
| min-width: 0; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 实验对比 | |||
| */ | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import { | |||
| getExpEvaluateInfosReq, | |||
| getExpMetricsReq, | |||
| @@ -13,7 +14,7 @@ import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd'; | |||
| import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | |||
| @@ -45,27 +46,27 @@ function ExperimentComparison() { | |||
| }); | |||
| const { message } = App.useApp(); | |||
| const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); | |||
| const config = comparisonConfig[comparisonType]; | |||
| useEffect(() => { | |||
| getComparisonData(); | |||
| }, [experimentId]); | |||
| // 获取对比数据列表 | |||
| const getComparisonData = async () => { | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| // 获取对比数据列表 | |||
| const getComparisonData = async () => { | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| }; | |||
| const [res] = await to(request(experimentId, params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }; | |||
| const [res] = await to(request(experimentId, params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }; | |||
| getComparisonData(); | |||
| }, [experimentId, pagination, comparisonType]); | |||
| // 获取对比 url | |||
| const getExpMetrics = async () => { | |||
| @@ -77,7 +78,7 @@ function ExperimentComparison() { | |||
| }; | |||
| // 对比按钮 click | |||
| const hanldeComparisonClick = () => { | |||
| const handleComparisonClick = () => { | |||
| if (selectedRowKeys.length < 2) { | |||
| message.error('请至少选择两项进行对比'); | |||
| return; | |||
| @@ -154,7 +155,6 @@ function ExperimentComparison() { | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(true, TableCellValueType.Array), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -162,17 +162,12 @@ function ExperimentComparison() { | |||
| title: `${config.title}参数`, | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| @@ -181,28 +176,23 @@ function ExperimentComparison() { | |||
| title: `${config.title}指标`, | |||
| align: 'center', | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: 120, | |||
| width: 150, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| ]; | |||
| }, [tableData]); | |||
| }, [tableData, config]); | |||
| return ( | |||
| <div className={styles['experiment-comparison']}> | |||
| <div className={styles['experiment-comparison__header']}> | |||
| <Button type="default" onClick={hanldeComparisonClick}> | |||
| <Button type="default" onClick={handleComparisonClick}> | |||
| 可视化对比 | |||
| </Button> | |||
| </div> | |||
| @@ -21,7 +21,6 @@ function ExperimentText() { | |||
| const [experimentIns, setExperimentIns] = useState(undefined); | |||
| const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); | |||
| const graphRef = useRef(); | |||
| const timerRef = useRef(); | |||
| const workflowRef = useRef(); | |||
| const locationParams = useParams(); // 新版本获取路由参数接口 | |||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | |||
| @@ -36,6 +35,16 @@ function ExperimentText() { | |||
| initGraph(); | |||
| getWorkflow(); | |||
| return () => { | |||
| if (evtSourceRef.current) { | |||
| evtSourceRef.current.close(); | |||
| evtSourceRef.current = null; | |||
| } | |||
| }; | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, []); | |||
| useEffect(() => { | |||
| const changeSize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current) return; | |||
| @@ -46,20 +55,9 @@ function ExperimentText() { | |||
| window.addEventListener('resize', changeSize); | |||
| return () => { | |||
| window.removeEventListener('resize', changeSize); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| } | |||
| if (evtSourceRef.current) { | |||
| evtSourceRef.current.close(); | |||
| evtSourceRef.current = null; | |||
| } | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| propsDrawerOpenRef.current = propsDrawerOpen; | |||
| }, [propsDrawerOpen]); | |||
| // 获取流水线模版 | |||
| const getWorkflow = async () => { | |||
| const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||
| @@ -30,10 +30,4 @@ | |||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| :global { | |||
| .ant-drawer-mask { | |||
| background: transparent !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| .experiment-drawer { | |||
| line-height: var(--ant-line-height); | |||
| :global { | |||
| .ant-drawer-body { | |||
| overflow-y: hidden; | |||
| @@ -12,7 +13,7 @@ | |||
| } | |||
| &__tabs { | |||
| height: calc(100% - 170px); | |||
| height: calc(100% - 169px); | |||
| :global { | |||
| .ant-tabs-nav { | |||
| padding-left: 24px; | |||
| @@ -90,15 +90,17 @@ const ExperimentDrawer = ({ | |||
| instanceNodeStatus, | |||
| workflowId, | |||
| instanceNodeStartTime, | |||
| experimentName, | |||
| experimentId, | |||
| pipelineId, | |||
| ], | |||
| ); | |||
| return ( | |||
| <Drawer | |||
| rootStyle={{ marginTop: '55px' }} | |||
| rootStyle={{ marginTop: '111px' }} | |||
| title="任务执行详情" | |||
| placement="right" | |||
| getContainer={false} | |||
| closeIcon={<CloseOutlined className={styles['experiment-drawer__close']} />} | |||
| onClose={onClose} | |||
| open={open} | |||
| @@ -13,7 +13,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | |||
| @@ -57,7 +57,7 @@ function ExperimentInstanceComponent({ | |||
| if (allIntanceIds.length === 0) { | |||
| setSelectedIns([]); | |||
| } | |||
| }, [experimentInsList]); | |||
| }, [allIntanceIds, setSelectedIns]); | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| @@ -186,9 +186,9 @@ function ExperimentInstanceComponent({ | |||
| <div className={styles.description}> | |||
| <div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div> | |||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||
| <Tooltip title={formatDate(item.create_time)}> | |||
| <span>{formatDate(item.create_time)}</span> | |||
| </Tooltip> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> | |||
| {formatDate(item.create_time)} | |||
| </Typography.Text> | |||
| </div> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| @@ -1,9 +1,8 @@ | |||
| import FormInfo from '@/components/FormInfo'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { Form, Select } from 'antd'; | |||
| import { Form } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ExperimentParameterProps = { | |||
| @@ -11,8 +10,6 @@ type ExperimentParameterProps = { | |||
| }; | |||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| const [resourceStandardList] = useComputingResource(); // 资源规模 | |||
| // 控制策略 | |||
| const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| @@ -100,7 +97,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| </Form.Item> | |||
| <Form.Item label="启动命令" name="command"> | |||
| <FormInfo multiline /> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="资源规格" | |||
| @@ -112,20 +109,13 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| options={resourceStandardList} | |||
| disabled | |||
| fieldNames={{ | |||
| label: 'description', | |||
| value: 'standard', | |||
| }} | |||
| /> | |||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> | |||
| </Form.Item> | |||
| <Form.Item label="挂载路径" name="mount_path"> | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <Form.Item label="环境变量" name="env_variables"> | |||
| <FormInfo multiline /> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| {controlStrategyList.map((item) => ( | |||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | |||
| @@ -146,7 +136,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| {item.value.type === 'select' ? ( | |||
| <ParameterSelect disabled /> | |||
| ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect dataType={item.value.item_type as any} display /> | |||
| ) : null | |||
| ) : ( | |||
| <FormInfo valuePropName="showValue" /> | |||
| )} | |||
| @@ -43,19 +43,18 @@ function ExperimentResult({ | |||
| : undefined; | |||
| useEffect(() => { | |||
| // 获取实验结果 | |||
| const getExperimentResult = async (params: any) => { | |||
| const [res] = await to(getNodeResult(params)); | |||
| if (res && res.data && Array.isArray(res.data)) { | |||
| const data = res.data.filter((item: ExperimentResultData) => item.value.length > 0); | |||
| setExperimentResults(data); | |||
| } else { | |||
| setExperimentResults([]); | |||
| } | |||
| }; | |||
| getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId }); | |||
| }, []); | |||
| // 获取实验结果 | |||
| const getExperimentResult = async (params: any) => { | |||
| const [res] = await to(getNodeResult(params)); | |||
| if (res && res.data && Array.isArray(res.data)) { | |||
| const data = res.data.filter((item: ExperimentResultData) => item.value.length > 0); | |||
| setExperimentResults(data); | |||
| } else { | |||
| setExperimentResults([]); | |||
| } | |||
| }; | |||
| }, [experimentInsId, pipelineNodeId]); | |||
| // 下载 | |||
| const download = (path: string) => { | |||
| @@ -53,8 +53,21 @@ function ExportModelModal({ | |||
| }; | |||
| useEffect(() => { | |||
| // 获取数据集、模型列表 | |||
| const requestResourceList = async () => { | |||
| const params = { | |||
| page: 0, | |||
| size: 1000, | |||
| is_public: false, // 个人 | |||
| }; | |||
| const [res] = await to(config.getList(params)); | |||
| if (res && res.data) { | |||
| setResources(res.data.content || []); | |||
| } | |||
| }; | |||
| requestResourceList(); | |||
| }, []); | |||
| }, [config]); | |||
| // 获取选中的数据集、模型 | |||
| const getSelectedResource = (id: number | undefined) => { | |||
| @@ -84,19 +97,6 @@ function ExportModelModal({ | |||
| } | |||
| }; | |||
| // 获取数据集、模型列表 | |||
| const requestResourceList = async () => { | |||
| const params = { | |||
| page: 0, | |||
| size: 1000, | |||
| is_public: false, // 个人 | |||
| }; | |||
| const [res] = await to(config.getList(params)); | |||
| if (res && res.data) { | |||
| setResources(res.data.content || []); | |||
| } | |||
| }; | |||
| // 获取数据集、模型版本列表 | |||
| const getRecourceVersions = async (id: number) => { | |||
| const resource = getSelectedResource(id); | |||
| @@ -20,7 +20,8 @@ | |||
| padding: 15px; | |||
| color: white; | |||
| font-size: 14px; | |||
| white-space: pre-line; | |||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||
| white-space: pre-wrap; | |||
| text-align: left; | |||
| word-break: break-all; | |||
| background: #19253b; | |||
| @@ -25,19 +25,6 @@ type Log = { | |||
| pod_name: string; // pod名称 | |||
| }; | |||
| // 滚动到底部 | |||
| const scrollToBottom = (smooth: boolean = true) => { | |||
| const element = document.getElementById('log-list'); | |||
| if (element) { | |||
| const optons: ScrollToOptions = { | |||
| top: element.scrollHeight, | |||
| behavior: smooth ? 'smooth' : 'instant', | |||
| }; | |||
| element.scrollTo(optons); | |||
| } | |||
| }; | |||
| function LogGroup({ | |||
| log_type = 'normal', | |||
| pod_name = '', | |||
| @@ -46,23 +33,115 @@ function LogGroup({ | |||
| status, | |||
| }: LogGroupProps) { | |||
| const [collapse, setCollapse] = useState(true); | |||
| const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); | |||
| const [logList, setLogList] = useState<Log[]>([]); | |||
| const [completed, setCompleted] = useState(false); | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||
| const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | |||
| const logElementRef = useRef<HTMLDivElement | null>(null); | |||
| // 如果是【运行中】状态,设置 hasRun 为 true,【运行中】或者从【运行中】切换到别的状态时,不显示【更多】按钮 | |||
| const [hasRun, setHasRun] = useState(false); | |||
| if (status === ExperimentStatus.Running && !hasRun) { | |||
| setHasRun(true); | |||
| } | |||
| // 进入页面时,滚动到底部 | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| }, []); | |||
| useEffect(() => { | |||
| // 建立 socket 连接 | |||
| const setupSockect = () => { | |||
| let { host } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| host = '172.20.32.197:31213'; | |||
| } | |||
| const socket = new WebSocket( | |||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | |||
| ); | |||
| socket.addEventListener('open', () => { | |||
| console.log('WebSocket is open now.'); | |||
| }); | |||
| socket.addEventListener('close', (event) => { | |||
| console.log('WebSocket is closed:', event); | |||
| // 有时候会出现连接失败,重试 3 次 | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| setupSockect(); | |||
| }, 2 * 1000); | |||
| } | |||
| }); | |||
| socket.addEventListener('error', (event) => { | |||
| console.error('WebSocket error observed:', event); | |||
| }); | |||
| socket.addEventListener('message', (event) => { | |||
| // console.log('message received.', event); | |||
| if (!event.data) { | |||
| return; | |||
| } | |||
| try { | |||
| const data = JSON.parse(event.data); | |||
| const streams = data.streams; | |||
| if (!streams || !Array.isArray(streams)) { | |||
| return; | |||
| } | |||
| let startTime = start_time; | |||
| const logContent = streams.reduce((result, item) => { | |||
| const values = item.values; | |||
| return ( | |||
| result + | |||
| values.reduce((prev: string, cur: [string, string]) => { | |||
| const [time, value] = cur; | |||
| startTime = time; | |||
| const str = `[${dayjs(Number(time) / 1.0e6).format( | |||
| 'YYYY-MM-DD HH:mm:ss', | |||
| )}] ${value}`; | |||
| return prev + str; | |||
| }, '') | |||
| ); | |||
| }, ''); | |||
| const logDetail: Log = { | |||
| start_time: startTime!, | |||
| log_content: logContent, | |||
| pod_name: pod_name, | |||
| }; | |||
| setLogList((oldList) => oldList.concat(logDetail)); | |||
| if (!isMouseDownRef.current && logContent) { | |||
| setTimeout(() => { | |||
| scrollToBottom(); | |||
| }, 100); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| }); | |||
| socketRef.current = socket; | |||
| }; | |||
| // 关闭 socket | |||
| const closeSocket = () => { | |||
| if (socketRef.current) { | |||
| socketRef.current.close(1000, 'completed'); | |||
| socketRef.current = undefined; | |||
| } | |||
| }; | |||
| if (status === ExperimentStatus.Running) { | |||
| setupSockect(); | |||
| } else if (preStatusRef.current === ExperimentStatus.Running) { | |||
| setCompleted(true); | |||
| } | |||
| preStatusRef.current = status; | |||
| }, [status]); | |||
| return () => { | |||
| closeSocket(); | |||
| }; | |||
| }, [status, start_time, pod_name, isMouseDownRef]); | |||
| // 鼠标拖到中不滚动到底部 | |||
| useEffect(() => { | |||
| @@ -77,14 +156,13 @@ function LogGroup({ | |||
| return () => { | |||
| document.removeEventListener('mousedown', mouseDown); | |||
| document.removeEventListener('mouseup', mouseUp); | |||
| closeSocket(); | |||
| }; | |||
| }, []); | |||
| }, [setIsMouseDown]); | |||
| // 请求日志 | |||
| const requestExperimentPodsLog = async () => { | |||
| const list = logListRef.current; | |||
| const startTime = list.length > 0 ? list[list.length - 1].start_time : start_time; | |||
| const last = logList[logList.length - 1]; | |||
| const startTime = last ? last.start_time : start_time; | |||
| const params = { | |||
| pod_name, | |||
| start_time: startTime, | |||
| @@ -131,91 +209,27 @@ function LogGroup({ | |||
| requestExperimentPodsLog(); | |||
| }; | |||
| // 建立 socket 连接 | |||
| const setupSockect = () => { | |||
| let { host } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| host = '172.20.32.181:31213'; | |||
| } | |||
| const socket = new WebSocket( | |||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | |||
| ); | |||
| socket.addEventListener('open', () => { | |||
| console.log('WebSocket is open now.'); | |||
| }); | |||
| socket.addEventListener('close', (event) => { | |||
| console.log('WebSocket is closed:', event); | |||
| // 有时候会出现连接失败,重试 3 次 | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| setupSockect(); | |||
| }, 2 * 1000); | |||
| } | |||
| }); | |||
| socket.addEventListener('error', (event) => { | |||
| console.error('WebSocket error observed:', event); | |||
| }); | |||
| socket.addEventListener('message', (event) => { | |||
| console.log('message received.', event); | |||
| if (!event.data) { | |||
| return; | |||
| } | |||
| try { | |||
| const data = JSON.parse(event.data); | |||
| const streams = data.streams; | |||
| if (!streams || !Array.isArray(streams)) { | |||
| return; | |||
| } | |||
| let startTime = start_time; | |||
| const logContent = streams.reduce((result, item) => { | |||
| const values = item.values; | |||
| return ( | |||
| result + | |||
| values.reduce((prev: string, cur: [string, string]) => { | |||
| const [time, value] = cur; | |||
| startTime = time; | |||
| const str = `[${dayjs(Number(time) / 1.0e6).format('YYYY-MM-DD HH:mm:ss')}] ${value}`; | |||
| return prev + str; | |||
| }, '') | |||
| ); | |||
| }, ''); | |||
| const logDetail: Log = { | |||
| start_time: startTime!, | |||
| log_content: logContent, | |||
| pod_name: pod_name, | |||
| }; | |||
| setLogList((oldList) => oldList.concat(logDetail)); | |||
| if (!isMouseDownRef.current && logContent) { | |||
| setTimeout(() => { | |||
| scrollToBottom(); | |||
| }, 100); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| // 滚动到底部 | |||
| const scrollToBottom = (smooth: boolean = true) => { | |||
| // const element = document.getElementById(listId); | |||
| // if (element) { | |||
| // const optons: ScrollToOptions = { | |||
| // top: element.scrollHeight, | |||
| // behavior: smooth ? 'smooth' : 'instant', | |||
| // }; | |||
| // element.scrollTo(optons); | |||
| // } | |||
| logElementRef?.current?.scrollIntoView({ | |||
| block: 'end', | |||
| behavior: smooth ? 'smooth' : 'instant', | |||
| }); | |||
| socketRef.current = socket; | |||
| }; | |||
| const closeSocket = () => { | |||
| if (socketRef.current) { | |||
| socketRef.current.close(1000, 'completed'); | |||
| socketRef.current = undefined; | |||
| } | |||
| }; | |||
| 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 ( | |||
| <div className={styles['log-group']}> | |||
| <div className={styles['log-group']} ref={logElementRef}> | |||
| {log_type === 'resource' && ( | |||
| <div className={styles['log-group__pod']} onClick={handleCollapse}> | |||
| <div className={styles['log-group__pod__name']}>{pod_name}</div> | |||
| @@ -1,8 +1,9 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { getQueryByExperimentLog } from '@/services/experiment/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import classNames from 'classnames'; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import React, { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import LogGroup from '../LogGroup'; | |||
| import styles from './index.less'; | |||
| @@ -14,12 +15,22 @@ 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; | |||
| }; | |||
| function LogList({ | |||
| @@ -29,25 +40,14 @@ function LogList({ | |||
| workflowId, | |||
| instanceNodeStartTime, | |||
| instanceNodeStatus, | |||
| className, | |||
| style, | |||
| }: LogListProps) { | |||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const [logGroups, setLogGroups] = useState<ExperimentLog[]>([]); | |||
| const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | |||
| // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 | |||
| useEffect(() => { | |||
| if ( | |||
| instanceNodeStatus && | |||
| instanceNodeStatus !== ExperimentStatus.Pending && | |||
| (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) | |||
| ) { | |||
| getExperimentLog(); | |||
| } | |||
| preStatusRef.current = instanceNodeStatus; | |||
| }, [instanceNodeStatus]); | |||
| // 获取实验日志 | |||
| const getExperimentLog = async () => { | |||
| // 获取实验 Pods 组 | |||
| const getExperimentLog = useCallback(async () => { | |||
| const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; | |||
| const params = { | |||
| task_id: pipelineNodeId, | |||
| @@ -66,7 +66,7 @@ function LogList({ | |||
| log_type, | |||
| }, | |||
| ]; | |||
| setLogList(list); | |||
| setLogGroups(list); | |||
| } else if (log_type === 'resource') { | |||
| const list = pods.map((v: string) => ({ | |||
| log_type, | |||
| @@ -74,7 +74,7 @@ function LogList({ | |||
| log_content: '', | |||
| start_time, | |||
| })); | |||
| setLogList(list); | |||
| setLogGroups(list); | |||
| } | |||
| } else { | |||
| if (retryRef.current > 0) { | |||
| @@ -84,12 +84,23 @@ function LogList({ | |||
| }, 2 * 1000); | |||
| } | |||
| } | |||
| }; | |||
| }, [pipelineNodeId, workflowId, instanceName, instanceNamespace, instanceNodeStartTime]); | |||
| // 当实例节点运行状态不是 Pending,获取实验日志组 | |||
| useEffect(() => { | |||
| if ( | |||
| instanceNodeStatus && | |||
| instanceNodeStatus !== ExperimentStatus.Pending && | |||
| logGroups.length === 0 | |||
| ) { | |||
| getExperimentLog(); | |||
| } | |||
| }, [getExperimentLog, logGroups, instanceNodeStatus]); | |||
| return ( | |||
| <div className={styles['log-list']} id="log-list"> | |||
| {logList.length > 0 ? ( | |||
| logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||
| <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> | |||
| {logGroups.length > 0 ? ( | |||
| logGroups.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) | |||
| ) : ( | |||
| <div className={styles['log-list__empty']}>暂无日志</div> | |||
| )} | |||
| @@ -20,7 +20,7 @@ import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import { ComparisonType } from './Comparison/config'; | |||
| import AddExperimentModal from './components/AddExperimentModal'; | |||
| @@ -35,12 +35,6 @@ function Experiment() { | |||
| const navigate = useNavigate(); | |||
| const [experimentList, setExperimentList] = useState([]); | |||
| const [workflowList, setWorkflowList] = useState([]); | |||
| const [queryFlow, setQueryFlow] = useState({ | |||
| offset: 1, | |||
| page: 0, | |||
| size: 10000, | |||
| name: null, | |||
| }); | |||
| const [experimentId, setExperimentId] = useState(null); | |||
| const [experimentInList, setExperimentInList] = useState([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState(null); | |||
| @@ -61,18 +55,28 @@ function Experiment() { | |||
| const { message } = App.useApp(); | |||
| useEffect(() => { | |||
| // 获取流水线列表 | |||
| const getWorkflowList = async () => { | |||
| const queryFlow = { | |||
| offset: 1, | |||
| page: 0, | |||
| size: 10000, | |||
| name: null, | |||
| }; | |||
| const [res] = await to(getWorkflow(queryFlow)); | |||
| if (res && res.data && res.data.content) { | |||
| setWorkflowList(res.data.content); | |||
| } | |||
| }; | |||
| getWorkflowList(); | |||
| return () => { | |||
| clearExperimentInTimers(); | |||
| }; | |||
| }, []); | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [pagination, searchText]); | |||
| // 获取实验列表 | |||
| const getExperimentList = async () => { | |||
| const getExperimentList = useCallback(async () => { | |||
| const params = { | |||
| page: pagination.current - 1, | |||
| size: pagination.pageSize, | |||
| @@ -88,15 +92,11 @@ function Experiment() { | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| }, [pagination, searchText]); | |||
| // 获取流水线列表 | |||
| const getWorkflowList = async () => { | |||
| const [res] = await to(getWorkflow(queryFlow)); | |||
| if (res && res.data && res.data.content) { | |||
| setWorkflowList(res.data.content); | |||
| } | |||
| }; | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // 搜索 | |||
| const onSearch = (value) => { | |||
| @@ -277,7 +277,6 @@ function Experiment() { | |||
| current, | |||
| pageSize, | |||
| }); | |||
| getExperimentList(); | |||
| }; | |||
| // 运行实验 | |||
| const runExperiment = async (id) => { | |||
| @@ -383,7 +382,7 @@ function Experiment() { | |||
| title: '实验名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: tableCellRender(), | |||
| render: tableCellRender(false), | |||
| width: '16%', | |||
| }, | |||
| { | |||
| @@ -400,7 +399,6 @@ function Experiment() { | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '最近五次运行状态', | |||
| @@ -549,11 +547,8 @@ function Experiment() { | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| ), | |||
| onExpand: (e, a) => { | |||
| expandChange(e, a); | |||
| }, | |||
| onExpand: expandChange, | |||
| expandedRowKeys: [expandedRowKeys], | |||
| rowExpandable: (record) => true, | |||
| }} | |||
| /> | |||
| </div> | |||
| @@ -1,4 +1,4 @@ | |||
| .create-hyperparameter { | |||
| .create-hyper-parameter { | |||
| height: 100%; | |||
| &__content { | |||
| @@ -11,11 +11,6 @@ | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__type { | |||
| color: @text-color; | |||
| font-size: @font-size-input-lg; | |||
| } | |||
| :global { | |||
| .ant-input-number { | |||
| width: 100%; | |||
| @@ -26,37 +26,37 @@ function CreateHyperParameter() { | |||
| const isCopy = pathname.includes('copy'); | |||
| useEffect(() => { | |||
| // 获取服务详情 | |||
| const getHyperParameterInfo = async (id: number) => { | |||
| const [res] = await to(getRayInfoReq({ id })); | |||
| if (res && res.data) { | |||
| const info: HyperParameterData = res.data; | |||
| const { name: name_str, parameters, points_to_evaluate, ...rest } = info; | |||
| const name = isCopy ? `${name_str}-copy` : name_str; | |||
| if (parameters && Array.isArray(parameters)) { | |||
| parameters.forEach((item) => { | |||
| const paramName = getReqParamName(item.type); | |||
| item.range = item[paramName]; | |||
| item[paramName] = undefined; | |||
| }); | |||
| } | |||
| const formData = { | |||
| ...rest, | |||
| name, | |||
| parameters, | |||
| points_to_evaluate: points_to_evaluate ?? [], | |||
| }; | |||
| form.setFieldsValue(formData); | |||
| } | |||
| }; | |||
| // 编辑,复制 | |||
| if (id && !Number.isNaN(id)) { | |||
| getHyperParameterInfo(id); | |||
| } | |||
| }, [id]); | |||
| // 获取服务详情 | |||
| const getHyperParameterInfo = async (id: number) => { | |||
| const [res] = await to(getRayInfoReq({ id })); | |||
| if (res && res.data) { | |||
| const info: HyperParameterData = res.data; | |||
| const { name: name_str, parameters, points_to_evaluate, ...rest } = info; | |||
| const name = isCopy ? `${name_str}-copy` : name_str; | |||
| if (parameters && Array.isArray(parameters)) { | |||
| parameters.forEach((item) => { | |||
| const paramName = getReqParamName(item.type); | |||
| item.range = item[paramName]; | |||
| item[paramName] = undefined; | |||
| }); | |||
| } | |||
| const formData = { | |||
| ...rest, | |||
| name, | |||
| parameters, | |||
| points_to_evaluate: points_to_evaluate ?? [undefined], | |||
| }; | |||
| form.setFieldsValue(formData); | |||
| } | |||
| }; | |||
| }, [id, form, isCopy]); | |||
| // 创建、更新、复制实验 | |||
| const createExperiment = async (formData: FormData) => { | |||
| @@ -118,9 +118,9 @@ function CreateHyperParameter() { | |||
| } | |||
| return ( | |||
| <div className={styles['create-hyperparameter']}> | |||
| <div className={styles['create-hyper-parameter']}> | |||
| <PageTitle title={title}></PageTitle> | |||
| <div className={styles['create-hyperparameter__content']}> | |||
| <div className={styles['create-hyper-parameter__content']}> | |||
| <div> | |||
| <Form | |||
| name="create-hyperparameter" | |||
| @@ -138,7 +138,7 @@ function CreateHyperParameter() { | |||
| name: '', | |||
| }, | |||
| ], | |||
| points_to_evaluate: [undefined], | |||
| points_to_evaluate: [], | |||
| }} | |||
| > | |||
| <BasicConfig /> | |||
| @@ -1,4 +1,4 @@ | |||
| .auto-ml-info { | |||
| .hyper-parameter-info { | |||
| position: relative; | |||
| height: 100%; | |||
| &__tabs { | |||
| @@ -8,7 +8,7 @@ import { getRayInfoReq } from '@/services/hyperParameter'; | |||
| import { safeInvoke } from '@/utils/functional'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams } from '@umijs/max'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import HyperParameterBasic from '../components/HyperParameterBasic'; | |||
| import { HyperParameterData } from '../types'; | |||
| import styles from './index.less'; | |||
| @@ -20,24 +20,24 @@ function HyperparameterInfo() { | |||
| undefined, | |||
| ); | |||
| useEffect(() => { | |||
| if (hyperparameterId) { | |||
| getHyperparameterInfo(); | |||
| } | |||
| }, []); | |||
| // 获取自动机器学习详情 | |||
| const getHyperparameterInfo = async () => { | |||
| const getHyperparameterInfo = useCallback(async () => { | |||
| const [res] = await to(getRayInfoReq({ id: hyperparameterId })); | |||
| if (res && res.data) { | |||
| setHyperparameterInfo(res.data); | |||
| } | |||
| }; | |||
| }, [hyperparameterId]); | |||
| useEffect(() => { | |||
| if (hyperparameterId) { | |||
| getHyperparameterInfo(); | |||
| } | |||
| }, [hyperparameterId, getHyperparameterInfo]); | |||
| return ( | |||
| <div className={styles['auto-ml-info']}> | |||
| <div className={styles['hyper-parameter-info']}> | |||
| <PageTitle title="实验详情"></PageTitle> | |||
| <div className={styles['auto-ml-info__content']}> | |||
| <div className={styles['hyper-parameter-info__content']}> | |||
| <HyperParameterBasic info={hyperparameterInfo} /> | |||
| </div> | |||
| </div> | |||
| @@ -1,4 +1,4 @@ | |||
| .auto-ml-instance { | |||
| .hyper-parameter-instance { | |||
| height: 100%; | |||
| &__tabs { | |||
| @@ -34,7 +34,7 @@ | |||
| &__log { | |||
| height: calc(100% - 10px); | |||
| margin-top: 10px; | |||
| padding: 20px calc(@content-padding - 8px); | |||
| padding: 8px calc(@content-padding - 8px) 20px; | |||
| overflow-y: visible; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| @@ -1,7 +1,6 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||
| import LogList from '@/pages/Experiment/components/LogList'; | |||
| import { getExperimentInsReq } from '@/services/autoML'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { getRayInsReq } from '@/services/hyperParameter'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { safeInvoke } from '@/utils/functional'; | |||
| @@ -10,9 +9,10 @@ import { useParams } from '@umijs/max'; | |||
| import { Tabs } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import ExperimentHistory from '../components/ExperimentHistory'; | |||
| import ExperimentLog from '../components/ExperimentLog'; | |||
| import ExperimentResult from '../components/ExperimentResult'; | |||
| import HyperParameterBasic from '../components/HyperParameterBasic'; | |||
| import { AutoMLInstanceData, HyperParameterData } from '../types'; | |||
| import { HyperParameterData, HyperParameterInstanceData } from '../types'; | |||
| import styles from './index.less'; | |||
| enum TabKeys { | |||
| @@ -22,12 +22,17 @@ enum TabKeys { | |||
| History = 'history', | |||
| } | |||
| function AutoMLInstance() { | |||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<HyperParameterData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const NodePrefix = 'workflow'; | |||
| function HyperParameterInstance() { | |||
| const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( | |||
| undefined, | |||
| ); | |||
| // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 | |||
| const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); | |||
| const [nodes, setNodes] = useState<Record<string, NodeStatus> | undefined>(undefined); | |||
| const params = useParams(); | |||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(null); | |||
| @@ -38,41 +43,58 @@ function AutoMLInstance() { | |||
| return () => { | |||
| closeSSE(); | |||
| }; | |||
| }, []); | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [instanceId]); | |||
| // 获取实验实例详情 | |||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | |||
| const [res] = await to(getExperimentInsReq(instanceId)); | |||
| const [res] = await to(getRayInsReq(instanceId)); | |||
| if (res && res.data) { | |||
| const info = res.data as AutoMLInstanceData; | |||
| const info = res.data as HyperParameterInstanceData; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | |||
| // 解析配置参数 | |||
| const paramJson = parseJsonText(param); | |||
| if (paramJson) { | |||
| setAutoMLInfo(paramJson); | |||
| // 实例详情返回的参数是字符串,需要转换 | |||
| if (typeof paramJson.parameters === 'string') { | |||
| paramJson.parameters = parseJsonText(paramJson.parameters); | |||
| } | |||
| if (!Array.isArray(paramJson.parameters)) { | |||
| paramJson.parameters = []; | |||
| } | |||
| // 实例详情返回的运行参数是字符串,需要转换 | |||
| if (typeof paramJson.points_to_evaluate === 'string') { | |||
| paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate); | |||
| } | |||
| if (!Array.isArray(paramJson.points_to_evaluate)) { | |||
| paramJson.points_to_evaluate = []; | |||
| } | |||
| setExperimentInfo(paramJson); | |||
| } | |||
| setInstanceInfo(info); | |||
| // 这个接口返回的状态有延时,SSE 返回的状态是最新的 | |||
| // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE | |||
| // SSE 调用时,不需要解析 node_status,也不要重新建立 SSE | |||
| if (isStatusDetermined) { | |||
| setInstanceInfo((prev) => ({ | |||
| ...info, | |||
| nodeStatus: prev!.nodeStatus, | |||
| })); | |||
| return; | |||
| } | |||
| // 进行节点状态 | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| Object.keys(nodeStatusJson).forEach((key) => { | |||
| if (key.startsWith('auto-ml')) { | |||
| const value = nodeStatusJson[key]; | |||
| info.nodeStatus = value; | |||
| setNodes(nodeStatusJson); | |||
| Object.keys(nodeStatusJson).some((key) => { | |||
| if (key.startsWith(NodePrefix)) { | |||
| const workflowStatus = nodeStatusJson[key]; | |||
| setWorkflowStatus(workflowStatus); | |||
| return true; | |||
| } | |||
| return false; | |||
| }); | |||
| } | |||
| setInstanceInfo(info); | |||
| // 运行中或者等待中,开启 SSE | |||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||
| setupSSE(argo_ins_name, argo_ins_ns); | |||
| @@ -81,10 +103,7 @@ function AutoMLInstance() { | |||
| }; | |||
| const setupSSE = (name: string, namespace: string) => { | |||
| let { origin } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| origin = 'http://172.20.32.181:31213'; | |||
| } | |||
| const { origin } = location; | |||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||
| const evtSource = new EventSource( | |||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||
| @@ -99,19 +118,21 @@ function AutoMLInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| const statusData = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith('auto-ml'), | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| if (statusData) { | |||
| setInstanceInfo((prev) => ({ | |||
| ...prev!, | |||
| nodeStatus: statusData, | |||
| })); | |||
| // 节点 | |||
| setNodes(nodes); | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| if ( | |||
| statusData.phase !== ExperimentStatus.Pending && | |||
| statusData.phase !== ExperimentStatus.Running | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| ) { | |||
| closeSSE(); | |||
| getExperimentInsInfo(true); | |||
| @@ -141,9 +162,9 @@ function AutoMLInstance() { | |||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <HyperParameterBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={instanceInfo?.nodeStatus} | |||
| className={styles['hyper-parameter-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={workflowStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| @@ -153,17 +174,8 @@ function AutoMLInstance() { | |||
| label: '日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| <div className={styles['hyper-parameter-instance__log']}> | |||
| {instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />} | |||
| </div> | |||
| ), | |||
| }, | |||
| @@ -174,24 +186,13 @@ function AutoMLInstance() { | |||
| key: TabKeys.Result, | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: ( | |||
| <ExperimentResult | |||
| fileUrl={instanceInfo?.result_path} | |||
| imageUrl={instanceInfo?.img_path} | |||
| modelPath={instanceInfo?.model_path} | |||
| /> | |||
| ), | |||
| children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />, | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| label: '寻优列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| ), | |||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />, | |||
| }, | |||
| ]; | |||
| @@ -201,15 +202,10 @@ function AutoMLInstance() { | |||
| : basicTabItems; | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| /> | |||
| <div className={styles['hyper-parameter-instance']}> | |||
| <Tabs className={styles['hyper-parameter-instance__tabs']} items={tabItems} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLInstance; | |||
| export default HyperParameterInstance; | |||
| @@ -1,14 +1,14 @@ | |||
| import CodeSelect from '@/components/CodeSelect'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import ResourceSelect, { | |||
| ResourceSelectorType, | |||
| requiredValidator, | |||
| } from '@/components/ResourceSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { hyperParameterOptimizedModeOptions } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { isEmpty } from '@/utils'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { modalConfirm, removeFormListItem } from '@/utils/ui'; | |||
| import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; | |||
| import { | |||
| Button, | |||
| @@ -86,7 +86,6 @@ function ExecuteConfig() { | |||
| const searchAlgorithm = Form.useWatch('search_alg', form); | |||
| const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; | |||
| const paramsTypeTooltip = searchAlgorithm === 'Ax' ? axParameterTooltip : parameterTooltip; | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); | |||
| const handleSearchAlgorithmChange = (value: string) => { | |||
| if ( | |||
| @@ -109,7 +108,7 @@ function ExecuteConfig() { | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="代码配置" | |||
| name="code" | |||
| name="code_config" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| @@ -157,7 +156,6 @@ function ExecuteConfig() { | |||
| type={ResourceSelectorType.Mirror} | |||
| placeholder="请选择镜像" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -180,7 +178,6 @@ function ExecuteConfig() { | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -193,7 +190,6 @@ function ExecuteConfig() { | |||
| type={ResourceSelectorType.Model} | |||
| placeholder="请选择模型" | |||
| canInput={false} | |||
| size="large" | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| @@ -202,16 +198,16 @@ function ExecuteConfig() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="总实验次数" | |||
| label="总试验次数" | |||
| name="num_samples" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入总实验次数', | |||
| message: '请输入总试验次数', | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入总实验次数" min={0} precision={0} /> | |||
| <InputNumber placeholder="请输入总试验次数" min={0} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -297,7 +293,7 @@ function ExecuteConfig() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="参数" | |||
| label="超参数" | |||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||
| required | |||
| ></Form.Item> | |||
| @@ -396,12 +392,14 @@ function ExecuteConfig() { | |||
| size="middle" | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '确定要删除该参数吗?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| }); | |||
| removeFormListItem( | |||
| form, | |||
| 'parameters', | |||
| name, | |||
| remove, | |||
| ['name', 'type'], | |||
| '删除后,该参数将不可恢复', | |||
| ); | |||
| }} | |||
| ></Button> | |||
| {index === fields.length - 1 && ( | |||
| @@ -460,7 +458,7 @@ function ExecuteConfig() { | |||
| ); | |||
| if (arr.length > 0 && arr.length < runParameters.length) { | |||
| return Promise.reject( | |||
| new Error(`手动运行参数 ${name} 必须全部填写或者都不填写`), | |||
| new Error(`手动运行超参数 "${name}" 必须全部填写或者都不填写`), | |||
| ); | |||
| } | |||
| } | |||
| @@ -475,7 +473,7 @@ function ExecuteConfig() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="手动运行参数" | |||
| label="手动运行超参数" | |||
| style={{ marginBottom: 0, marginTop: '-14px' }} | |||
| ></Form.Item> | |||
| </Col> | |||
| @@ -513,13 +511,13 @@ function ExecuteConfig() { | |||
| marginRight: '3px', | |||
| }} | |||
| shape="circle" | |||
| disabled={fields.length === 1} | |||
| type="text" | |||
| size="middle" | |||
| icon={<MinusCircleOutlined />} | |||
| onClick={() => { | |||
| modalConfirm({ | |||
| title: '确定要删除该运行参数吗?', | |||
| title: '删除后,该运行参数将不可恢复', | |||
| content: '是否确认删除?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| @@ -538,6 +536,20 @@ function ExecuteConfig() { | |||
| </div> | |||
| </Flex> | |||
| ))} | |||
| {fields.length === 0 && ( | |||
| <Form.Item className={styles['add-weight']}> | |||
| <Button | |||
| className={styles['add-weight__button']} | |||
| color="primary" | |||
| variant="dashed" | |||
| onClick={() => add()} | |||
| block | |||
| icon={<PlusCircleOutlined />} | |||
| > | |||
| 添加手动运行参数 | |||
| </Button> | |||
| </Form.Item> | |||
| )} | |||
| <Form.ErrorList errors={errors} className={styles['run-parameter__error']} /> | |||
| </div> | |||
| </> | |||
| @@ -567,9 +579,9 @@ function ExecuteConfig() { | |||
| <Row gutter={0}> | |||
| <Col span={24}> | |||
| <Form.Item | |||
| label="优化方向" | |||
| label="指标优化方向" | |||
| name="mode" | |||
| rules={[{ required: true, message: '请选择优化方向' }]} | |||
| rules={[{ required: true, message: '请选择指标优化方向' }]} | |||
| > | |||
| <Radio.Group options={hyperParameterOptimizedModeOptions}></Radio.Group> | |||
| </Form.Item> | |||
| @@ -580,7 +592,7 @@ function ExecuteConfig() { | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="资源规格" | |||
| name="resource" | |||
| name="computing_resource_id" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| @@ -588,16 +600,7 @@ function ExecuteConfig() { | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| showSearch | |||
| placeholder="请选择资源规格" | |||
| filterOption={filterResourceStandard} | |||
| options={resourceStandardList} | |||
| fieldNames={{ | |||
| label: 'description', | |||
| value: 'standard', | |||
| }} | |||
| /> | |||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -1,5 +1,6 @@ | |||
| import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; | |||
| import { Button, Flex, Form, Input, InputNumber } from 'antd'; | |||
| import React from 'react'; | |||
| import { ParameterType, getFormOptions, parameterTooltip } from '../utils'; | |||
| import styles from './index.less'; | |||
| @@ -67,7 +68,7 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { | |||
| <Flex | |||
| style={{ | |||
| marginLeft: '10px', | |||
| marginBottom: '20px', | |||
| marginBottom: '24px', | |||
| flex: 'none', | |||
| width: '66px', | |||
| }} | |||
| @@ -115,9 +116,8 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { | |||
| <Flex align="start" style={{ width: '100%', marginBottom: '20px' }}> | |||
| {formOptions.map((item, index) => { | |||
| return ( | |||
| <> | |||
| <React.Fragment key={item.name}> | |||
| <Form.Item | |||
| key={item.name} | |||
| name={item.name} | |||
| style={{ flex: 1, marginInlineEnd: 0 }} | |||
| rules={[ | |||
| @@ -134,7 +134,7 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { | |||
| {index === 0 ? '-' : ' '} | |||
| </span> | |||
| )} | |||
| </> | |||
| </React.Fragment> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| @@ -11,6 +11,7 @@ | |||
| // 增加样式权重 | |||
| & &__button { | |||
| width: calc(100% - 126px); | |||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||
| box-shadow: none !important; | |||
| &:hover { | |||
| @@ -8,7 +8,66 @@ | |||
| border-radius: 10px; | |||
| &__table { | |||
| height: 100%; | |||
| height: calc(100% - 52px); | |||
| margin-top: 20px; | |||
| } | |||
| :global { | |||
| .ant-table-container { | |||
| border: none !important; | |||
| } | |||
| .ant-table-thead { | |||
| .ant-table-cell { | |||
| background-color: rgb(247, 247, 247); | |||
| border-color: @border-color !important; | |||
| } | |||
| } | |||
| .ant-table-tbody { | |||
| .ant-table-cell { | |||
| border-right: none !important; | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-tbody-virtual::after { | |||
| border-bottom: none !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .cell-index { | |||
| position: relative; | |||
| width: 100%; | |||
| white-space: nowrap; | |||
| &__best-tag { | |||
| margin-left: 8px; | |||
| padding: 1px 10px; | |||
| color: @success-color; | |||
| font-weight: normal; | |||
| font-size: 13px; | |||
| white-space: nowrap; | |||
| background-color: .addAlpha(@success-color, 0.1) []; | |||
| border-radius: 2px; | |||
| } | |||
| } | |||
| .table-best-row { | |||
| color: @success-color; | |||
| font-weight: bold; | |||
| } | |||
| .trail-result { | |||
| :global { | |||
| .ant-tree-node-selected { | |||
| .trail-result__icon { | |||
| color: white; | |||
| } | |||
| } | |||
| .trail-result__icon { | |||
| margin-left: 8px; | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,115 +1,223 @@ | |||
| import { getFileReq } from '@/services/file'; | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import TableColTitle from '@/components/TableColTitle'; | |||
| import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | |||
| import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types'; | |||
| import { getExpMetricsReq } from '@/services/hyperParameter'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { App, Button, Table, Tree, type TableProps, type TreeDataNode } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| const { DirectoryTree } = Tree; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| isClassification: boolean; | |||
| trialList?: HyperParameterTrial[]; | |||
| }; | |||
| type TableData = { | |||
| id?: string; | |||
| accuracy?: number; | |||
| duration?: number; | |||
| train_loss?: number; | |||
| status?: string; | |||
| feature?: string; | |||
| althorithm?: string; | |||
| }; | |||
| function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const { message } = App.useApp(); | |||
| const [tableData, setTableData] = useState<HyperParameterTrial[]>([]); | |||
| const [loading, setLoading] = useState(false); | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| // 防止 Tabs 卡顿 | |||
| useEffect(() => { | |||
| 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) => { | |||
| setLoading(true); | |||
| setTimeout(() => { | |||
| setTableData(trialList); | |||
| setLoading(false); | |||
| }, 500); | |||
| }, [trialList]); | |||
| // 计算 column | |||
| const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined; | |||
| const config: Record<string, any> = first?.config ?? {}; | |||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | |||
| const paramsNames = Object.keys(config); | |||
| const metricNames = Object.keys(metricAnalysis); | |||
| const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: 100, | |||
| fixed: 'left', | |||
| render: (_text, record, index: number) => { | |||
| return ( | |||
| <div className={styles['cell-index']}> | |||
| <span className={styles['cell-index__text']}>{index + 1}</span> | |||
| {record.is_best && <span className={styles['cell-index__best-tag']}>最佳</span>} | |||
| </div> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: '基本信息', | |||
| align: 'center', | |||
| children: [ | |||
| { | |||
| title: '运行次数', | |||
| dataIndex: 'training_iteration', | |||
| key: 'training_iteration', | |||
| width: 120, | |||
| fixed: 'left', | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '平均时长(秒)', | |||
| dataIndex: 'time_avg', | |||
| key: 'time_avg', | |||
| width: 150, | |||
| fixed: 'left', | |||
| render: tableCellRender(false, TableCellValueType.Custom, { | |||
| format: (value = 0) => Number(value).toFixed(2), | |||
| }), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| fixed: 'left', | |||
| render: TrialStatusCell, | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| if (paramsNames.length) { | |||
| trialColumns.push({ | |||
| title: '运行参数', | |||
| dataIndex: 'config', | |||
| key: 'config', | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['config', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| })), | |||
| }); | |||
| } | |||
| if (metricNames.length) { | |||
| trialColumns.push({ | |||
| title: `指标分析(${first.metric ?? ''})`, | |||
| dataIndex: 'metrics', | |||
| key: 'metrics', | |||
| align: 'center', | |||
| children: metricNames.map((name) => ({ | |||
| title: <TableColTitle title={name} />, | |||
| dataIndex: ['metric_analysis', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| })), | |||
| }); | |||
| } | |||
| // 自定义展开视图 | |||
| const expandedRowRender = (record: HyperParameterTrial) => { | |||
| const filesToTreeData = ( | |||
| files: HyperParameterFile[], | |||
| parent?: HyperParameterFile, | |||
| ): TreeDataNode[] => | |||
| files.map((file) => { | |||
| const key = parent ? `${parent.name}/${file.name}` : file.name; | |||
| 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], | |||
| ...file, | |||
| key, | |||
| title: file.name, | |||
| children: file.children ? filesToTreeData(file.children, file) : undefined, | |||
| }; | |||
| }); | |||
| 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); | |||
| const treeData: TreeDataNode[] = filesToTreeData([record.file]); | |||
| return ( | |||
| <InfoGroup title="寻优结果" className={styles['trail-result']}> | |||
| <DirectoryTree | |||
| // @ts-ignore | |||
| treeData={treeData} | |||
| defaultExpandAll | |||
| titleRender={(record: TreeDataNode & HyperParameterFile) => { | |||
| const label = record.title + (record.isFile ? `(${record.size})` : ''); | |||
| return ( | |||
| <> | |||
| <span style={{ fontSize: 14 }}>{label}</span> | |||
| <KFIcon | |||
| type="icon-xiazai" | |||
| className="trail-result__icon" | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| downLoadZip( | |||
| record.isFile | |||
| ? `/api/mmp/minioStorage/downloadFile` | |||
| : `/api/mmp/minioStorage/download`, | |||
| { path: record.url }, | |||
| ); | |||
| }} | |||
| /> | |||
| </> | |||
| ); | |||
| }} | |||
| /> | |||
| </InfoGroup> | |||
| ); | |||
| }; | |||
| // 展开实例 | |||
| const handleExpandChange = (expanded: boolean, record: HyperParameterTrial) => { | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.trial_id]); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| }; | |||
| const columns: TableProps<TableData>['columns'] = [ | |||
| { | |||
| title: 'ID', | |||
| dataIndex: 'id', | |||
| key: 'id', | |||
| width: 80, | |||
| render: tableCellRender(false), | |||
| }, | |||
| { | |||
| title: '准确率', | |||
| dataIndex: 'accuracy', | |||
| key: 'accuracy', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '耗时', | |||
| dataIndex: 'duration', | |||
| key: 'duration', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '训练损失', | |||
| dataIndex: 'train_loss', | |||
| key: 'train_loss', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| // 选择行 | |||
| const rowSelection: TableProps<HyperParameterTrial>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| { | |||
| title: '特征处理', | |||
| dataIndex: 'feature', | |||
| key: 'feature', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '算法', | |||
| dataIndex: 'althorithm', | |||
| key: 'althorithm', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 120, | |||
| render: tableCellRender(false), | |||
| }, | |||
| ]; | |||
| }; | |||
| // 对比 | |||
| const handleComparisonClick = () => { | |||
| if (selectedRowKeys.length < 1) { | |||
| message.error('请至少选择一项'); | |||
| return; | |||
| } | |||
| getExpMetrics(); | |||
| }; | |||
| // 获取对比 url | |||
| const getExpMetrics = async () => { | |||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| window.open(url, '_blank'); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| <Button type="default" onClick={handleComparisonClick}> | |||
| 可视化对比 | |||
| </Button> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| @@ -117,11 +225,20 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| )} | |||
| > | |||
| <Table | |||
| loading={loading} | |||
| rowClassName={(record) => (record.is_best ? styles['table-best-row'] : '')} | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| columns={trialColumns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| bordered={true} | |||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||
| rowKey="trial_id" | |||
| expandable={{ | |||
| expandedRowRender: expandedRowRender, | |||
| onExpand: handleExpandChange, | |||
| expandedRowKeys: expandedRowKeys, | |||
| }} | |||
| rowSelection={rowSelection} | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,16 @@ | |||
| .experiment-log { | |||
| height: 100%; | |||
| &__tabs { | |||
| height: 100%; | |||
| :global { | |||
| .ant-tabs-nav-list { | |||
| padding-left: 0 !important; | |||
| background: none !important; | |||
| } | |||
| } | |||
| &__log { | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,109 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import LogList from '@/pages/Experiment/components/LogList'; | |||
| import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { Tabs } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentLogProps = { | |||
| instanceInfo: HyperParameterInstanceData; | |||
| nodes: Record<string, NodeStatus>; | |||
| }; | |||
| function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| let hpoNodeStatus: NodeStatus | undefined; | |||
| let frameworkCloneNodeStatus: NodeStatus | undefined; | |||
| let trainCloneNodeStatus: NodeStatus | undefined; | |||
| Object.keys(nodes) | |||
| .sort((key1, key2) => { | |||
| const node1 = nodes[key1]; | |||
| const node2 = nodes[key2]; | |||
| return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime(); | |||
| }) | |||
| .forEach((key) => { | |||
| const node = nodes[key]; | |||
| if (node.displayName.startsWith('auto-hpo')) { | |||
| hpoNodeStatus = node; | |||
| } else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) { | |||
| frameworkCloneNodeStatus = node; | |||
| } else if ( | |||
| node.displayName.startsWith('git-clone') && | |||
| frameworkCloneNodeStatus && | |||
| node.displayName !== frameworkCloneNodeStatus?.displayName | |||
| ) { | |||
| trainCloneNodeStatus = node; | |||
| } | |||
| }); | |||
| const tabItems = [ | |||
| // { | |||
| // key: 'git-clone-framework', | |||
| // label: '框架代码日志', | |||
| // // icon: <KFIcon type="icon-rizhi1" />, | |||
| // children: ( | |||
| // <div className={styles['experiment-log__tabs__log']}> | |||
| // {frameworkCloneNodeStatus && ( | |||
| // <LogList | |||
| // instanceName={instanceInfo.argo_ins_name} | |||
| // instanceNamespace={instanceInfo.argo_ins_ns} | |||
| // pipelineNodeId={frameworkCloneNodeStatus.displayName} | |||
| // workflowId={frameworkCloneNodeStatus.id} | |||
| // instanceNodeStartTime={frameworkCloneNodeStatus.startedAt} | |||
| // instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus} | |||
| // ></LogList> | |||
| // )} | |||
| // </div> | |||
| // ), | |||
| // }, | |||
| { | |||
| key: 'git-clone-train', | |||
| label: '系统日志', | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {trainCloneNodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={trainCloneNodeStatus.displayName} | |||
| workflowId={trainCloneNodeStatus.id} | |||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | |||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| key: 'auto-hpo', | |||
| label: '超参寻优日志', | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {hpoNodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={hpoNodeStatus.displayName} | |||
| workflowId={hpoNodeStatus.id} | |||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | |||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| useEffect(() => {}, []); | |||
| return ( | |||
| <div className={styles['experiment-log']}> | |||
| <Tabs className={styles['experiment-log__tabs']} items={tabItems} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentLog; | |||
| @@ -6,47 +6,12 @@ | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__download { | |||
| padding-top: 16px; | |||
| padding-bottom: 16px; | |||
| padding-left: @content-padding; | |||
| color: @text-color; | |||
| font-size: 13px; | |||
| background-color: #f8f8f9; | |||
| border-radius: 4px; | |||
| &__btn { | |||
| display: block; | |||
| height: 36px; | |||
| margin-top: 15px; | |||
| font-size: 14px; | |||
| } | |||
| &__table { | |||
| height: 400px; | |||
| } | |||
| &__text { | |||
| white-space: pre-wrap; | |||
| } | |||
| &__images { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: 100%; | |||
| overflow-x: auto; | |||
| :global { | |||
| .ant-image { | |||
| margin-right: 20px; | |||
| &:last-child { | |||
| margin-right: 0; | |||
| } | |||
| } | |||
| } | |||
| &__item { | |||
| height: 248px; | |||
| border: 1px solid rgba(96, 107, 122, 0.3); | |||
| } | |||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||
| white-space: pre; | |||
| } | |||
| } | |||
| @@ -1,81 +1,35 @@ | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import { getFileReq } from '@/services/file'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Image } from 'antd'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentResultProps = { | |||
| fileUrl?: string; | |||
| imageUrl?: string; | |||
| modelPath?: string; | |||
| }; | |||
| function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { | |||
| function ExperimentResult({ fileUrl }: ExperimentResultProps) { | |||
| const [result, setResult] = useState<string | undefined>(''); | |||
| const images = useMemo(() => { | |||
| if (imageUrl) { | |||
| return imageUrl.split(',').map((item) => item.trim()); | |||
| } | |||
| return []; | |||
| }, [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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| <InfoGroup title="最佳实验结果" width="100%"> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| </InfoGroup> | |||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||
| <div className={styles['experiment-result__images']}> | |||
| <Image.PreviewGroup | |||
| preview={{ | |||
| onChange: (current, prev) => | |||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||
| }} | |||
| > | |||
| {images.map((item) => ( | |||
| <Image | |||
| key={item} | |||
| className={styles['experiment-result__images__item']} | |||
| src={item} | |||
| height={248} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </Image.PreviewGroup> | |||
| </div> | |||
| </InfoGroup> | |||
| {modelPath && ( | |||
| <div className={styles['experiment-result__download']}> | |||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||
| <span>save_model.joblib</span> | |||
| <Button | |||
| type="primary" | |||
| className={styles['experiment-result__download__btn']} | |||
| onClick={() => { | |||
| window.location.href = modelPath; | |||
| }} | |||
| > | |||
| 模型下载 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -41,16 +41,7 @@ function HyperParameterBasic({ | |||
| runStatus, | |||
| isInstance = false, | |||
| }: HyperParameterBasicProps) { | |||
| const getResourceDescription = useComputingResource()[2]; | |||
| // 格式化资源规格 | |||
| const formatResource = (resource?: string) => { | |||
| if (!resource) { | |||
| return undefined; | |||
| } | |||
| return getResourceDescription(resource); | |||
| }; | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| @@ -90,7 +81,7 @@ function HyperParameterBasic({ | |||
| return [ | |||
| { | |||
| label: '代码', | |||
| value: info.code, | |||
| value: info.code_config, | |||
| format: formatCodeConfig, | |||
| }, | |||
| { | |||
| @@ -113,7 +104,7 @@ function HyperParameterBasic({ | |||
| format: formatModel, | |||
| }, | |||
| { | |||
| label: '总实验次数', | |||
| label: '总试验次数', | |||
| value: info.num_samples, | |||
| }, | |||
| { | |||
| @@ -135,7 +126,7 @@ function HyperParameterBasic({ | |||
| value: info.min_samples_required, | |||
| }, | |||
| { | |||
| label: '优化方向', | |||
| label: '指标优化方向', | |||
| value: info.mode, | |||
| format: formatOptimizeMode, | |||
| }, | |||
| @@ -145,11 +136,11 @@ function HyperParameterBasic({ | |||
| }, | |||
| { | |||
| label: '资源规格', | |||
| value: info.resource, | |||
| format: formatResource, | |||
| value: info.computing_resource_id, | |||
| format: getResourceDescription, | |||
| }, | |||
| ]; | |||
| }, [info]); | |||
| }, [info, getResourceDescription]); | |||
| const instanceDatas = useMemo(() => { | |||
| if (!runStatus) { | |||