diff --git a/.gitignore b/.gitignore index 21a12484..5510490a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ mvnw # web **/node_modules + +*storybook.log diff --git a/k8s/build.sh b/k8s/build.sh index 921a2ec5..5e77bdf9 100755 --- a/k8s/build.sh +++ b/k8s/build.sh @@ -13,7 +13,7 @@ show_help() { echo echo "Options:" echo " -b Branch to deploy, default is master" - echo " -s Service to deploy (manage-front, manage, front, all, default is manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system, default is manage-front)" echo " -h Show this help message" } @@ -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 index 44ea23fa..6e561cdd 100755 --- a/k8s/build_and_deploy.sh +++ b/k8s/build_and_deploy.sh @@ -19,7 +19,7 @@ show_help() { echo echo "Options:" echo " -b Branch to deploy, default: master" - echo " -s Service to deploy (manage-front, manage, front, all, default: manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" echo " -e Environment (e.g., dev, test, default: dev)" echo " -h Show this help message" } diff --git a/k8s/deploy.sh b/k8s/deploy.sh index d06c05d9..2dffc04f 100755 --- a/k8s/deploy.sh +++ b/k8s/deploy.sh @@ -10,7 +10,7 @@ show_help() { echo "Usage: $0 [-s service] [-e environment]" echo echo "Options:" - echo " -s Service to deploy (manage-front, manage, front, all default: manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" echo " -e Environment (e.g., dev, test, default: dev)" echo " -h Show this help message" } @@ -43,7 +43,7 @@ fi # 根据环境设置 IP 地址 if [ "$env" == "dev" ]; then - remote_ip="172.20.32.181" + remote_ip="172.20.32.197" elif [ "$env" == "test" ]; then remote_ip="172.20.32.185" else 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-13oauth2.yaml b/k8s/template-yaml/k8s-13oauth2.yaml new file mode 100644 index 00000000..88be9902 --- /dev/null +++ b/k8s/template-yaml/k8s-13oauth2.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ci4s-oauth2-authenticator-deployment + namespace: argo +spec: + replicas: 1 + selector: + matchLabels: + app: ci4s-oauth2-authenticator + template: + metadata: + labels: + app: ci4s-oauth2-authenticator + spec: + containers: + - name: ci4s-oauth2-authenticator + image: 172.20.32.187/ci4s/spring-oauth2-authenticator:latest + env: + - name: DB_URL + value: mysql.argo.svc:3306 + - name: DB_USERNAME + value: root + - name: DB_PASSWORD + value: qazxc123456. + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: ci4s-oauth2-authenticator-service + namespace: argo +spec: + type: NodePort + ports: + - name: http + port: 8080 + nodePort: 31080 + protocol: TCP + selector: + app: ci4s-oauth2-authenticator + diff --git a/k8s/template-yaml/k8s-3nacos.yaml b/k8s/template-yaml/k8s-3nacos.yaml index 225c6b23..6b242c2f 100644 --- a/k8s/template-yaml/k8s-3nacos.yaml +++ b/k8s/template-yaml/k8s-3nacos.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: nacos-ci4s - image: nacos/nacos-server:v2.2.0 + image: 172.20.32.187/ci4s/nacos-server:v2.2.0 env: - name: SPRING_DATASOURCE_PLATFORM value: mysql @@ -38,8 +38,8 @@ spec: - containerPort: 9848 - containerPort: 9849 initContainers: - - name: init-mydb - image: busybox:1.31 + - name: init-mydb-check + image: 172.20.32.187/ci4s/busybox:1.31 command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] restartPolicy: Always diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index edc1c621..57088d5d 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -16,6 +16,8 @@ spec: containers: - name: ci4s-management-platform image: ${k8s-7management-image} + securityContext: + privileged: true env: - name: TZ value: Asia/Shanghai @@ -27,10 +29,30 @@ spec: - name: resource-volume mountPath: /home/resource/ subPath: mini-model-platform-data + mountPropagation: Bidirectional volumes: - name: resource-volume hostPath: path: /platform-data + initContainers: + - name: init-fs-check + image: 172.20.32.187/ci4s/ci4s-managent:202502141722 + securityContext: + privileged: true + volumeMounts: + - name: resource-volume + mountPath: /home/resource/ + subPath: mini-model-platform-data + mountPropagation: Bidirectional + command: [ "/bin/sh", "-c" ] + args: + - | + mounted=$(findmnt /home/resource/ | grep 'fuse.juicefs') + if [ -z "$mounted" ]; then + echo "/platform-data not mounted"; + exit 1 + fi + restartPolicy: Always --- apiVersion: v1 kind: Service diff --git a/k8s/template-yaml/rolebindings.yaml b/k8s/template-yaml/rolebindings.yaml new file mode 100644 index 00000000..54c2e13d --- /dev/null +++ b/k8s/template-yaml/rolebindings.yaml @@ -0,0 +1,68 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: custom-workflow + namespace: argo +rules: + - apiGroups: + - argoproj.io + resources: + - workflows + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods + - services + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: custom-workflow-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: custom-workflow +subjects: + - kind: ServiceAccount + name: default diff --git a/react-ui/.eslintignore b/react-ui/.eslintignore index 8336e935..3bc705a6 100644 --- a/react-ui/.eslintignore +++ b/react-ui/.eslintignore @@ -5,4 +5,5 @@ public dist .umi -mock \ No newline at end of file +mock +/src/iconfont/ \ No newline at end of file diff --git a/react-ui/.eslintrc.js b/react-ui/.eslintrc.js index 564a28d2..85537dd8 100644 --- a/react-ui/.eslintrc.js +++ b/react-ui/.eslintrc.js @@ -1,10 +1,16 @@ module.exports = { - extends: [require.resolve('@umijs/lint/dist/config/eslint')], + extends: [ + require.resolve('@umijs/lint/dist/config/eslint'), + 'plugin:react/recommended', + "plugin:react-hooks/recommended" + ], globals: { page: true, REACT_APP_ENV: true, }, rules: { '@typescript-eslint/no-use-before-define': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off' }, }; diff --git a/react-ui/.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/.storybook/babel-plugin-auto-css-modules.js b/react-ui/.storybook/babel-plugin-auto-css-modules.js new file mode 100644 index 00000000..9c7709ff --- /dev/null +++ b/react-ui/.storybook/babel-plugin-auto-css-modules.js @@ -0,0 +1,16 @@ +export default function(babel) { + const { types: t } = babel; + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value; + // console.log("zzzz", source); + if (source.endsWith('.less')) { + if (path.node.specifiers.length > 0) { + path.node.source.value += "?modules"; + } + } + }, + }, + }; +}; \ No newline at end of file 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..820a0eeb --- /dev/null +++ b/react-ui/.storybook/main.ts @@ -0,0 +1,117 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import path from 'path'; +import webpack from 'webpack'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + // '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-webpack5-compiler-babel', + '@storybook/addon-onboarding', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + staticDirs: ['../public'], + docs: { + defaultName: 'Documentation', + }, + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.alias = { + ...config.resolve.alias, + '@': path.resolve(__dirname, '../src'), + '@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'), + }; + } + if (config.module && config.module.rules) { + config.module.rules.push( + { + test: /\.less$/, + oneOf: [ + { + resourceQuery: /modules/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + import: true, + esModule: true, + modules: { + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + }, + { + use: [ + 'style-loader', + 'css-loader', + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + }, + ], + }, + { + test: /\.(tsx?|jsx?)$/, + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + include: [ + path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + path.resolve(__dirname, './'), + ], + }, + ); + } + if (config.plugins) { + config.plugins.push( + new webpack.ProvidePlugin({ + React: 'react', // 全局注入 React + }), + ); + } + + return config; + }, + babel: async (config: any) => { + if (!config.plugins) { + config.plugins = []; + } + + config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js')); + return config; + }, +}; +export default config; 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..3661b99b --- /dev/null +++ b/react-ui/.storybook/mock/websocket.mock.js @@ -0,0 +1,92 @@ +export const createWebSocketMock = () => { + class WebSocketMock { + constructor(url) { + this.url = url; + this.readyState = WebSocket.OPEN; + this.listeners = {}; + this.count = 0; + + console.log("Mock WebSocket connected to:", url); + + // 模拟服务器推送消息 + this.intervalId = setInterval(() => { + this.count += 1; + if (this.count > 5) { + this.count = 0; + clearInterval(this.intervalId); + return; + } + this.sendMessage(JSON.stringify(logStreamData)); + }, 3000); + } + + sendMessage(data) { + if (this.listeners["message"]) { + this.listeners["message"].forEach((callback) => callback({ data })); + } + } + + addEventListener(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + removeEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); + } + } + + close() { + this.readyState = WebSocket.CLOSED; + console.log("Mock WebSocket closed"); + } + } + + return WebSocketMock; +}; + +export const logStreamData = { + streams: [ + { + stream: { + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + }, + values: [ + [ + '1742179591969785990', + 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', + ], + ], + }, + { + stream: { + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + }, + values: [ + [ + '1742179591973414064', + 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', + ], + ], + }, + ], +}; \ No newline at end of file diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx 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/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/routes.ts b/react-ui/config/routes.ts index eaddb001..b2c41f14 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', }, ], }, @@ -181,6 +181,42 @@ export default [ }, ], }, + { + name: '超参数自动寻优', + path: 'hyperparameter', + routes: [ + { + name: '超参数寻优', + path: '', + component: './HyperParameter/List/index', + }, + { + name: '实验详情', + path: 'info/:id', + component: './HyperParameter/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './HyperParameter/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './HyperParameter/Create/index', + }, + { + name: '复制实验', + path: 'copy/:id', + component: './HyperParameter/Create/index', + }, + { + name: '实验实例详情', + path: 'instance/:autoMLId/:id', + component: './HyperParameter/Instance/index', + }, + ], + }, ], }, { @@ -255,60 +291,61 @@ 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', + component: './ModelDeployment/List', }, { - name: '新增服务版本', - path: 'createVersion', - component: './ModelDeployment/CreateVersion', + name: '创建推理服务', + path: 'createService', + component: './ModelDeployment/CreateService', }, { - name: '更新服务版本', - path: 'updateVersion', - component: './ModelDeployment/CreateVersion', + name: '编辑推理服务', + path: 'editService/:serviceId', + component: './ModelDeployment/CreateService', }, { - name: '重启服务版本', - path: 'restartVersion', - component: './ModelDeployment/CreateVersion', - }, - { - 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', + }, + ], }, ], }, ], }, + { name: '应用开发', path: '/appsDeployment', diff --git a/react-ui/package.json b/react-ui/package.json index dc5be1c5..fbc014d1 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -8,6 +8,7 @@ "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", @@ -36,6 +37,10 @@ "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", + "storybook": "storybook dev -p 6006", + "storybook-build": "storybook build", + "storybook-docs": "storybook dev --docs", + "storybook-docs-build": "storybook build --docs", "test": "jest", "test:coverage": "npm run jest -- --coverage", "test:update": "npm run jest -- -u", @@ -83,6 +88,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,15 +114,23 @@ "@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", "typescript": "^5.0.4", "umi-presets-pro": "^2.0.0" @@ -140,5 +166,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/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..7c026d3d 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; import './dayjsConfig'; -import './global.less'; import { removeAllPageCacheState } from './hooks/pageCacheState'; import { getRemoteMenu, @@ -41,7 +40,7 @@ export async function getInitialState(): Promise { roleNames: response.user.roles, } as API.CurrentUser; } catch (error) { - console.error('1111', error); + console.error('getInitialState', error); gotoLoginPage(); } return undefined; @@ -169,7 +168,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { } }; -export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { +export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { //console.log('patchRoutes', e); }; @@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => { defaultColor: themes['textColor'], defaultHoverBg: 'rgba(22, 100, 255, 0.06)', defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', - defaultHoverColor: '#3F7FFF ', + defaultHoverColor: '#3F7FFF', defaultActiveBg: 'rgba(22, 100, 255, 0.12)', defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', defaultActiveColor: themes['primaryColor'], @@ -228,11 +227,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 +245,12 @@ export const antd: RuntimeAntdConfig = (memo) => { linkColor: 'rgba(29, 29, 32, 0.7)', separatorColor: 'rgba(29, 29, 32, 0.7)', }; + memo.theme.components.Tree = { + directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', + }; memo.theme.cssVar = true; - // memo.theme.hashed = false; + memo.theme.hashed = false; memo.appConfig = { message: { 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/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 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx rename to react-ui/src/components/CodeConfigItem/index.tsx 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.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.tsx b/react-ui/src/components/PageTitle/index.tsx index ca192454..2703e032 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -8,10 +8,17 @@ import React from 'react'; import './index.less'; type PageTitleProps = { - title: string; + /** 标题 */ + title: React.ReactNode; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; + +/** + * 页面标题 + */ function PageTitle({ title, style, className = '' }: PageTitleProps) { return (
diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 4b22d208..fff69eb1 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -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..32672b98 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 } 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, )} @@ -110,7 +134,7 @@ function ParameterInput({ ['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..b765ea61 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -1,60 +1,95 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数选择组件 + * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { PipelineNodeModelParameter } from '@/types'; +import { useComputingResource } from '@/hooks/resource'; import { to } from '@/utils/promise'; -import { Select } from 'antd'; +import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; +import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -type ParameterSelectProps = { - value?: PipelineNodeModelParameter; - onChange?: (value: PipelineNodeModelParameter) => void; - disabled?: boolean; +export type ParameterSelectObject = { + value: any; + [key: string]: any; }; -function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { - const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; - const propsConfig = paramSelectConfig[item_type]; +export interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: 'dataset' | 'model' | 'service' | 'resource'; + /** 是否只是展示信息 */ + display?: boolean; + /** 值,支持对象,对象必须包含 value */ + value?: string | ParameterSelectObject; + /** 修改后回调 */ + onChange?: (value: string | ParameterSelectObject) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ + dataType, + display = false, + value, + onChange, + ...rest +}: ParameterSelectProps) { + const [options, setOptions] = useState([]); + const propsConfig = paramSelectConfig[dataType]; + const valueText = typeof value === 'object' && value !== null ? value.value : value; + const [resourceStandardList] = useComputingResource(); useEffect(() => { + // 获取下拉数据 + const getSelectOptions = async () => { + if (!propsConfig) { + return; + } + const getOptions = propsConfig.getOptions; + const [res] = await to(getOptions()); + if (res) { + setOptions(res); + } + }; + getSelectOptions(); - }, []); + }, [propsConfig]); - const hangleChange = (e: string) => { - onChange?.({ - ...valueNonNullable, - value: e, - }); - }; + const selectOptions = dataType === 'resource' ? resourceStandardList : options; - // 获取下拉数据 - const getSelectOptions = async () => { - if (!propsConfig) { - return; - } - const getOptions = propsConfig.getOptions; - const [res] = await to(getOptions()); - if (res) { - setOptions(res); + const handleChange = (text: string) => { + if (typeof value === 'object' && value !== null) { + onChange?.({ + ...value, + value: text, + }); + } else { + onChange?.(text); } }; + // 只用于展示,FormInfo 组件带有 Tooltip + if (display) { + return ( + + ); + } + return ( + @@ -181,7 +171,6 @@ function EditorCreate() { type={ResourceSelectorType.Mirror} placeholder="请选择镜像" canInput={false} - size="large" /> @@ -193,7 +182,6 @@ function EditorCreate() { type={ResourceSelectorType.Model} placeholder="请选择模型" canInput={false} - size="large" /> @@ -205,7 +193,6 @@ function EditorCreate() { type={ResourceSelectorType.Dataset} placeholder="请选择数据集" canInput={false} - size="large" /> diff --git a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx index 71a7766f..4af043ba 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx @@ -29,7 +29,7 @@ import { type TableProps, } from 'antd'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import CreateMirrorModal from '../components/CreateMirrorModal'; import EditorStatusCell from '../components/EditorStatusCell'; import styles from './index.less'; @@ -57,12 +57,8 @@ function EditorList() { }, ); - useEffect(() => { - getEditorList(); - }, [pagination]); - // 获取编辑器列表 - const getEditorList = async () => { + const getEditorList = useCallback(async () => { const params: Record = { page: pagination.current! - 1, size: pagination.pageSize, @@ -73,7 +69,11 @@ function EditorList() { setTableData(content); setTotal(totalElements); } - }; + }, [pagination]); + + useEffect(() => { + getEditorList(); + }, [getEditorList]); // 删除编辑器 const deleteEditor = async (id: number) => { diff --git a/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx index 6af530e1..8d2b27fa 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx @@ -67,8 +67,8 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { message: '请输入镜像Tag', }, { - pattern: /^[a-zA-Z0-9_-]*$/, - message: '只支持字母、数字、下划线(_)、中横线(-)', + pattern: /^[a-zA-Z0-9._-]+$/, + message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', }, ]} > 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..4b2d6d5b 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, @@ -13,7 +14,7 @@ import { tableSorter } from '@/utils'; import { to } from '@/utils/promise'; import tableCellRender, { TableCellValueType } from '@/utils/table'; import { useSearchParams } from '@umijs/max'; -import { App, Button, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd'; +import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import ExperimentStatusCell from '../components/ExperimentStatusCell'; @@ -45,27 +46,27 @@ function ExperimentComparison() { }); const { message } = App.useApp(); - const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); + const config = comparisonConfig[comparisonType]; useEffect(() => { - getComparisonData(); - }, [experimentId]); - - // 获取对比数据列表 - const getComparisonData = async () => { - const request = - comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, + // 获取对比数据列表 + const getComparisonData = async () => { + const request = + comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + }; + const [res] = await to(request(experimentId, params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } }; - const [res] = await to(request(experimentId, params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; + + getComparisonData(); + }, [experimentId, pagination, comparisonType]); // 获取对比 url const getExpMetrics = async () => { @@ -77,7 +78,7 @@ function ExperimentComparison() { }; // 对比按钮 click - const hanldeComparisonClick = () => { + const handleComparisonClick = () => { if (selectedRowKeys.length < 2) { message.error('请至少选择两项进行对比'); return; @@ -154,7 +155,6 @@ function ExperimentComparison() { fixed: 'left', align: 'center', render: tableCellRender(true, TableCellValueType.Array), - ellipsis: { showTitle: false }, }, ], }, @@ -162,17 +162,12 @@ function ExperimentComparison() { title: `${config.title}参数`, align: 'center', children: paramsNames.map((name) => ({ - title: ( - - {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 +176,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..f0cf9ae6 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -21,7 +21,6 @@ function ExperimentText() { const [experimentIns, setExperimentIns] = useState(undefined); const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); const graphRef = useRef(); - const timerRef = useRef(); const workflowRef = useRef(); const locationParams = useParams(); // 新版本获取路由参数接口 const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); @@ -36,6 +35,16 @@ function ExperimentText() { initGraph(); getWorkflow(); + return () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { const changeSize = () => { if (!graph || graph.get('destroyed')) return; if (!graphRef.current) return; @@ -46,20 +55,9 @@ function ExperimentText() { window.addEventListener('resize', changeSize); return () => { window.removeEventListener('resize', changeSize); - if (timerRef.current) { - clearTimeout(timerRef.current); - } - if (evtSourceRef.current) { - evtSourceRef.current.close(); - evtSourceRef.current = null; - } }; }, []); - useEffect(() => { - propsDrawerOpenRef.current = propsDrawerOpen; - }, [propsDrawerOpen]); - // 获取流水线模版 const getWorkflow = async () => { const [res] = await to(getWorkflowById(locationParams.workflowId)); 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/ExperimentDrawer/index.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less index c1f1b7ef..e524a987 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,7 @@ } &__tabs { - height: calc(100% - 170px); + height: calc(100% - 169px); :global { .ant-tabs-nav { padding-left: 24px; diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index cdcaea19..c1a70141 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -90,15 +90,17 @@ const ExperimentDrawer = ({ instanceNodeStatus, workflowId, instanceNodeStartTime, + experimentName, + experimentId, + pipelineId, ], ); return ( } onClose={onClose} open={open} diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 4d68d93c..d184deee 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -13,14 +13,14 @@ import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { DoubleRightOutlined } from '@ant-design/icons'; -import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo } from 'react'; import TensorBoardStatusCell from '../TensorBoardStatus'; import styles from './index.less'; type ExperimentInstanceProps = { - experimentInList?: ExperimentInstance[]; + experimentInsList?: ExperimentInstance[]; experimentInsTotal: number; onClickInstance?: (instance: ExperimentInstance) => void; onClickTensorBoard?: (instance: ExperimentInstance) => void; @@ -30,7 +30,7 @@ type ExperimentInstanceProps = { }; function ExperimentInstanceComponent({ - experimentInList, + experimentInsList, experimentInsTotal, onClickInstance, onClickTensorBoard, @@ -40,8 +40,8 @@ function ExperimentInstanceComponent({ }: ExperimentInstanceProps) { 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,7 +57,7 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { @@ -118,8 +118,8 @@ function ExperimentInstanceComponent({ } }; - if (!experimentInList || experimentInList.length === 0) { - return null; + if (!experimentInsList || experimentInsList.length === 0) { + return
暂无数据
; } return ( @@ -152,7 +152,7 @@ function ExperimentInstanceComponent({
- {experimentInList.map((item, index) => ( + {experimentInsList.map((item, index) => (
{elapsedTime(item.create_time, item.finish_time)}
- - {formatDate(item.create_time)} - + + {formatDate(item.create_time)} +
@@ -244,7 +244,7 @@ function ExperimentInstanceComponent({
))} - {experimentInsTotal > experimentInList.length ? ( + {experimentInsTotal > experimentInsList.length ? (