Browse Source

Merge branch 'dev-zw' into dev-zw-active-learn

pull/197/head
cp3hnu 10 months ago
parent
commit
b53ea7a3a1
100 changed files with 1809 additions and 1426 deletions
  1. +2
    -0
      .gitignore
  2. +2
    -1
      react-ui/.eslintignore
  3. +7
    -1
      react-ui/.eslintrc.js
  4. +1
    -1
      react-ui/.nvmrc
  5. +19
    -0
      react-ui/.storybook/blocks/StoryName.tsx
  6. +1
    -1
      react-ui/.storybook/main.ts
  7. +6
    -0
      react-ui/.storybook/manager.ts
  8. +92
    -0
      react-ui/.storybook/mock/websocket.mock.js
  9. +5
    -0
      react-ui/.storybook/preview.tsx
  10. +7
    -0
      react-ui/.storybook/theme.ts
  11. +1
    -133
      react-ui/README.md
  12. +50
    -38
      react-ui/config/routes.ts
  13. +11
    -5
      react-ui/package.json
  14. BIN
      react-ui/public/fonts/DingTalk-JinBuTi.ttf
  15. BIN
      react-ui/public/fonts/DingTalk-JinBuTi.woff
  16. BIN
      react-ui/public/fonts/DingTalk-JinBuTi.woff2
  17. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf
  18. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf
  19. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff
  20. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2
  21. +22
    -4
      react-ui/public/fonts/font.css
  22. +0
    -0
      react-ui/public/mockServiceWorker.js
  23. +6
    -3
      react-ui/src/app.tsx
  24. BIN
      react-ui/src/assets/img/user-points-bg.png
  25. BIN
      react-ui/src/assets/img/workspace-experiment.png
  26. BIN
      react-ui/src/assets/img/workspace-pipeline.png
  27. +3
    -3
      react-ui/src/components/BasicInfo/index.tsx
  28. +5
    -3
      react-ui/src/components/BasicTableInfo/index.tsx
  29. +16
    -8
      react-ui/src/components/CodeSelect/index.tsx
  30. +14
    -14
      react-ui/src/components/CodeSelectorModal/index.tsx
  31. +0
    -1
      react-ui/src/components/FormInfo/index.less
  32. +43
    -10
      react-ui/src/components/FormInfo/index.tsx
  33. +11
    -14
      react-ui/src/components/IFramePage/index.tsx
  34. +1
    -1
      react-ui/src/components/KFSpin/index.tsx
  35. +12
    -6
      react-ui/src/components/ParameterInput/index.tsx
  36. +4
    -22
      react-ui/src/components/ParameterSelect/config.tsx
  37. +68
    -33
      react-ui/src/components/ParameterSelect/index.tsx
  38. +40
    -44
      react-ui/src/components/ResourceSelect/index.tsx
  39. +52
    -52
      react-ui/src/components/ResourceSelectorModal/index.tsx
  40. +3
    -0
      react-ui/src/components/TableColTitle/index.less
  41. +32
    -0
      react-ui/src/components/TableColTitle/index.tsx
  42. +20
    -0
      react-ui/src/enums/index.ts
  43. +5
    -3
      react-ui/src/hooks/index.ts
  44. +40
    -31
      react-ui/src/hooks/resource.ts
  45. +0
    -25
      react-ui/src/hooks/sessionStorage.ts
  46. +18
    -17
      react-ui/src/pages/Authorize/index.tsx
  47. +50
    -50
      react-ui/src/pages/AutoML/Create/index.tsx
  48. +9
    -9
      react-ui/src/pages/AutoML/Info/index.tsx
  49. +9
    -15
      react-ui/src/pages/AutoML/Instance/index.tsx
  50. +0
    -1
      react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx
  51. +6
    -1
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx
  52. +9
    -7
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  53. +29
    -33
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx
  54. +1
    -1
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.less
  55. +6
    -6
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  56. +10
    -16
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  57. +2
    -1
      react-ui/src/pages/AutoML/components/ExperimentResult/index.less
  58. +8
    -8
      react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx
  59. +3
    -0
      react-ui/src/pages/AutoML/components/TrialStatusCell/index.less
  60. +67
    -0
      react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx
  61. +7
    -7
      react-ui/src/pages/CodeConfig/List/index.tsx
  62. +49
    -52
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  63. +23
    -23
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  64. +24
    -21
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  65. +8
    -2
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  66. +18
    -18
      react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx
  67. +2
    -1
      react-ui/src/pages/Dataset/config.tsx
  68. +6
    -19
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  69. +7
    -7
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  70. +2
    -2
      react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx
  71. +3
    -23
      react-ui/src/pages/Experiment/Comparison/index.less
  72. +27
    -37
      react-ui/src/pages/Experiment/Comparison/index.tsx
  73. +10
    -12
      react-ui/src/pages/Experiment/Info/index.jsx
  74. +0
    -6
      react-ui/src/pages/Experiment/Info/index.less
  75. +2
    -1
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less
  76. +4
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  77. +5
    -5
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  78. +7
    -15
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  79. +11
    -12
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  80. +14
    -14
      react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx
  81. +2
    -1
      react-ui/src/pages/Experiment/components/LogGroup/index.less
  82. +116
    -102
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  83. +39
    -28
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  84. +22
    -27
      react-ui/src/pages/Experiment/index.jsx
  85. +1
    -6
      react-ui/src/pages/HyperParameter/Create/index.less
  86. +30
    -30
      react-ui/src/pages/HyperParameter/Create/index.tsx
  87. +1
    -1
      react-ui/src/pages/HyperParameter/Info/index.less
  88. +11
    -11
      react-ui/src/pages/HyperParameter/Info/index.tsx
  89. +2
    -2
      react-ui/src/pages/HyperParameter/Instance/index.less
  90. +68
    -72
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  91. +37
    -34
      react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
  92. +4
    -4
      react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx
  93. +1
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/index.less
  94. +60
    -1
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
  95. +211
    -94
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
  96. +16
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
  97. +109
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  98. +4
    -39
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
  99. +11
    -57
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
  100. +7
    -16
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx

