Browse Source

Merge branch 'dev' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev

dev-active_learn
somunslotus 11 months ago
parent
commit
ff4e73c665
100 changed files with 2220 additions and 981 deletions
  1. +2
    -0
      .gitignore
  2. +68
    -0
      k8s/template-yaml/rolebindings.yaml
  3. +2
    -7
      react-ui/.gitignore
  4. +1
    -0
      react-ui/.nvmrc
  5. +16
    -0
      react-ui/.storybook/babel-plugin-auto-css-modules.js
  6. +117
    -0
      react-ui/.storybook/main.ts
  7. +19
    -0
      react-ui/.storybook/mock/umijs.mock.tsx
  8. +107
    -0
      react-ui/.storybook/preview.tsx
  9. +19
    -0
      react-ui/.storybook/storybook.css
  10. +26
    -0
      react-ui/.storybook/tsconfig.json
  11. +20
    -0
      react-ui/.storybook/typings.d.ts
  12. +1
    -1
      react-ui/config/config.ts
  13. +1
    -1
      react-ui/config/defaultSettings.ts
  14. +37
    -1
      react-ui/config/routes.ts
  15. +28
    -0
      react-ui/package.json
  16. BIN
      react-ui/public/favicon-cc.ico
  17. BIN
      react-ui/public/favicon-cl.ico
  18. BIN
      react-ui/public/favicon.ico
  19. +307
    -0
      react-ui/public/mockServiceWorker.js
  20. +3
    -3
      react-ui/src/app.tsx
  21. BIN
      react-ui/src/assets/img/logo-cc.png
  22. BIN
      react-ui/src/assets/img/logo-cl.png
  23. BIN
      react-ui/src/assets/img/logo.png
  24. BIN
      react-ui/src/assets/img/popover-bg.png
  25. +86
    -0
      react-ui/src/components/BasicInfo/BasicInfoItem.tsx
  26. +58
    -0
      react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx
  27. +0
    -113
      react-ui/src/components/BasicInfo/components.tsx
  28. +0
    -48
      react-ui/src/components/BasicInfo/format.ts
  29. +26
    -2
      react-ui/src/components/BasicInfo/index.less
  30. +35
    -6
      react-ui/src/components/BasicInfo/index.tsx
  31. +2
    -2
      react-ui/src/components/BasicInfo/types.ts
  32. +8
    -1
      react-ui/src/components/BasicTableInfo/index.less
  33. +9
    -11
      react-ui/src/components/BasicTableInfo/index.tsx
  34. +0
    -0
      react-ui/src/components/CodeConfigItem/index.less
  35. +0
    -0
      react-ui/src/components/CodeConfigItem/index.tsx
  36. +20
    -5
      react-ui/src/components/CodeSelect/index.tsx
  37. +49
    -0
      react-ui/src/components/CodeSelectorModal/index.less
  38. +11
    -7
      react-ui/src/components/CodeSelectorModal/index.tsx
  39. +41
    -0
      react-ui/src/components/ConfigInfo/index.tsx
  40. +1
    -1
      react-ui/src/components/DisabledInput/index.less
  41. +4
    -3
      react-ui/src/components/DisabledInput/index.tsx
  42. +13
    -5
      react-ui/src/components/FullScreenFrame/index.tsx
  43. +12
    -8
      react-ui/src/components/IFramePage/index.tsx
  44. +2
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.less
  45. +7
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.tsx
  46. +12
    -3
      react-ui/src/components/InfoGroup/index.tsx
  47. +1
    -1
      react-ui/src/components/KFEmpty/index.less
  48. +16
    -6
      react-ui/src/components/KFEmpty/index.tsx
  49. +8
    -2
      react-ui/src/components/KFIcon/index.tsx
  50. +0
    -0
      react-ui/src/components/KFModal/KFModalTitle.less
  51. +5
    -1
      react-ui/src/components/KFModal/KFModalTitle.tsx
  52. +6
    -3
      react-ui/src/components/KFModal/index.tsx
  53. +12
    -4
      react-ui/src/components/KFRadio/index.tsx
  54. +11
    -5
      react-ui/src/components/KFSpin/index.tsx
  55. +0
    -19
      react-ui/src/components/LabelValue/index.less
  56. +0
    -20
      react-ui/src/components/LabelValue/index.tsx
  57. +1
    -2
      react-ui/src/components/MenuIconSelector/index.less
  58. +3
    -0
      react-ui/src/components/MenuIconSelector/index.tsx
  59. +8
    -1
      react-ui/src/components/PageTitle/index.tsx
  60. +18
    -5
      react-ui/src/components/ParameterInput/index.less
  61. +21
    -3
      react-ui/src/components/ParameterInput/index.tsx
  62. +1
    -1
      react-ui/src/components/ParameterSelect/index.tsx
  63. +55
    -15
      react-ui/src/components/ResourceSelect/index.tsx
  64. +0
    -0
      react-ui/src/components/ResourceSelectorModal/config.tsx
  65. +0
    -0
      react-ui/src/components/ResourceSelectorModal/index.less
  66. +26
    -10
      react-ui/src/components/ResourceSelectorModal/index.tsx
  67. +7
    -5
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  68. +2
    -5
      react-ui/src/components/RightContent/index.tsx
  69. +9
    -2
      react-ui/src/components/SubAreaTitle/index.tsx
  70. +11
    -0
      react-ui/src/enums/index.ts
  71. +1
    -1
      react-ui/src/global.less
  72. +1
    -1
      react-ui/src/iconfont/iconfont-menu.json
  73. +1
    -1
      react-ui/src/iconfont/iconfont.js
  74. +5
    -0
      react-ui/src/overrides.less
  75. +1
    -1
      react-ui/src/pages/404.tsx
  76. +1
    -1
      react-ui/src/pages/AutoML/Create/index.less
  77. +3
    -4
      react-ui/src/pages/AutoML/Create/index.tsx
  78. +0
    -16
      react-ui/src/pages/AutoML/Info/index.tsx
  79. +3
    -411
      react-ui/src/pages/AutoML/List/index.tsx
  80. +6
    -51
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  81. +0
    -20
      react-ui/src/pages/AutoML/components/ConfigInfo/index.less
  82. +0
    -26
      react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx
  83. +1
    -5
      react-ui/src/pages/AutoML/components/CopyingText/index.tsx
  84. +1
    -4
      react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx
  85. +10
    -1
      react-ui/src/pages/AutoML/components/CreateForm/index.less
  86. +10
    -8
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  87. +75
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/config.ts
  88. +1
    -1
      react-ui/src/pages/AutoML/components/ExperimentList/index.less
  89. +429
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  90. +1
    -1
      react-ui/src/pages/CodeConfig/List/index.tsx
  91. +1
    -4
      react-ui/src/pages/Dataset/components/CategoryItem/index.tsx
  92. +6
    -73
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  93. +1
    -1
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  94. +1
    -0
      react-ui/src/pages/Dataset/components/VersionCompareModal/index.less
  95. +5
    -5
      react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx
  96. +1
    -3
      react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx
  97. +2
    -2
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  98. +55
    -0
      react-ui/src/pages/HyperParameter/Create/index.less
  99. +167
    -0
      react-ui/src/pages/HyperParameter/Create/index.tsx
  100. +40
    -0
      react-ui/src/pages/HyperParameter/Info/index.less

+ 2
- 0
.gitignore View File

@@ -58,3 +58,5 @@ mvnw

# web
**/node_modules

*storybook.log

+ 68
- 0
k8s/template-yaml/rolebindings.yaml View File

@@ -0,0 +1,68 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: custom-workflow
namespace: argo
rules:
- apiGroups:
- argoproj.io
resources:
- workflows
verbs:
- create
- get
- list
- watch
- update
- patch
- delete
- apiGroups:
- ""
resources:
- pods
- services
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- pods/exec
verbs:
- create
- get
- list
- watch
- update
- patch
- delete
- apiGroups:
- "apps"
resources:
- deployments
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: custom-workflow-default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: custom-workflow
subjects:
- kind: ServiceAccount
name: default

+ 2
- 7
react-ui/.gitignore View File

@@ -41,10 +41,5 @@ screenshot
build

pnpm-lock.yaml
/src/services/codeConfig/index.js
/src/pages/CodeConfig/components/AddCodeConfigModal/index.less
/src/pages/CodeConfig/List/index.less
/src/pages/Dataset/components/ResourceItem/index.less
/src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx
/src/pages/CodeConfig/components/CodeConfigItem/index.tsx
/src/pages/Dataset/components/ResourceItem/index.tsx

*storybook.log

+ 1
- 0
react-ui/.nvmrc View File

@@ -0,0 +1 @@
v18.16.0

+ 16
- 0
react-ui/.storybook/babel-plugin-auto-css-modules.js View File

@@ -0,0 +1,16 @@
export default function(babel) {
const { types: t } = babel;
return {
visitor: {
ImportDeclaration(path) {
const source = path.node.source.value;
// console.log("zzzz", source);
if (source.endsWith('.less')) {
if (path.node.specifiers.length > 0) {
path.node.source.value += "?modules";
}
}
},
},
};
};

+ 117
- 0
react-ui/.storybook/main.ts View File

@@ -0,0 +1,117 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import path from 'path';
import webpack from 'webpack';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
// '@storybook/addon-webpack5-compiler-swc',
'@storybook/addon-webpack5-compiler-babel',
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-webpack5',
options: {},
},
staticDirs: ['../public'],
docs: {
defaultName: 'Documentation',
},
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../src'),
'@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'),
};
}
if (config.module && config.module.rules) {
config.module.rules.push(
{
test: /\.less$/,
oneOf: [
{
resourceQuery: /modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
import: true,
esModule: true,
modules: {
localIdentName: '[local]___[hash:base64:5]',
},
},
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项
modifyVars: {
hack: 'true; @import "@/styles/theme.less";',
},
},
},
},
],
include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules
},
{
use: [
'style-loader',
'css-loader',
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项
modifyVars: {
hack: 'true; @import "@/styles/theme.less";',
},
},
},
},
],
include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules
},
],
},
{
test: /\.(tsx?|jsx?)$/,
loader: 'ts-loader',
options: {
transpileOnly: true,
},
include: [
path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules
path.resolve(__dirname, './'),
],
},
);
}
if (config.plugins) {
config.plugins.push(
new webpack.ProvidePlugin({
React: 'react', // 全局注入 React
}),
);
}

return config;
},
babel: async (config: any) => {
if (!config.plugins) {
config.plugins = [];
}

config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js'));
return config;
},
};
export default config;

+ 19
- 0
react-ui/.storybook/mock/umijs.mock.tsx View File

@@ -0,0 +1,19 @@
export const Link = ({ to, children, ...props }: any) => (
<a href={typeof to === 'string' ? to : '#'} {...props}>
{children}
</a>
);

export const request = (url: string, options: any) => {
return fetch(url, options)
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
})
.then((res) => res.json());
};

export { useNavigate, useParams, useSearchParams } from 'react-router-dom';
export const history = window.history;

+ 107
- 0
react-ui/.storybook/preview.tsx View File

@@ -0,0 +1,107 @@
import '@/global.less';
import '@/overrides.less';
import themes from '@/styles/theme.less';
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 './storybook.css';

/*
* Initializes MSW
* See https://github.com/mswjs/msw-storybook-addon#configuring-msw
* to learn how to customize it
*/
initialize();

