diff --git a/.gitignore b/.gitignore index 21a12484..54c883c5 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,10 @@ mvnw # web **/node_modules + +*storybook.log + +/react-ui/docs +/react-ui/types/tsconfig.tsbuildinfo +/react-ui/storybook-static +/react-ui/.storybook/scripts diff --git a/k8s/build-java.sh b/k8s/build-java.sh old mode 100755 new mode 100644 diff --git a/k8s/build-node.sh b/k8s/build-node.sh old mode 100755 new mode 100644 diff --git a/k8s/build.sh b/k8s/build.sh old mode 100755 new mode 100644 index 49041f30..5e77bdf9 --- a/k8s/build.sh +++ b/k8s/build.sh @@ -54,7 +54,7 @@ compile_front() { # 编译前端 docker run -v ${baseDir}:${baseDir} \ -e http_proxy=http://172.20.32.253:3128 -e https_proxy=http://172.20.32.253:3128 \ - 172.20.32.187/ci4s/node:16.16.0 ${baseDir}/k8s/build-node.sh + 172.20.32.187/tempimagefile/node:18.16.0 ${baseDir}/k8s/build-node.sh if [ $? -ne 0 ]; then echo "编译失败,请检查代码!" exit 1 diff --git a/k8s/build_and_deploy.sh b/k8s/build_and_deploy.sh old mode 100755 new mode 100644 index 6e561cdd..dcf6c1b9 --- a/k8s/build_and_deploy.sh +++ b/k8s/build_and_deploy.sh @@ -61,7 +61,7 @@ if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then exit 1 fi -valid_envs=("dev" "test") +valid_envs=("dev" "test" "test1") if [[ ! " ${valid_envs[@]} " =~ " $env " ]]; then echo "Invalid environment: $env" >&2 echo "Valid environments are: ${valid_envs[*]}" diff --git a/k8s/deploy.sh b/k8s/deploy.sh old mode 100755 new mode 100644 index 2dffc04f..6e9d2974 --- a/k8s/deploy.sh +++ b/k8s/deploy.sh @@ -34,7 +34,7 @@ if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then exit 1 fi -valid_envs=("dev" "test") +valid_envs=("dev" "test" "test1") if [[ ! " ${valid_envs[@]} " =~ " $env " ]]; then echo "Invalid environment: $env" >&2 echo "Valid environments are: ${valid_envs[*]}" @@ -46,6 +46,8 @@ if [ "$env" == "dev" ]; then remote_ip="172.20.32.197" elif [ "$env" == "test" ]; then remote_ip="172.20.32.185" +elif [ "$env" == "test1" ]; then + remote_ip="172.20.32.235" else echo "Invalid environment - $env" exit 1 diff --git a/k8s/dockerfiles/conf/nginx.conf b/k8s/dockerfiles/conf/nginx.conf index c23e02c3..0d04beef 100644 --- a/k8s/dockerfiles/conf/nginx.conf +++ b/k8s/dockerfiles/conf/nginx.conf @@ -29,11 +29,25 @@ http { location /label-studio/ { # rewrite ^/label-studio/(.*)$ /$1 break; - proxy_pass http://label-studio-service.argo.svc:8080/projects/; + proxy_pass http://label-studio-service.argo.svc:9000/projects/; proxy_hide_header X-Frame-Options; add_header X-Frame-Options ALLOWALL; } + location /minio/ { + # rewrite ^/label-studio/(.*)$ /$1 break; + proxy_pass http://juicefs-s3-gateway.juicefs.svc:9000/; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options ALLOWALL; + } + + location /neo4j/ { + # rewrite ^/label-studio/(.*)$ /$1 break; + proxy_pass http://172.20.20.88:7474/; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options ALLOWALL; + } + location / { rewrite ^/prod-api/(.*)$ /$1 break; root /home/ruoyi/projects/ruoyi-ui; diff --git a/k8s/k8s-10gen.yaml b/k8s/k8s-10gen.yaml deleted file mode 100644 index aaec3d8a..00000000 --- a/k8s/k8s-10gen.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-gen-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-gen - template: - metadata: - labels: - app: ci4s-gen - spec: - containers: - - name: ci4s-gen - image: ci4s-gen:v1.0 - ports: - - containerPort: 9202 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-gen-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9202 - nodePort: 31211 - protocol: TCP - selector: - app: ci4s-gen - diff --git a/k8s/k8s-11visual.yaml b/k8s/k8s-11visual.yaml deleted file mode 100644 index 3c2b25fb..00000000 --- a/k8s/k8s-11visual.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-visual-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-visual - template: - metadata: - labels: - app: ci4s-visual - spec: - containers: - - name: ci4s-visual - image: ci4s-visual:v1.0 - ports: - - containerPort: 9100 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-visual-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9100 - nodePort: 31212 - protocol: TCP - selector: - app: ci4s-visual - diff --git a/k8s/k8s-12front.yaml b/k8s/k8s-12front.yaml deleted file mode 100644 index fb24fc2d..00000000 --- a/k8s/k8s-12front.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-front-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-front - template: - metadata: - labels: - app: ci4s-front - spec: - containers: - - name: ci4s-front - image: 172.20.32.187/ci4s/ci4s-front:20240401 - ports: - - containerPort: 8000 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-front-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8000 - nodePort: 31213 - protocol: TCP - selector: - app: ci4s-front - diff --git a/k8s/k8s-3nacos.yaml b/k8s/k8s-3nacos.yaml deleted file mode 100644 index 0c293016..00000000 --- a/k8s/k8s-3nacos.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - namespace: argo - name: nacos-ci4s - labels: - app: nacos-ci4s -spec: - replicas: 1 - selector: - matchLabels: - app: nacos-ci4s - template: - metadata: - labels: - app: nacos-ci4s - spec: - containers: - - name: nacos-ci4s - image: nacos/nacos-server:v2.2.0 - env: - - name: SPRING_DATASOURCE_PLATFORM - value: mysql - - name: MODE - value: standalone - - name: MYSQL_SERVICE_HOST - value: mysql.argo.svc - - name: MYSQL_SERVICE_PORT - value: "3306" - - name: MYSQL_SERVICE_DB_NAME - value: nacos-ci4s-config - - name: MYSQL_SERVICE_USER - value: root - - name: MYSQL_SERVICE_PASSWORD - value: qazxc123456. - ports: - - containerPort: 8848 - - containerPort: 9848 - restartPolicy: Always - ---- - -apiVersion: v1 -kind: Service -metadata: - namespace: argo - name: nacos-ci4s - labels: - app: nacos-ci4s -spec: - type: NodePort - selector: - app: nacos-ci4s - ports: - - port: 8848 - targetPort: 8848 - nodePort: 31203 - name: web - - port: 9848 - targetPort: 9848 - nodePort: 31204 - name: podsa diff --git a/k8s/k8s-4gateway.yaml b/k8s/k8s-4gateway.yaml deleted file mode 100644 index b0cf7991..00000000 --- a/k8s/k8s-4gateway.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-gateway-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-gateway - template: - metadata: - labels: - app: ci4s-gateway - spec: - containers: - - name: ci4s-gateway - image: 172.20.32.187/ci4s/ci4s-gateway:20240401 - ports: - - containerPort: 8082 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-gateway-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8082 - nodePort: 31205 - protocol: TCP - selector: - app: ci4s-gateway - diff --git a/k8s/k8s-5auth.yaml b/k8s/k8s-5auth.yaml deleted file mode 100644 index 2066bd5d..00000000 --- a/k8s/k8s-5auth.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-auth-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-auth - template: - metadata: - labels: - app: ci4s-auth - spec: - containers: - - name: ci4s-auth - image: 172.20.32.187/ci4s/ci4s-auth:20240401 - ports: - - containerPort: 9200 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-auth-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9200 - nodePort: 31206 - protocol: TCP - selector: - app: ci4s-auth - diff --git a/k8s/k8s-6system.yaml b/k8s/k8s-6system.yaml deleted file mode 100644 index 8c6830bf..00000000 --- a/k8s/k8s-6system.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-system-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-system - template: - metadata: - labels: - app: ci4s-system - spec: - containers: - - name: ci4s-system - image: 172.20.32.187/ci4s/ci4s-system:20240401 - ports: - - containerPort: 9201 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-system-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9201 - nodePort: 31207 - protocol: TCP - selector: - app: ci4s-system - diff --git a/k8s/k8s-7management.yaml b/k8s/k8s-7management.yaml deleted file mode 100644 index cb07a130..00000000 --- a/k8s/k8s-7management.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-management-platform-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-management-platform - template: - metadata: - labels: - app: ci4s-management-platform - spec: - containers: - - name: ci4s-management-platform - image: 172.20.32.187/ci4s/managent:20240401 - ports: - - containerPort: 9213 - volumeMounts: - - name: resource - mountPath: /home/resource/ - volumes: - - name: resource - hostPath: - path: /home/resource/ - type: DirectoryOrCreate - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-management-platform-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9213 - nodePort: 31208 - protocol: TCP - selector: - app: ci4s-management-platform - diff --git a/k8s/k8s-8file.yaml b/k8s/k8s-8file.yaml deleted file mode 100644 index 3f54b8d0..00000000 --- a/k8s/k8s-8file.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-file-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-file - template: - metadata: - labels: - app: ci4s-file - spec: - containers: - - name: ci4s-file - image: ci4s-file:v1.0 - ports: - - containerPort: 9300 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-file-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9300 - nodePort: 31209 - protocol: TCP - selector: - app: ci4s-file - diff --git a/k8s/k8s-9job.yaml b/k8s/k8s-9job.yaml deleted file mode 100644 index b52cb355..00000000 --- a/k8s/k8s-9job.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-job-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-job - template: - metadata: - labels: - app: ci4s-job - spec: - containers: - - name: ci4s-job - image: ci4s-job:v1.0 - ports: - - containerPort: 9203 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-job-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9203 - nodePort: 31210 - protocol: TCP - selector: - app: ci4s-job - diff --git a/k8s/template-yaml/deploy/k8s-12front.yaml b/k8s/template-yaml/deploy/k8s-12front.yaml deleted file mode 100644 index 565b12ec..00000000 --- a/k8s/template-yaml/deploy/k8s-12front.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-front-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-front - template: - metadata: - labels: - app: ci4s-front - spec: - containers: - - name: ci4s-front - image: 172.20.32.187/ci4s/ci4s-front:202406120836 - ports: - - containerPort: 8000 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-front-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8000 - nodePort: 31213 - protocol: TCP - selector: - app: ci4s-front - diff --git a/k8s/template-yaml/deploy/k8s-7management.yaml b/k8s/template-yaml/deploy/k8s-7management.yaml deleted file mode 100644 index 75f1b522..00000000 --- a/k8s/template-yaml/deploy/k8s-7management.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-management-platform-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-management-platform - template: - metadata: - labels: - app: ci4s-management-platform - spec: - containers: - - name: ci4s-management-platform - image: 172.20.32.187/ci4s/ci4s-managent:202409201355 - env: - - name: TZ - value: Asia/Shanghai - - name: JAVA_TOOL_OPTIONS - value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" - ports: - - containerPort: 9213 - volumeMounts: - - name: resource-volume - mountPath: /home/resource/ - volumes: - - name: resource-volume - persistentVolumeClaim: - claimName: platform-data-pvc-nfs ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-management-platform-service - namespace: argo -spec: - type: NodePort - ports: - - name: http - port: 9213 - nodePort: 31208 - protocol: TCP - - name: debug - nodePort: 34567 - port: 5005 - protocol: TCP - targetPort: 5005 - selector: - app: ci4s-management-platform - diff --git a/k8s/template-yaml/k8s-6system.yaml b/k8s/template-yaml/k8s-6system.yaml index cc17e5ea..3691116c 100644 --- a/k8s/template-yaml/k8s-6system.yaml +++ b/k8s/template-yaml/k8s-6system.yaml @@ -18,7 +18,11 @@ spec: image: ${k8s-6system-image} ports: - containerPort: 9201 - + env: + - name: TZ + value: Asia/Shanghai + - name: JAVA_TOOL_OPTIONS + value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" --- apiVersion: v1 kind: Service @@ -28,9 +32,15 @@ metadata: spec: type: NodePort ports: - - port: 9201 + - name: http + port: 9201 nodePort: 31207 protocol: TCP + - name: debug + nodePort: 31220 + port: 5005 + protocol: TCP + targetPort: 5005 selector: app: ci4s-system diff --git a/react-ui/.eslintignore b/react-ui/.eslintignore index 8336e935..3bc705a6 100644 --- a/react-ui/.eslintignore +++ b/react-ui/.eslintignore @@ -5,4 +5,5 @@ public dist .umi -mock \ No newline at end of file +mock +/src/iconfont/ \ No newline at end of file diff --git a/react-ui/.eslintrc.js b/react-ui/.eslintrc.js index 564a28d2..4d55233b 100644 --- a/react-ui/.eslintrc.js +++ b/react-ui/.eslintrc.js @@ -1,10 +1,16 @@ module.exports = { - extends: [require.resolve('@umijs/lint/dist/config/eslint')], + extends: [ + require.resolve('@umijs/lint/dist/config/eslint'), + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + ], globals: { page: true, REACT_APP_ENV: true, }, rules: { '@typescript-eslint/no-use-before-define': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off', }, }; diff --git a/react-ui/.gitignore b/react-ui/.gitignore index 889039c2..9d2581cd 100644 --- a/react-ui/.gitignore +++ b/react-ui/.gitignore @@ -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 diff --git a/react-ui/.nvmrc b/react-ui/.nvmrc new file mode 100644 index 00000000..216afccf --- /dev/null +++ b/react-ui/.nvmrc @@ -0,0 +1 @@ +v18.20.7 diff --git a/react-ui/.prettierignore b/react-ui/.prettierignore index 7999ccda..e726b5a2 100644 --- a/react-ui/.prettierignore +++ b/react-ui/.prettierignore @@ -20,3 +20,4 @@ yarn-error.log CNAME /build /public +/src/iconfont/ diff --git a/react-ui/.storybook/babel-plugin-auto-css-modules.js b/react-ui/.storybook/babel-plugin-auto-css-modules.js new file mode 100644 index 00000000..d24babf5 --- /dev/null +++ b/react-ui/.storybook/babel-plugin-auto-css-modules.js @@ -0,0 +1,15 @@ +export default function (babel) { + const { types: t } = babel; + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value; + if (source.endsWith('.less')) { + if (path.node.specifiers.length > 0) { + path.node.source.value += '?modules'; + } + } + }, + }, + }; +} diff --git a/react-ui/.storybook/blocks/StoryName.tsx b/react-ui/.storybook/blocks/StoryName.tsx new file mode 100644 index 00000000..074c73cb --- /dev/null +++ b/react-ui/.storybook/blocks/StoryName.tsx @@ -0,0 +1,19 @@ +import { Of, useOf } from '@storybook/blocks'; + +/** + * A block that displays the story name or title from the of prop + * - if a story reference is passed, it renders the story name + * - if a meta reference is passed, it renders the stories' title + * - if nothing is passed, it defaults to the primary story + */ +export const StoryName = ({ of }: { of?: Of }) => { + const resolvedOf = useOf(of || 'story', ['story', 'meta']); + switch (resolvedOf.type) { + case 'story': { + return