+ 2
- 0
.gitignore View File

@@ -60,3 +60,5 @@ mvnw
**/node_modules

*storybook.log

/react-ui/docs

+ 2
- 1
react-ui/.eslintignore View File

@@ -5,4 +5,5 @@
public
dist
.umi
mock
mock
/src/iconfont/

+ 7
- 1
react-ui/.eslintrc.js View File

@@ -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
react-ui/.nvmrc View File

@@ -1 +1 @@
v18.16.0
v18.20.7

+ 19
- 0
react-ui/.storybook/blocks/StoryName.tsx View File

@@ -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>;
}
}
};

+ 1
- 1
react-ui/.storybook/main.ts View File

@@ -16,7 +16,7 @@ const config: StorybookConfig = {
name: '@storybook/react-webpack5',
options: {},
},
staticDirs: ['../static'],
staticDirs: ['../public', { from: '../docs', to: '/docs' }],
docs: {
defaultName: 'Documentation',
},


+ 6
- 0
react-ui/.storybook/manager.ts View File

@@ -0,0 +1,6 @@
import { addons } from '@storybook/manager-api';
import theme from './theme';

addons.setConfig({
theme: theme,
});

+ 92
- 0
react-ui/.storybook/mock/websocket.mock.js View File

@@ -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
- 0
react-ui/.storybook/preview.tsx View File

@@ -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: {


+ 7
- 0
react-ui/.storybook/theme.ts View File

@@ -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
react-ui/README.md View File

@@ -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.

[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) ![Github Action](https://github.com/ant-design/ant-design-pro/workflows/Node%20CI/badge.svg) ![Deploy](https://github.com/ant-design/ant-design-pro/workflows/Deploy%20CI/badge.svg)

[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/) ![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label)

![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png)

</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

+ 50
- 38
react-ui/config/routes.ts View File

@@ -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,


+ 11
- 5
react-ui/package.json View File

@@ -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"
]
}
}