const preview: Preview = {
parameters: {
controls: {
expanded: true,
sort: 'requiredFirst',
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
values: [
{ name: 'Dark', value: '#000' },
{ name: 'Gray', value: '#f9fafb' },
{ name: 'Light', value: '#FFF' },
],
default: 'Light',
},
options: {
storySort: {
method: 'alphabetical',
order: ['Documentation', 'Components'],
},
},
},
decorators: [
(Story) => (
<ConfigProvider
locale={zhCN}
theme={{
cssVar: true,
token: {
colorPrimary: themes['primaryColor'],
colorSuccess: themes['successColor'],
colorError: themes['errorColor'],
colorWarning: themes['warningColor'],
colorLink: themes['primaryColor'],
colorText: themes['textColor'],
controlHeightLG: 46,
},
components: {
Button: {
defaultBg: 'rgba(22, 100, 255, 0.06)',
defaultBorderColor: 'rgba(22, 100, 255, 0.11)',
defaultColor: themes['textColor'],
defaultHoverBg: 'rgba(22, 100, 255, 0.06)',
defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)',
defaultHoverColor: '#3F7FFF ',
defaultActiveBg: 'rgba(22, 100, 255, 0.12)',
defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)',
defaultActiveColor: themes['primaryColor'],
contentFontSize: parseInt(themes['fontSize']),
},
Input: {
inputFontSize: parseInt(themes['fontSizeInput']),
inputFontSizeLG: parseInt(themes['fontSizeInputLg']),
paddingBlockLG: 10,
},
Select: {
singleItemHeightLG: 46,
optionSelectedColor: themes['primaryColor'],
},
Table: {
headerBg: 'rgba(242, 244, 247, 0.36)',
headerBorderRadius: 4,
rowSelectedBg: 'rgba(22, 100, 255, 0.05)',
},
Tabs: {
titleFontSize: 16,
},
Form: {
labelColor: 'rgba(29, 29, 32, 0.8);',
},
Breadcrumb: {
iconFontSize: parseInt(themes['fontSize']),
linkColor: 'rgba(29, 29, 32, 0.7)',
separatorColor: 'rgba(29, 29, 32, 0.7)',
},
},
}}
>
<App message={{ maxCount: 3 }}>
<Story />
</App>
</ConfigProvider>
),
],
loaders: [mswLoader], // 👈 Add the MSW loader to all stories
};

export default preview;

+ 19
- 0
react-ui/.storybook/storybook.css View File

@@ -0,0 +1,19 @@
html,
body,
#root {
min-width: unset;
height: 100%;
margin: 0;
padding: 0;
overflow-y: visible;
}

.ant-input-search-large .ant-input-affix-wrapper, .ant-input-search-large .ant-input-search-button {
height: 46px;
}

*,
*::before,
*::after {
box-sizing: border-box;
}

+ 26
- 0
react-ui/.storybook/tsconfig.json View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "esnext", // 指定ECMAScript目标版本
"lib": ["dom", "dom.iterable", "esnext"], // 要包含在编译中的库文件列表
"allowJs": true, // 允许编译JavaScript文件
"skipLibCheck": true, // 跳过所有声明文件的类型检查
"esModuleInterop": true, // 禁用命名空间导入(import * as fs from "fs"),并启用CJS/AMD/UMD样式的导入(import fs from "fs")
"allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
"strict": true, // 启用所有严格类型检查选项
"forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写
"module": "esnext", // 指定模块代码生成
"moduleResolution": "bundler", // 使用bundlers样式解析模块
"isolatedModules": true, // 无条件地为未解析的文件发出导入
"resolveJsonModule": true, // 包含.json扩展名的模块
"noEmit": true, // 不发出输出(即不编译代码,只进行类型检查)
"jsx": "react-jsx", // 在.tsx文件中支持JSX
"sourceMap": true, // 生成相应的.map文件
"declaration": true, // 生成相应的.d.ts文件
"noUnusedLocals": true, // 报告未使用的局部变量错误
"noUnusedParameters": true, // 报告未使用的参数错误
"incremental": true, // 通过读写磁盘上的文件来启用增量编译
"noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误
"strictNullChecks": true, // 启用严格的null检查
"baseUrl": "./"
}
}

+ 20
- 0
react-ui/.storybook/typings.d.ts View File

@@ -0,0 +1,20 @@
declare module 'slash2';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'omit.js';
declare module 'numeral';
declare module '@antv/data-set';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';

declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

+ 1
- 1
react-ui/config/config.ts View File

@@ -75,7 +75,7 @@ export default defineConfig({
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
title: '复杂智能软件',
title: '智能材料科研平台',
layout: {
...defaultSettings,
},


+ 1
- 1
react-ui/config/defaultSettings.ts View File

@@ -17,7 +17,7 @@ const Settings: ProLayoutProps & {
fixSiderbar: false,
splitMenus: false,
colorWeak: false,
title: '复杂智能软件',
title: '智能材料科研平台',
pwa: true,
token: {
// 参见ts声明,demo 见文档,通过token 修改样式


+ 37
- 1
react-ui/config/routes.ts View File

@@ -44,7 +44,7 @@ export default [
{
name: 'login',
path: '/user/login',
component: './User/Login/login',
component: process.env.NO_SSO ? './User/Login/login' : './User/Login',
},
],
},
@@ -181,6 +181,42 @@ export default [
},
],
},
{
name: '超参数自动寻优',
path: 'hyperparameter',
routes: [
{
name: '超参数寻优',
path: '',
component: './HyperParameter/List/index',
},
{
name: '实验详情',
path: 'info/:id',
component: './HyperParameter/Info/index',
},
{
name: '创建实验',
path: 'create',
component: './HyperParameter/Create/index',
},
{
name: '编辑实验',
path: 'edit/:id',
component: './HyperParameter/Create/index',
},
{
name: '复制实验',
path: 'copy/:id',
component: './HyperParameter/Create/index',
},
{
name: '实验实例详情',
path: 'instance/:autoMLId/:id',
component: './HyperParameter/Instance/index',
},
],
},
],
},
{


+ 28
- 0
react-ui/package.json View File

@@ -8,6 +8,7 @@
"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",
"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",
@@ -36,6 +37,10 @@
"start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"storybook": "storybook dev -p 6006",
"storybook-build": "storybook build",
"storybook-docs": "storybook dev --docs",
"storybook-docs-build": "storybook build --docs",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
@@ -83,6 +88,17 @@
},
"devDependencies": {
"@ant-design/pro-cli": "^3.1.0",
"@chromatic-com/storybook": "~3.2.4",
"@storybook/addon-essentials": "~8.5.3",
"@storybook/addon-interactions": "~8.5.3",
"@storybook/addon-onboarding": "~8.5.3",
"@storybook/addon-styling-webpack": "~1.0.1",
"@storybook/addon-webpack5-compiler-babel": "~3.0.5",
"@storybook/addon-webpack5-compiler-swc": "~2.0.0",
"@storybook/blocks": "~8.5.3",
"@storybook/react": "~8.5.3",
"@storybook/react-webpack5": "~8.5.3",
"@storybook/test": "~8.5.3",
"@testing-library/react": "^14.0.0",
"@types/antd": "^1.0.0",
"@types/express": "^4.17.14",
@@ -96,15 +112,22 @@
"@umijs/max": "^4.0.66",
"cross-env": "^7.0.3",
"eslint": "^8.39.0",
"eslint-plugin-storybook": "~0.11.2",
"express": "^4.18.2",
"gh-pages": "^5.0.0",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"less": "~4.2.2",
"less-loader": "~12.2.0",
"lint-staged": "^13.2.0",
"mockjs": "^1.1.0",
"msw": "~2.7.0",
"msw-storybook-addon": "~2.0.4",
"prettier": "^2.8.1",
"storybook": "~8.5.3",
"swagger-ui-dist": "^4.18.2",
"ts-loader": "~9.5.2",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"umi-presets-pro": "^2.0.0"
@@ -140,5 +163,10 @@
"CNAME",
"create-umi"
]
},
"msw": {
"workerDirectory": [
"public"
]
}
}

BIN
react-ui/public/favicon-cc.ico View File

Before After

BIN
react-ui/public/favicon-cl.ico View File

Before After

BIN
react-ui/public/favicon.ico View File

Before After

+ 307
- 0
react-ui/public/mockServiceWorker.js View File

@@ -0,0 +1,307 @@
/* eslint-disable */
/* tslint:disable */

/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/

const PACKAGE_VERSION = '2.7.0'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

self.addEventListener('install', function () {
self.skipWaiting()
})

self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})

self.addEventListener('message', async function (event) {
const clientId = event.source.id

if (!clientId || !self.clients) {
return
}

const client = await self.clients.get(clientId)

if (!client) {
return
}

const allClients = await self.clients.matchAll({
type: 'window',
})

switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}

case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}

case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)

sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}

case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}

case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)

const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})

// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}

break
}
}
})

self.addEventListener('fetch', function (event) {
const { request } = event

// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}

// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}

// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}

// Generate unique request ID.
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})

async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)

// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()

sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseClone.body],
)
})()
}

return response
}

// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)

if (activeClientIds.has(event.clientId)) {
return client
}

if (client?.frameType === 'top-level') {
return client
}

const allClients = await self.clients.matchAll({
type: 'window',
})

return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}

async function getResponse(event, client, requestId) {
const { request } = event

// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()

function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)

// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)

if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}

return fetch(requestClone, { headers })
}

// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}

// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}

// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
[requestBuffer],
)

switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}

case 'PASSTHROUGH': {
return passthrough()
}
}

return passthrough()
}

function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()

channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}

resolve(event.data)
}

client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
})
}

async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}

const mockedResponse = new Response(response.body, response)

Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})

return mockedResponse
}

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