{resolvedOf.story.name}

; + } + case 'meta': { + return

{resolvedOf.preparedMeta.title}

; + } + } +}; diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts new file mode 100644 index 00000000..c21ab45b --- /dev/null +++ b/react-ui/.storybook/main.ts @@ -0,0 +1,121 @@ +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', + { from: '../docs', to: '/docs' }, + { from: '../docs/index.html', to: '/docs/index.html' }, + ], + 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; diff --git a/react-ui/.storybook/manager.ts b/react-ui/.storybook/manager.ts new file mode 100644 index 00000000..baf80b25 --- /dev/null +++ b/react-ui/.storybook/manager.ts @@ -0,0 +1,6 @@ +import { addons } from '@storybook/manager-api'; +import theme from './theme'; + +addons.setConfig({ + theme: theme, +}); diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx new file mode 100644 index 00000000..ae8a7646 --- /dev/null +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -0,0 +1,19 @@ +export const Link = ({ to, children, ...props }: any) => ( + + {children} + +); + +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; diff --git a/react-ui/.storybook/mock/websocket.mock.js b/react-ui/.storybook/mock/websocket.mock.js new file mode 100644 index 00000000..4825c2c4 --- /dev/null +++ b/react-ui/.storybook/mock/websocket.mock.js @@ -0,0 +1,92 @@ +export const createWebSocketMock = () => { + class WebSocketMock { + constructor(url) { + this.url = url; + this.readyState = WebSocket.OPEN; + this.listeners = {}; + this.count = 0; + + console.log('Mock WebSocket connected to:', url); + + // 模拟服务器推送消息 + this.intervalId = setInterval(() => { + this.count += 1; + if (this.count > 5) { + this.count = 0; + clearInterval(this.intervalId); + return; + } + this.sendMessage(JSON.stringify(logStreamData)); + }, 3000); + } + + sendMessage(data) { + if (this.listeners['message']) { + this.listeners['message'].forEach((callback) => callback({ data })); + } + } + + addEventListener(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + removeEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); + } + } + + close() { + this.readyState = WebSocket.CLOSED; + console.log('Mock WebSocket closed'); + } + } + + return WebSocketMock; +}; + +export const logStreamData = { + streams: [ + { + stream: { + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + }, + values: [ + [ + '1742179591969785990', + 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', + ], + ], + }, + { + stream: { + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + }, + values: [ + [ + '1742179591973414064', + 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', + ], + ], + }, + ], +}; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx new file mode 100644 index 00000000..0ec22de0 --- /dev/null +++ b/react-ui/.storybook/preview.tsx @@ -0,0 +1,112 @@ +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 { createWebSocketMock } from './mock/websocket.mock'; +import './storybook.css'; + +/* + * Initializes MSW + * See https://github.com/mswjs/msw-storybook-addon#configuring-msw + * to learn how to customize it + */ +initialize(); + +// 替换全局 WebSocket 为 Mock 版本 +// @ts-ignore +global.WebSocket = createWebSocketMock(); + +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) => ( + + + + + + ), + ], + loaders: [mswLoader], // 👈 Add the MSW loader to all stories +}; + +export default preview; diff --git a/react-ui/.storybook/storybook.css b/react-ui/.storybook/storybook.css new file mode 100644 index 00000000..6c592a3c --- /dev/null +++ b/react-ui/.storybook/storybook.css @@ -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; +} diff --git a/react-ui/.storybook/theme.ts b/react-ui/.storybook/theme.ts new file mode 100644 index 00000000..7b624111 --- /dev/null +++ b/react-ui/.storybook/theme.ts @@ -0,0 +1,7 @@ +import { create } from '@storybook/theming'; +export default create({ + base: 'light', + brandTitle: '组件库文档', + brandUrl: 'https://storybook.js.org/docs', + brandTarget: '_blank', +}); diff --git a/react-ui/.storybook/tsconfig.json b/react-ui/.storybook/tsconfig.json new file mode 100644 index 00000000..e30a508b --- /dev/null +++ b/react-ui/.storybook/tsconfig.json @@ -0,0 +1,27 @@ +{ + "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检查 + "importHelpers": true, + "baseUrl": "./" + } +} diff --git a/react-ui/.storybook/typings.d.ts b/react-ui/.storybook/typings.d.ts new file mode 100644 index 00000000..742f70c6 --- /dev/null +++ b/react-ui/.storybook/typings.d.ts @@ -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; diff --git a/react-ui/Dockerfile b/react-ui/Dockerfile new file mode 100644 index 00000000..077c862f --- /dev/null +++ b/react-ui/Dockerfile @@ -0,0 +1,3 @@ +# Dockerfile +FROM nginx:alpine +COPY storybook-static/ /usr/share/nginx/html \ No newline at end of file diff --git a/react-ui/README.md b/react-ui/README.md index 2cf4d673..25f8d456 100644 --- a/react-ui/README.md +++ b/react-ui/README.md @@ -1,133 +1 @@ -Language : 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md) - -

Ant Design Pro

- -
- -An out-of-box UI solution for enterprise applications as a React boilerplate. - -[![Build Status](https://dev.azure.com/ant-design/ant-design-pro/_apis/build/status/ant-design.ant-design-pro?branchName=master)](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) ![Github Action](https://github.com/ant-design/ant-design-pro/workflows/Node%20CI/badge.svg) ![Deploy](https://github.com/ant-design/ant-design-pro/workflows/Deploy%20CI/badge.svg) - -[![Gitter](https://img.shields.io/gitter/room/ant-design/pro-english.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjEyMzUiIGhlaWdodD0iNjUwIiB2aWV3Qm94PSIwIDAgNzQxMCAzOTAwIj4NCjxyZWN0IHdpZHRoPSI3NDEwIiBoZWlnaHQ9IjM5MDAiIGZpbGw9IiNiMjIyMzQiLz4NCjxwYXRoIGQ9Ik0wLDQ1MEg3NDEwbTAsNjAwSDBtMCw2MDBINzQxMG0wLDYwMEgwbTAsNjAwSDc0MTBtMCw2MDBIMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjMwMCIvPg0KPHJlY3Qgd2lkdGg9IjI5NjQiIGhlaWdodD0iMjEwMCIgZmlsbD0iIzNjM2I2ZSIvPg0KPGcgZmlsbD0iI2ZmZiI%2BDQo8ZyBpZD0iczE4Ij4NCjxnIGlkPSJzOSI%2BDQo8ZyBpZD0iczUiPg0KPGcgaWQ9InM0Ij4NCjxwYXRoIGlkPSJzIiBkPSJNMjQ3LDkwIDMxNy41MzQyMzAsMzA3LjA4MjAzOSAxMzIuODczMjE4LDE3Mi45MTc5NjFIMzYxLjEyNjc4MkwxNzYuNDY1NzcwLDMwNy4wODIwMzl6Ii8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB5PSI0MjAiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHk9Ijg0MCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTI2MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjcyIgeT0iMTY4MCIvPg0KPC9nPg0KPHVzZSB4bGluazpocmVmPSIjczQiIHg9IjI0NyIgeT0iMjEwIi8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzOSIgeD0iNDk0Ii8%2BDQo8L2c%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzMTgiIHg9Ijk4OCIvPg0KPHVzZSB4bGluazpocmVmPSIjczkiIHg9IjE5NzYiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3M1IiB4PSIyNDcwIi8%2BDQo8L2c%2BDQo8L3N2Zz4%3D)](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Join the chat at https://gitter.im/ant-design/ant-design-pro](https://img.shields.io/gitter/room/ant-design/ant-design-pro.svg?style=flat-square&logoWidth=20&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjkwMCIgaGVpZ2h0PSI2MDAiIHZpZXdCb3g9IjAgMCAzMCAyMCI%2BDQo8ZGVmcz4NCjxwYXRoIGlkPSJzIiBkPSJNMCwtMSAwLjU4Nzc4NSwwLjgwOTAxNyAtMC45NTEwNTcsLTAuMzA5MDE3SDAuOTUxMDU3TC0wLjU4Nzc4NSwwLjgwOTAxN3oiIGZpbGw9IiNmZmRlMDAiLz4NCjwvZGVmcz4NCjxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2RlMjkxMCIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNSw1KSBzY2FsZSgzKSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsMikgcm90YXRlKDIzLjAzNjI0MykiLz4NCjx1c2UgeGxpbms6aHJlZj0iI3MiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyLDQpIHJvdGF0ZSg0NS44Njk4OTgpIi8%2BDQo8dXNlIHhsaW5rOmhyZWY9IiNzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMiw3KSByb3RhdGUoNjkuOTQ1Mzk2KSIvPg0KPHVzZSB4bGluazpocmVmPSIjcyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAsOSkgcm90YXRlKDIwLjY1OTgwOCkiLz4NCjwvc3ZnPg%3D%3D)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Build With Umi](https://img.shields.io/badge/build%20with-umi-028fe4.svg?style=flat-square)](http://umijs.org/) ![](https://badgen.net/badge/icon/Ant%20Design?icon=https://gw.alipayobjects.com/zos/antfincdn/Pp4WPgVDB3/KDpgvguMpGfqaHPjicRK.svg&label) - -![](https://user-images.githubusercontent.com/8186664/44953195-581e3d80-aec4-11e8-8dcb-54b9db38ec11.png) - -
- -- Preview: http://preview.pro.ant.design -- Home Page: http://pro.ant.design -- Documentation: http://pro.ant.design/docs/getting-started -- ChangeLog: http://pro.ant.design/docs/changelog -- FAQ: http://pro.ant.design/docs/faq -- Mirror Site in China: http://ant-design-pro.gitee.io - -## 5.0 is out! 🎉🎉🎉 - -[Ant Design Pro 5.0.0](https://github.com/ant-design/ant-design-pro/issues/8656) - -## Translation Recruitment :loudspeaker: - -We need your help: https://github.com/ant-design/ant-design-pro/issues/120 - -## Features - -- :bulb: **TypeScript**: A language for application-scale JavaScript -- :scroll: **Blocks**: Build page with block template -- :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/) -- :triangular_ruler: **Common Templates**: Typical templates for enterprise applications -- :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd -- :iphone: **Responsive**: Designed for variable screen sizes -- :art: **Theming**: Customizable theme with simple config -- :globe_with_meridians: **International**: Built-in i18n solution -- :gear: **Best Practices**: Solid workflow to make your code healthy -- :1234: **Mock development**: Easy to use mock development solution -- :white_check_mark: **UI Test**: Fly safely with unit and e2e tests - -## Templates - -``` -- Dashboard - - Analytic - - Monitor - - Workspace -- Form - - Basic Form - - Step Form - - Advanced From -- List - - Standard Table - - Standard List - - Card List - - Search List (Project/Applications/Article) -- Profile - - Simple Profile - - Advanced Profile -- Account - - Account Center - - Account Settings -- Result - - Success - - Failed -- Exception - - 403 - - 404 - - 500 -- User - - Login - - Register - - Register Result -``` - -## Usage - -### Use bash - -We provide pro-cli to quickly initialize scaffolding. - -```bash -# use npm -npm i @ant-design/pro-cli -g -pro create myapp -``` - -select umi version - -```shell -🐂 Use umi@4 or umi@3 ? (Use arrow keys) -❯ umi@4 - umi@3 -``` - -> If the umi@4 version is selected, full blocks are not yet supported. - -If you choose umi@3, you can also choose the pro template. Pro is the basic template, which only provides the basic content of the framework operation. Complete contains all blocks, which is not suitable for secondary development as a basic template. - -```shell -? 🚀 Full or a simple scaffold? (Use arrow keys) -❯ simple - complete -``` - -Install dependencies: - -```shell -$ cd myapp && tyarn -// or -$ cd myapp && npm install -``` - -## Browsers support - -Modern browsers. - -| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | -| --- | --- | --- | --- | --- | -| Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | - -## Contributing - -Any type of contribution is welcome, here are some examples of how you may contribute to this project: - -- Use Ant Design Pro in your daily work. -- Submit [issues](http://github.com/ant-design/ant-design-pro/issues) to report bugs or ask questions. -- Propose [pull requests](http://github.com/ant-design/ant-design-pro/pulls) to improve our code. +# Documentation diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts index f64c8c77..04ab330d 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -75,7 +75,7 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: '复杂智能软件', + title: '智能材料科研平台', layout: { ...defaultSettings, }, diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index 306c89db..c4c59d2f 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -17,7 +17,7 @@ const Settings: ProLayoutProps & { fixSiderbar: false, splitMenus: false, colorWeak: false, - title: '复杂智能软件', + title: '智能材料科研平台', pwa: true, token: { // 参见ts声明,demo 见文档,通过token 修改样式 diff --git a/react-ui/config/proxy.ts b/react-ui/config/proxy.ts index 35232b47..0739ab9f 100644 --- a/react-ui/config/proxy.ts +++ b/react-ui/config/proxy.ts @@ -26,10 +26,10 @@ export default { // 配置了这个可以从 http 代理到 https // 依赖 origin 的功能可能需要这个,比如 cookie changeOrigin: true, - // pathRewrite: { '^/api': '' }, + pathRewrite: { '^/api': '' }, }, '/profile/avatar/': { - target: 'http://172.20.32.185:31213', + target: 'http://172.20.32.235:31213', changeOrigin: true, }, }, diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index eaddb001..e64318e0 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -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', }, ], }, @@ -143,6 +143,11 @@ export default [ path: 'compare', component: './Experiment/Comparison/index', }, + { + name: '实验可视化对比', + path: 'compare-visual', + component: './Experiment/Aim/index', + }, ], }, { @@ -176,11 +181,83 @@ export default [ }, { name: '实验实例详情', - path: 'instance/:autoMLId/:id', + path: 'instance/:experimentId/:id', component: './AutoML/Instance/index', }, ], }, + { + 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/:experimentId/:id', + component: './HyperParameter/Instance/index', + }, + ], + }, + { + name: '主动学习', + path: 'active-learn', + routes: [ + { + name: '超参数寻优', + path: '', + component: './ActiveLearn/List/index', + }, + { + name: '实验详情', + path: 'info/:id', + component: './ActiveLearn/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './ActiveLearn/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './ActiveLearn/Create/index', + }, + { + name: '复制实验', + path: 'copy/:id', + component: './ActiveLearn/Create/index', + }, + { + name: '实验实例详情', + path: 'instance/:experimentId/:id', + component: './ActiveLearn/Instance/index', + }, + ], + }, ], }, { @@ -235,7 +312,18 @@ export default [ { name: '镜像详情', path: 'info/:id', - component: './Mirror/Info', + routes: [ + { + name: '镜像详情', + path: '', + component: './Mirror/Info', + }, + { + name: '新增镜像版本', + path: 'add-version', + component: './Mirror/Create', + }, + ], }, { name: '创建镜像', @@ -255,55 +343,55 @@ export default [ }, ], }, - ], - }, - { - name: '模型部署', - path: '/modelDeployment', - routes: [ { name: '模型部署', - path: '', - component: './ModelDeployment/List', - }, - { - name: '创建推理服务', - path: 'createService', - component: './ModelDeployment/CreateService', - }, - { - name: '编辑推理服务', - path: 'editService/:serviceId', - component: './ModelDeployment/CreateService', - }, - { - name: '服务详情', - path: 'serviceInfo/:serviceId', + path: 'modelDeployment', routes: [ { - name: '服务详情', + name: '模型部署', path: '', - component: './ModelDeployment/ServiceInfo', - }, - { - name: '新增服务版本', - path: 'createVersion', - component: './ModelDeployment/CreateVersion', + component: './ModelDeployment/List', }, { - name: '更新服务版本', - path: 'updateVersion', - component: './ModelDeployment/CreateVersion', + name: '创建推理服务', + path: 'createService', + component: './ModelDeployment/CreateService', }, { - name: '重启服务版本', - path: 'restartVersion', - component: './ModelDeployment/CreateVersion', + name: '编辑推理服务', + path: 'editService/:serviceId', + component: './ModelDeployment/CreateService', }, { - name: '服务版本详情', - path: 'versionInfo/:id', - component: './ModelDeployment/VersionInfo', + name: '服务详情', + path: 'serviceInfo/:serviceId', + routes: [ + { + name: '服务详情', + path: '', + component: './ModelDeployment/ServiceInfo', + }, + { + name: '新增服务版本', + path: 'createVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '更新服务版本', + path: 'updateVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '重启服务版本', + path: 'restartVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '服务版本详情', + path: 'versionInfo/:id', + component: './ModelDeployment/VersionInfo', + }, + ], }, ], }, @@ -461,6 +549,30 @@ export default [ }, ], }, + { + name: '算力积分', + path: '/points', + routes: [ + { + name: '算力积分', + path: '', + key: 'points', + component: './Points/index', + }, + ], + }, + { + name: '知识图谱', + path: '/knowledge', + routes: [ + { + name: '知识图谱', + path: '', + key: 'knowledge', + component: './Knowledge/index', + }, + ], + }, { path: '*', layout: false, diff --git a/react-ui/package.json b/react-ui/package.json index dc5be1c5..21710bca 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -1,13 +1,14 @@ { - "name": "ant-design-pro", - "version": "6.0.0", + "name": "cl-model", + "version": "1.0.0", "private": true, - "description": "An out-of-box UI solution for enterprise applications", + "description": "", "scripts": { "analyze": "cross-env ANALYZE=1 max build", "build": "max build", "deploy": "npm run build && npm run gh-pages", "dev": "npm run start:dev", + "dev-no-sso": "cross-env NO_SSO=true npm run start:dev", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", @@ -15,6 +16,7 @@ "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", + "docs": "typedoc", "gh-pages": "gh-pages -d dist", "i18n-remove": "pro i18n-remove --locale=zh-CN --write", "postinstall": "max setup", @@ -36,6 +38,11 @@ "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-deploy": "./.storybook/scripts/upload-deploy.sh", + "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", @@ -61,6 +68,7 @@ "@types/crypto-js": "^4.2.2", "@umijs/route-utils": "^4.0.1", "antd": "~5.21.4", + "caniuse-lite": "~1.0.30001707", "classnames": "^2.3.2", "crypto-js": "^4.2.0", "echarts": "^5.5.0", @@ -83,6 +91,19 @@ }, "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/manager-api": "~8.6.0", + "@storybook/react": "~8.5.3", + "@storybook/react-webpack5": "~8.5.3", + "@storybook/test": "~8.5.3", + "@storybook/theming": "~8.6.0", "@testing-library/react": "^14.0.0", "@types/antd": "^1.0.0", "@types/express": "^4.17.14", @@ -96,21 +117,30 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-react-hooks": "~5.2.0", + "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", "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", + "typedoc": "~0.28.1", "typescript": "^5.0.4", "umi-presets-pro": "^2.0.0" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.18.0" }, "create-umi": { "ignoreScript": [ @@ -140,5 +170,10 @@ "CNAME", "create-umi" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/react-ui/public/favicon-cc.ico b/react-ui/public/favicon-cc.ico new file mode 100644 index 00000000..4d544cb1 Binary files /dev/null and b/react-ui/public/favicon-cc.ico differ diff --git a/react-ui/public/favicon-cl.ico b/react-ui/public/favicon-cl.ico deleted file mode 100644 index 408b8a23..00000000 Binary files a/react-ui/public/favicon-cl.ico and /dev/null differ diff --git a/react-ui/public/favicon.ico b/react-ui/public/favicon.ico index 4d544cb1..408b8a23 100644 Binary files a/react-ui/public/favicon.ico and b/react-ui/public/favicon.ico differ diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.ttf b/react-ui/public/fonts/DingTalk-JinBuTi.ttf new file mode 100644 index 00000000..c4efa55a Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.ttf differ diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.woff b/react-ui/public/fonts/DingTalk-JinBuTi.woff new file mode 100644 index 00000000..5a8efa8e Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.woff differ diff --git a/react-ui/public/fonts/DingTalk-JinBuTi.woff2 b/react-ui/public/fonts/DingTalk-JinBuTi.woff2 new file mode 100644 index 00000000..c8d272a5 Binary files /dev/null and b/react-ui/public/fonts/DingTalk-JinBuTi.woff2 differ diff --git a/react-ui/public/fonts/font.css b/react-ui/public/fonts/font.css index ea5ffc5c..012b9f59 100644 --- a/react-ui/public/fonts/font.css +++ b/react-ui/public/fonts/font.css @@ -1,5 +1,13 @@ @font-face { - font-family: Alibaba; - src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); - font-display: swap; - } \ No newline at end of file + font-family: Alibaba; + src: url('./ALIBABA-PUHUITI-MEDIUM.TTF'); + font-display: swap; +} + +@font-face { + font-family: 'DingTalk-JinBuTi'; + src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */ + url('./DingTalk-JinBuTi.woff') format('woff'), /* 兼容性较好的 woff */ + url('./DingTalk-JinBuTi.ttf') format('truetype'); /* ttf 作为备选 */ + font-display: swap; /* 优化页面加载时的字体显示 */ +} \ No newline at end of file diff --git a/react-ui/public/mockServiceWorker.js b/react-ui/public/mockServiceWorker.js new file mode 100644 index 00000000..ec47a9a5 --- /dev/null +++ b/react-ui/public/mockServiceWorker.js @@ -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 +} diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index d08ca129..686c495d 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -1,14 +1,17 @@ import RightContent from '@/components/RightContent'; import themes from '@/styles/theme.less'; +import { type GlobalInitialState } from '@/types'; +import { menuItemRender } from '@/utils/menuRender'; import type { Settings as LayoutSettings } from '@ant-design/pro-components'; import { RuntimeConfig, history } from '@umijs/max'; import { RuntimeAntdConfig } from 'umi'; import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; +import ErrorBoundary from './components/ErrorBoundary'; import './dayjsConfig'; -import './global.less'; -import { removeAllPageCacheState } from './hooks/pageCacheState'; +import { removeAllPageCacheState } from './hooks/useCacheState'; +import { globalGetSeverTime } from './hooks/useServerTime'; import { getRemoteMenu, getRoutersInfo, @@ -17,21 +20,17 @@ import { setRemoteMenu, } from './services/session'; import './styles/menu.less'; -export { requestConfig as request } from './requestConfig'; -// const isDev = process.env.NODE_ENV === 'development'; -import { type GlobalInitialState } from '@/types'; -// import '@/utils/clipboard'; -import { menuItemRender } from '@/utils/menuRender'; -import ErrorBoundary from './components/ErrorBoundary'; import { needAuth } from './utils'; import { gotoLoginPage } from './utils/ui'; +export { requestConfig as request } from './requestConfig'; /** * @see https://umijs.org/zh-CN/plugins/plugin-initial-state - * */ + */ export async function getInitialState(): Promise { const fetchUserInfo = async () => { try { + globalGetSeverTime(); const response = await getUserInfo(); return { ...response.user, @@ -41,7 +40,7 @@ export async function getInitialState(): Promise { roleNames: response.user.roles, } as API.CurrentUser; } catch (error) { - console.error('1111', error); + console.error('getInitialState', error); gotoLoginPage(); } return undefined; @@ -140,7 +139,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { onClick: () => { // 点击菜单项,删除所有的页面 state 缓存 removeAllPageCacheState(); - // console.log('click menu'); }, }, ...initialState?.settings, @@ -169,7 +167,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { } }; -export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { +export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { //console.log('patchRoutes', e); }; @@ -215,7 +213,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,11 +226,12 @@ 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)', headerBorderRadius: 4, - rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + // rowSelectedBg: 'rgba(22, 100, 255, 0.05)', 固定列时,横向滑动导致重叠 }; memo.theme.components.Tabs = { titleFontSize: 16, @@ -245,9 +244,12 @@ export const antd: RuntimeAntdConfig = (memo) => { linkColor: 'rgba(29, 29, 32, 0.7)', separatorColor: 'rgba(29, 29, 32, 0.7)', }; + memo.theme.components.Tree = { + directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', + }; memo.theme.cssVar = true; - // memo.theme.hashed = false; + memo.theme.hashed = false; memo.appConfig = { message: { diff --git a/react-ui/src/assets/img/confirm-icon.png b/react-ui/src/assets/img/confirm-icon.png new file mode 100644 index 00000000..6865b719 Binary files /dev/null and b/react-ui/src/assets/img/confirm-icon.png differ diff --git a/react-ui/src/assets/img/comfirm-icon.png b/react-ui/src/assets/img/copy-icon.png similarity index 100% rename from react-ui/src/assets/img/comfirm-icon.png rename to react-ui/src/assets/img/copy-icon.png diff --git a/react-ui/src/assets/img/logo-cc.png b/react-ui/src/assets/img/logo-cc.png new file mode 100644 index 00000000..cae91fe5 Binary files /dev/null and b/react-ui/src/assets/img/logo-cc.png differ diff --git a/react-ui/src/assets/img/logo-cl.png b/react-ui/src/assets/img/logo-cl.png deleted file mode 100644 index e2fbcfe5..00000000 Binary files a/react-ui/src/assets/img/logo-cl.png and /dev/null differ diff --git a/react-ui/src/assets/img/logo.png b/react-ui/src/assets/img/logo.png index cae91fe5..e2fbcfe5 100644 Binary files a/react-ui/src/assets/img/logo.png and b/react-ui/src/assets/img/logo.png differ diff --git a/react-ui/src/assets/img/popover-bg.png b/react-ui/src/assets/img/popover-bg.png new file mode 100644 index 00000000..d783c637 Binary files /dev/null and b/react-ui/src/assets/img/popover-bg.png differ diff --git a/react-ui/src/assets/img/user-points-bg.png b/react-ui/src/assets/img/user-points-bg.png new file mode 100644 index 00000000..3f25b128 Binary files /dev/null and b/react-ui/src/assets/img/user-points-bg.png differ diff --git a/react-ui/src/assets/img/workspace-experiment.png b/react-ui/src/assets/img/workspace-experiment.png index bcd69981..0bca4327 100644 Binary files a/react-ui/src/assets/img/workspace-experiment.png and b/react-ui/src/assets/img/workspace-experiment.png differ diff --git a/react-ui/src/assets/img/workspace-pipeline.png b/react-ui/src/assets/img/workspace-pipeline.png index fbbc3ed7..f551cb9c 100644 Binary files a/react-ui/src/assets/img/workspace-pipeline.png and b/react-ui/src/assets/img/workspace-pipeline.png differ diff --git a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx new file mode 100644 index 00000000..86e63891 --- /dev/null +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -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 =
{formatValue}
; + } else if (Array.isArray(formatValue)) { + valueComponent = ( +
+ {formatValue.map((item: BasicInfoLink) => ( + + ))} +
+ ); + } else if (typeof formatValue === 'object' && formatValue) { + valueComponent = ( + + ); + } else { + valueComponent = ( + + ); + } + return ( +
+
+ + {label} + +
+ {valueComponent} +
+ ); +} + +export default BasicInfoItem; diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx new file mode 100644 index 00000000..c5a993e4 --- /dev/null +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -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 = ( + + {value} + + ); + } else if (link && value) { + component = ( + + {value} + + ); + } else { + component = {!isEmpty(value) ? value : '--'}; + } + + return ( +
+ + {component} + +
+ ); +} + +export default BasicInfoItemValue; diff --git a/react-ui/src/components/BasicInfo/components.tsx b/react-ui/src/components/BasicInfo/components.tsx deleted file mode 100644 index b8932a25..00000000 --- a/react-ui/src/components/BasicInfo/components.tsx +++ /dev/null @@ -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 = ( -
- {formatValue.map((item: BasicInfoLink) => ( - - ))} -
- ); - } else if (React.isValidElement(formatValue)) { - // 这个判断必须在下面的判断之前 - valueComponent = ( - - ); - } else if (typeof formatValue === 'object' && formatValue) { - valueComponent = ( - - ); - } else { - valueComponent = ( - - ); - } - return ( -
-
- {label} -
- {valueComponent} -
- ); -} - -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 = ( - - {value} - - ); - } else if (link && value) { - component = ( - - {value} - - ); - } else if (React.isValidElement(value)) { - return value; - } else { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} diff --git a/react-ui/src/components/BasicInfo/format.ts b/react-ui/src/components/BasicInfo/format.ts deleted file mode 100644 index 0dae2422..00000000 --- a/react-ui/src/components/BasicInfo/format.ts +++ /dev/null @@ -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 : '--'; - }; -}; diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index e4570868..ced2d2ba 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -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; + } } } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 1336d0b6..a918b8ee 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -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, + labelWidth, + labelEllipsis = true, + labelAlign = 'start', + threeColumns = false, + className, + style, +}: BasicInfoProps) { return ( -
+
{datas.map((item) => ( ))}
diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts index a7c10ba0..be2ac774 100644 --- a/react-ui/src/components/BasicInfo/types.ts +++ b/react-ui/src/components/BasicInfo/types.ts @@ -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; }; diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index 850af79c..1207d033 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -4,7 +4,7 @@ flex-wrap: wrap; align-items: stretch; width: 100%; - border: 1px solid @border-color-base; + border: 1px solid @border-color; border-bottom: none; border-radius: 4px; @@ -12,7 +12,7 @@ display: flex; align-items: stretch; width: 25%; - border-bottom: 1px solid @border-color-base; + border-bottom: 1px solid @border-color; &__label { flex: none; @@ -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; + } } } diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index 571c4b5b..357a82fb 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,22 +1,21 @@ 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 type BasicTableInfoProps = Omit; +/** + * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 + */ export default function BasicTableInfo({ datas, + labelWidth, + labelEllipsis, className, style, - labelWidth, }: BasicTableInfoProps) { const remainder = datas.length % 4; const array = []; @@ -24,7 +23,7 @@ export default function BasicTableInfo({ for (let i = 0; i < 4 - remainder; i++) { array.push({ label: '', - value: '', + value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined }); } } @@ -37,6 +36,7 @@ export default function BasicTableInfo({ key={`${item.label}-${index}`} data={item} labelWidth={labelWidth} + labelEllipsis={labelEllipsis} classPrefix="kf-basic-table-info" /> ))} diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less b/react-ui/src/components/CodeConfigItem/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less rename to react-ui/src/components/CodeConfigItem/index.less diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx b/react-ui/src/components/CodeConfigItem/index.tsx similarity index 73% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx rename to react-ui/src/components/CodeConfigItem/index.tsx index 673bca40..ff48368c 100644 --- a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx +++ b/react-ui/src/components/CodeConfigItem/index.tsx @@ -2,6 +2,7 @@ import { AvailableRange } from '@/enums'; import { type CodeConfigData } from '@/pages/CodeConfig/List'; import { Flex, Typography } from 'antd'; import classNames from 'classnames'; +import { useState } from 'react'; import styles from './index.less'; type CodeConfigItemProps = { @@ -10,6 +11,7 @@ type CodeConfigItemProps = { }; function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { + const [isEllipsis, setIsEllipsis] = useState(false); return (
onClick?.(item)}> @@ -32,11 +34,20 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { setIsEllipsis(ellipsis), + }} > {item.git_url} -
{item.git_branch}
+ + {item.git_branch} +
); } diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 79401b25..059d857f 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -4,59 +4,82 @@ * @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) => { if (res) { - const { git_url, git_branch, code_repo_name } = res; + const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = + res; const jsonObj = { + id, + name: code_repo_name, code_path: git_url, branch: git_branch, + username: git_user_name, + password: git_password, + ssh_private_key: ssh_key, }; const jsonObjStr = JSON.stringify(jsonObj); - const showValue = code_repo_name; onChange?.({ value: jsonObjStr, - showValue, + showValue: code_repo_name, fromSelect: true, ...jsonObj, }); } else { - onChange?.({ - value: undefined, - showValue: undefined, - fromSelect: false, - }); + onChange?.(undefined); } close(); }, }); }; + // 删除 + const handleRemove = () => { + onChange?.(undefined); + }; + return ( -
+
)} diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index d84257a7..38d3644c 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ type IconFontProps = Parameters[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, diff --git a/react-ui/src/components/ModalTitle/index.less b/react-ui/src/components/KFModal/KFModalTitle.less similarity index 100% rename from react-ui/src/components/ModalTitle/index.less rename to react-ui/src/components/KFModal/KFModalTitle.less diff --git a/react-ui/src/components/ModalTitle/index.tsx b/react-ui/src/components/KFModal/KFModalTitle.tsx similarity index 84% rename from react-ui/src/components/ModalTitle/index.tsx rename to react-ui/src/components/KFModal/KFModalTitle.tsx index 4c0179eb..d2ec3265 100644 --- a/react-ui/src/components/ModalTitle/index.tsx +++ b/react-ui/src/components/KFModal/KFModalTitle.tsx @@ -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; }; diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index c073ab27..8156d6a9 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -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={} + title={} > {children} diff --git a/react-ui/src/components/KFRadio/index.tsx b/react-ui/src/components/KFRadio/index.tsx index 4bb4ccce..7191dae6 100644 --- a/react-ui/src/components/KFRadio/index.tsx +++ b/react-ui/src/components/KFRadio/index.tsx @@ -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 ( {items.map((item) => { return ( onChange?.(item.key)} + onClick={() => onChange?.(item.value)} > {item.icon} {item.title} diff --git a/react-ui/src/components/KFSpin/index.less b/react-ui/src/components/KFSpin/index.less index 753154d7..7d532d2d 100644 --- a/react-ui/src/components/KFSpin/index.less +++ b/react-ui/src/components/KFSpin/index.less @@ -4,7 +4,7 @@ right: 0; bottom: 0; left: 0; - z-index: 1001; + z-index: 1001; // 设置大于 Modal 的 z-index display: flex; flex-direction: column; align-items: center; diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 519ab6ef..ee054aea 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -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 ( -
- -
加载中
+
+ +
{label}
); } diff --git a/react-ui/src/components/LabelValue/index.less b/react-ui/src/components/LabelValue/index.less deleted file mode 100644 index 5f1b9b0c..00000000 --- a/react-ui/src/components/LabelValue/index.less +++ /dev/null @@ -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; - } -} diff --git a/react-ui/src/components/LabelValue/index.tsx b/react-ui/src/components/LabelValue/index.tsx deleted file mode 100644 index 22b9b3eb..00000000 --- a/react-ui/src/components/LabelValue/index.tsx +++ /dev/null @@ -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 ( -
-
{label}
-
{value ?? '--'}
-
- ); -} - -export default LabelValue; diff --git a/react-ui/src/components/MenuIconSelector/index.less b/react-ui/src/components/MenuIconSelector/index.less index 77529762..5a64a8d3 100644 --- a/react-ui/src/components/MenuIconSelector/index.less +++ b/react-ui/src/components/MenuIconSelector/index.less @@ -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; diff --git a/react-ui/src/components/MenuIconSelector/index.tsx b/react-ui/src/components/MenuIconSelector/index.tsx index dd57320e..fa38910b 100644 --- a/react-ui/src/components/MenuIconSelector/index.tsx +++ b/react-ui/src/components/MenuIconSelector/index.tsx @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; import styles from './index.less'; interface MenuIconSelectorProps extends Omit { + /** 选中的图标 */ 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([]); useEffect(() => { diff --git a/react-ui/src/components/PageTitle/index.less b/react-ui/src/components/PageTitle/index.less index 47907246..40913e00 100644 --- a/react-ui/src/components/PageTitle/index.less +++ b/react-ui/src/components/PageTitle/index.less @@ -7,4 +7,25 @@ background-repeat: no-repeat; background-position: top center; background-size: 100% 100%; + + &__tips { + position: relative; + margin-left: 18px; + padding: 3px 15px; + color: @primary-color; + background: .addAlpha(@primary-color, 0.1) []; + 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.1) []; + border-bottom: 4px solid transparent; + content: ''; + } + } } diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx index ca192454..113f9b7b 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -3,19 +3,37 @@ * @Date: 2024-04-17 14:01:46 * @Description: 页面标题 */ +import KFIcon from '@/components/KFIcon'; import classNames from 'classnames'; import React from 'react'; import './index.less'; type PageTitleProps = { - title: string; + /** 标题 */ + title: React.ReactNode; + /** 图标 */ + tooltip?: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义标题 */ + titleRender?: () => React.ReactNode; }; -function PageTitle({ title, style, className = '' }: PageTitleProps) { + +/** + * 页面标题 + */ +function PageTitle({ title, style, className, tooltip }: PageTitleProps) { return (
- {title} +
{title}
+ {tooltip && ( +
+ + {tooltip} +
+ )}
); } diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 4b22d208..ff0a21f7 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -22,7 +22,7 @@ border-radius: 4px; &__value { - .singleLine(); + //.singleLine(); margin-right: 8px; font-size: @font-size-input; line-height: 1.5714285714285714; @@ -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; } } diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 94fdddde..08cf8649 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -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 { ConfigProvider, Form, Input, Typography } 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; } @@ -50,7 +67,7 @@ function ParameterInput({ allowClear, className, style, - size = 'middle', + size, disabled = false, id, ...rest @@ -64,10 +81,17 @@ function ParameterInput({ const placeholder = valueObj?.placeholder || rest?.placeholder; const InputComponent = textArea ? Input.TextArea : Input; const { status } = Form.Item.useStatus(); + const { componentSize } = ConfigProvider.useConfig(); + const mySize = size || componentSize; // 删除 const handleRemove = (e: React.MouseEvent) => { e.stopPropagation(); + if (onRemove) { + onRemove(); + return; + } + onChange?.({ ...valueObj, value: undefined, @@ -77,7 +101,6 @@ function ParameterInput({ expandedKeys: [], checkedKeys: [], }); - onRemove?.(); }; return ( @@ -87,7 +110,8 @@ function ParameterInput({ id={id} className={classNames( 'parameter-input', - { 'parameter-input--large': size === 'large' }, + { 'parameter-input--large': mySize === 'large' }, + { 'parameter-input--small': mySize === 'small' }, { [`parameter-input--${status}`]: status }, className, )} @@ -96,7 +120,12 @@ function ParameterInput({ > {valueObj?.showValue ? (
- {valueObj?.showValue} + + {valueObj.showValue} + ['filterOption'] = ( - input: string, - option?: ComputingResource, -) => { - return ( - option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false - ); -}; - // id 从 number 转换为 string const convertId = (item: any) => ({ ...item, @@ -86,17 +75,10 @@ export const paramSelectConfig: Record = { }, resource: { getOptions: async () => { - const res = await getComputingResourceReq({ - page: 0, - size: 1000, - resource_type: '', - }); - return res?.data?.content ?? []; - }, - fieldNames: { - label: 'description', - value: 'standard', + // 不需要这个函数 + return []; }, + fieldNames: resourceFieldNames, filterOption: filterResourceStandard as SelectProps['filterOption'], }, }; diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index ffaf8415..c9a78069 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -1,60 +1,106 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数选择组件 + * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { PipelineNodeModelParameter } from '@/types'; +import { useComputingResource } from '@/hooks/useComputingResource'; import { to } from '@/utils/promise'; -import { Select } from 'antd'; +import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; +import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -type ParameterSelectProps = { - value?: PipelineNodeModelParameter; - onChange?: (value: PipelineNodeModelParameter) => void; - disabled?: boolean; +export type ParameterSelectObject = { + value: any; + [key: string]: any; }; -function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { - const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; - const propsConfig = paramSelectConfig[item_type]; +export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource'; + +export interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: ParameterSelectDataType; + /** 是否只是展示信息 */ + display?: boolean; + /** 值,支持对象,对象必须包含 value */ + value?: string | ParameterSelectObject; + /** 用于流水线, 流水线资源规格要求 id 为字符串 */ + isPipeline?: boolean; + /** 修改后回调 */ + onChange?: (value: string | ParameterSelectObject) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ + dataType, + display = false, + value, + isPipeline = false, + onChange, + ...rest +}: ParameterSelectProps) { + const [options, setOptions] = useState([]); + const propsConfig = paramSelectConfig[dataType]; + const valueText = typeof value === 'object' && value !== null ? value.value : value; + const [resourceStandardList] = useComputingResource(); + const computingResource = isPipeline + ? resourceStandardList.map((v) => ({ + ...v, + id: String(v.id), + })) + : resourceStandardList; useEffect(() => { + // 获取下拉数据 + const getSelectOptions = async () => { + if (!propsConfig) { + return; + } + const getOptions = propsConfig.getOptions; + const [res] = await to(getOptions()); + if (res) { + setOptions(res); + } + }; + getSelectOptions(); - }, []); + }, [propsConfig]); - const hangleChange = (e: string) => { - onChange?.({ - ...valueNonNullable, - value: e, - }); - }; + const selectOptions = dataType === 'resource' ? computingResource : options; - // 获取下拉数据 - const getSelectOptions = async () => { - if (!propsConfig) { - return; - } - const getOptions = propsConfig.getOptions; - const [res] = await to(getOptions()); - if (res) { - setOptions(res); + const handleChange = (text: string) => { + if (typeof value === 'object' && value !== null) { + onChange?.({ + ...value, + value: text, + }); + } else { + onChange?.(text); } }; + // 只用于展示,FormInfo 组件带有 Tooltip + if (display) { + return ( + + ); + } + return ( + + + + + + + + + + + + ); +} + +export default BasicConfig; diff --git a/react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..addb402c --- /dev/null +++ b/react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,517 @@ +import CodeSelect from '@/components/CodeSelect'; +import ParameterSelect from '@/components/ParameterSelect'; +import ResourceSelect, { + requiredValidator, + ResourceSelectorType, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { AutoMLTaskType, autoMLTaskTypeOptions } from '@/enums'; +import { Col, Form, Input, InputNumber, Radio, Row, Select, Switch } from 'antd'; +import { + classifierAlgorithms, + FrameworkType, + frameworkTypeOptions, + queryStrategies, + regressorAlgorithms, +} from './utils'; + +function ExecuteConfig() { + const form = Form.useFormInstance(); + return ( + <> + + + + + + form.resetFields(['metrics'])} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {frameworkType === FrameworkType.Pytorch ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + ) : null} + + ); + } else if (frameworkType === FrameworkType.Sklearn) { + if (taskType === AutoMLTaskType.Classification) { + return ( + <> + + + + + + + + + ); + } + } else { + return null; + } + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -28,7 +34,7 @@ function BasicConfig() { + + + + + + + + ); } diff --git a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx index b3b3f2dd..ae70e806 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx @@ -30,7 +30,6 @@ function DatasetConfig() { type={ResourceSelectorType.Dataset} placeholder="请选择数据集" canInput={false} - size="large" /> diff --git a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx index d03445f3..c548e079 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx @@ -27,6 +27,9 @@ const classificationAlgorithms = [ 'qda', 'random_forest', 'sgd', + 'LightGBMClassification', + 'XGBoostClassification', + 'StackingClassification', ].map((name) => ({ label: name, value: name })); // 回归算法 @@ -43,6 +46,8 @@ const regressorAlgorithms = [ 'mlp', 'random_forest', 'sgd', + 'LightGBMRegression', + 'XGBoostRegression', ].map((name) => ({ label: name, value: name })); // 特征预处理算法 @@ -289,7 +294,7 @@ function ExecuteConfig() { name="ensemble_nbest" tooltip="仅集成最佳的N个模型" > - + @@ -305,7 +310,7 @@ function ExecuteConfig() { name="max_models_on_disc" tooltip="定义在磁盘中保存的模型的最大数量。额外的模型数量将被永久删除,它设置了一个集成可以使用多少个模型的上限。必须是大于等于1的整数,默认50" > - + @@ -421,7 +426,7 @@ function ExecuteConfig() { }, ]} > - + @@ -431,7 +436,12 @@ function ExecuteConfig() { - + diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TextExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TextExecuteConfig.tsx new file mode 100644 index 00000000..8910c473 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/TextExecuteConfig.tsx @@ -0,0 +1,137 @@ +import ParameterSelect from '@/components/ParameterSelect'; +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, InputNumber, Row, Select } from 'antd'; + +// 模型 +const modelTypeOptions = [ + 'TextCNN', + 'TextRNN', + 'FasetText', + 'TextRCNN', + 'TextRNN_Att', + 'DPCNN', + 'Transformer', +].map((name) => ({ label: name, value: name })); + +function TextExecuteConfig() { + return ( + <> + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + const is_validate = getFieldValue('is_validate'); + if (is_validate) { + return ( + <> + + + + + + + + + + + + + + + + + ); + } + }} + + + {/* + {({ getFieldValue }) => { + const is_test = getFieldValue('is_test'); + if (is_test) { + return ( + <> + + + + + + + + + + + + + + + + + ); + } + }} + */} + + ); +} + +export default VideoExecuteConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/index.less b/react-ui/src/pages/AutoML/components/CreateForm/index.less index 5ff9a94e..00caa827 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/index.less +++ b/react-ui/src/pages/AutoML/components/CreateForm/index.less @@ -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; } } } diff --git a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx index e95ccd42..dbe06179 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx @@ -4,11 +4,13 @@ import tableCellRender from '@/utils/table'; import { Table, type TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; +import TrialStatusCell from '../TrialStatusCell'; import styles from './index.less'; type ExperimentHistoryProps = { - fileUrl?: string; - isClassification: boolean; + calcMetrics?: string; // 计算指标 + fileUrl?: string; // 文件url + isClassification: boolean; // 是否是分类 }; type TableData = { @@ -21,39 +23,39 @@ type TableData = { althorithm?: string; }; -function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { +function ExperimentHistory({ calcMetrics, fileUrl, isClassification }: ExperimentHistoryProps) { const [tableData, setTableData] = useState([]); useEffect(() => { + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: calcMetrics ? item[1]?.[5]?.[calcMetrics] : undefined, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + if (fileUrl) { getHistoryFile(); } - }, [fileUrl]); - - // 获取实验运行历史记录 - const getHistoryFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - const data: any[] = res.data; - const list: TableData[] = data.map((item) => { - return { - id: item[0]?.[0], - accuracy: item[1]?.[5]?.accuracy, - duration: item[1]?.[5]?.duration, - train_loss: item[1]?.[5]?.train_loss, - status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], - }; - }); - list.forEach((item) => { - if (!item.id) return; - const config = (res as any).configs?.[item.id]; - item.feature = config?.['feature_preprocessor:__choice__']; - item.althorithm = isClassification - ? config?.['classifier:__choice__'] - : config?.['regressor:__choice__']; - }); - setTableData(list); - } - }; + }, [fileUrl, isClassification, calcMetrics]); const columns: TableProps['columns'] = [ { @@ -63,50 +65,48 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps width: 80, render: tableCellRender(false), }, - { - title: '准确率', - dataIndex: 'accuracy', - key: 'accuracy', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, { title: '耗时', dataIndex: 'duration', key: 'duration', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '训练损失', dataIndex: 'train_loss', key: 'train_loss', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '特征处理', dataIndex: 'feature', key: 'feature', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '算法', dataIndex: 'althorithm', key: 'althorithm', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '状态', dataIndex: 'status', key: 'status', - width: 120, - render: tableCellRender(false), + width: 150, + render: TrialStatusCell, }, ]; + if (calcMetrics) { + columns.splice(0, 0, { + title: `指标:${calcMetrics}`, + dataIndex: 'accuracy', + key: 'accuracy', + render: tableCellRender(true), + }); + } + return (
diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less similarity index 77% rename from react-ui/src/pages/AutoML/components/ExperimentInstance/index.less rename to react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less index 3e7d2eec..94258c7f 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less @@ -1,3 +1,5 @@ +@cellWidth: calc(100% + 32px + 33px - 48px - 200px - 344px); + .tableExpandBox { display: flex; align-items: center; @@ -11,22 +13,22 @@ } .check { - width: calc((100% + 32px + 33px) / 6.25 / 2); + width: calc(@cellWidth * 3 / 20); // 15% } .index { - width: calc((100% + 32px + 33px) / 6.25 / 2); + width: calc(@cellWidth * 3 / 20); // 15% } .description { display: flex; - flex: 1; align-items: center; + width: calc(@cellWidth / 2); // 50% } .startTime { .singleLine(); - width: calc(20% + 10px); + width: calc(@cellWidth / 5); // 20% } .status { diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx similarity index 74% rename from react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx rename to react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx index 72fe3473..05d6d786 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx @@ -1,24 +1,20 @@ 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 { useCheck } from '@/hooks/useCheck'; import themes from '@/styles/theme.less'; import { type ExperimentInstance } from '@/types'; -import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { DoubleRightOutlined } from '@ant-design/icons'; -import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import { App, Button, Checkbox, ConfigProvider } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo } from 'react'; +import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; import styles from './index.less'; +import ExperimentInstanceComponent from './instance'; -type ExperimentInstanceProps = { +type ExperimentInstanceListProps = { + type: ExperimentListType; experimentInsList?: ExperimentInstance[]; experimentInsTotal: number; onClickInstance?: (instance: ExperimentInstance) => void; @@ -27,14 +23,15 @@ type ExperimentInstanceProps = { onLoadMore?: () => void; }; -function ExperimentInstanceComponent({ +function ExperimentInstanceList({ + type, experimentInsList, experimentInsTotal, onClickInstance, onRemove, onTerminate, onLoadMore, -}: ExperimentInstanceProps) { +}: ExperimentInstanceListProps) { const { message } = App.useApp(); const allIntanceIds = useMemo(() => { return experimentInsList?.map((item) => item.id) || []; @@ -48,18 +45,20 @@ function ExperimentInstanceComponent({ isSingleChecked, checkSingle, ] = useCheck(allIntanceIds); + const config = experimentListConfig[type]; useEffect(() => { // 关闭时清空 if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInsList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定删除该条实例吗?', + title: '删除后,该实验实例将不可恢复', + content: '是否确认删除?', onOk: () => { deleteExperimentInstance(instance.id); }, @@ -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([]); @@ -95,9 +96,22 @@ function ExperimentInstanceComponent({ } }; + // 终止实验实例 + const handleTerminate = (instance: ExperimentInstance) => { + modalConfirm({ + title: '终止后,该次实验运行将不可恢复', + content: '是否确认终止?', + isDelete: false, + onOk: () => { + terminateExperimentInstance(instance); + }, + }); + }; + // 终止实验实例 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); @@ -105,7 +119,7 @@ function ExperimentInstanceComponent({ }; if (!experimentInsList || experimentInsList.length === 0) { - return null; + return
暂无数据
; } return ( @@ -144,6 +158,9 @@ function ExperimentInstanceComponent({ checkSingle(item.id)} + disabled={ + item.status === ExperimentStatus.Running || item.status === ExperimentStatus.Pending + } >
{index + 1} -
- {elapsedTime(item.create_time, item.finish_time)} -
-
- - {formatDate(item.create_time)} - -
-
- - - {experimentStatusInfo[item.status as ExperimentStatus]?.label} - -
+
@@ -226,4 +229,4 @@ function ExperimentInstanceComponent({ ); } -export default ExperimentInstanceComponent; +export default ExperimentInstanceList; diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx new file mode 100644 index 00000000..402d27dd --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx @@ -0,0 +1,68 @@ +import RunDuration from '@/components/RunDuration'; +import { ExperimentStatus } from '@/enums'; +import { useSSE, type MessageHandler } from '@/hooks/useSSE'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { ExperimentCompleted } from '@/utils/constant'; +import { formatDate } from '@/utils/date'; +import { Typography } from 'antd'; +import React, { useCallback } from 'react'; +import styles from './index.less'; + +type ExperimentInstanceProps = { + create_time?: string; + finish_time?: string; + status: ExperimentStatus; + argo_ins_name: string; + argo_ins_ns: string; + experimentInsId: number; +}; + +function ExperimentInstance({ + create_time, + finish_time, + status, + argo_ins_name, + argo_ins_ns, + experimentInsId, +}: ExperimentInstanceProps) { + const handleSSEMessage: MessageHandler = useCallback( + (experimentInsId: number, status: string, finish_time: string) => { + window.postMessage({ + type: ExperimentCompleted, + payload: { + id: experimentInsId, + status, + finish_time, + }, + }); + }, + [], + ); + useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); + + return ( + +
+ +
+
+ + {formatDate(create_time)} + +
+
+ + + {experimentStatusInfo[status]?.label} + +
+
+ ); +} + +export default ExperimentInstance; diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/config.ts b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts new file mode 100644 index 00000000..f793b958 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentList/config.ts @@ -0,0 +1,99 @@ +/* + * @Author: 赵伟 + * @Date: 2025-01-08 14:30:58 + * @Description: 实验列表组件配置 + */ + +import { + batchDeleteActiveLearnInsReq, + deleteActiveLearnInsReq, + deleteActiveLearnReq, + getActiveLearnInsListReq, + getActiveLearnListReq, + runActiveLearnReq, + stopActiveLearnInsReq, +} from '@/services/activeLearn'; +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', + ActiveLearn = 'ActiveLearn', +} + +type ExperimentListInfo = { + getListReq: (params: any) => Promise; // 获取列表 + getInsListReq: (params: any) => Promise; // 获取实例列表 + deleteRecordReq: (params: any) => Promise; // 删除 + runRecordReq: (params: any) => Promise; // 运行 + deleteInsReq: (params: any) => Promise; // 删除实例 + batchDeleteInsReq: (params: any) => Promise; // 批量删除实例 + stopInsReq: (params: any) => Promise; // 终止实例 + title: string; // 标题 + pathPrefix: string; // 路由路径前缀 + idProperty: string; // ID属性 + nameProperty: string; // 名称属性 + descProperty: string; // 描述属性 +}; + +export const experimentListConfig: Record = { + [ExperimentListType.AutoML]: { + getListReq: getAutoMLListReq, + getInsListReq: getExperimentInsListReq, + deleteRecordReq: deleteAutoMLReq, + runRecordReq: runAutoMLReq, + deleteInsReq: deleteExperimentInsReq, + batchDeleteInsReq: batchDeleteExperimentInsReq, + stopInsReq: stopExperimentInsReq, + title: '自主机器学习', + pathPrefix: 'automl', + nameProperty: 'name', + descProperty: 'description', + idProperty: 'machineLearnId', + }, + [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', + }, + [ExperimentListType.ActiveLearn]: { + getListReq: getActiveLearnListReq, + getInsListReq: getActiveLearnInsListReq, + deleteRecordReq: deleteActiveLearnReq, + runRecordReq: runActiveLearnReq, + deleteInsReq: deleteActiveLearnInsReq, + batchDeleteInsReq: batchDeleteActiveLearnInsReq, + stopInsReq: stopActiveLearnInsReq, + title: '自动学习', + pathPrefix: 'active-learn', + nameProperty: 'name', + descProperty: 'description', + idProperty: 'activeLearnId', + }, +}; diff --git a/react-ui/src/pages/AutoML/List/index.less b/react-ui/src/pages/AutoML/components/ExperimentList/index.less similarity index 94% rename from react-ui/src/pages/AutoML/List/index.less rename to react-ui/src/pages/AutoML/components/ExperimentList/index.less index 735e3442..fb00521e 100644 --- a/react-ui/src/pages/AutoML/List/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.less @@ -1,4 +1,4 @@ -.auto-ml-list { +.experiment-list { height: 100%; &__content { height: calc(100% - 60px); diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx new file mode 100644 index 00000000..15846f9c --- /dev/null +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -0,0 +1,490 @@ +/* + * @Author: 赵伟 + * @Date: 2025-01-08 13:58:08 + * @Description: 自主机器学习和超参数寻优列表组件 + */ + +import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; +import { ExperimentStatus, autoMLTypeOptions } from '@/enums'; +import { useCacheState } from '@/hooks/useCacheState'; +import { useServerTime } from '@/hooks/useServerTime'; +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 { ExperimentCompleted } from '@/utils/constant'; +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 { useCallback, useEffect, useRef, useState } from 'react'; +import ExperimentInstanceList from '../ExperimentInstanceList'; +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([]); + const [total, setTotal] = useState(0); + const [experimentInsList, setExperimentInsList] = useState([]); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); + const [experimentInsTotal, setExperimentInsTotal] = useState(0); + const [now] = useServerTime(); + const [pagination, setPagination] = useState( + cacheState?.pagination ?? { + current: 1, + pageSize: 10, + }, + ); + const config = experimentListConfig[type]; + const timerRef = useRef | undefined>(); + + // 获取自主机器学习或超参数自动优化列表 + const getAutoMLList = useCallback(async () => { + const params: Record = { + page: pagination.current! - 1, + size: pagination.pageSize, + [config.nameProperty]: 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); + } + }, [pagination, searchText, config]); + + useEffect(() => { + getAutoMLList(); + }, [getAutoMLList]); + + // 获取实验实例列表 + const getExperimentInsList = useCallback( + async (recordId: number, page: number, size: number) => { + const params = { + [config.idProperty]: recordId, + page: page, + size: size, + }; + 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); + } + } + }, + [config.getInsListReq, config.idProperty], + ); + + // 刷新实验列表状态, + // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 + const refreshExperimentList = useCallback(() => { + getAutoMLList(); + }, [getAutoMLList]); + + // 刷新实验实例列表 + const refreshExperimentIns = useCallback( + (experimentId: number) => { + const length = experimentInsList.length; + getExperimentInsList(experimentId, 0, length); + }, + [getExperimentInsList, experimentInsList], + ); + + // 新增,删除版本时,重置分页,然后刷新版本列表 + useEffect(() => { + const handleMessage = (e: MessageEvent) => { + const { type, payload } = e.data; + if (type === ExperimentCompleted) { + const { id, status, finish_time } = payload; + + // 修改实例的状态和结束时间 + setExperimentInsList((prev) => + prev.map((v) => + v.id === id + ? { + ...v, + status: status, + finish_time: finish_time, + } + : v, + ), + ); + + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = undefined; + } + + timerRef.current = setTimeout(() => { + refreshExperimentList(); + }, 10000); + } + }; + + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = undefined; + } + }; + }, [refreshExperimentList]); + + // 搜索 + 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('删除成功'); + // 如果是一页的唯一数据,删除后,请求第一页的数据 + // 否则直接刷新这一页的数据 + setPagination((prev) => { + return { + ...prev, + current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, + }; + }); + } + }; + + // 处理删除 + 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(); + getExperimentInsList(record.id, 0, 5); + } + }; + + // --------------------------- 实验实例 --------------------------- + // 展开实例 + const handleExpandChange = (expanded: boolean, record: AutoMLData) => { + setExperimentInsList([]); + if (expanded) { + setExpandedRowKeys([record.id]); + getExperimentInsList(record.id, 0, 5); + refreshExperimentList(); + } else { + setExpandedRowKeys([]); + } + }; + + // 跳转到实验实例详情 + const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { + navigate(`instance/${autoML.id}/${record.id}`); + }; + + // 加载更多实验实例 + const loadMoreExperimentIns = () => { + const page = Math.round(experimentInsList.length / 5); + const recordId = expandedRowKeys[0]; + getExperimentInsList(recordId, page, 5); + }; + + // 实验实例终止 + const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { + // 刷新实验列表 + refreshExperimentList(); + setExperimentInsList((prevList) => { + return prevList.map((item) => { + if (item.id === experimentIns.id) { + return { + ...item, + status: ExperimentStatus.Terminated, + finish_time: now().toISOString(), + }; + } + return item; + }); + }); + }; + // --------------------------- Table --------------------------- + // 分页切换 + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); + } + }; + + const typeColumns = [ + { + title: '类型', + dataIndex: 'type', + key: 'type', + width: '15%', + render: tableCellRender(false, TableCellValueType.Enum, { + options: autoMLTypeOptions, + }), + }, + ]; + + const diffColumns = type === ExperimentListType.AutoML ? typeColumns : []; + + const columns: TableProps['columns'] = [ + { + title: '实验名称', + dataIndex: config.nameProperty, + key: 'name', + width: '30%', + render: tableCellRender(false, TableCellValueType.Link, { + onClick: gotoDetail, + }), + }, + { + title: '实验描述', + dataIndex: config.descProperty, + key: 'description', + render: tableCellRender(true), + width: type === ExperimentListType.AutoML ? '35%' : '50%', + }, + ...diffColumns, + { + title: '创建时间', + dataIndex: 'update_time', + key: 'update_time', + width: '20%', + render: tableCellRender(false, TableCellValueType.Date), + }, + { + 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 ( + + + + ); + }) + : null} + + ); + }, + }, + { + title: '操作', + dataIndex: 'operation', + width: 360, + key: 'operation', + render: (_: any, record: AutoMLData) => ( +
+ + + + + + + +
+ ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + allowClear + /> + +
+
+ `共${total}条`, + }} + onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(record, item)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: handleExpandChange, + expandedRowKeys: expandedRowKeys, + }} + rowKey="id" + /> + + + + ); +} + +export default ExperimentList; diff --git a/react-ui/src/pages/AutoML/components/ExperimentLog/index.less b/react-ui/src/pages/AutoML/components/ExperimentLog/index.less index e69de29b..beaacea3 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentLog/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentLog/index.less @@ -0,0 +1,7 @@ +.experiment-log { + height: 100%; + + &__log { + height: 100%; + } +} diff --git a/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx index e69de29b..219e5782 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx @@ -0,0 +1,37 @@ +import { ExperimentStatus } from '@/enums'; +import { AutoMLInstanceData } from '@/pages/AutoML/types'; +import LogList from '@/pages/Experiment/components/LogList'; +import { NodeStatus } from '@/types'; +import styles from './index.less'; + +const NodePrefix = 'auto-ml'; + +type ExperimentLogProps = { + instanceInfo: AutoMLInstanceData; + nodes: Record; +}; + +function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { + const nodeStatus: NodeStatus | undefined = Object.values(nodes).find((node: any) => + node.displayName.startsWith(NodePrefix), + ) as NodeStatus; + + return ( +
+
+ {nodeStatus && ( + + )} +
+
+ ); +} + +export default ExperimentLog; diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index 342817c3..87d1e38d 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -9,7 +9,7 @@ &__download { padding-top: 16px; padding-bottom: 16px; - + margin-top: 16px; padding-left: @content-padding; color: @text-color; font-size: 13px; @@ -24,8 +24,19 @@ } } + &__file { + height: 100%; + :global { + .kf-info-group__content { + height: calc(100% - 56px); + overflow-y: auto; + } + } + } + &__text { - white-space: pre-wrap; + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; + white-space: pre; } &__images { diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx index a826155d..bc21edf4 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx @@ -1,4 +1,7 @@ import InfoGroup from '@/components/InfoGroup'; +import { AutoMLType } from '@/enums'; +import { type HyperParameterFile } from '@/pages/AutoML/types'; +import TrialFileTree from '@/pages/HyperParameter/components/TrialFileTree'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; import { Button, Image } from 'antd'; @@ -9,9 +12,11 @@ type ExperimentResultProps = { fileUrl?: string; imageUrl?: string; modelPath?: string; + type?: string; + file?: HyperParameterFile; }; -function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { +function ExperimentResult({ fileUrl, imageUrl, modelPath, type, file }: ExperimentResultProps) { const [result, setResult] = useState(''); const images = useMemo(() => { @@ -22,49 +27,61 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp }, [imageUrl]); useEffect(() => { - if (fileUrl) { - getResultFile(); - } - }, [fileUrl]); + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; - // 获取实验运行历史记录 - const getResultFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - setResult(res as any as string); + if (fileUrl && type !== AutoMLType.Video) { + getResultFile(); } - }; + }, [fileUrl, type]); return (
- -
{result}
-
- -
- - console.log(`current index: ${current}, prev index: ${prev}`), - }} - > - {images.map((item) => ( - - ))} - -
-
+ {type === AutoMLType.Video ? ( + + ) : ( + +
{result}
+
+ )} + + {type === AutoMLType.Table && ( + +
+ + console.log(`current index: ${current}, prev index: ${prev}`), + }} + > + {images.map((item) => ( + + ))} + +
+
+ )} + {modelPath && (
文件名 - save_model.joblib + {modelPath.split('/').pop()}
diff --git a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx index de903f47..fd9c967d 100644 --- a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx +++ b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx @@ -70,7 +70,12 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps > {item.git_url} -
{item.git_branch}
+ + {item.git_branch} +
diff --git a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx index db05393e..624ef833 100644 --- a/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx @@ -4,7 +4,12 @@ import KFModal from '@/components/KFModal'; import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; import { addDataset } from '@/services/dataset/index.js'; import { to } from '@/utils/promise'; -import { getFileListFromEvent, limitUploadFileType, validateUploadFiles } from '@/utils/ui'; +import { + getFileListFromEvent, + limitUploadFileType, + removeUploadedFile, + validateUploadFiles, +} from '@/utils/ui'; import { Button, Form, @@ -29,11 +34,6 @@ interface AddDatasetModalProps extends Omit { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { const [uuid] = useState(Date.now()); - // const [clusterOptions, setClusterOptions] = useState([]); - - // useEffect(() => { - // getClusterOptions(); - // }, []); // 上传组件参数 const uploadProps: UploadProps = { @@ -44,16 +44,9 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr defaultFileList: [], accept: '.zip,.tgz', beforeUpload: limitUploadFileType('zip,tgz'), + onRemove: removeUploadedFile, }; - // 获取集群版本数据 - // const getClusterOptions = async () => { - // const [res] = await to(getDictSelectOption('available_cluster')); - // if (res) { - // setClusterOptions(res); - // } - // }; - // 上传请求 const createDataset = async (params: any) => { const [res] = await to(addDataset(params)); @@ -113,7 +106,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr }, ]} > - + - {/* - + + + + - + {item.name}
diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx index c0bfdb33..2eeb09b8 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -13,12 +13,13 @@ import { } from '@/pages/Dataset/config'; import GraphLegend from '@/pages/Model/components/GraphLegend'; import ModelEvolution from '@/pages/Model/components/ModelEvolution'; +import { VersionChangedMessage } from '@/utils/constant'; import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { useParams, useSearchParams } from '@umijs/max'; import { App, Button, Flex, Select, Tabs } from 'antd'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import AddVersionModal from '../AddVersionModal'; import ResourceIntro from '../ResourceIntro'; import ResourceVersion from '../ResourceVersion'; @@ -45,7 +46,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { // 模型演化传入的 tab const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; // 模型演化传入的版本 - let versionParam = searchParams.get('version'); + const versionParam = searchParams.get('version'); const name = searchParams.get('name') || ''; const owner = searchParams.get('owner') || ''; const identifier = searchParams.get('identifier') || ''; @@ -57,63 +58,60 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const typeName = config.name; // 数据集/模型 const { message } = App.useApp(); - useEffect(() => { - getVersionList(); - }, [resourceId, owner, identifier]); - - useEffect(() => { - if (version) { - getResourceDetail({ - id: resourceId, - owner, - name, - identifier, - version, - is_public: is_public, - }); - } - }, [version]); - // 获取详情 - const getResourceDetail = async (params: { - owner: string; - name: string; - id: number; - identifier: string; - version?: string; - is_public: boolean; - }) => { + const getResourceDetail = useCallback(async () => { + const params = { + id: resourceId, + owner, + name, + identifier, + version, + is_public, + }; const request = config.getInfo; const [res] = await to(request(params)); if (res && res.data) { setInfo(res.data); } - }; + }, [config, resourceId, owner, name, identifier, version, is_public]); // 获取版本列表 - const getVersionList = async () => { - const request = config.getVersions; - const [res] = await to( - request({ - owner, - identifier, - }), - ); - if (res && res.data && res.data.length > 0) { - setVersionList(res.data); - if ( - versionParam && - res.data.find((item: ResourceVersionData) => item.name === versionParam) - ) { - setVersion(versionParam); - versionParam = null; + const getVersionList = useCallback( + async (refresh: boolean) => { + const request = config.getVersions; + const [res] = await to( + request({ + owner, + identifier, + }), + ); + if (res && res.data && res.data.length > 0) { + setVersionList(res.data); + if ( + !refresh && + versionParam && + res.data.find((item: ResourceVersionData) => item.name === versionParam) + ) { + setVersion(versionParam); + } else { + setVersion(res.data[0].name); + } } else { - setVersion(res.data[0].name); + setVersion(undefined); } - } else { - setVersion(undefined); + }, + [config, owner, identifier, versionParam], + ); + + useEffect(() => { + if (version) { + getResourceDetail(); } - }; + }, [version, getResourceDetail]); + + useEffect(() => { + getVersionList(false); + }, [getVersionList]); // 新建版本 const showModal = () => { @@ -125,8 +123,9 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { identifier: info.identifier, is_public: is_public, onOk: () => { - getVersionList(); + getVersionList(true); close(); + window.postMessage({ type: VersionChangedMessage }); }, }); }; @@ -172,12 +171,13 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const [res] = await to(request(params)); if (res) { message.success('删除成功'); - getVersionList(); + getVersionList(true); + window.postMessage({ type: VersionChangedMessage }); } }; // 处理删除 - const hanldeDelete = () => { + const handleDelete = () => { modalConfirm({ title: '删除后,该版本将不可恢复', content: '是否确认删除?', @@ -268,7 +268,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
-
+
); } diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less index 70de3494..4a1c0d94 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less @@ -3,6 +3,7 @@ .title(@color, @background) { width: 100%; margin-bottom: 20px; + padding: 0 15px; color: @color; font-weight: 500; font-size: @font-size; @@ -10,7 +11,6 @@ text-align: center; background: @background; border-radius: 4px 4px 0 0; - .singleLine(); } .text() { @@ -19,7 +19,6 @@ font-size: 13px; line-height: 22px; word-break: break-all; - .singleLine(); } .version-container(@background) { diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx index 2ee76e78..a5b37131 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx @@ -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 = { @@ -36,7 +36,7 @@ interface VersionCompareModalProps extends Omit { repo_id: number; } -const formatDataset = (datasets?: DatasetData[]) => { +const formatDatasets = (datasets?: DatasetData[]) => { if (!datasets || datasets.length === 0) { return undefined; } @@ -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; } @@ -88,7 +88,7 @@ function VersionCompareModal({ format: formatProject, }, { - key: 'description', + key: 'version_desc', text: '版本描述', }, ] @@ -105,12 +105,12 @@ function VersionCompareModal({ { key: 'train_datasets', text: '训练数据集', - format: formatDataset, + format: formatDatasets, }, { key: 'test_datasets', text: '测试数据集', - format: formatDataset, + format: formatDatasets, }, { key: 'model_source', @@ -123,32 +123,32 @@ function VersionCompareModal({ format: formatTrainTask, }, { - key: 'description', + key: 'version_desc', text: '版本描述', }, ], - [], + [resourceType], ); useEffect(() => { - getServiceVersionCompare(); - }, []); - - // 获取对比数据 - const getServiceVersionCompare = async () => { - const params = { - versions, - identifier, - is_public, - owner, - repo_id, + // 获取对比数据 + const getServiceVersionCompare = async () => { + const params = { + versions, + identifier, + is_public, + owner, + repo_id, + }; + const request = config.compareVersion; + const [res] = await to(request(params)); + if (res && res.data) { + setCompareData(res.data); + } }; - const request = config.compareVersion; - const [res] = await to(request(params)); - if (res && res.data) { - setCompareData(res.data); - } - }; + + getServiceVersionCompare(); + }, [versions, identifier, is_public, owner, repo_id, config]); // 获取值 function getValue( @@ -193,7 +193,14 @@ function VersionCompareModal({ ))}
-
{v1.version}
+
+ + {v1.version} + +
{fields.map(({ key, format }) => { const text = getValue(v1, key as keyof typeof v1, format); return ( @@ -203,7 +210,7 @@ function VersionCompareModal({ [styles['version-compare__left__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text}
@@ -211,7 +218,14 @@ function VersionCompareModal({ })}
-
{v2.version}
+
+ + {v2.version} + +
{fields.map(({ key, format }) => { const text = getValue(v2, key as keyof typeof v2, format); return ( @@ -221,7 +235,7 @@ function VersionCompareModal({ [styles['version-compare__right__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text}
diff --git a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx index c58bf87e..b63d02fc 100644 --- a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx @@ -49,9 +49,7 @@ function VersionSelectorModal({ versions, onOk, ...rest }: VersionSelectorModalP onClick={() => handleClick(item.name)} > - - {item.name} - + {item.name} ); })} diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx index d24586a6..61c63531 100644 --- a/react-ui/src/pages/Dataset/config.tsx +++ b/react-ui/src/pages/Dataset/config.tsx @@ -25,9 +25,10 @@ export enum ResourceType { } export enum DataSource { - AtuoExport = 'auto_export', // 自动导出 + AutoExport = 'auto_export', // 自动导出 HandExport = 'hand_export', // 手动导出 Create = 'add', // 新增 + LabelStudioExport = 'label_studio_export', // LabelStudio 导出 } type ResourceTypeInfo = { diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 90ad4d15..c85247a4 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -6,18 +6,17 @@ import KFIcon from '@/components/KFIcon'; import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import PageTitle from '@/components/PageTitle'; +import ParameterSelect from '@/components/ParameterSelect'; import ResourceSelect, { requiredValidator, ResourceSelectorType, type ParameterInputObject, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; -import { useComputingResource } from '@/hooks/resource'; import { createEditorReq } from '@/services/developmentEnvironment'; import { to } from '@/utils/promise'; import { useNavigate } from '@umijs/max'; -import { App, Button, Col, Form, Input, Row, Select } from 'antd'; -import { omit, pick } from 'lodash'; +import { App, Button, Col, Form, Input, Row } from 'antd'; import styles from './index.less'; type FormData = { @@ -36,13 +35,13 @@ enum ComputingResourceType { const EditorRadioItems: KFRadioItem[] = [ { - key: ComputingResourceType.GPU, title: '英伟达GPU', + value: ComputingResourceType.GPU, icon: , }, { - key: ComputingResourceType.NPU, title: '昇腾NPU', + value: ComputingResourceType.NPU, icon: , }, ]; @@ -51,21 +50,10 @@ function EditorCreate() { const navigate = useNavigate(); const [form] = Form.useForm(); const { message } = App.useApp(); - const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 创建编辑器 const createEditor = async (formData: FormData) => { - // 根据后台要求,修改表单数据 - const image = formData['image']; - const model = formData['model']; - const dataset = formData['dataset']; - const params = { - ...omit(formData, ['image', 'model', 'dataset']), - image: image.value, - model: pick(model, ['id', 'version', 'path', 'showValue']), - dataset: pick(dataset, ['id', 'version', 'path', 'showValue']), - }; - const [res] = await to(createEditorReq(params)); + const [res] = await to(createEditorReq(formData)); if (res) { message.success('创建成功'); navigate(-1); @@ -105,16 +93,16 @@ function EditorCreate() {
- + @@ -138,7 +126,7 @@ function EditorCreate() { - - + diff --git a/react-ui/src/pages/Experiment/Aim/index.tsx b/react-ui/src/pages/Experiment/Aim/index.tsx new file mode 100644 index 00000000..3a8d1d0d --- /dev/null +++ b/react-ui/src/pages/Experiment/Aim/index.tsx @@ -0,0 +1,12 @@ +/* + * @Author: 赵伟 + * @Date: 2025-03-31 16:38:59 + * @Description: 实验对比 Aim + */ + +import IframePage, { IframePageType } from '@/components/IFramePage'; + +function AimPage() { + return ; +} +export default AimPage; diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index 3be69ed9..e34f03ad 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -18,26 +18,6 @@ background-color: white; border-radius: 10px; - &__footer { - display: flex; - align-items: center; - padding-top: 20px; - color: @text-color-secondary; - font-size: 12px; - background-color: white; - - div { - flex: 1; - height: 1px; - background-color: @border-color-base; - } - - p { - flex: none; - margin: 0 8px; - } - } - :global { .ant-table-container { border: none !important; @@ -45,7 +25,7 @@ .ant-table-thead { .ant-table-cell { background-color: rgb(247, 247, 247); - border-color: @border-color-base !important; + border-color: @border-color !important; } } .ant-table-tbody { @@ -54,13 +34,13 @@ border-left: none !important; } } - .ant-table-tbody-virtual::after { - border-bottom: none !important; - } .ant-table-footer { padding: 0; border: none !important; } + .ant-table-column-title { + min-width: 0; + } } } } diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index b900842c..200b4ba1 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -4,6 +4,7 @@ * @Description: 实验对比 */ +import TableColTitle from '@/components/TableColTitle'; import { getExpEvaluateInfosReq, getExpMetricsReq, @@ -11,9 +12,10 @@ import { } from '@/services/experiment'; import { tableSorter } from '@/utils'; import { to } from '@/utils/promise'; +import SessionStorage from '@/utils/sessionStorage'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { useSearchParams } from '@umijs/max'; -import { App, Button, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd'; +import { useNavigate, useSearchParams } from '@umijs/max'; +import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import ExperimentStatusCell from '../components/ExperimentStatusCell'; @@ -45,39 +47,42 @@ function ExperimentComparison() { }); const { message } = App.useApp(); - const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); + const navigate = useNavigate(); + const config = comparisonConfig[comparisonType]; useEffect(() => { - getComparisonData(); - }, [experimentId]); - - // 获取对比数据列表 - const getComparisonData = async () => { - const request = - comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, + // 获取对比数据列表 + const getComparisonData = async () => { + const request = + comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + }; + const [res] = await to(request(experimentId, params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } }; - const [res] = await to(request(experimentId, params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; + + getComparisonData(); + }, [experimentId, pagination, comparisonType]); // 获取对比 url const getExpMetrics = async () => { const [res] = await to(getExpMetricsReq(selectedRowKeys)); if (res && res.data) { const url = res.data; - window.open(url, '_blank'); + // window.open(url, '_blank'); + SessionStorage.setItem(SessionStorage.aimUrlKey, url); + navigate('../compare-visual'); } }; // 对比按钮 click - const hanldeComparisonClick = () => { + const handleComparisonClick = () => { if (selectedRowKeys.length < 2) { message.error('请至少选择两项进行对比'); return; @@ -154,7 +159,6 @@ function ExperimentComparison() { fixed: 'left', align: 'center', render: tableCellRender(true, TableCellValueType.Array), - ellipsis: { showTitle: false }, }, ], }, @@ -162,17 +166,12 @@ function ExperimentComparison() { title: `${config.title}参数`, align: 'center', children: paramsNames.map((name) => ({ - title: ( - - {name} - - ), + title: , dataIndex: ['params', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), showSorterTooltip: false, })), @@ -181,28 +180,23 @@ function ExperimentComparison() { title: `${config.title}指标`, align: 'center', children: metricsNames.map((name) => ({ - title: ( - - {name} - - ), + title: , dataIndex: ['metrics', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), showSorterTooltip: false, })), }, ]; - }, [tableData]); + }, [tableData, config]); return (
-
diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index c96d781e..29c21151 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -1,10 +1,12 @@ +import RunDuration from '@/components/RunDuration'; import { ExperimentStatus } from '@/enums'; -import { useStateRef, useVisible } from '@/hooks'; +import { useStateRef } from '@/hooks/useStateRef'; +import { useVisible } from '@/hooks/useVisible'; import { getExperimentIns } from '@/services/experiment/index.js'; import { getWorkflowById } from '@/services/pipeline/index.js'; import themes from '@/styles/theme.less'; import { fittingString, parseJsonText } from '@/utils'; -import { elapsedTime, formatDate } from '@/utils/date'; +import { formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import G6, { Util } from '@antv/g6'; import { Button } from 'antd'; @@ -21,7 +23,6 @@ function ExperimentText() { const [experimentIns, setExperimentIns] = useState(undefined); const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); const graphRef = useRef(); - const timerRef = useRef(); const workflowRef = useRef(); const locationParams = useParams(); // 新版本获取路由参数接口 const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); @@ -36,6 +37,16 @@ function ExperimentText() { initGraph(); getWorkflow(); + return () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { const changeSize = () => { if (!graph || graph.get('destroyed')) return; if (!graphRef.current) return; @@ -46,20 +57,9 @@ function ExperimentText() { window.addEventListener('resize', changeSize); return () => { window.removeEventListener('resize', changeSize); - if (timerRef.current) { - clearTimeout(timerRef.current); - } - if (evtSourceRef.current) { - evtSourceRef.current.close(); - evtSourceRef.current = null; - } }; }, []); - useEffect(() => { - propsDrawerOpenRef.current = propsDrawerOpen; - }, [propsDrawerOpen]); - // 获取流水线模版 const getWorkflow = async () => { const [res] = await to(getWorkflowById(locationParams.workflowId)); @@ -86,7 +86,7 @@ function ExperimentText() { const [res] = await to(getExperimentIns(locationParams.id)); if (res && res.data && workflowRef.current) { setExperimentIns(res.data); - const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; + const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data; const workflowData = workflowRef.current; const experimentStatusObjs = parseJsonText(nodes_status); workflowData.nodes.forEach((item) => { @@ -181,11 +181,12 @@ function ExperimentText() { if (!statusNode) { return; } - const { finishedAt, startedAt, phase, id } = statusNode; + const { finishedAt, startedAt, phase, id, message } = statusNode; workflowNode.experimentStartTime = startedAt; workflowNode.experimentEndTime = finishedAt; workflowNode.experimentStatus = phase; workflowNode.workflowId = id; + workflowNode.message = message; workflowNode.img = phase ? `${workflowNode.imgName}-${phase}.png` : `${workflowNode.imgName}.png`; @@ -474,7 +475,10 @@ function ExperimentText() {
执行时长: - {elapsedTime(experimentIns?.create_time, experimentIns?.finish_time)} +
状态: diff --git a/react-ui/src/pages/Experiment/Info/index.less b/react-ui/src/pages/Experiment/Info/index.less index f6df0cb6..70b27284 100644 --- a/react-ui/src/pages/Experiment/Info/index.less +++ b/react-ui/src/pages/Experiment/Info/index.less @@ -30,10 +30,4 @@ background-image: url(@/assets/img/pipeline-canvas-bg.png); background-size: 100% 100%; } - - :global { - .ant-drawer-mask { - background: transparent !important; - } - } } diff --git a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx index 38a88b3a..710a4df4 100644 --- a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx @@ -3,7 +3,7 @@ import editExperimentIcon from '@/assets/img/edit-experiment.png'; import KFModal from '@/components/KFModal'; import { type PipelineGlobalParam } from '@/types'; import { to } from '@/utils/promise'; -import { Button, Form, Input, Radio, Select, type FormRule } from 'antd'; +import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; import { useState } from 'react'; import styles from './index.less'; @@ -63,13 +63,14 @@ export const getParamRules = (paramType: number, required: boolean = false): For }; // 根据参数设置 label -export const getParamType = (param: PipelineGlobalParam): string => { +export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { const paramTypes: Readonly> = { 1: '字符串', 2: '整型', 3: '布尔类型', }; - return param.param_name + `(${paramTypes[param.param_type]})`; + const label = param.param_name + `(${paramTypes[param.param_type]})`; + return {label}; }; function AddExperimentModal({ @@ -99,8 +100,8 @@ function AddExperimentModal({ }; const paramLayout = { - labelCol: { span: 8 }, - wrapperCol: { span: 16 }, + labelCol: { span: 6 }, + wrapperCol: { span: 18 }, }; // 除了流水线选择发生变化 @@ -157,7 +158,6 @@ function AddExperimentModal({ form={form} {...layout} labelAlign="left" - labelWrap > {getParamComponent( globalParam[name]['param_type'], diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less index c1f1b7ef..41cb8a19 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less @@ -1,4 +1,5 @@ .experiment-drawer { + line-height: var(--ant-line-height); :global { .ant-drawer-body { overflow-y: hidden; @@ -12,7 +13,6 @@ } &__tabs { - height: calc(100% - 170px); :global { .ant-tabs-nav { padding-left: 24px; @@ -34,7 +34,7 @@ display: flex; align-items: center; margin-bottom: 15px; - padding-left: 24px; + padding: 0 24px; color: @text-color; font-size: 15px; } diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index cdcaea19..11d0ff2e 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -1,9 +1,10 @@ +import RunDuration from '@/components/RunDuration'; import { ExperimentStatus } from '@/enums'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { PipelineNodeModelSerialize } from '@/types'; -import { elapsedTime, formatDate } from '@/utils/date'; +import { formatDate } from '@/utils/date'; import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; -import { Drawer, Tabs } from 'antd'; +import { Drawer, Tabs, Typography } from 'antd'; import { useMemo } from 'react'; import ExperimentParameter from '../ExperimentParameter'; import ExperimentResult from '../ExperimentResult'; @@ -90,15 +91,17 @@ const ExperimentDrawer = ({ instanceNodeStatus, workflowId, instanceNodeStartTime, + experimentName, + experimentId, + pipelineId, ], ); return ( } onClose={onClose} open={open} @@ -127,15 +130,30 @@ const ExperimentDrawer = ({ '--' )}
+ {instanceNodeData.message && ( +
+
消息:
+ + {instanceNodeData.message ?? '--'} + +
+ )}
启动时间:{formatDate(instanceNodeStartTime)}
耗时: - {elapsedTime(instanceNodeStartTime, instanceNodeEndTime)} +
- + ); }; diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less b/react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less similarity index 100% rename from react-ui/src/pages/Experiment/components/ExperimentInstance/index.less rename to react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx similarity index 77% rename from react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx rename to react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx index 4d68d93c..029a78c7 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx @@ -1,7 +1,6 @@ import KFIcon from '@/components/KFIcon'; import { ExperimentStatus } from '@/enums'; -import { useCheck } from '@/hooks'; -import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { useCheck } from '@/hooks/useCheck'; import { deleteManyExperimentIns, deleteQueryByExperimentInsId, @@ -9,18 +8,18 @@ import { } from '@/services/experiment/index.js'; import themes from '@/styles/theme.less'; import { type ExperimentInstance } from '@/types'; -import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { DoubleRightOutlined } from '@ant-design/icons'; -import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import { App, Button, Checkbox, ConfigProvider } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo } from 'react'; import TensorBoardStatusCell from '../TensorBoardStatus'; import styles from './index.less'; +import ExperimentInstanceComponent from './instance'; -type ExperimentInstanceProps = { - experimentInList?: ExperimentInstance[]; +type ExperimentInstanceListProps = { + experimentInsList?: ExperimentInstance[]; experimentInsTotal: number; onClickInstance?: (instance: ExperimentInstance) => void; onClickTensorBoard?: (instance: ExperimentInstance) => void; @@ -29,19 +28,19 @@ type ExperimentInstanceProps = { onLoadMore?: () => void; }; -function ExperimentInstanceComponent({ - experimentInList, +function ExperimentInstanceList({ + experimentInsList, experimentInsTotal, onClickInstance, onClickTensorBoard, onRemove, onTerminate, onLoadMore, -}: ExperimentInstanceProps) { +}: ExperimentInstanceListProps) { const { message } = App.useApp(); const allIntanceIds = useMemo(() => { - return experimentInList?.map((item) => item.id) || []; - }, [experimentInList]); + return experimentInsList?.map((item) => item.id) || []; + }, [experimentInsList]); const [ selectedIns, setSelectedIns, @@ -57,12 +56,13 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定删除该条实例吗?', + title: '删除后,该实验实例将不可恢复', + content: '是否确认删除?', onOk: () => { deleteExperimentInstance(instance.id); }, @@ -101,7 +101,8 @@ function ExperimentInstanceComponent({ // 终止实验实例 const handleTerminate = (instance: ExperimentInstance) => { modalConfirm({ - title: '确定要终止此次实验运行吗?', + title: '终止后,该次实验运行将不可恢复', + content: '是否确认终止?', isDelete: false, onOk: () => { terminateExperimentInstance(instance); @@ -118,8 +119,8 @@ function ExperimentInstanceComponent({ } }; - if (!experimentInList || experimentInList.length === 0) { - return null; + if (!experimentInsList || experimentInsList.length === 0) { + return
暂无数据
; } return ( @@ -152,7 +153,7 @@ function ExperimentInstanceComponent({ - {experimentInList.map((item, index) => ( + {experimentInsList.map((item, index) => (
-
-
{elapsedTime(item.create_time, item.finish_time)}
-
- - {formatDate(item.create_time)} - -
-
-
- - - {experimentStatusInfo[item.status as ExperimentStatus]?.label} - -
+ + +
))} - {experimentInsTotal > experimentInList.length ? ( + {experimentInsTotal > experimentInsList.length ? (