BIN
react-ui/public/fonts/DingTalk-JinBuTi.ttf View File


BIN
react-ui/public/fonts/DingTalk-JinBuTi.woff View File


BIN
react-ui/public/fonts/DingTalk-JinBuTi.woff2 View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2 View File


+ 22
- 4
react-ui/public/fonts/font.css View File

@@ -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; /* 优化页面加载时的字体显示 */
}

react-ui/static/mockServiceWorker.js → react-ui/public/mockServiceWorker.js View File


+ 6
- 3
react-ui/src/app.tsx View File

@@ -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: {


BIN
react-ui/src/assets/img/user-points-bg.png View File

Before After
Width: 981  |  Height: 672  |  Size: 220 kB

BIN
react-ui/src/assets/img/workspace-experiment.png View File

Before After
Width: 147  |  Height: 148  |  Size: 17 kB Width: 216  |  Height: 216  |  Size: 24 kB

BIN
react-ui/src/assets/img/workspace-pipeline.png View File

Before After
Width: 169  |  Height: 159  |  Size: 21 kB Width: 219  |  Height: 216  |  Size: 31 kB

+ 3
- 3
react-ui/src/components/BasicInfo/index.tsx View File

@@ -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
- 3
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -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) {


+ 16
- 8
react-ui/src/components/CodeSelect/index.tsx View File

@@ -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"


+ 14
- 14
react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -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);


+ 0
- 1
react-ui/src/components/FormInfo/index.less View File

@@ -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;


+ 43
- 10
react-ui/src/components/FormInfo/index.tsx View File

@@ -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>
);


+ 11
- 14
react-ui/src/components/IFramePage/index.tsx View File

@@ -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);


+ 1
- 1
react-ui/src/components/KFSpin/index.tsx View File

@@ -9,7 +9,7 @@ import './index.less';

interface KFSpinProps extends SpinProps {
/** 加载文本 */
label: string;
label?: string;
}

/** 自定义 Spin */


+ 12
- 6
react-ui/src/components/ParameterInput/index.tsx View File

@@ -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}


+ 4
- 22
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -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'],
},
};

+ 68
- 33
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -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
/>


+ 40
- 44
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -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}


+ 52
- 52
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -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);
}
}
};


+ 3
- 0
react-ui/src/components/TableColTitle/index.less View File

@@ -0,0 +1,3 @@
.ant-table .ant-table-cell .kf-table-col-title {
margin-bottom: 0;
}

+ 32
- 0
react-ui/src/components/TableColTitle/index.tsx View File

@@ -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;

+ 20
- 0
react-ui/src/enums/index.ts View File

@@ -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', // 取消
}

+ 5
- 3
react-ui/src/hooks/index.ts View File

@@ -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 [


+ 40
- 31
react-ui/src/hooks/resource.ts View File

@@ -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;
}

+ 0
- 25
react-ui/src/hooks/sessionStorage.ts View File

@@ -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];
}

+ 18
- 17
react-ui/src/pages/Authorize/index.tsx View File

@@ -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>;
}


+ 50
- 50
react-ui/src/pages/AutoML/Create/index.tsx View File

@@ -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) => {


+ 9
- 9
react-ui/src/pages/AutoML/Info/index.tsx View File

@@ -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']}>


+ 9
- 15
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -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>
);
}


+ 0
- 1
react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx View File

@@ -30,7 +30,6 @@ function DatasetConfig() {
type={ResourceSelectorType.Dataset}
placeholder="请选择数据集"
canInput={false}
size="large"
/>
</Form.Item>
</Col>


+ 6
- 1
react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx View File

@@ -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>


+ 9
- 7
react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx View File

@@ -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 && (


+ 29
- 33
react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx View File

@@ -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,
},
];



+ 1
- 1
react-ui/src/pages/AutoML/components/ExperimentInstance/index.less View File

@@ -26,7 +26,7 @@