@@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css';
import { getAccessToken } from './access';
import './dayjsConfig';
import './global.less';
import { removeAllPageCacheState } from './hooks/pageCacheState';
import {
getRemoteMenu,
@@ -41,7 +40,7 @@ export async function getInitialState(): Promise<GlobalInitialState> {
roleNames: response.user.roles,
} as API.CurrentUser;
} catch (error) {
console.error('1111', error);
console.error('getInitialState', error);
gotoLoginPage();
}
return undefined;
@@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => {
defaultColor: themes['textColor'],
defaultHoverBg: 'rgba(22, 100, 255, 0.06)',
defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)',
defaultHoverColor: '#3F7FFF ',
defaultHoverColor: '#3F7FFF',
defaultActiveBg: 'rgba(22, 100, 255, 0.12)',
defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)',
defaultActiveColor: themes['primaryColor'],
@@ -228,6 +227,7 @@ export const antd: RuntimeAntdConfig = (memo) => {
};
memo.theme.components.Select = {
singleItemHeightLG: 46,
optionSelectedColor: themes['primaryColor'],
};
memo.theme.components.Table = {
headerBg: 'rgba(242, 244, 247, 0.36)',


BIN
react-ui/src/assets/img/logo-cc.png View File

Before After
Width: 104  |  Height: 132  |  Size: 9.2 kB

BIN
react-ui/src/assets/img/logo-cl.png View File

Before After
Width: 112  |  Height: 112  |  Size: 5.3 kB

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

Before After
Width: 104  |  Height: 132  |  Size: 9.2 kB Width: 112  |  Height: 112  |  Size: 5.3 kB

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

Before After
Width: 1200  |  Height: 452  |  Size: 49 kB

+ 86
- 0
react-ui/src/components/BasicInfo/BasicInfoItem.tsx View File

@@ -0,0 +1,86 @@
/*
* @Author: 赵伟
* @Date: 2024-11-29 09:27:19
* @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件
*/

import { Typography } from 'antd';
import React from 'react';
import BasicInfoItemValue from './BasicInfoItemValue';
import { type BasicInfoData, type BasicInfoLink } from './types';

type BasicInfoItemProps = {
/** 基础信息 */
data: BasicInfoData;
/** 标题宽度 */
labelWidth: number;
/** 自定义类名前缀 */
classPrefix: string;
/** 标题是否显示省略号 */
labelEllipsis?: boolean;
/** 标签对齐方式 */
labelAlign?: 'start' | 'end' | 'justify';
};

function BasicInfoItem({
data,
labelWidth,
classPrefix,
labelEllipsis = true,
labelAlign = 'start',
}: BasicInfoItemProps) {
const { label, value, format, ellipsis } = data;
const formatValue = format ? format(value) : value;
const myClassName = `${classPrefix}__item`;
let valueComponent = undefined;
if (React.isValidElement(formatValue)) {
valueComponent = <div className={`${myClassName}__node`}>{formatValue}</div>;
} else if (Array.isArray(formatValue)) {
valueComponent = (
<div className={`${myClassName}__value-container`}>
{formatValue.map((item: BasicInfoLink) => (
<BasicInfoItemValue
key={item.value}
value={item.value}
link={item.link}
url={item.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
))}
</div>
);
} else if (typeof formatValue === 'object' && formatValue) {
valueComponent = (
<BasicInfoItemValue
value={formatValue.value}
link={formatValue.link}
url={formatValue.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
);
} else {
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
}
return (
<div className={myClassName} key={label}>
<div
className={`${myClassName}__label`}
style={{ width: labelWidth, textAlign: labelAlign, textAlignLast: labelAlign }}
>
<Typography.Text
ellipsis={labelEllipsis !== false ? { tooltip: label } : false}
style={{ width: labelAlign === 'justify' ? '100%' : 'auto' }}
>
{label}
</Typography.Text>
</div>
{valueComponent}
</div>
);
}

export default BasicInfoItem;

+ 58
- 0
react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx View File

@@ -0,0 +1,58 @@
/*
* @Author: 赵伟
* @Date: 2024-11-29 09:27:19
* @Description: 用于 BasicInfoItem 的组件
*/

import { isEmpty } from '@/utils';
import { Link } from '@umijs/max';
import { Typography } from 'antd';

type BasicInfoItemValueProps = {
/** 值是否显示省略号 */
ellipsis?: boolean;
/** 自定义类名前缀 */
classPrefix: string;
/** 值 */
value?: string;
/** 内部链接 */
link?: string;
/** 外部链接 */
url?: string;
};

function BasicInfoItemValue({
value,
link,
url,
classPrefix,
ellipsis = true,
}: BasicInfoItemValueProps) {
const myClassName = `${classPrefix}__item__value`;
let component = undefined;
if (url && value) {
component = (
<a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer">
{value}
</a>
);
} else if (link && value) {
component = (
<Link to={link} className={`${myClassName}__link`}>
{value}
</Link>
);
} else {
component = <span className={`${myClassName}__text`}>{!isEmpty(value) ? value : '--'}</span>;
}

return (
<div className={myClassName}>
<Typography.Text ellipsis={ellipsis !== false ? { tooltip: value } : false}>
{component}
</Typography.Text>
</div>
);
}

export default BasicInfoItemValue;

+ 0
- 113
react-ui/src/components/BasicInfo/components.tsx View File

@@ -1,113 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-11-29 09:27:19
* @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件
*/

import { Link } from '@umijs/max';
import { Typography } from 'antd';
import React from 'react';
import { type BasicInfoData, type BasicInfoLink } from './types';

type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth: number;
classPrefix: string;
};

export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) {
const { label, value, format, ellipsis } = data;
const formatValue = format ? format(value) : value;
const myClassName = `${classPrefix}__item`;
let valueComponent = undefined;
if (Array.isArray(formatValue)) {
valueComponent = (
<div className={`${myClassName}__value-container`}>
{formatValue.map((item: BasicInfoLink) => (
<BasicInfoItemValue
key={item.value}
value={item.value}
link={item.link}
url={item.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
))}
</div>
);
} else if (React.isValidElement(formatValue)) {
// 这个判断必须在下面的判断之前
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
} else if (typeof formatValue === 'object' && formatValue) {
valueComponent = (
<BasicInfoItemValue
value={formatValue.value}
link={formatValue.link}
url={formatValue.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
);
} else {
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
}
return (
<div className={myClassName} key={label}>
<div className={`${myClassName}__label`} style={{ width: labelWidth }}>
{label}
</div>
{valueComponent}
</div>
);
}

type BasicInfoItemValueProps = {
ellipsis?: boolean;
classPrefix: string;
value: string | React.ReactNode;
link?: string;
url?: string;
};

export function BasicInfoItemValue({
value,
link,
url,
ellipsis,
classPrefix,
}: BasicInfoItemValueProps) {
const myClassName = `${classPrefix}__item__value`;
let component = undefined;
if (url && value) {
component = (
<a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer">
{value}
</a>
);
} else if (link && value) {
component = (
<Link to={link} className={`${myClassName}__link`}>
{value}
</Link>
);
} else if (React.isValidElement(value)) {
return value;
} else {
component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>;
}

return (
<div className={myClassName}>
<Typography.Text
ellipsis={ellipsis ? { tooltip: value } : false}
style={{ fontSize: 'inherit' }}
>
{component}
</Typography.Text>
</div>
);
}

+ 0
- 48
react-ui/src/components/BasicInfo/format.ts View File

@@ -1,48 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-11-29 09:27:19
* @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式
*/

// 格式化日期
export { formatDate } from '@/utils/date';

/**
* 格式化字符串数组
* @param value - 字符串数组
* @returns 逗号分隔的字符串
*/
export const formatList = (value: string[] | null | undefined): string => {
if (
value === undefined ||
value === null ||
Array.isArray(value) === false ||
value.length === 0
) {
return '--';
}
return value.join(',');
};

/**
* 格式化布尔值
* @param value - 布尔值
* @returns "是" 或 "否"
*/
export const formatBoolean = (value: boolean): string => {
return value ? '是' : '否';
};

type FormatEnum = (value: string | number) => string;

/**
* 格式化枚举
* @param options - 枚举选项
* @returns 格式化枚举函数
*/
export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => {
return (value: string | number) => {
const option = options.find((item) => item.value === value);
return option ? option.label : '--';
};
};

+ 26
- 2
react-ui/src/components/BasicInfo/index.less View File

@@ -17,8 +17,6 @@
color: @text-color-secondary;
font-size: @font-size-content;
line-height: 1.6;
text-align: justify;
text-align-last: justify;

&::after {
position: absolute;
@@ -31,10 +29,12 @@
flex: 1;
flex-direction: column;
gap: 5px 0;
min-width: 0;
}

&__value {
flex: 1;
min-width: 0;
margin-left: 16px;
font-size: @font-size-content;
line-height: 1.6;
@@ -49,5 +49,29 @@
text-underline-offset: 3px;
}
}

&__node {
flex: 1;
min-width: 0;
margin-left: 16px;
font-size: @font-size-content;
line-height: 1.6;
word-break: break-all;
}
}
}

.kf-basic-info--three-columns {
width: 100%;

.kf-basic-info__item {
width: calc((100% - 80px) / 3);

&__label {
font-size: @font-size;
}
&__value {
font-size: @font-size;
}
}
}

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

@@ -1,27 +1,56 @@
import classNames from 'classnames';
import React from 'react';
import { BasicInfoItem } from './components';
import BasicInfoItem from './BasicInfoItem';
import './index.less';
import type { BasicInfoData, BasicInfoLink } from './types';
export * from './format';
export type { BasicInfoData, BasicInfoLink };

type BasicInfoProps = {
export type BasicInfoProps = {
/** 基础信息 */
datas: BasicInfoData[];
/** 标题宽度 */
labelWidth: number;
/** 标题是否显示省略号 */
labelEllipsis?: boolean;
/** 是否一行三列 */
threeColumns?: boolean;
/** 标签对齐方式 */
labelAlign?: 'start' | 'end' | 'justify';
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
labelWidth: number;
};

export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) {
/**
* 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化
*/
export default function BasicInfo({
datas,
className,
style,
labelWidth,
labelEllipsis = true,
threeColumns = false,
labelAlign = 'start',
}: BasicInfoProps) {
return (
<div className={classNames('kf-basic-info', className)} style={style}>
<div
className={classNames(
'kf-basic-info',
{ 'kf-basic-info--three-columns': threeColumns },
className,
)}
style={style}
>
{datas.map((item) => (
<BasicInfoItem
key={item.label}
data={item}
labelWidth={labelWidth}
classPrefix="kf-basic-info"
labelEllipsis={labelEllipsis}
labelAlign={labelAlign}
/>
))}
</div>


+ 2
- 2
react-ui/src/components/BasicInfo/types.ts View File

@@ -3,12 +3,12 @@ export type BasicInfoData = {
label: string;
value?: any;
ellipsis?: boolean;
format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined;
format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined;
};

// 值为链接的类型
export type BasicInfoLink = {
value: string;
value?: string;
link?: string;
url?: string;
};

+ 8
- 1
react-ui/src/components/BasicTableInfo/index.less View File

@@ -34,7 +34,6 @@
&__value {
flex: 1;
min-width: 0;
margin: 0 !important;
padding: 12px 20px 4px;
font-size: @font-size;
word-break: break-all;
@@ -56,5 +55,13 @@
text-underline-offset: 3px;
}
}

&__node {
flex: 1;
min-width: 0;
padding: 12px 20px;
font-size: @font-size;
word-break: break-all;
}
}
}

+ 9
- 11
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -1,30 +1,27 @@
import classNames from 'classnames';
import { BasicInfoItem } from '../BasicInfo/components';
import { BasicInfoProps } from '../BasicInfo';
import BasicInfoItem from '../BasicInfo/BasicInfoItem';
import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types';
import './index.less';
export * from '../BasicInfo/format';
export type { BasicInfoData, BasicInfoLink };

type BasicTableInfoProps = {
datas: BasicInfoData[];
className?: string;
style?: React.CSSProperties;
labelWidth: number;
};

/**
* 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化
*/
export default function BasicTableInfo({
datas,
className,
style,
labelWidth,
}: BasicTableInfoProps) {
labelEllipsis,
}: BasicInfoProps) {
const remainder = datas.length % 4;
const array = [];
if (remainder > 0) {
for (let i = 0; i < 4 - remainder; i++) {
array.push({
label: '',
value: '',
value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined
});
}
}
@@ -37,6 +34,7 @@ export default function BasicTableInfo({
key={`${item.label}-${index}`}
data={item}
labelWidth={labelWidth}
labelEllipsis={labelEllipsis}
classPrefix="kf-basic-table-info"
/>
))}


react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less → react-ui/src/components/CodeConfigItem/index.less View File


react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx → react-ui/src/components/CodeConfigItem/index.tsx View File


+ 20
- 5
react-ui/src/components/CodeSelect/index.tsx View File

@@ -4,18 +4,32 @@
* @Description: 流水线选择代码配置表单
*/

import CodeSelectorModal from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon';
import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal';
import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd';
import classNames from 'classnames';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less';

export { requiredValidator, type ParameterInputObject } from '../ParameterInput';
export {
requiredValidator,
type ParameterInputObject,
type ParameterInputValue,
} from '../ParameterInput';

type CodeSelectProps = ParameterInputProps;

