diff --git a/.gitignore b/.gitignore index 0014ee8f..5510490a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ mvnw /k8s/template-yaml/deploy/ /k8s/dockerfiles/html/ /k8s/dockerfiles/jar + +# web +**/node_modules + +*storybook.log diff --git a/k8s/build-java.sh b/k8s/build-java.sh index 7bb2180b..bb0884a3 100755 --- a/k8s/build-java.sh +++ b/k8s/build-java.sh @@ -6,8 +6,14 @@ baseDir="/home/somuns/ci4s" #判断$1是否为all,如果是,则编译所有模块,否则只编译management-platform模块 if [ "$1" == "all" ]; then buildDir=$baseDir -else +elif [ "$1" == "manage" ]; then buildDir="$baseDir/ruoyi-modules/management-platform" +elif [ "$1" == "auth" ]; then + buildDir="$baseDir/ruoyi-auth" +elif [ "$1" == "gateway" ]; then + buildDir="$baseDir/ruoyi-gateway" +elif [ "$1" == "system" ]; then + buildDir="$baseDir/ruoyi-modules/ruoyi-system" fi echo "Building $buildDir" diff --git a/k8s/build.sh b/k8s/build.sh index a84e56f1..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" } @@ -30,7 +30,7 @@ done echo "branch: $branch" echo "service: $service" -valid_services=("manage-front" "manage" "front" "all") +valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then echo "Invalid service name: $service" >&2 echo "Valid services are: ${valid_services[*]}" @@ -41,25 +41,6 @@ fi baseDir="/home/somuns/ci4s" cd ${baseDir} -# 拉取指定分支的最新代码 -echo "Checking out and pulling branch $branch..." - -git stash -git checkout $branch -if [ $? -ne 0 ]; then - echo "切换到分支 $branch 失败,请检查分支名称是否正确!" - exit 1 -fi - -git stash -git pull origin $branch -if [ $? -ne 0 ]; then - echo "拉取代码失败,请检查网络或联系管理员!" - exit 1 -fi - -chmod +777 ${baseDir}/k8s/*.sh - # 创建目录 mkdir -p ${baseDir}/k8s/dockerfiles/jar mkdir -p ${baseDir}/k8s/dockerfiles/html @@ -73,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 @@ -99,13 +80,45 @@ compile_java() { fi # 复制jar包 - cp -rf ${baseDir}/ruoyi-modules/management-platform/target/management-platform.jar ${baseDir}/k8s/dockerfiles/jar/management-platform.jar - if [ $? -ne 0 ]; then - echo "复制jar包失败,请检查代码!" - exit 1 + if [ "$param" == "manage-front" ] || [ "$param" == "manage" ]; then + cp -rf ${baseDir}/ruoyi-modules/management-platform/target/management-platform.jar ${baseDir}/k8s/dockerfiles/jar/management-platform.jar + if [ $? -ne 0 ]; then + echo "复制jar包失败,请检查代码!" + exit 1 + fi + fi + + if [ "$param" == "gateway" ]; then + cp -rf ${baseDir}/ruoyi-gateway/target/ruoyi-gateway.jar ${baseDir}/k8s/dockerfiles/jar/ruoyi-gateway.jar + if [ $? -ne 0 ]; then + echo "复制jar包失败,请检查代码!" + exit 1 + fi + fi + + if [ "$param" == "auth" ]; then + cp -rf ${baseDir}/ruoyi-auth/target/ruoyi-auth.jar ${baseDir}/k8s/dockerfiles/jar/ruoyi-auth.jar + if [ $? -ne 0 ]; then + echo "复制jar包失败,请检查代码!" + exit 1 + fi + fi + + if [ "$param" == "system" ]; then + cp -rf ${baseDir}/ruoyi-modules/ruoyi-system/target/ruoyi-modules-system.jar ${baseDir}/k8s/dockerfiles/jar/ruoyi-modules-system.jar + if [ $? -ne 0 ]; then + echo "复制jar包失败,请检查代码!" + exit 1 + fi fi if [ "$param" == "all" ]; then + cp -rf ${baseDir}/ruoyi-modules/management-platform/target/management-platform.jar ${baseDir}/k8s/dockerfiles/jar/management-platform.jar + if [ $? -ne 0 ]; then + echo "复制jar包失败,请检查代码!" + exit 1 + fi + cp -rf ${baseDir}/ruoyi-modules/ruoyi-system/target/ruoyi-modules-system.jar ${baseDir}/k8s/dockerfiles/jar/ruoyi-modules-system.jar if [ $? -ne 0 ]; then echo "复制jar包失败,请检查代码!" @@ -126,13 +139,19 @@ compile_java() { fi } -if [ "$service" == "manage-front" ] || [ "$service" == "front" ]; then +if [ "$service" == "front" ]; then # 编译前端 compile_front fi +if [ "$service" == "manage-front" ]; then + # 编译前端 + compile_front + # 编译java + compile_java "manage" +fi -if [ "$service" == "manage-front" ] || [ "$service" == "manage" ]; then +if [ "$service" != "manage-front" ] && [ "$service" != "all" ] && [ "$service" != "front" ]; then # 编译java compile_java $service fi diff --git a/k8s/build_and_deploy.sh b/k8s/build_and_deploy.sh index eacc9c6b..6e561cdd 100755 --- a/k8s/build_and_deploy.sh +++ b/k8s/build_and_deploy.sh @@ -7,7 +7,6 @@ startTime=$(date +%s) baseDir="/home/somuns/ci4s" cd ${baseDir} - #build # 默认参数 branch="master" @@ -20,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" } @@ -36,7 +35,26 @@ while getopts "b:s:e:h" opt; do esac done -valid_services=("manage-front" "manage" "front" "all") +# 拉取指定分支的最新代码 +echo "Checking out and pulling branch $branch..." + +git stash +git checkout $branch +if [ $? -ne 0 ]; then + echo "切换到分支 $branch 失败,请检查分支名称是否正确!" + exit 1 +fi + +git stash +git pull origin $branch +if [ $? -ne 0 ]; then + echo "拉取代码失败,请检查网络或联系管理员!" + exit 1 +fi + +chmod +777 ${baseDir}/k8s/*.sh + +valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then echo "Invalid service name: $service" >&2 echo "Valid services are: ${valid_services[*]}" diff --git a/k8s/deploy.sh b/k8s/deploy.sh index 5b3f7887..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" } @@ -27,7 +27,7 @@ done echo "Deploy service: $service, environment: $env" -valid_services=("manage-front" "manage" "front" "all") +valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then echo "Invalid service name: $service" >&2 echo "Valid services are: ${valid_services[*]}" @@ -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 @@ -129,15 +129,34 @@ build_and_deploy() { deploy_service ${yaml_file} } +if [ "$service" == "front" ]; then + build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml" +fi + # 构建和部署 manage 服务 -if [ "$service" == "manage-front" ] || [ "$service" == "manage" ]; then +if [ "$service" == "manage" ]; then build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml" fi +if [ "$service" == "auth" ]; then + #部署认证中心 + build_and_deploy "auth-dockerfile" "172.20.32.187/ci4s/ci4s-auth:${tag}" "k8s-5auth.yaml" +fi + +if [ "$service" == "gateway" ]; then + #部署网关 + build_and_deploy "gateway-dockerfile" "172.20.32.187/ci4s/ci4s-gateway:${tag}" "k8s-4gateway.yaml" +fi + +if [ "$service" == "system" ]; then + #部署系统服务 + build_and_deploy "system-dockerfile" "172.20.32.187/ci4s/ci4s-system:${tag}" "k8s-6system.yaml" +fi # 构建和部署 front 服务 -if [ "$service" == "manage-front" ] || [ "$service" == "front" ]; then +if [ "$service" == "manage-front" ]; then build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml" + build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml" fi 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 c21d6cf5..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 @@ -37,6 +37,10 @@ spec: - containerPort: 8848 - containerPort: 9848 - containerPort: 9849 + initContainers: + - 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 4fcddb15..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 @@ -26,10 +28,31 @@ spec: volumeMounts: - name: resource-volume mountPath: /home/resource/ + subPath: mini-model-platform-data + mountPropagation: Bidirectional volumes: - name: resource-volume - persistentVolumeClaim: - claimName: platform-data-pvc-nfs + 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..8ddbc0c6 --- /dev/null +++ b/react-ui/.nvmrc @@ -0,0 +1 @@ +v18.16.0 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 c10b23b6..04ab330d 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -156,4 +156,5 @@ export default defineConfig({ }, javascriptEnabled: true, }, + valtio: {}, }); diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index 0639985b..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', + component: process.env.NO_SSO ? './User/Login/login' : './User/Login', }, ], }, @@ -145,6 +145,78 @@ export default [ }, ], }, + { + name: '自动机器学习', + path: 'automl', + routes: [ + { + name: '自动机器学习', + path: '', + component: './AutoML/List/index', + }, + { + name: '实验详情', + path: 'info/:id', + component: './AutoML/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './AutoML/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './AutoML/Create/index', + }, + { + name: '复制实验', + path: 'copy/:id', + component: './AutoML/Create/index', + }, + { + name: '实验实例详情', + path: 'instance/:autoMLId/: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/:autoMLId/:id', + component: './HyperParameter/Instance/index', + }, + ], + }, ], }, { @@ -219,39 +291,61 @@ export default [ }, ], }, - ], - }, - { - name: '模型部署', - path: '/modelDeployment', - routes: [ { name: '模型部署', - path: '', - component: './ModelDeployment/List', - }, - { - name: '服务详情', - path: 'serviceInfo/:id', - component: './ModelDeployment/ServiceInfo', - }, - { - name: '服务版本详情', - path: 'versionInfo/:id', - component: './ModelDeployment/VersionInfo', - }, - { - name: '创建推理服务', - path: 'createService', - component: './ModelDeployment/CreateService', - }, - { - name: '新增服务版本', - path: 'addVersion/:id', - component: './ModelDeployment/CreateVersion', + 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', + 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/mock/listTableList.ts b/react-ui/mock/listTableList.ts deleted file mode 100644 index 35ec3cec..00000000 --- a/react-ui/mock/listTableList.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Request, Response } from 'express'; -import moment from 'moment'; -import { parse } from 'url'; - -// mock tableListDataSource -const genList = (current: number, pageSize: number) => { - const tableListDataSource: API.RuleListItem[] = []; - - for (let i = 0; i < pageSize; i += 1) { - const index = (current - 1) * 10 + i; - tableListDataSource.push({ - key: index, - disabled: i % 6 === 0, - href: 'https://ant.design', - avatar: [ - 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', - 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', - ][i % 2], - name: `TradeCode ${index}`, - owner: '曲丽丽', - desc: '这是一段描述', - callNo: Math.floor(Math.random() * 1000), - status: Math.floor(Math.random() * 10) % 4, - updatedAt: moment().format('YYYY-MM-DD'), - createdAt: moment().format('YYYY-MM-DD'), - progress: Math.ceil(Math.random() * 100), - }); - } - tableListDataSource.reverse(); - return tableListDataSource; -}; - -let tableListDataSource = genList(1, 100); - -function getRule(req: Request, res: Response, u: string) { - let realUrl = u; - if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { - realUrl = req.url; - } - const { current = 1, pageSize = 10 } = req.query; - const params = parse(realUrl, true).query as unknown as API.PageParams & - API.RuleListItem & { - sorter: any; - filter: any; - }; - - let dataSource = [...tableListDataSource].slice( - ((current as number) - 1) * (pageSize as number), - (current as number) * (pageSize as number), - ); - if (params.sorter) { - const sorter = JSON.parse(params.sorter); - dataSource = dataSource.sort((prev, next) => { - let sortNumber = 0; - (Object.keys(sorter) as Array).forEach((key) => { - let nextSort = next?.[key] as number; - let preSort = prev?.[key] as number; - if (sorter[key] === 'descend') { - if (preSort - nextSort > 0) { - sortNumber += -1; - } else { - sortNumber += 1; - } - return; - } - if (preSort - nextSort > 0) { - sortNumber += 1; - } else { - sortNumber += -1; - } - }); - return sortNumber; - }); - } - if (params.filter) { - const filter = JSON.parse(params.filter as any) as { - [key: string]: string[]; - }; - if (Object.keys(filter).length > 0) { - dataSource = dataSource.filter((item) => { - return (Object.keys(filter) as Array).some((key) => { - if (!filter[key]) { - return true; - } - if (filter[key].includes(`${item[key]}`)) { - return true; - } - return false; - }); - }); - } - } - - if (params.name) { - dataSource = dataSource.filter((data) => data?.name?.includes(params.name || '')); - } - const result = { - data: dataSource, - total: tableListDataSource.length, - success: true, - pageSize, - current: parseInt(`${params.current}`, 10) || 1, - }; - - return res.json(result); -} - -function postRule(req: Request, res: Response, u: string, b: Request) { - let realUrl = u; - if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { - realUrl = req.url; - } - - const body = (b && b.body) || req.body; - const { method, name, desc, key } = body; - - switch (method) { - /* eslint no-case-declarations:0 */ - case 'delete': - tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1); - break; - case 'post': - (() => { - const i = Math.ceil(Math.random() * 10000); - const newRule: API.RuleListItem = { - key: tableListDataSource.length, - href: 'https://ant.design', - avatar: [ - 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', - 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', - ][i % 2], - name, - owner: '曲丽丽', - desc, - callNo: Math.floor(Math.random() * 1000), - status: Math.floor(Math.random() * 10) % 2, - updatedAt: moment().format('YYYY-MM-DD'), - createdAt: moment().format('YYYY-MM-DD'), - progress: Math.ceil(Math.random() * 100), - }; - tableListDataSource.unshift(newRule); - return res.json(newRule); - })(); - return; - - case 'update': - (() => { - let newRule = {}; - tableListDataSource = tableListDataSource.map((item) => { - if (item.key === key) { - newRule = { ...item, desc, name }; - return { ...item, desc, name }; - } - return item; - }); - return res.json(newRule); - })(); - return; - default: - break; - } - - const result = { - list: tableListDataSource, - pagination: { - total: tableListDataSource.length, - }, - }; - - res.json(result); -} - -export default { - 'GET /api/rule': getRule, - 'POST /api/rule': postRule, -}; diff --git a/react-ui/package.json b/react-ui/package.json index aebf21ca..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", @@ -67,7 +72,6 @@ "fabric": "^5.3.0", "highlight.js": "^11.7.0", "lodash": "^4.17.21", - "moment": "^2.30.1", "omit.js": "^2.0.2", "pnpm": "^8.9.0", "query-string": "^8.1.0", @@ -84,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", @@ -97,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" @@ -141,5 +166,10 @@ "CNAME", "create-umi" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/react-ui/public/CNAME b/react-ui/public/CNAME deleted file mode 100644 index 30c2d4d3..00000000 --- a/react-ui/public/CNAME +++ /dev/null @@ -1 +0,0 @@ -preview.pro.ant.design \ No newline at end of file diff --git a/react-ui/public/assets/images/component-icon-9-Failed.png b/react-ui/public/assets/images/component-icon-9-Failed.png new file mode 100644 index 00000000..64fae87a Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Failed.png differ diff --git a/react-ui/public/assets/images/component-icon-9-Omitted.png b/react-ui/public/assets/images/component-icon-9-Omitted.png new file mode 100644 index 00000000..2b2911c6 Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Omitted.png differ diff --git a/react-ui/public/assets/images/component-icon-9-Pending.png b/react-ui/public/assets/images/component-icon-9-Pending.png new file mode 100644 index 00000000..a684bdc0 Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Pending.png differ diff --git a/react-ui/public/assets/images/component-icon-9-Running.png b/react-ui/public/assets/images/component-icon-9-Running.png new file mode 100644 index 00000000..b96deabe Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Running.png differ diff --git a/react-ui/public/assets/images/component-icon-9-Skipped.png b/react-ui/public/assets/images/component-icon-9-Skipped.png new file mode 100644 index 00000000..2b2911c6 Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Skipped.png differ diff --git a/react-ui/public/assets/images/component-icon-9-Succeeded.png b/react-ui/public/assets/images/component-icon-9-Succeeded.png new file mode 100644 index 00000000..f18af9c9 Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9-Succeeded.png differ diff --git a/react-ui/public/assets/images/component-icon-9.png b/react-ui/public/assets/images/component-icon-9.png new file mode 100644 index 00000000..92dae064 Binary files /dev/null and b/react-ui/public/assets/images/component-icon-9.png differ 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/logo.svg b/react-ui/public/logo.svg deleted file mode 100644 index 239bf69f..00000000 --- a/react-ui/public/logo.svg +++ /dev/null @@ -1 +0,0 @@ -Group 28 Copy 5Created with Sketch. \ 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/public/pro_icon.svg b/react-ui/public/pro_icon.svg deleted file mode 100644 index e075b78d..00000000 --- a/react-ui/public/pro_icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index c018253e..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, @@ -20,6 +19,7 @@ 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'; @@ -40,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; @@ -49,7 +49,7 @@ export async function getInitialState(): Promise { // 如果不是登录页面,执行 const { location } = history; - console.log('getInitialState', needAuth(location.pathname)); + // console.log('getInitialState', needAuth(location.pathname)); if (needAuth(location.pathname)) { const currentUser = await fetchUserInfo(); return { @@ -162,23 +162,23 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { const { location } = e; const menus = getRemoteMenu(); - console.log('onRouteChange', menus); + // console.log('onRouteChange', menus); if (menus === null && needAuth(location.pathname)) { history.go(0); } }; -export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { +export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { //console.log('patchRoutes', e); }; export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { - console.log('patchClientRoutes', e); + // console.log('patchClientRoutes', e); patchRouteWithRemoteMenus(e.routes); }; export function render(oldRender: () => void) { - console.log('render'); + // console.log('render'); const token = getAccessToken(); if (!token || token?.length === 0) { oldRender(); @@ -214,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'], @@ -227,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, @@ -244,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/dataset-config-icon.png b/react-ui/src/assets/img/dataset-config-icon.png new file mode 100644 index 00000000..1afc4f72 Binary files /dev/null and b/react-ui/src/assets/img/dataset-config-icon.png differ diff --git a/react-ui/src/assets/img/dataset-version.png b/react-ui/src/assets/img/dataset-version.png new file mode 100644 index 00000000..c17d46cf Binary files /dev/null and b/react-ui/src/assets/img/dataset-version.png differ diff --git a/react-ui/src/assets/img/editor-parameter.png b/react-ui/src/assets/img/editor-parameter.png index b5fd9f41..06fb4f2b 100644 Binary files a/react-ui/src/assets/img/editor-parameter.png and b/react-ui/src/assets/img/editor-parameter.png differ 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/mirror-basic.png b/react-ui/src/assets/img/mirror-basic.png index d9ca34a9..f022b2ac 100644 Binary files a/react-ui/src/assets/img/mirror-basic.png and b/react-ui/src/assets/img/mirror-basic.png differ diff --git a/react-ui/src/assets/img/model-deployment.png b/react-ui/src/assets/img/model-deployment.png index bf8511c8..59a32d5f 100644 Binary files a/react-ui/src/assets/img/model-deployment.png and b/react-ui/src/assets/img/model-deployment.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/resample-icon.png b/react-ui/src/assets/img/resample-icon.png new file mode 100644 index 00000000..fa24c1aa Binary files /dev/null and b/react-ui/src/assets/img/resample-icon.png differ diff --git a/react-ui/src/assets/img/search-config-icon.png b/react-ui/src/assets/img/search-config-icon.png new file mode 100644 index 00000000..6cc8a78b Binary files /dev/null and b/react-ui/src/assets/img/search-config-icon.png differ diff --git a/react-ui/src/assets/img/trial-config-icon.png b/react-ui/src/assets/img/trial-config-icon.png new file mode 100644 index 00000000..69408563 Binary files /dev/null and b/react-ui/src/assets/img/trial-config-icon.png differ diff --git a/react-ui/src/components/ArrayTableCell/index.tsx b/react-ui/src/components/ArrayTableCell/index.tsx deleted file mode 100644 index de109ff4..00000000 --- a/react-ui/src/components/ArrayTableCell/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * @Author: 赵伟 - * @Date: 2024-04-28 14:18:11 - * @Description: 自定义 Table 数组类单元格 - */ - -import { Tooltip } from 'antd'; - -function ArrayTableCell(ellipsis: boolean = false, property?: string) { - return (value?: any | null) => { - if ( - value === undefined || - value === null || - Array.isArray(value) === false || - value.length === 0 - ) { - return --; - } - - let list = value; - if (property && typeof value[0] === 'object') { - list = value.map((item) => item[property]); - } - const text = list.join(','); - if (ellipsis) { - return ( - - {text}; - - ); - } else { - return {text}; - } - }; -} - -export default ArrayTableCell; 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/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 bd07db34..a918b8ee 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -1,129 +1,58 @@ -import { Link } from '@umijs/max'; -import { Typography } from 'antd'; import classNames from 'classnames'; +import React from 'react'; +import BasicInfoItem from './BasicInfoItem'; import './index.less'; +import type { BasicInfoData, BasicInfoLink } from './types'; +export type { BasicInfoData, BasicInfoLink }; -export type BasicInfoLink = { - value: string; - link?: string; - url?: string; -}; - -export type BasicInfoData = { - label: string; - value?: any; - ellipsis?: boolean; - format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; -}; - -type BasicInfoProps = { +export type BasicInfoProps = { + /** 基础信息 */ datas: BasicInfoData[]; + /** 标题宽度 */ + labelWidth: number; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; + /** 是否一行三列 */ + threeColumns?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; - labelWidth: number; -}; - -type BasicInfoItemProps = { - data: BasicInfoData; - labelWidth: number; - classPrefix: string; -}; - -type BasicInfoItemValueProps = BasicInfoLink & { - ellipsis?: boolean; - classPrefix: string; }; -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) => ( ))}
); } - -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 (typeof formatValue === 'object' && formatValue) { - valueComponent = ( - - ); - } else { - valueComponent = ( - - ); - } - return ( -
-
- {label} -
- {valueComponent} -
- ); -} - -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 { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts new file mode 100644 index 00000000..be2ac774 --- /dev/null +++ b/react-ui/src/components/BasicInfo/types.ts @@ -0,0 +1,14 @@ +// 基础信息 +export type BasicInfoData = { + label: string; + value?: any; + ellipsis?: boolean; + format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined; +}; + +// 值为链接的类型 +export type BasicInfoLink = { + 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 314b05ca..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; @@ -27,14 +27,13 @@ display: flex; flex: 1; flex-direction: column; - align-items: flex-start; + align-items: stretch; min-width: 0; } &__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 df167ae2..357a82fb 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,20 +1,21 @@ import classNames from 'classnames'; -import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; +import { BasicInfoProps } from '../BasicInfo'; +import BasicInfoItem from '../BasicInfo/BasicInfoItem'; +import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; 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 = []; @@ -22,7 +23,7 @@ export default function BasicTableInfo({ for (let i = 0; i < 4 - remainder; i++) { array.push({ label: '', - value: '', + value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined }); } } @@ -30,11 +31,12 @@ export default function BasicTableInfo({ return (
- {showDatas.map((item) => ( + {showDatas.map((item, index) => ( ))} 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 03474c00..d2afbb94 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -1,21 +1,35 @@ /* * @Author: 赵伟 * @Date: 2024-10-08 15:36:08 - * @Description: 代码配置选择表单组件 + * @Description: 流水线选择代码配置表单 */ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; -import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; +import classNames from 'classnames'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; -export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +export { + requiredValidator, + type ParameterInputObject, + type ParameterInputValue, +} from '../ParameterInput'; type CodeSelectProps = ParameterInputProps; -function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { +/** 代码配置选择表单组件 */ +function CodeSelect({ + value, + size, + disabled, + className, + style, + onChange, + ...rest +}: CodeSelectProps) { const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { @@ -46,9 +60,10 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { }; return ( -
+
)} diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index e50dabec..38d3644c 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -14,20 +14,26 @@ 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 }: KFIconProps) { +/** 封装 iconfont 图标 */ +function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) { const iconStyle = { ...style, fontSize: font, color, }; - return ; + return ; } export default KFIcon; 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..47ef5d0e 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 { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; -// 对象 +// 如果值是对象时的类型 export type ParameterInputObject = { value?: any; // 值 showValue?: any; // 显示值 fromSelect?: boolean; // 是否来自选择 - activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab + activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys [key: string]: any; @@ -25,18 +26,34 @@ export type ParameterInputObject = { export type ParameterInputValue = ParameterInputObject | string; export interface ParameterInputProps { + /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ value?: ParameterInputValue; + /** + * 值变化时的回调 + * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 + */ onChange?: (value?: ParameterInputValue) => void; + /** 点击时的回调 */ onClick?: () => void; + /** 删除时的回调 */ onRemove?: () => void; + /** 是否可以手动输入 */ canInput?: boolean; + /** 是否是文本框 */ textArea?: boolean; + /** 占位符 */ placeholder?: string; + /** 是否允许清除 */ allowClear?: boolean; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 大小 */ size?: 'middle' | 'small' | 'large'; + /** 是否禁用 */ disabled?: boolean; + /** 元素 id */ id?: string; } @@ -88,6 +105,7 @@ function ParameterInput({ className={classNames( 'parameter-input', { 'parameter-input--large': size === 'large' }, + { 'parameter-input--small': size === 'small' }, { [`parameter-input--${status}`]: status }, className, )} diff --git a/react-ui/src/components/ParameterSelect/config.tsx b/react-ui/src/components/ParameterSelect/config.tsx index 757e2a11..2548f44c 100644 --- a/react-ui/src/components/ParameterSelect/config.tsx +++ b/react-ui/src/components/ParameterSelect/config.tsx @@ -1,7 +1,10 @@ +import { ServiceData } from '@/pages/ModelDeployment/types'; import { getDatasetList, getModelList } from '@/services/dataset/index.js'; +import { getServiceListReq } from '@/services/modelDeployment'; import { getComputingResourceReq } from '@/services/pipeline'; import { ComputingResource } from '@/types'; import { type SelectProps } from 'antd'; +import { pick } from 'lodash'; // 过滤资源规格 const filterResourceStandard: SelectProps['filterOption'] = ( @@ -62,6 +65,25 @@ export const paramSelectConfig: Record = { }, optionFilterProp: 'name', }, + service: { + getOptions: async () => { + const res = await getServiceListReq({ + page: 0, + size: 1000, + }); + return ( + res?.data?.content?.map((item: ServiceData) => ({ + label: item.service_name, + value: JSON.stringify(pick(item, ['id', 'service_name'])), + })) ?? [] + ); + }, + fieldNames: { + label: 'label', + value: 'value', + }, + optionFilterProp: 'label', + }, resource: { getOptions: async () => { const res = await getComputingResourceReq({ diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index ffaf8415..f1902e5c 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -1,60 +1,91 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数选择组件 + * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { PipelineNodeModelParameter } from '@/types'; 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) { +export interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: 'dataset' | 'model' | 'service' | 'resource'; + /** 是否只是展示信息 */ + display?: boolean; + /** 值 */ + value?: string | ParameterSelectObject; + /** 修改后回调 */ + onChange?: (value: string | ParameterSelectObject) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ + dataType, + display = false, + value, + onChange, + ...rest +}: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; - const propsConfig = paramSelectConfig[item_type]; + const propsConfig = paramSelectConfig[dataType]; + const valueText = typeof value === 'object' && value !== null ? value.value : value; useEffect(() => { - getSelectOptions(); - }, []); + // 获取下拉数据 + const getSelectOptions = async () => { + if (!propsConfig) { + return; + } + const getOptions = propsConfig.getOptions; + const [res] = await to(getOptions()); + if (res) { + setOptions(res); + } + }; - const hangleChange = (e: string) => { - onChange?.({ - ...valueNonNullable, - value: e, - }); - }; + getSelectOptions(); + }, [propsConfig]); - // 获取下拉数据 - 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/AutoML/components/CreateForm/DatasetConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx new file mode 100644 index 00000000..b3b3f2dd --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx @@ -0,0 +1,59 @@ +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; + +function DatasetConfig() { + return ( + <> + + + + + + + + + + + + + + + + + ); +} + +export default DatasetConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..d03445f3 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,455 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { + AutoMLEnsembleClass, + AutoMLResamplingStrategy, + AutoMLTaskType, + autoMLEnsembleClassOptions, + autoMLResamplingStrategyOptions, + autoMLTaskTypeOptions, +} from '@/enums'; +import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd'; + +// 分类算法 +const classificationAlgorithms = [ + 'adaboost', + 'bernoulli_nb', + 'decision_tree', + 'extra_trees', + 'gaussian_nb', + 'gradient_boosting', + 'k_nearest_neighbors', + 'lda', + 'liblinear_svc', + 'libsvm_svc', + 'mlp', + 'multinomial_nb', + 'passive_aggressive', + 'qda', + 'random_forest', + 'sgd', +].map((name) => ({ label: name, value: name })); + +// 回归算法 +const regressorAlgorithms = [ + 'adaboost', + 'ard_regression', + 'decision_tree', + 'extra_trees', + 'gaussian_process', + 'gradient_boosting', + 'k_nearest_neighbors', + 'liblinear_svr', + 'libsvm_svr', + 'mlp', + 'random_forest', + 'sgd', +].map((name) => ({ label: name, value: name })); + +// 特征预处理算法 +const featureAlgorithms = [ + 'densifier', + 'extra_trees_preproc_for_classification', + 'extra_trees_preproc_for_regression', + 'fast_ica', + 'feature_agglomeration', + 'kernel_pca', + 'kitchen_sinks', + 'liblinear_svc_preprocessor', + 'no_preprocessing', + 'nystroem_sampler', + 'pca', + 'polynomial', + 'random_trees_embedding', + 'select_percentile_classification', + 'select_percentile_regression', + 'select_rates_classification', + 'select_rates_regression', + 'truncatedSVD', +].map((name) => ({ label: name, value: name })); + +// 分类指标 +export const classificationMetrics = [ + 'accuracy', + 'balanced_accuracy', + 'roc_auc', + 'average_precision', + 'log_loss', + 'precision_macro', + 'precision_micro', + 'precision_samples', + 'precision_weighted', + 'recall_macro', + 'recall_micro', + 'recall_samples', + 'recall_weighted', + 'f1_macro', + 'f1_micro', + 'f1_samples', + 'f1_weighted', +].map((name) => ({ label: name, value: name })); + +// 回归指标 +export const regressionMetrics = [ + 'mean_absolute_error', + 'mean_squared_error', + 'root_mean_squared_error', + 'mean_squared_log_error', + 'median_absolute_error', + 'r2', +].map((name) => ({ label: name, value: name })); + +function ExecuteConfig() { + const form = Form.useFormInstance(); + const task_type = Form.useWatch('task_type', form); + const include_classifier = Form.useWatch('include_classifier', form); + const exclude_classifier = Form.useWatch('exclude_classifier', form); + const include_regressor = Form.useWatch('include_regressor', form); + const exclude_regressor = Form.useWatch('exclude_regressor', form); + const include_feature_preprocessor = Form.useWatch('include_feature_preprocessor', form); + const exclude_feature_preprocessor = Form.useWatch('exclude_feature_preprocessor', form); + + return ( + <> + + + + + form.resetFields(['metrics'])} + > + + + + + + + + 0} + mode="multiple" + showSearch + /> + + + + + + {({ getFieldValue }) => { + return getFieldValue('task_type') === AutoMLTaskType.Classification ? ( + <> + + + + 0} + showSearch + /> + + + + + ) : ( + <> + + + + 0} + showSearch + /> + + + + + ); + }} + + + + + + + + + + + + {({ getFieldValue }) => { + return getFieldValue('ensemble_class') === AutoMLEnsembleClass.Default ? ( + <> + + + + + + + + + + + + + + + + + ) : null; + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + return getFieldValue('resampling_strategy') === AutoMLResamplingStrategy.CrossValid ? ( + + + + + + + + ) : null; + }} + + + + + + + + + + + + + + + + + + + ); +} + +export default ExecuteConfig; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx new file mode 100644 index 00000000..6a965e9a --- /dev/null +++ b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx @@ -0,0 +1,137 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { AutoMLTaskType } from '@/enums'; +import { removeFormListItem } from '@/utils/ui'; +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; +import { classificationMetrics, regressionMetrics } from './ExecuteConfig'; +import styles from './index.less'; + +function TrialConfig() { + const form = Form.useFormInstance(); + const task_type = Form.useWatch('task_type', form); + const metrics = Form.useWatch('metrics', form) || []; + const selectedMetrics = metrics + .map((item: { name: string; value: number }) => item?.name) + .filter(Boolean); + const allMetricsOptions = + task_type === AutoMLTaskType.Classification ? classificationMetrics : regressionMetrics; + const metricsOptions = allMetricsOptions.filter((item) => !selectedMetrics.includes(item.label)); + + return ( + <> + + + + + + + + + + + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + ; - params_names: string[]; - params: Record; + metrics_names?: string[]; + metrics?: Record; + params_names?: string[]; + params?: Record; }; -const pageSize = 30; - -// function Footer() { -// return ( -//
-//
-//

我是有底线的

-//
-//
-// ); -// } - function ExperimentComparison() { const [searchParams] = useSearchParams(); const comparisonType = searchParams.get('type') as ComparisonType; const experimentId = searchParams.get('id'); const [tableData, setTableData] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [total, setTotal] = useState(0); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); + const { message } = App.useApp(); - const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); - const [tableRef, { width: tableWidth, height: tableHeight }] = useDomSize( - 0, - 0, - [], - ); - const [loadCompleted, setLoadCompleted] = useState(false); - const [loading, setLoading] = useState(false); // 避免误触发加载更多 + const config = comparisonConfig[comparisonType]; useEffect(() => { - getComparisonData(); - }, [experimentId]); - - // 获取对比数据列表 - const getComparisonData = async (offset: string = '') => { - const request = - comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; - const [res] = await to(request(experimentId, { offset: offset, limit: pageSize })); - if (res && res.data) { - setTableData((prev) => [...prev, ...res.data]); - if (res.data.length === 0) { - setLoadCompleted(true); - const ele = document.getElementsByClassName('ant-table-body')[0]; - if (ele) { - const div = document.createElement('div'); - div.className = styles['experiment-comparison__table__footer']; - div.innerHTML = '

我是有底线的

'; - ele.appendChild(div); - } + // 获取对比数据列表 + 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); } - } - setLoading(false); - }; + }; + + getComparisonData(); + }, [experimentId, pagination, comparisonType]); // 获取对比 url const getExpMetrics = async () => { @@ -95,7 +78,7 @@ function ExperimentComparison() { }; // 对比按钮 click - const hanldeComparisonClick = () => { + const handleComparisonClick = () => { if (selectedRowKeys.length < 2) { message.error('请至少选择两项进行对比'); return; @@ -114,21 +97,22 @@ function ExperimentComparison() { }, }; - const handleTableScroll = (e: React.UIEvent) => { - const target = e.target as HTMLDivElement; - - const { scrollTop, scrollHeight, clientHeight } = target; - - // 实现自动加载更多 - if (!loadCompleted && !loading && scrollHeight - scrollTop - clientHeight <= 0) { - const last = tableData[tableData.length - 1]; - setLoading(true); - getComparisonData(last?.run_id); + // 分页切换 + const handleTableChange: TableProps['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); } }; const columns: TableProps['columns'] = useMemo(() => { - const first: TableData | undefined = tableData[0]; + const first: TableData | undefined = tableData.find( + (item) => item.metrics_names && item.metrics_names.length > 0, + ); const metricsNames = first?.metrics_names ?? []; const paramsNames = first?.params_names ?? []; return [ @@ -171,7 +155,6 @@ function ExperimentComparison() { fixed: 'left', align: 'center', render: tableCellRender(true, TableCellValueType.Array), - ellipsis: { showTitle: false }, }, ], }, @@ -179,18 +162,13 @@ 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]), + sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), showSorterTooltip: false, })), }, @@ -198,41 +176,41 @@ 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]), + sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), showSorterTooltip: false, })), }, ]; - }, [tableData]); + }, [tableData, config]); return (
-
-
+
record.run_id || record.experiment_ins_id} /> 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/AddExperimentModal/index.tsx b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx index 2b7e80c5..38a88b3a 100644 --- a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx @@ -41,7 +41,7 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX. ); } if (isSensitive && Number(isSensitive) === 1) { - return ; + return ; } return ; }; diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less index 1d5bdc34..e524a987 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less @@ -1,12 +1,19 @@ .experiment-drawer { + line-height: var(--ant-line-height); :global { .ant-drawer-body { overflow-y: hidden; } + + .ant-drawer-close { + position: absolute; + top: 16px; + right: 16px; + } } &__tabs { - height: calc(100% - 170px); + height: calc(100% - 169px); :global { .ant-tabs-nav { padding-left: 24px; @@ -39,4 +46,10 @@ margin-right: 6px; border-radius: 50%; } + + &__log { + height: 100%; + padding: 8px; + background: white; + } } diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index 48cd0064..c1a70141 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -2,7 +2,7 @@ import { ExperimentStatus } from '@/enums'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { PipelineNodeModelSerialize } from '@/types'; import { elapsedTime, formatDate } from '@/utils/date'; -import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; +import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; import { Drawer, Tabs } from 'antd'; import { useMemo } from 'react'; import ExperimentParameter from '../ExperimentParameter'; @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ key: '1', label: '日志详情', children: ( - +
+ +
), icon: , }, @@ -88,15 +90,18 @@ const ExperimentDrawer = ({ instanceNodeStatus, workflowId, instanceNodeStartTime, + experimentName, + experimentId, + pipelineId, ], ); return ( } onClose={onClose} open={open} width={520} diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less index 31df6572..3201581f 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.less @@ -33,7 +33,7 @@ } .status { - width: 200px; + width: 160px; } .operation { @@ -54,16 +54,12 @@ .statusBox { display: flex; align-items: center; - width: 200px; + width: 160px; .statusIcon { - visibility: hidden; - transition: all 0.2s; + visibility: visible; } } - .statusBox:hover .statusIcon { - visibility: visible; - } } .loadMoreBox { diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 754034aa..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) => { @@ -98,6 +98,17 @@ function ExperimentInstanceComponent({ } }; + // 终止实验实例 + const handleTerminate = (instance: ExperimentInstance) => { + modalConfirm({ + title: '确定要终止此次实验运行吗?', + isDelete: false, + onOk: () => { + terminateExperimentInstance(instance); + }, + }); + }; + // 终止实验实例 const terminateExperimentInstance = async (instance: ExperimentInstance) => { const [res] = await to(putQueryByExperimentInsId(instance.id)); @@ -107,8 +118,8 @@ function ExperimentInstanceComponent({ } }; - if (!experimentInList || experimentInList.length === 0) { - return null; + if (!experimentInsList || experimentInsList.length === 0) { + return
暂无数据
; } return ( @@ -141,7 +152,7 @@ function ExperimentInstanceComponent({ - {experimentInList.map((item, index) => ( + {experimentInsList.map((item, index) => (
checkSingle(item.id)} + disabled={ + item.status === ExperimentStatus.Running || item.status === ExperimentStatus.Pending + } >
{elapsedTime(item.create_time, item.finish_time)}
- - {formatDate(item.create_time)} - + + {formatDate(item.create_time)} +
@@ -202,7 +216,7 @@ function ExperimentInstanceComponent({ item.status === ExperimentStatus.Terminated } icon={} - onClick={() => terminateExperimentInstance(item)} + onClick={() => handleTerminate(item)} > 终止 @@ -230,7 +244,7 @@ function ExperimentInstanceComponent({
))} - {experimentInsTotal > experimentInList.length ? ( + {experimentInsTotal > experimentInsList.length ? (