.startTime {
.singleLine();
width: calc(20% + 10px);
width: 200px;
}

.status {


+ 6
- 6
react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx View File

@@ -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


+ 10
- 16
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -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"
/>


+ 2
- 1
react-ui/src/pages/AutoML/components/ExperimentResult/index.less View File

@@ -25,7 +25,8 @@
}

&__text {
white-space: pre-wrap;
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre;
}

&__images {


+ 8
- 8
react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx View File

@@ -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%">


+ 3
- 0
react-ui/src/pages/AutoML/components/TrialStatusCell/index.less View File

@@ -0,0 +1,3 @@
.trial-status-cell {
height: 100%;
}

+ 67
- 0
react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx View File

@@ -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;

+ 7
- 7
react-ui/src/pages/CodeConfig/List/index.tsx View File

@@ -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) => {


+ 49
- 52
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -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


+ 23
- 23
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -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;


+ 24
- 21
react-ui/src/pages/Dataset/components/ResourcePage/index.tsx View File

@@ -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();


+ 8
- 2
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -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>
);
}


+ 18
- 18
react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx View File

@@ -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>(


+ 2
- 1
react-ui/src/pages/Dataset/config.tsx View File

@@ -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
- 19
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -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>


+ 7
- 7
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -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) => {


+ 2
- 2
react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx View File

@@ -67,8 +67,8 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) {
message: '请输入镜像Tag',
},
{
pattern: /^[a-zA-Z0-9_-]*$/,
message: '只支持字母、数字、下划线(_)、中横线(-)',
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
]}
>


+ 3
- 23
react-ui/src/pages/Experiment/Comparison/index.less View File

@@ -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;
}
}
}
}

+ 27
- 37
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -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>


+ 10
- 12
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -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));


+ 0
- 6
react-ui/src/pages/Experiment/Info/index.less View File

@@ -30,10 +30,4 @@
background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%;
}

:global {
.ant-drawer-mask {
background: transparent !important;
}
}
}

+ 2
- 1
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less View File

@@ -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;


+ 4
- 2
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -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}


+ 5
- 5
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -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}>


+ 7
- 15
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -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" />
)}


+ 11
- 12
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -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) => {


+ 14
- 14
react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx View File

@@ -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);


+ 2
- 1
react-ui/src/pages/Experiment/components/LogGroup/index.less View File

@@ -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;


+ 116
- 102
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -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>


+ 39
- 28
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -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>
)}


+ 22
- 27
react-ui/src/pages/Experiment/index.jsx View File

@@ -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
- 6
react-ui/src/pages/HyperParameter/Create/index.less View File

@@ -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%;


+ 30
- 30
react-ui/src/pages/HyperParameter/Create/index.tsx View File

@@ -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
- 1
react-ui/src/pages/HyperParameter/Info/index.less View File

@@ -1,4 +1,4 @@
.auto-ml-info {
.hyper-parameter-info {
position: relative;
height: 100%;
&__tabs {


+ 11
- 11
react-ui/src/pages/HyperParameter/Info/index.tsx View File

@@ -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>


+ 2
- 2
react-ui/src/pages/HyperParameter/Instance/index.less View File

@@ -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;


+ 68
- 72
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -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;

+ 37
- 34
react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx View File

@@ -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>


+ 4
- 4
react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx View File

@@ -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>


+ 1
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/index.less View File

@@ -11,6 +11,7 @@

// 增加样式权重
& &__button {
width: calc(100% - 126px);
border-color: .addAlpha(@primary-color, 0.5) [];
box-shadow: none !important;
&:hover {


+ 60
- 1
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less View File

@@ -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;
}
}
}

+ 211
- 94
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx View File

@@ -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>


+ 16
- 0
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less View File

@@ -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%;
}
}
}

+ 109
- 0
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx View File

@@ -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;

+ 4
- 39
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less View File

@@ -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;
}
}

+ 11
- 57
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx View File

@@ -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>
);
}


+ 7
- 16
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

@@ -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) {


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save