function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) {
/** 代码配置选择表单组件 */
function CodeSelect({
value,
size,
disabled,
className,
style,
onChange,
...rest
}: CodeSelectProps) {
const selectResource = () => {
const { close } = openAntdModal(CodeSelectorModal, {
onOk: (res) => {
@@ -46,9 +60,10 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) {
};

return (
<div className="kf-code-select">
<div className={classNames('kf-code-select', className)} style={style}>
<ParameterInput
{...rest}
size={size}
disabled={disabled}
value={value}
onChange={onChange}
@@ -56,7 +71,7 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) {
></ParameterInput>
<Button
className="kf-code-select__button"
size="large"
size={size}
type="link"
icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />}
disabled={disabled}


+ 49
- 0
react-ui/src/components/CodeSelectorModal/index.less View File

@@ -0,0 +1,49 @@
.kf-code-selector-modal {
width: 100%;
height: 100%;

&__search {
width: 100%;
}

&__content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
max-height: 50vh;
margin-top: 24px;
margin-bottom: 30px;
overflow-x: hidden;
overflow-y: auto;
}

&__empty {
padding-top: 40px;
}

// 覆盖 antd 样式
.ant-input-affix-wrapper {
border-radius: 23px !important;
.ant-input-prefix {
margin-inline-end: 12px;
}
.ant-input-suffix {
margin-inline-end: 12px;
}
.ant-input-clear-icon {
font-size: 16px;
}
}

.ant-input-group-addon {
display: none;
}

.ant-pagination {
.ant-select-single {
height: 32px !important;
}
}
}

react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx → react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -4,16 +4,16 @@
* @Description: 选择代码
*/

import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { getCodeConfigListReq } from '@/services/codeConfig';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { ModalProps, PaginationProps } from 'antd';
import { Empty, Input, Pagination } from 'antd';
import { useEffect, useState } from 'react';
import CodeConfigItem from '../CodeConfigItem';
import styles from './index.less';
import './index.less';

export { type CodeConfigData };

@@ -21,6 +21,7 @@ export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> {
onOk?: (params: CodeConfigData | undefined) => void;
}

/** 选择代码配置的弹窗,推荐使用函数的方式打开 */
function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
const [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0);
@@ -79,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
footer={null}
destroyOnClose
>
<div className={styles['code-selector']}>
<div className="kf-code-selector-modal">
<Input.Search
className={styles['code-selector__search']}
className="kf-code-selector-modal__search"
placeholder="按代码仓库名称筛选"
allowClear
onSearch={handleSearch}
@@ -90,12 +91,15 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
suffix={null}
value={inputText}
prefix={
<Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} />
<KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} />
}
// prefix={
// <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} />
// }
/>
{dataList?.length !== 0 ? (
<>
<div className={styles['code-selector__content']}>
<div className="kf-code-selector-modal__content">
{dataList?.map((item) => (
<CodeConfigItem item={item} key={item.id} onClick={handleClick} />
))}
@@ -112,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
/>
</>
) : (
<div className={styles['code-selector__empty']}>
<div className="kf-code-selector-modal__empty">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
</div>
)}

+ 41
- 0
react-ui/src/components/ConfigInfo/index.tsx View File

@@ -0,0 +1,41 @@
import BasicInfo, { type BasicInfoData, type BasicInfoProps } from '@/components/BasicInfo';
import InfoGroup from '@/components/InfoGroup';
import classNames from 'classnames';
export type { BasicInfoData };

interface ConfigInfoProps extends BasicInfoProps {
/** 标题 */
title: string;
/** 子元素 */
children?: React.ReactNode;
}

/** 详情基本信息块,目前主要用于主动机器学习、超参数寻优、自主学习详情中 */
function ConfigInfo({
title,
datas,
labelWidth,
labelAlign = 'start',
labelEllipsis = true,
threeColumns = true,
className,
style,
children,
}: ConfigInfoProps) {
return (
<InfoGroup title={title} className={classNames('kf-config-info', className)} style={style}>
<div className={'kf-config-info__content'}>
<BasicInfo
datas={datas}
labelWidth={labelWidth}
labelAlign={labelAlign}
labelEllipsis={labelEllipsis}
threeColumns={threeColumns}
/>
{children}
</div>
</InfoGroup>
);
}

export default ConfigInfo;

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

@@ -1,6 +1,6 @@
.disabled-input {
padding: 4px 11px;
color: rgba(0, 0, 0, 0.25);
color: @text-disabled-color;
font-size: @font-size-input;
background-color: rgba(0, 0, 0, 0.04);
border: 1px solid #d9d9d9;


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

@@ -6,13 +6,14 @@ type DisabledInputProps = {
valuePropName?: string;
};

/**
* 模拟禁用的输入框,但是完全显示内容
*/
function DisabledInput({ value, valuePropName }: DisabledInputProps) {
const data = valuePropName ? value[valuePropName] : value;
return (
<div className={styles['disabled-input']}>
<Typography.Text ellipsis={{ tooltip: data }} style={{ color: 'inherit' }}>
{data}
</Typography.Text>
<Typography.Text ellipsis={{ tooltip: data }}>{data}</Typography.Text>
</div>
);
}


+ 13
- 5
react-ui/src/components/FullScreenFrame/index.tsx View File

@@ -2,22 +2,30 @@ import classNames from 'classnames';
import './index.less';

type FullScreenFrameProps = {
/** URL */
url: string;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
onload?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void;
onerror?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void;
/** 加载完成回调 */
onLoad?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void;
/** 加载失败回调 */
onError?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void;
};

function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) {
/**
* 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage
*/
function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) {
return (
<div className={classNames('kf-full-screen-frame', className ?? '')} style={style}>
{url && (
<iframe
src={url}
className="kf-full-screen-frame__iframe"
onLoad={onload}
onError={onerror}
onLoad={onLoad}
onError={onError}
></iframe>
)}
</div>


+ 12
- 8
react-ui/src/components/IFramePage/index.tsx View File

@@ -1,6 +1,6 @@
import FullScreenFrame from '@/components/FullScreenFrame';
import KFSpin from '@/components/KFSpin';
// import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import classNames from 'classnames';
@@ -12,32 +12,36 @@ export enum IframePageType {
DatasetAnnotation = 'DatasetAnnotation', // 数据标注
AppDevelopment = 'AppDevelopment', // 应用开发
DevEnv = 'DevEnv', // 开发环境
GitLink = 'GitLink',
GitLink = 'GitLink', // git link
}

const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
switch (type) {
case IframePageType.DatasetAnnotation:
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl;
case IframePageType.AppDevelopment:
case IframePageType.DatasetAnnotation: // 数据标注
return getLabelStudioUrl;
case IframePageType.AppDevelopment: // 应用开发
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' });
case IframePageType.DevEnv:
case IframePageType.DevEnv: // 开发环境
return () =>
Promise.resolve({
code: 200,
data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '',
});
case IframePageType.GitLink:
case IframePageType.GitLink: // git link
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' });
}
};

type IframePageProps = {
/** 子系统 */
type: IframePageType;
/** 自定义样式类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */
function IframePage({ type, className, style }: IframePageProps) {
const [iframeUrl, setIframeUrl] = useState('');
const [loading, setLoading] = useState(false);
@@ -66,7 +70,7 @@ function IframePage({ type, className, style }: IframePageProps) {
return (
<div className={classNames('kf-iframe-page', className)} style={style}>
{loading && createPortal(<KFSpin size="large" />, document.body)}
<FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} />
<FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} />
</div>
);
}


react-ui/src/components/InfoGroupTitle/index.less → react-ui/src/components/InfoGroup/InfoGroupTitle.less View File

@@ -1,7 +1,7 @@
.kf-info-group-title {
width: 100%;
height: 56px;
padding-left: @content-padding;
padding: 0 @content-padding;
background: linear-gradient(
179.03deg,
rgba(199, 223, 255, 0.12) 0%,
@@ -21,6 +21,7 @@
color: @text-color;
font-weight: 500;
font-size: @font-size-title;
.singleLine();

&::after {
position: absolute;

react-ui/src/components/InfoGroupTitle/index.tsx → react-ui/src/components/InfoGroup/InfoGroupTitle.tsx View File

@@ -1,13 +1,19 @@
import { Flex } from 'antd';
import classNames from 'classnames';
import './index.less';
import './InfoGroupTitle.less';

type InfoGroupTitleProps = {
/** 标题 */
title: string;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

/**
* 信息组标题
*/
function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) {
return (
<Flex align="center" className={classNames('kf-info-group-title', className)} style={style}>

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

@@ -1,16 +1,25 @@
import classNames from 'classnames';
import InfoGroupTitle from '../InfoGroupTitle';
import InfoGroupTitle from './InfoGroupTitle';
import './index.less';

type InfoGroupProps = {
/** 标题 */
title: string;
height?: string | number; // 如果要纵向滚动,需要设置高度
width?: string | number; // 如果要横向滚动,需要设置宽度
/** 高度, 如果要纵向滚动,需要设置高度 */
height?: string | number;
/** 宽度, 如果要横向滚动,需要设置宽度 */
width?: string | number;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 子元素 */
children?: React.ReactNode;
};

/**
* 信息组,用于展示基本信息,支持横向、纵向滚动。自动机器学习、超参数寻优都是使用这个组件
*/
function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) {
const contentStyle: React.CSSProperties = {};
if (height) {


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

@@ -33,7 +33,7 @@
margin-top: 20px;
margin-bottom: 30px;

&__back-btn {
&__button {
height: 32px;
}
}


+ 16
- 6
react-ui/src/components/KFEmpty/index.tsx View File

@@ -9,15 +9,24 @@ export enum EmptyType {
}

type EmptyProps = {
className?: string;
style?: React.CSSProperties;
/** 类型 */
type: EmptyType;
/** 标题 */
title?: string;
/** 内容 */
content?: string;
/** 是否有页脚,如果有默认是一个按钮 */
hasFooter?: boolean;
footer?: () => React.ReactNode;
/** 按钮标题,默认是"刷新" */
buttonTitle?: string;
onRefresh?: () => void;
/** 按钮点击回调 */
onButtonClick?: () => void;
/** 自定义页脚内容 */
footer?: () => React.ReactNode;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

function getEmptyImage(type: EmptyType) {
@@ -31,6 +40,7 @@ function getEmptyImage(type: EmptyType) {
}
}

/** 空状态 */
function KFEmpty({
className,
style,
@@ -40,7 +50,7 @@ function KFEmpty({
hasFooter = true,
footer,
buttonTitle = '刷新',
onRefresh,
onButtonClick,
}: EmptyProps) {
const image = getEmptyImage(type);

@@ -54,7 +64,7 @@ function KFEmpty({
{footer ? (
footer()
) : (
<Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}>
<Button className="kf-empty__footer__button" type="primary" onClick={onButtonClick}>
{buttonTitle}
</Button>
)}


+ 8
- 2
react-ui/src/components/KFIcon/index.tsx View File

@@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({
type IconFontProps = Parameters<typeof Icon>[0];

interface KFIconProps extends IconFontProps {
/** 图标 */
type: string;
/** 字体大小 */
font?: number;
/** 字体颜色 */
color?: string;
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
}

function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) {
/** 封装 iconfont 图标 */
function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) {
const iconStyle = {
...style,
fontSize: font,


react-ui/src/components/ModalTitle/index.less → react-ui/src/components/KFModal/KFModalTitle.less View File


react-ui/src/components/ModalTitle/index.tsx → react-ui/src/components/KFModal/KFModalTitle.tsx View File

@@ -6,12 +6,16 @@

import classNames from 'classnames';
import React from 'react';
import './index.less';
import './KFModalTitle.less';

type ModalTitleProps = {
/** 标题 */
title: React.ReactNode;
/** 图片 */
image?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
};


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

@@ -4,19 +4,22 @@
* @Description: 自定义 Modal
*/

import ModalTitle from '@/components/ModalTitle';
import { Modal, type ModalProps } from 'antd';
import classNames from 'classnames';
import KFModalTitle from './KFModalTitle';
import './index.less';

export interface KFModalProps extends ModalProps {
/** 标题图片 */
image?: string;
}

/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */
function KFModal({
title,
image,
children,
className = '',
className,
centered,
maskClosable,
...rest
@@ -27,7 +30,7 @@ function KFModal({
{...rest}
centered={centered ?? true}
maskClosable={maskClosable ?? false}
title={<ModalTitle title={title} image={image}></ModalTitle>}
title={<KFModalTitle title={title} image={image} />}
>
{children}
</Modal>


+ 12
- 4
react-ui/src/components/KFRadio/index.tsx View File

@@ -8,32 +8,40 @@ import classNames from 'classnames';
import './index.less';

export type KFRadioItem = {
key: string;
title: string;
value: string;
icon?: React.ReactNode;
};

type KFRadioProps = {
/** 选项 */
items: KFRadioItem[];
/** 当前选中项 */
value?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
/** 选中回调 */
onChange?: (value: string) => void;
};

/**
* 自定义 Radio
*/
function KFRadio({ items, value, style, className, onChange }: KFRadioProps) {
return (
<span className={classNames('kf-radio', className)} style={style}>
{items.map((item) => {
return (
<span
key={item.key}
key={item.value}
className={
value === item.key
value === item.value
? classNames('kf-radio__item', 'kf-radio__item--active')
: 'kf-radio__item'
}
onClick={() => onChange?.(item.key)}
onClick={() => onChange?.(item.value)}
>
{item.icon}
<span style={{ marginLeft: '5px' }}>{item.title}</span>


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

@@ -5,13 +5,19 @@
*/

import { Spin, SpinProps } from 'antd';
import styles from './index.less';
import './index.less';

function KFSpin(props: SpinProps) {
interface KFSpinProps extends SpinProps {
/** 加载文本 */
label: string;
}

/** 自定义 Spin */
function KFSpin({ label = '加载中', ...rest }: KFSpinProps) {
return (
<div className={styles['kf-spin']}>
<Spin {...props} />
<div className={styles['kf-spin__label']}>加载中</div>
<div className={'kf-spin'}>
<Spin {...rest} />
<div className={'kf-spin__label'}>{label}</div>
</div>
);
}


+ 0
- 19
react-ui/src/components/LabelValue/index.less View File

@@ -1,19 +0,0 @@
.kf-label-value {
display: flex;
align-items: flex-start;
font-size: 16px;
line-height: 1.6;

&__label {
flex: none;
width: 80px;
color: @text-color-secondary;
}

&__value {
flex: 1;
color: @text-color;
white-space: pre-line;
word-break: break-all;
}
}

+ 0
- 20
react-ui/src/components/LabelValue/index.tsx View File

@@ -1,20 +0,0 @@
import classNames from 'classnames';
import './index.less';

type labelValueProps = {
label: string;
value?: any;
className?: string;
style?: React.CSSProperties;
};

function LabelValue({ label, value, className, style }: labelValueProps) {
return (
<div className={classNames('kf-label-value', className)} style={style}>
<div className="kf-label-value__label">{label}</div>
<div className="kf-label-value__value">{value ?? '--'}</div>
</div>
);
}

export default LabelValue;

+ 1
- 2
react-ui/src/components/MenuIconSelector/index.less View File

@@ -1,5 +1,4 @@
.menu-icon-selector {
// grid 布局,每行显示 8 个图标
display: grid;
grid-template-columns: repeat(4, 80px);
gap: 20px;
@@ -10,7 +9,7 @@
display: flex;
align-items: center;
justify-content: center;
width: 80x;
width: 80px;
height: 80px;
border: 1px solid transparent;
border-radius: 4px;


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

@@ -12,7 +12,9 @@ import { useEffect, useState } from 'react';
import styles from './index.less';

interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> {
/** 选中的图标 */
selectedIcon?: string;
/** 选择回调 */
onOk: (param: string) => void;
}

@@ -21,6 +23,7 @@ type IconObject = {
font_class: string;
};

/** 菜单图标选择器 */
function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) {
const [icons, setIcons] = useState<IconObject[]>([]);
useEffect(() => {


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

@@ -8,10 +8,17 @@ import React from 'react';
import './index.less';

type PageTitleProps = {
title: string;
/** 标题 */
title: React.ReactNode;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

/**
* 页面标题
*/
function PageTitle({ title, style, className = '' }: PageTitleProps) {
return (
<div className={classNames('kf-page-title', className)} style={style}>


+ 18
- 5
react-ui/src/components/ParameterInput/index.less View File

@@ -39,7 +39,7 @@

&__placeholder {
min-height: 22px;
color: rgba(0, 0, 0, 0.25);
color: @text-placeholder-color;
font-size: @font-size-input;
line-height: 1.5714285714285714;
}
@@ -49,18 +49,31 @@
padding: 10px 11px;
font-size: @font-size-input-lg;

.parameter-input__placeholder {
.parameter-input__placeholder,
.parameter-input__content__value {
min-height: 24px;
font-size: @font-size-input-lg;
line-height: 1.5;
}

.parameter-input__content__close-icon {
font-size: 12px;
}
}

.parameter-input.parameter-input--small {
padding: 0 7px;
font-size: @font-size-input;

.parameter-input__placeholder,
.parameter-input__content__value {
font-size: @font-size-input-lg;
line-height: 1.5;
min-height: 22px;
font-size: @font-size-input;
line-height: 1.5714285714285714;
}

.parameter-input__content__close-icon {
font-size: 12px;
font-size: 10px;
}
}



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

@@ -1,21 +1,22 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 08:42:57
* @Description: 参数输入组件
* @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数
*/

import { CommonTabKeys } from '@/enums';
import { CloseOutlined } from '@ant-design/icons';
import { Form, Input } from 'antd';
import { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import './index.less';

// 对象
// 如果值是对象时的类型
export type ParameterInputObject = {
value?: any; // 值
showValue?: any; // 显示值
fromSelect?: boolean; // 是否来自选择
activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab
activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab
expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys
checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys
[key: string]: any;
@@ -25,18 +26,34 @@ export type ParameterInputObject = {
export type ParameterInputValue = ParameterInputObject | string;

export interface ParameterInputProps {
/** 值,可以是字符串,也可以是 ParameterInputObject 对象 */
value?: ParameterInputValue;
/**
* 值变化时的回调
* @param value 值,可以是字符串,也可以是 ParameterInputObject 对象
*/
onChange?: (value?: ParameterInputValue) => void;
/** 点击时的回调 */
onClick?: () => void;
/** 删除时的回调 */
onRemove?: () => void;
/** 是否可以手动输入 */
canInput?: boolean;
/** 是否是文本框 */
textArea?: boolean;
/** 占位符 */
placeholder?: string;
/** 是否允许清除 */
allowClear?: boolean;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
/** 大小 */
size?: 'middle' | 'small' | 'large';
/** 是否禁用 */
disabled?: boolean;
/** 元素 id */
id?: string;
}

@@ -88,6 +105,7 @@ function ParameterInput({
className={classNames(
'parameter-input',
{ 'parameter-input--large': size === 'large' },
{ 'parameter-input--small': size === 'small' },
{ [`parameter-input--${status}`]: status },
className,
)}


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

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 08:42:57
* @Description: 参数选择组件
* @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/

import { PipelineNodeModelParameter } from '@/types';


+ 55
- 15
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -9,30 +9,71 @@ import ResourceSelectorModal, {
ResourceSelectorResponse,
ResourceSelectorType,
selectorTypeConfig,
} from '@/pages/Pipeline/components/ResourceSelectorModal';
} from '@/components/ResourceSelectorModal';
import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd';
import { useState } from 'react';
import classNames from 'classnames';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less';

export { requiredValidator, type ParameterInputObject } from '../ParameterInput';
export {
requiredValidator,
type ParameterInputObject,
type ParameterInputValue,
} from '../ParameterInput';
export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse };

type ResourceSelectProps = {
interface ResourceSelectProps extends ParameterInputProps {
/** 类型,数据集、模型、镜像 */
type: ResourceSelectorType;
} & ParameterInputProps;
}

// 获取选择数据集、模型后面按钮 icon
// 获取选择数据集、模型、镜像后面按钮 icon
const getSelectBtnIcon = (type: ResourceSelectorType) => {
return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />;
};

function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) {
/** 数据集、模型、镜像选择表单组件 */
function ResourceSelect({
type,
value,
size,
disabled,
className,
style,
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 selectResource = () => {
const resource = selectedResource;
const { close } = openAntdModal(ResourceSelectorModal, {
@@ -50,8 +91,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
showValue: path,
fromSelect: true,
activeTab,
expandedKeys: [`${id}`],
checkedKeys: [`${id}-${version}`],
id,
name,
version,
path,
});
} else {
const jsonObj = {
@@ -69,8 +112,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
showValue,
fromSelect: true,
activeTab,
expandedKeys: [`${id}`],
checkedKeys: [`${id}-${version}`],
...jsonObj,
});
}
@@ -80,8 +121,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
}
close();
@@ -90,18 +129,19 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
};

return (
<div className="kf-resource-select">
<div className={classNames('kf-resource-select', className)} style={style}>
<ParameterInput
{...rest}
disabled={disabled}
value={value}
size={size}
onChange={onChange}
onRemove={() => setSelectedResource(undefined)}
onClick={selectResource}
></ParameterInput>
<Button
className="kf-resource-select__button"
size="large"
size={size}
type="link"
icon={getSelectBtnIcon(type)}
disabled={disabled}


react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx → react-ui/src/components/ResourceSelectorModal/config.tsx View File


react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less → react-ui/src/components/ResourceSelectorModal/index.less View File


react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx → react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -4,11 +4,11 @@
* @Description: 选择数据集、模型、镜像
*/

import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
@@ -16,22 +16,30 @@ import { ResourceSelectorType, selectorTypeConfig } from './config';
import styles from './index.less';
export { ResourceSelectorType, selectorTypeConfig };

// 选择数据集\模型\镜像的返回类型
// 选择数据集、模型、镜像的返回类型
export type ResourceSelectorResponse = {
activeTab: CommonTabKeys; // 是我的还是公开的
id: string; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
version: string; // 数据集\模型\镜像版本
path: string; // 数据集\模型\镜像版本路径
identifier: string; // 数据集\模型 identifier
owner: string; // 数据集\模型 owner
activeTab: CommonTabKeys; // 是我的还是公开的
identifier: string; // 数据集\模型 identifier,镜像这个字段为空
owner: string; // 数据集\模型 owner,镜像这个字段为空
};

export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 数据集\模型\镜像
/** 类型,数据集、模型、镜像 */
type: ResourceSelectorType;
/** 默认展开的节点 */
defaultExpandedKeys?: React.Key[];
/** 默认展开的节点 */
defaultCheckedKeys?: React.Key[];
/** 默认激活的 Tab */
defaultActiveTab?: CommonTabKeys;
/**
* 确认回调
* @param params 选择的数据
*/
onOk?: (params: ResourceSelectorResponse | undefined) => void;
}

@@ -61,6 +69,7 @@ const getIdAndVersion = (versionKey: string) => {
};
};

/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */
function ResourceSelectorModal({
type,
defaultExpandedKeys = [],
@@ -69,7 +78,7 @@ function ResourceSelectorModal({
onOk,
...rest
}: ResourceSelectorModalProps) {
const [activeTab, setActiveTab] = useState<string>(defaultActiveTab);
const [activeTab, setActiveTab] = useState<CommonTabKeys>(defaultActiveTab);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
@@ -234,7 +243,7 @@ function ResourceSelectorModal({
version,
identifier,
owner,
activeTab: activeTab as CommonTabKeys,
activeTab: activeTab,
};
onOk?.(res);
} else {
@@ -255,7 +264,7 @@ function ResourceSelectorModal({
<Tabs
activeKey={activeTab}
items={tabItems}
onChange={setActiveTab}
onChange={(e) => setActiveTab(e as CommonTabKeys)}
className={styles['model-tabs']}
/>
<div className={styles['model-selector']}>
@@ -267,7 +276,14 @@ function ResourceSelectorModal({
variant="borderless"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
prefix={
<KFIcon
type="icon-sousuo"
color="rgba(22,100,255,0.4)"
style={{ height: '15px' }}
/>
}
// prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
/>
<Tree
ref={treeRef}

+ 7
- 5
react-ui/src/components/RightContent/AvatarDropdown.tsx View File

@@ -1,6 +1,8 @@
import { clearSessionToken } from '@/access';
import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types';
import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components';
@@ -64,11 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage();
// const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
// if (clientInfo) {
// const { logoutUri } = clientInfo;
// location.replace(logoutUri);
// }
const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
if (clientInfo) {
const { logoutUri } = clientInfo;
location.replace(logoutUri);
}
};
const actionClassName = useEmotionCss(({ token }) => {
return {


+ 2
- 5
react-ui/src/components/RightContent/index.tsx View File

@@ -1,9 +1,8 @@
import { useModel } from '@umijs/max';
import React from 'react';
// import KFBreadcrumb from '../KFBreadcrumb';
import KFIcon from '@/components/KFIcon';
import { ProBreadcrumb } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
import { Button } from 'antd';
import React from 'react';
import Avatar from './AvatarDropdown';
import styles from './index.less';
// import { SelectLang } from '@umijs/max';
@@ -44,8 +43,6 @@ const GlobalHeaderRight: React.FC = () => {

<ProBreadcrumb></ProBreadcrumb>

{/* <KFBreadcrumb /> */}

<Avatar menu={true} />
{/* <SelectLang className={actionClassName} /> */}
</div>


+ 9
- 2
react-ui/src/components/SubAreaTitle/index.tsx View File

@@ -8,13 +8,20 @@ import classNames from 'classnames';
import './index.less';

type SubAreaTitleProps = {
/** 标题 */
title: string;
/** 图片 */
image?: string;
style?: React.CSSProperties;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) {
/**
* 表单或者详情页的分区标题
*/
function SubAreaTitle({ title, image, className, style }: SubAreaTitleProps) {
return (
<div className={classNames('kf-subarea-title', className)} style={style}>
{image && (


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

@@ -118,3 +118,14 @@ export const autoMLResamplingStrategyOptions = [
{ label: 'holdout', value: AutoMLResamplingStrategy.Holdout },
{ label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid },
];

// 超参数自动寻优优化方向
export enum hyperParameterOptimizedMode {
Min = 'min',
Max = 'max',
}

export const hyperParameterOptimizedModeOptions = [
{ label: '越大越好', value: hyperParameterOptimizedMode.Max },
{ label: '越小越好', value: hyperParameterOptimizedMode.Min },
];

+ 1
- 1
react-ui/src/global.less View File

@@ -5,7 +5,7 @@ body,
height: 100%;
margin: 0;
padding: 0;
overflow-y: hidden;
overflow-y: visible;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';


+ 1
- 1
react-ui/src/iconfont/iconfont-menu.json View File

@@ -1,6 +1,6 @@
{
"id": "4511326",
"name": "复杂智能软件-导航",
"name": "智能材料科研平台-导航",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",


+ 1
- 1
react-ui/src/iconfont/iconfont.js
File diff suppressed because it is too large
View File


+ 5
- 0
react-ui/src/overrides.less View File

@@ -261,3 +261,8 @@
}
}
}

.ant-typography {
color: inherit;
font-size: inherit;
}

+ 1
- 1
react-ui/src/pages/404.tsx View File

@@ -12,7 +12,7 @@ const NoFoundPage = () => {
content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'}
hasFooter={true}
buttonTitle="返回首页"
onRefresh={() => navigate('/')}
onButtonClick={() => navigate('/')}
></KFEmpty>
);
};


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

@@ -33,7 +33,7 @@
}

.ant-btn-variant-text:disabled {
color: rgba(0, 0, 0, 0.25);
color: @text-disabled-color;
}

.ant-btn-variant-text {


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

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建服务版本
* @Description: 创建实验
*/
import PageTitle from '@/components/PageTitle';
import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums';
@@ -11,7 +11,6 @@ import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useLocation, useNavigate, useParams } from '@umijs/max';
import { App, Button, Form } from 'antd';
import { omit } from 'lodash';
import { useEffect } from 'react';
import BasicConfig from '../components/CreateForm/BasicConfig';
import DatasetConfig from '../components/CreateForm/DatasetConfig';
@@ -106,7 +105,7 @@ function CreateAutoML() {

// 根据后台要求,修改表单数据
const object = {
...omit(formData),
...formData,
include_classifier: convertEmptyStringToUndefined(include_classifier),
include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor),
include_regressor: convertEmptyStringToUndefined(include_regressor),
@@ -191,7 +190,7 @@ function CreateAutoML() {
<TrialConfig />
<DatasetConfig />

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>


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

@@ -3,9 +3,7 @@
* @Date: 2024-04-16 13:58:08
* @Description: 自主机器学习详情
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { CommonTabKeys } from '@/enums';
import { getAutoMLInfoReq } from '@/services/autoML';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
@@ -16,24 +14,10 @@ import { AutoMLData } from '../types';
import styles from './index.less';

function AutoMLInfo() {
const [activeTab, setActiveTab] = useState<string>(CommonTabKeys.Public);
const params = useParams();
const autoMLId = safeInvoke(Number)(params.id);
const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined);

const tabItems = [
{
key: CommonTabKeys.Public,
label: '基本信息',
icon: <KFIcon type="icon-jibenxinxi" />,
},
{
key: CommonTabKeys.Private,
label: 'Trial列表',
icon: <KFIcon type="icon-Trialliebiao" />,
},
];

useEffect(() => {
if (autoMLId) {
getAutoMLInfo();


+ 3
- 411
react-ui/src/pages/AutoML/List/index.tsx View File

@@ -3,419 +3,11 @@
* @Date: 2024-04-16 13:58:08
* @Description: 自主机器学习列表
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteAutoMLReq,
getAutoMLListReq,
getExperimentInsListReq,
runAutoMLReq,
} from '@/services/autoML';
import themes from '@/styles/theme.less';
import { type ExperimentInstance as ExperimentInstanceData } from '@/types';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
Tooltip,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ExperimentInstance from '../components/ExperimentInstance';
import { AutoMLData } from '../types';
import styles from './index.less';

function AutoMLList() {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<AutoMLData[]>([]);
const [total, setTotal] = useState(0);
const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getAutoMLList();
}, [pagination, searchText]);

// 获取自主机器学习列表
const getAutoMLList = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
ml_name: searchText || undefined,
};
const [res] = await to(getAutoMLListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
setPagination((prev) => ({
...prev,
current: 1,
}));
};

// 删除模型部署
const deleteAutoML = async (record: AutoMLData) => {
const [res] = await to(deleteAutoMLReq(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getAutoMLList();
}
}
};

// 处理删除
const handleAutoMLDelete = (record: AutoMLData) => {
modalConfirm({
title: '删除后,该实验将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteAutoML(record);
},
});
};

// 创建、编辑、复制自动机器学习
const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => {
setCacheState({
pagination,
searchText,
});

if (record) {
if (isCopy) {
navigate(`/pipeline/autoML/copy/${record.id}`);
} else {
navigate(`/pipeline/autoML/edit/${record.id}`);
}
} else {
navigate(`/pipeline/autoML/create`);
}
};

// 查看自动机器学习详情
const gotoDetail = (record: AutoMLData) => {
setCacheState({
pagination,
searchText,
});

navigate(`/pipeline/autoML/info/${record.id}`);
};

// 启动自动机器学习
const startAutoML = async (record: AutoMLData) => {
const [res] = await to(runAutoMLReq(record.id));
if (res) {
message.success('运行成功');
setExpandedRowKeys([record.id]);
refreshExperimentList();
refreshExperimentIns(record.id);
}
};
import ExperimentList, { ExperimentListType } from '../components/ExperimentList';

// --------------------------- 实验实例 ---------------------------
// 获取实验实例列表
const getExperimentInsList = async (autoMLId: number, page: number) => {
const params = {
autoMlId: autoMLId,
page: page,
size: 5,
};
const [res] = await to(getExperimentInsListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
try {
if (page === 0) {
setExperimentInsList(content);
} else {
setExperimentInsList((prev) => [...prev, ...content]);
}
setExperimentInsTotal(totalElements);
} catch (error) {
console.error('JSON parse error: ', error);
}
}
};
// 展开实例
const handleExpandChange = (expanded: boolean, record: AutoMLData) => {
setExperimentInsList([]);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0);
} else {
setExpandedRowKeys([]);
}
};

// 跳转到实验实例详情
const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => {
navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` });
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId: number) => {
getExperimentInsList(experimentId, 0);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5);
const autoMLId = expandedRowKeys[0];
getExperimentInsList(autoMLId, page);
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIns.id) {
return {
...item,
status: ExperimentStatus.Terminated,
};
}
return item;
});
});
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getAutoMLList();
};

// --------------------------- Table ---------------------------
// 分页切换
const handleTableChange: TableProps<AutoMLData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const columns: TableProps<AutoMLData>['columns'] = [
{
title: '实验名称',
dataIndex: 'ml_name',
key: 'ml_name',
width: '16%',
render: tableCellRender(false, TableCellValueType.Link, {
onClick: gotoDetail,
}),
},
{
title: '实验描述',
dataIndex: 'ml_description',
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 },
},
{
title: '最近五次运行状态',
dataIndex: 'status_list',
key: 'status_list',
width: 200,
render: (text) => {
const newText: string[] = text && text.replace(/\s+/g, '').split(',');
return (
<>
{newText && newText.length > 0
? newText.map((item, index) => {
return (
<Tooltip
key={index}
placement="top"
title={experimentStatusInfo[item as ExperimentStatus].label}
>
<img
style={{ width: '17px', marginRight: '6px' }}
src={experimentStatusInfo[item as ExperimentStatus].icon}
draggable={false}
alt=""
/>
</Tooltip>
);
})
: null}
</>
);
},
},
{
title: '操作',
dataIndex: 'operation',
width: 360,
key: 'operation',
render: (_: any, record: AutoMLData) => (
<div>
<Button
type="link"
size="small"
key="start"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => startAutoML(record)}
>
运行
</Button>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createAutoML(record, false)}
>
编辑
</Button>
<Button
type="link"
size="small"
key="copy"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => createAutoML(record, true)}
>
复制
</Button>

<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleAutoMLDelete(record)}
>
删除
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['auto-ml-list']}>
<PageTitle title="自动机器学习列表"></PageTitle>
<div className={styles['auto-ml-list__content']}>
<div className={styles['auto-ml-list__content__filter']}>
<Input.Search
placeholder="按实验名称筛选"
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
allowClear
/>
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={() => createAutoML()}
icon={<KFIcon type="icon-xinjian2" />}
>
新建实验
</Button>
</div>
<div
className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
expandable={{
expandedRowRender: (record) => (
<ExperimentInstance
experimentInsList={experimentInsList}
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(record, item)}
onRemove={() => {
refreshExperimentIns(record.id);
refreshExperimentList();
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
),
onExpand: (e, a) => {
handleExpandChange(e, a);
},
expandedRowKeys: expandedRowKeys,
rowExpandable: () => true,
}}
rowKey="id"
/>
</div>
</div>
</div>
);
function AutoMLList() {
return <ExperimentList type={ExperimentListType.AutoML} />;
}

export default AutoMLList;

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

@@ -1,28 +1,16 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums';
import { AutoMLData } from '@/pages/AutoML/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { type NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { elapsedTime } from '@/utils/date';
import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format';
import { Flex } from 'antd';
import classNames from 'classnames';
import { useMemo } from 'react';
import ConfigInfo, {
formatBoolean,
formatDate,
formatEnum,
type BasicInfoData,
} from '../ConfigInfo';
import styles from './index.less';

// 格式化数据集
const formatDataset = (dataset: { name: string; version: string }) => {
if (!dataset || !dataset.name || !dataset.version) {
return '--';
}
return `${dataset.name}:${dataset.version}`;
};

// 格式化优化方向
const formatOptimizeMode = (value: boolean) => {
return value ? '越大越好' : '越小越好';
@@ -58,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{
label: '实验名称',
value: info.ml_name,
ellipsis: true,
},
{
label: '实验描述',
value: info.ml_description,
ellipsis: true,
},
{
label: '创建人',
value: info.create_by,
ellipsis: true,
},
{
label: '创建时间',
value: info.create_time,
ellipsis: true,
format: formatDate,
},
{
label: '更新时间',
value: info.update_time,
ellipsis: true,
format: formatDate,
},
];
@@ -93,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{
label: '任务类型',
value: info.task_type,
ellipsis: true,
format: formatEnum(autoMLTaskTypeOptions),
},
{
label: '特征预处理算法',
value: info.include_feature_preprocessor,
ellipsis: true,
},
{
label: '排除的特征预处理算法',
value: info.exclude_feature_preprocessor,
ellipsis: true,
},
{
label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法',
@@ -112,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
info.task_type === AutoMLTaskType.Regression
? info.include_regressor
: info.include_classifier,
ellipsis: true,
},
{
label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法',
@@ -120,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
info.task_type === AutoMLTaskType.Regression
? info.exclude_regressor
: info.exclude_classifier,
ellipsis: true,
},
{
label: '集成方式',
value: info.ensemble_class,
ellipsis: true,
format: formatEnum(autoMLEnsembleClassOptions),
},
{
label: '集成模型数量',
value: info.ensemble_size,
ellipsis: true,
},
{
label: '集成最佳模型数量',
value: info.ensemble_nbest,
ellipsis: true,
},
{
label: '最大数量',
value: info.max_models_on_disc,
ellipsis: true,
},
{
label: '内存限制(MB)',
value: info.memory_limit,
ellipsis: true,
},
{
label: '单次时间限制(秒)',
value: info.per_run_time_limit,
ellipsis: true,
},
{
label: '搜索时间限制(秒)',
value: info.time_left_for_this_task,
ellipsis: true,
},
{
label: '重采样策略',
value: info.resampling_strategy,
ellipsis: true,
},
{
label: '交叉验证折数',
value: info.folds,
ellipsis: true,
},
{
label: '是否打乱',
value: info.shuffle,
ellipsis: true,
format: formatBoolean,
},
{
label: '训练集比率',
value: info.train_size,
ellipsis: true,
},
{
label: '测试集比率',
value: info.test_size,
ellipsis: true,
},
{
label: '计算指标',
value: info.scoring_functions,
ellipsis: true,
},
{
label: '随机种子',
value: info.seed,
ellipsis: true,
},

{
label: '数据集',
value: info.dataset,
ellipsis: true,
format: formatDataset,
},
{
label: '预测目标列',
value: info.target_columns,
ellipsis: true,
},
];
}, [info]);
@@ -217,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{
label: '指标名称',
value: info.metric_name,
ellipsis: true,
},
{
label: '优化方向',
value: info.greater_is_better,
ellipsis: true,
format: formatOptimizeMode,
},
{
label: '指标权重',
value: info.metrics,
ellipsis: true,
format: formatMetricsWeight,
},
];
@@ -243,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{
label: '启动时间',
value: formatDate(runStatus.startedAt),
ellipsis: true,
},
{
label: '执行时长',
value: elapsedTime(runStatus.startedAt, runStatus.finishedAt),
ellipsis: true,
},
{
label: '状态',
@@ -271,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
</div>
</Flex>
),
ellipsis: true,
},
];
}, [runStatus]);
@@ -281,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{isInstance && runStatus && (
<ConfigInfo
title="运行信息"
data={instanceDatas}
datas={instanceDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
@@ -289,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB
{!isInstance && (
<ConfigInfo
title="基本信息"
data={basicDatas}
datas={basicDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
<ConfigInfo
title="配置信息"
data={configDatas}
datas={configDatas}
labelWidth={150}
style={{ marginBottom: '20px' }}
/>
<ConfigInfo title="优化指标" data={metricsData} labelWidth={70} />
<ConfigInfo title="优化指标" datas={metricsData} labelWidth={70} />
</div>
);
}


+ 0
- 20
react-ui/src/pages/AutoML/components/ConfigInfo/index.less View File

@@ -1,20 +0,0 @@
.config-info {
:global {
.kf-basic-info {
width: 100%;

&__item {
width: calc((100% - 80px) / 3);
&__label {
font-size: @font-size;
text-align: left;
text-align-last: left;
}
&__value {
min-width: 0;
font-size: @font-size;
}
}
}
}
}

+ 0
- 26
react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx View File

@@ -1,26 +0,0 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import InfoGroup from '@/components/InfoGroup';
import classNames from 'classnames';
import styles from './index.less';
export * from '@/components/BasicInfo/format';
export type { BasicInfoData };

type ConfigInfoProps = {
title: string;
data: BasicInfoData[];
labelWidth: number;
className?: string;
style?: React.CSSProperties;
};

function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) {
return (
<InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}>
<div className={styles['config-info__content']}>
<BasicInfo datas={data} labelWidth={labelWidth} />
</div>
</InfoGroup>
);
}

export default ConfigInfo;

+ 1
- 5
react-ui/src/pages/AutoML/components/CopyingText/index.tsx View File

@@ -9,11 +9,7 @@ export type CopyingTextProps = {
function CopyingText({ text }: CopyingTextProps) {
return (
<div className={styles['copying-text']}>
<Typography.Text
ellipsis={{ tooltip: text }}
style={{ color: 'inherit' }}
className={styles['copying-text__text']}
>
<Typography.Text ellipsis={{ tooltip: text }} className={styles['copying-text__text']}>
{text}
</Typography.Text>
<KFIcon


+ 1
- 4
react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx View File

@@ -62,10 +62,7 @@ function TrialConfig() {
>
<InputNumber placeholder="请输入指标权重" min={0} precision={0} />
</Form.Item>
<Flex
style={{ width: '76px', marginLeft: '18px', height: '46px' }}
align="center"
>
<Flex className={styles['metrics-weight__operation']} align="center">
<Button
style={{
marginRight: '3px',


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

@@ -1,9 +1,18 @@
.metrics-weight {
position: relative;
margin-bottom: 20px;

&:last-child {
margin-bottom: 0;
}

&__operation {
position: absolute;
left: calc(100% + 10px);
width: 76px;
height: 46px;
margin-left: 6px;
}
}

.add-weight {
@@ -14,7 +23,7 @@
border-color: .addAlpha(@primary-color, 0.5) [];
box-shadow: none !important;
&:hover {
border-style: solid;
border-style: solid !important;
}
}
}

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

@@ -2,11 +2,6 @@ import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
batchDeleteExperimentInsReq,
deleteExperimentInsReq,
stopExperimentInsReq,
} from '@/services/autoML';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
@@ -16,9 +11,11 @@ import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import { ExperimentListType, experimentListConfig } from '../ExperimentList/config';
import styles from './index.less';

type ExperimentInstanceProps = {
type: ExperimentListType;
experimentInsList?: ExperimentInstance[];
experimentInsTotal: number;
onClickInstance?: (instance: ExperimentInstance) => void;
@@ -28,6 +25,7 @@ type ExperimentInstanceProps = {
};

function ExperimentInstanceComponent({
type,
experimentInsList,
experimentInsTotal,
onClickInstance,
@@ -48,6 +46,7 @@ function ExperimentInstanceComponent({
isSingleChecked,
checkSingle,
] = useCheck(allIntanceIds);
const config = experimentListConfig[type];

useEffect(() => {
// 关闭时清空
@@ -68,7 +67,8 @@ function ExperimentInstanceComponent({

// 删除实验实例
const deleteExperimentInstance = async (id: number) => {
const [res] = await to(deleteExperimentInsReq(id));
const request = config.deleteInsReq;
const [res] = await to(request(id));
if (res) {
message.success('删除成功');
onRemove?.();
@@ -87,7 +87,8 @@ function ExperimentInstanceComponent({

// 批量删除实验实例
const batchDeleteExperimentInstances = async () => {
const [res] = await to(batchDeleteExperimentInsReq(selectedIns));
const request = config.batchDeleteInsReq;
const [res] = await to(request(selectedIns));
if (res) {
message.success('删除成功');
setSelectedIns([]);
@@ -97,7 +98,8 @@ function ExperimentInstanceComponent({

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const [res] = await to(stopExperimentInsReq(instance.id));
const request = config.stopInsReq;
const [res] = await to(request(instance.id));
if (res) {
message.success('终止成功');
onTerminate?.(instance);


+ 75
- 0
react-ui/src/pages/AutoML/components/ExperimentList/config.ts View File

@@ -0,0 +1,75 @@
/*
* @Author: 赵伟
* @Date: 2025-01-08 14:30:58
* @Description: 实验列表组件配置
*/

import {
batchDeleteExperimentInsReq,
deleteAutoMLReq,
deleteExperimentInsReq,
getAutoMLListReq,
getExperimentInsListReq,
runAutoMLReq,
stopExperimentInsReq,
} from '@/services/autoML';
import {
batchDeleteRayInsReq,
deleteRayInsReq,
deleteRayReq,
getRayInsListReq,
getRayListReq,
runRayReq,
stopRayInsReq,
} from '@/services/hyperParameter';

export enum ExperimentListType {
AutoML = 'AutoML',
HyperParameter = 'HyperParameter',
}

type ExperimentListInfo = {
getListReq: (params: any) => Promise<any>; // 获取列表
getInsListReq: (params: any) => Promise<any>; // 获取实例列表
deleteRecordReq: (params: any) => Promise<any>; // 删除
runRecordReq: (params: any) => Promise<any>; // 运行
deleteInsReq: (params: any) => Promise<any>; // 删除实例
batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例
stopInsReq: (params: any) => Promise<any>; // 终止实例
title: string; // 标题
pathPrefix: string; // 路由路径前缀
idProperty: string; // ID属性
nameProperty: string; // 名称属性
descProperty: string; // 描述属性
};

export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = {
[ExperimentListType.AutoML]: {
getListReq: getAutoMLListReq,
getInsListReq: getExperimentInsListReq,
deleteRecordReq: deleteAutoMLReq,
runRecordReq: runAutoMLReq,
deleteInsReq: deleteExperimentInsReq,
batchDeleteInsReq: batchDeleteExperimentInsReq,
stopInsReq: stopExperimentInsReq,
title: '自主机器学习',
pathPrefix: 'automl',
nameProperty: 'ml_name',
descProperty: 'ml_description',
idProperty: 'autoMlId',
},
[ExperimentListType.HyperParameter]: {
getListReq: getRayListReq,
getInsListReq: getRayInsListReq,
deleteRecordReq: deleteRayReq,
runRecordReq: runRayReq,
deleteInsReq: deleteRayInsReq,
batchDeleteInsReq: batchDeleteRayInsReq,
stopInsReq: stopRayInsReq,
title: '超参数自动寻优',
pathPrefix: 'hyperparameter',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'rayId',
},
};

react-ui/src/pages/AutoML/List/index.less → react-ui/src/pages/AutoML/components/ExperimentList/index.less View File

@@ -1,4 +1,4 @@
.auto-ml-list {
.experiment-list {
height: 100%;
&__content {
height: calc(100% - 60px);

+ 429
- 0
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -0,0 +1,429 @@
/*
* @Author: 赵伟
* @Date: 2025-01-08 13:58:08
* @Description: 自主机器学习和超参数寻优列表组件
*/

import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { AutoMLData } from '@/pages/AutoML/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import { type ExperimentInstance as ExperimentInstanceData } from '@/types';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
Tooltip,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ExperimentInstance from '../ExperimentInstance';
import { ExperimentListType, experimentListConfig } from './config';
import styles from './index.less';

export { ExperimentListType };

type ExperimentListProps = {
type: ExperimentListType;
};

function ExperimentList({ type }: ExperimentListProps) {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<AutoMLData[]>([]);
const [total, setTotal] = useState(0);
const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);
const config = experimentListConfig[type];

useEffect(() => {
getAutoMLList();
}, [pagination, searchText]);

// 获取自主机器学习或超参数自动优化列表
const getAutoMLList = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
ml_name: searchText || undefined,
};
const request = config.getListReq;
const [res] = await to(request(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
setPagination((prev) => ({
...prev,
current: 1,
}));
};

// 删除一条记录
const deleteAutoML = async (record: AutoMLData) => {
const request = config.deleteRecordReq;
const [res] = await to(request(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getAutoMLList();
}
}
};

// 处理删除
const handleAutoMLDelete = (record: AutoMLData) => {
modalConfirm({
title: '删除后,该实验将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteAutoML(record);
},
});
};

// 创建、编辑、复制自动机器学习
const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => {
setCacheState({
pagination,
searchText,
});

if (record) {
if (isCopy) {
navigate(`copy/${record.id}`);
} else {
navigate(`edit/${record.id}`);
}
} else {
navigate(`create`);
}
};

// 查看自动机器学习详情
const gotoDetail = (record: AutoMLData) => {
setCacheState({
pagination,
searchText,
});

navigate(`info/${record.id}`);
};

// 启动自动机器学习
const startAutoML = async (record: AutoMLData) => {
const request = config.runRecordReq;
const [res] = await to(request(record.id));
if (res) {
message.success('运行成功');
setExpandedRowKeys([record.id]);
refreshExperimentList();
refreshExperimentIns(record.id);
}
};

// --------------------------- 实验实例 ---------------------------
// 获取实验实例列表
const getExperimentInsList = async (recordId: number, page: number) => {
const params = {
[config.idProperty]: recordId,
page: page,
size: 5,
};
const request = config.getInsListReq;
const [res] = await to(request(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
try {
if (page === 0) {
setExperimentInsList(content);
} else {
setExperimentInsList((prev) => [...prev, ...content]);
}
setExperimentInsTotal(totalElements);
} catch (error) {
console.error('JSON parse error: ', error);
}
}
};
// 展开实例
const handleExpandChange = (expanded: boolean, record: AutoMLData) => {
setExperimentInsList([]);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0);
} else {
setExpandedRowKeys([]);
}
};

// 跳转到实验实例详情
const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => {
navigate(`instance/${autoML.id}/${record.id}`);
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId: number) => {
getExperimentInsList(experimentId, 0);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5);
const recordId = expandedRowKeys[0];
getExperimentInsList(recordId, page);
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIns.id) {
return {
...item,
status: ExperimentStatus.Terminated,
};
}
return item;
});
});
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getAutoMLList();
};

// --------------------------- Table ---------------------------
// 分页切换
const handleTableChange: TableProps<AutoMLData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const columns: TableProps<AutoMLData>['columns'] = [
{
title: '实验名称',
dataIndex: config.nameProperty,
key: 'ml_name',
width: '16%',
render: tableCellRender(false, TableCellValueType.Link, {
onClick: gotoDetail,
}),
},
{
title: '实验描述',
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 },
},
{
title: '最近五次运行状态',
dataIndex: 'status_list',
key: 'status_list',
width: 200,
render: (text) => {
const newText: string[] = text && text.replace(/\s+/g, '').split(',');
return (
<>
{newText && newText.length > 0
? newText.map((item, index) => {
return (
<Tooltip
key={index}
placement="top"
title={experimentStatusInfo[item as ExperimentStatus].label}
>
<img
style={{ width: '17px', marginRight: '6px' }}
src={experimentStatusInfo[item as ExperimentStatus].icon}
draggable={false}
alt=""
/>
</Tooltip>
);
})
: null}
</>
);
},
},
{
title: '操作',
dataIndex: 'operation',
width: 360,
key: 'operation',
render: (_: any, record: AutoMLData) => (
<div>
<Button
type="link"
size="small"
key="start"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => startAutoML(record)}
>
运行
</Button>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createAutoML(record, false)}
>
编辑
</Button>
<Button
type="link"
size="small"
key="copy"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => createAutoML(record, true)}
>
复制
</Button>

<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleAutoMLDelete(record)}
>
删除
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['experiment-list']}>
<PageTitle title={config.title + '列表'}></PageTitle>
<div className={styles['experiment-list__content']}>
<div className={styles['experiment-list__content__filter']}>
<Input.Search
placeholder="按实验名称筛选"
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
allowClear
/>
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={() => createAutoML()}
icon={<KFIcon type="icon-xinjian2" />}
>
新建实验
</Button>
</div>
<div
className={classNames('vertical-scroll-table', styles['experiment-list__content__table'])}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
expandable={{
expandedRowRender: (record) => (
<ExperimentInstance
type={type}
experimentInsList={experimentInsList}
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(record, item)}
onRemove={() => {
refreshExperimentIns(record.id);
refreshExperimentList();
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>
),
onExpand: (e, a) => {
handleExpandChange(e, a);
},
expandedRowKeys: expandedRowKeys,
rowExpandable: () => true,
}}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ExperimentList;

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

@@ -197,7 +197,7 @@ function CodeConfigList() {
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
onButtonClick={getDataList}
/>
)}
</div>


+ 1
- 4
react-ui/src/pages/Dataset/components/CategoryItem/index.tsx View File

@@ -33,10 +33,7 @@ function CategoryItem({ resourceType, item, isSelected, onClick }: CategoryItemP
alt=""
draggable={false}
/>
<Typography.Text
ellipsis={{ tooltip: item.name }}
style={{ color: 'inherit', marginTop: '4px' }}
>
<Typography.Text ellipsis={{ tooltip: item.name }} style={{ marginTop: '4px' }}>
{item.name}
</Typography.Text>
</div>


+ 6
- 73
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -1,17 +1,8 @@
import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import {
DataSource,
DatasetData,
ModelData,
ProjectDependency,
ResourceType,
TrainTask,
resourceConfig,
} from '@/pages/Dataset/config';
import { DatasetData, ModelData, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import ModelMetrics from '@/pages/Model/components/ModelMetrics';
import { getGitUrl } from '@/utils';
import { formatCodeConfig, formatDatasets, formatSource, formatTrainTask } from '@/utils/format';
import classNames from 'classnames';
import styles from './index.less';

@@ -24,103 +15,45 @@ type ResourceIntroProps = {
version?: string;
};

export const formatDataset = (datasets?: DatasetData[]) => {
if (!datasets || datasets.length === 0) {
return undefined;
}
return datasets.map((item) => ({
value: item.name,
url: `${origin}/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Version}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`,
}));
};

export const getProjectUrl = (project?: ProjectDependency) => {
if (!project || !project.url || !project.branch) {
return undefined;
}
const { url, branch } = project;
return getGitUrl(url, branch);
};

export const formatProject = (project?: ProjectDependency) => {
if (!project) {
return undefined;
}
return {
value: project.name,
url: getProjectUrl(project),
};
};

export const formatTrainTask = (task?: TrainTask) => {
if (!task) {
return undefined;
}
return {
value: task.name,
url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`,
};
};

export const formatSource = (source?: string) => {
if (source === DataSource.Create) {
return '用户上传';
} else if (source === DataSource.HandExport) {
return '手动导入';
} else if (source === DataSource.AtuoExport) {
return '实验自动导入';
}
return source;
};

const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [
{
label: '数据集名称',
value: data.name,
ellipsis: true,
},
{
label: '版本',
value: data.version,
ellipsis: true,
},
{
label: '创建人',
value: data.create_by,
ellipsis: true,
},
{
label: '更新时间',
value: data.update_time,
ellipsis: true,
},
{
label: '数据来源',
value: data.dataset_source,
format: formatSource,
ellipsis: true,
},
{
label: '训练任务',
value: data.train_task,
format: formatTrainTask,
ellipsis: true,
},
{
label: '处理代码',
value: data.processing_code,
format: formatProject,
ellipsis: true,
format: formatCodeConfig,
},
{
label: '数据集分类',
value: data.data_type,
ellipsis: true,
},
{
label: '研究方向',
value: data.data_tag,
ellipsis: true,
},
];

@@ -153,19 +86,19 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [
{
label: '训练代码',
value: data.project_depency,
format: formatProject,
format: formatCodeConfig,
ellipsis: true,
},
{
label: '训练数据集',
value: data.train_datasets,
format: formatDataset,
format: formatDatasets,
ellipsis: true,
},
{
label: '测试数据集',
value: data.test_datasets,
format: formatDataset,
format: formatDatasets,
ellipsis: true,
},
{


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

@@ -226,7 +226,7 @@ function ResourceList(
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
onButtonClick={getDataList}
/>
)}
</div>


+ 1
- 0
react-ui/src/pages/Dataset/components/VersionCompareModal/index.less View File

@@ -3,6 +3,7 @@
.title(@color, @background) {
width: 100%;
margin-bottom: 20px;
padding: 0 15px;
color: @color;
font-weight: 500;
font-size: @font-size;


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

@@ -8,11 +8,11 @@ import {
resourceConfig,
} from '@/pages/Dataset/config';
import { isEmpty } from '@/utils';
import { formatSource } from '@/utils/format';
import { to } from '@/utils/promise';
import { Typography, type ModalProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { formatSource } from '../ResourceIntro';
import styles from './index.less';

type CompareData = {
@@ -47,10 +47,10 @@ const formatProject = (project?: ProjectDependency) => {
if (!project) {
return undefined;
}
return project.name;
return `${project.name}:${project.branch}`;
};

export const formatTrainTask = (task?: TrainTask) => {
const formatTrainTask = (task?: TrainTask) => {
if (!task) {
return undefined;
}
@@ -203,7 +203,7 @@ function VersionCompareModal({
[styles['version-compare__left__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}>
<Typography.Text ellipsis={{ tooltip: text }}>
{isEmpty(text) ? '--' : text}
</Typography.Text>
</div>
@@ -221,7 +221,7 @@ function VersionCompareModal({
[styles['version-compare__right__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}>
<Typography.Text ellipsis={{ tooltip: text }}>
{isEmpty(text) ? '--' : text}
</Typography.Text>
</div>


+ 1
- 3
react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx View File

@@ -49,9 +49,7 @@ function VersionSelectorModal({ versions, onOk, ...rest }: VersionSelectorModalP
onClick={() => handleClick(item.name)}
>
<img src={require('@/assets/img/dataset-version.png')} alt="" draggable={false} />
<Typography.Text ellipsis={{ tooltip: item.name }} style={{ color: 'inherit' }}>
{item.name}
</Typography.Text>
<Typography.Text ellipsis={{ tooltip: item.name }}>{item.name}</Typography.Text>
</div>
);
})}


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

@@ -36,13 +36,13 @@ enum ComputingResourceType {

const EditorRadioItems: KFRadioItem[] = [
{
key: ComputingResourceType.GPU,
title: '英伟达GPU',
value: ComputingResourceType.GPU,
icon: <KFIcon type="icon-jiyugongwangjingxiang" />,
},
{
key: ComputingResourceType.NPU,
title: '昇腾NPU',
value: ComputingResourceType.NPU,
icon: <KFIcon type="icon-bendishangchuan" />,
},
];


+ 55
- 0
react-ui/src/pages/HyperParameter/Create/index.less View File

@@ -0,0 +1,55 @@
.create-hyperparameter {
height: 100%;

&__content {
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 10px;
overflow: auto;
color: @text-color;
font-size: @font-size-content;
background-color: white;
border-radius: 10px;

&__type {
color: @text-color;
font-size: @font-size-input-lg;
}

:global {
.ant-input-number {
width: 100%;
}

.ant-form-item {
margin-bottom: 20px;
}

.image-url {
margin-top: -15px;
.ant-form-item-label > label::after {
content: '';
}
}

.ant-btn-variant-text:disabled {
color: @text-disabled-color;
}

.ant-btn-variant-text {
color: #565658;
}

.ant-btn.ant-btn-icon-only .anticon {
font-size: 20px;
}

.anticon-question-circle {
margin-top: -12px;
margin-left: 1px !important;
color: @text-color-tertiary !important;
font-size: 12px !important;
}
}
}
}

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

@@ -0,0 +1,167 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建实验
*/
import PageTitle from '@/components/PageTitle';
import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useLocation, useNavigate, useParams } from '@umijs/max';
import { App, Button, Form } from 'antd';
import { useEffect } from 'react';
import BasicConfig from '../components/CreateForm/BasicConfig';
import ExecuteConfig from '../components/CreateForm/ExecuteConfig';
import { getReqParamName } from '../components/CreateForm/utils';
import { FormData, HyperParameterData } from '../types';
import styles from './index.less';

function CreateHyperParameter() {
const navigate = useNavigate();
const [form] = Form.useForm();
const { message } = App.useApp();
const params = useParams();
const id = safeInvoke(Number)(params.id);
const { pathname } = useLocation();
const isCopy = pathname.includes('copy');

useEffect(() => {
// 编辑,复制
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);
}
};

// 创建、更新、复制实验
const createExperiment = async (formData: FormData) => {
// 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value"
const formParameters = formData['parameters'];

const parameters = formParameters.map((item) => {
const paramName = getReqParamName(item.type);
const range = item.range;
return {
...item,
[paramName]: range,
range: undefined,
};
});

// 根据后台要求,修改表单数据
const object = {
...formData,
parameters: parameters,
};

const params =
id && !isCopy
? {
id: id,
...object,
}
: object;

const request = id && !isCopy ? updateRayReq : addRayReq;
const [res] = await to(request(params));
if (res) {
message.success('操作成功');
navigate(-1);
}
};

// 提交
const handleSubmit = (values: FormData) => {
createExperiment(values);
};

// 取消
const cancel = () => {
navigate(-1);
};

let buttonText = '新建';
let title = '新建实验';
if (id) {
if (isCopy) {
title = '复制实验';
buttonText = '确定';
} else {
title = '编辑实验';
buttonText = '更新';
}
}

return (
<div className={styles['create-hyperparameter']}>
<PageTitle title={title}></PageTitle>
<div className={styles['create-hyperparameter__content']}>
<div>
<Form
name="create-hyperparameter"
labelCol={{ flex: '160px' }}
labelAlign="left"
form={form}
onFinish={handleSubmit}
size="large"
autoComplete="off"
scrollToFirstError
initialValues={{
mode: 'max',
parameters: [
{
name: '',
},
],
points_to_evaluate: [undefined],
}}
>
<BasicConfig />
<ExecuteConfig />

<Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>
<Button
type="default"
htmlType="button"
onClick={cancel}
style={{ marginLeft: '20px' }}
>
取消
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
}

export default CreateHyperParameter;

+ 40
- 0
react-ui/src/pages/HyperParameter/Info/index.less View File

@@ -0,0 +1,40 @@
.auto-ml-info {
position: relative;
height: 100%;
&__tabs {
height: 50px;
padding-left: 25px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

&__content {
height: calc(100% - 60px);
margin-top: 10px;
}

&__tips {
position: absolute;
top: 11px;
left: 256px;
padding: 3px 12px;
color: #565658;
font-size: @font-size-content;
background: .addAlpha(@primary-color, 0.09) [];
border-radius: 4px;

&::before {
position: absolute;
top: 10px;
left: -6px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-right: 6px solid .addAlpha(@primary-color, 0.09) [];
border-bottom: 4px solid transparent;
content: '';
}
}
}

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

Loading…
Cancel
Save