Browse Source

Merge remote-tracking branch 'origin/dev'

dev-lhz
chenzhihang 1 year ago
parent
commit
7206aec9dc
100 changed files with 2998 additions and 1147 deletions
  1. +2
    -0
      k8s/build-java.sh
  2. +7
    -6
      k8s/build-node.sh
  3. +6
    -1
      k8s/build.sh
  4. +1
    -1
      k8s/deploy.sh
  5. +1
    -1
      k8s/dockerfiles/auth-dockerfile
  6. +131
    -60
      k8s/dockerfiles/conf/nginx.conf
  7. +1
    -1
      k8s/dockerfiles/file-dockerfile
  8. +1
    -1
      k8s/dockerfiles/gateway-dockerfile
  9. +1
    -1
      k8s/dockerfiles/gen-dockerfile
  10. +1
    -1
      k8s/dockerfiles/job-dockerfile
  11. +3
    -3
      k8s/dockerfiles/managent-dockerfile
  12. +1
    -1
      k8s/dockerfiles/system-dockerfile
  13. +1
    -1
      k8s/dockerfiles/visual-dockerfile
  14. +8
    -0
      k8s/k8s-7management.yaml
  15. +20
    -3
      k8s/template-yaml/deploy/k8s-7management.yaml
  16. +6
    -1
      k8s/template-yaml/k8s-3nacos.yaml
  17. +19
    -2
      k8s/template-yaml/k8s-7management.yaml
  18. +8
    -1
      react-ui/.gitignore
  19. +3
    -3
      react-ui/config/config.ts
  20. +15
    -5
      react-ui/config/routes.ts
  21. +2
    -0
      react-ui/src/app.tsx
  22. BIN
      react-ui/src/assets/img/404.png
  23. BIN
      react-ui/src/assets/img/missing-back.png
  24. BIN
      react-ui/src/assets/img/modal-code-config.png
  25. BIN
      react-ui/src/assets/img/no-data.png
  26. BIN
      react-ui/src/assets/img/service-version.png
  27. BIN
      react-ui/src/assets/img/usage-icon.png
  28. +52
    -0
      react-ui/src/components/BasicInfo/index.less
  29. +98
    -0
      react-ui/src/components/BasicInfo/index.tsx
  30. +11
    -0
      react-ui/src/components/CodeSelect/index.less
  31. +71
    -0
      react-ui/src/components/CodeSelect/index.tsx
  32. +0
    -0
      react-ui/src/components/ErrorBoundary/index.less
  33. +78
    -0
      react-ui/src/components/ErrorBoundary/index.tsx
  34. +2
    -1
      react-ui/src/components/IFramePage/index.tsx
  35. +40
    -0
      react-ui/src/components/KFEmpty/index.less
  36. +67
    -0
      react-ui/src/components/KFEmpty/index.tsx
  37. +2
    -2
      react-ui/src/components/KFSpin/index.less
  38. +6
    -0
      react-ui/src/components/KFSpin/index.tsx
  39. +11
    -2
      react-ui/src/components/MenuIconSelector/index.less
  40. +6
    -0
      react-ui/src/components/MenuIconSelector/index.tsx
  41. +1
    -1
      react-ui/src/components/ModalTitle/index.tsx
  42. +6
    -0
      react-ui/src/components/ParameterInput/index.tsx
  43. +11
    -3
      react-ui/src/components/ParameterSelect/config.tsx
  44. +6
    -0
      react-ui/src/components/ParameterSelect/index.tsx
  45. +14
    -2
      react-ui/src/components/ResourceSelect/index.tsx
  46. +1
    -1
      react-ui/src/components/SubAreaTitle/index.tsx
  47. +23
    -10
      react-ui/src/enums/index.ts
  48. +3
    -0
      react-ui/src/global.less
  49. +1
    -1
      react-ui/src/iconfont/iconfont.js
  50. +4
    -0
      react-ui/src/overrides.less
  51. +17
    -15
      react-ui/src/pages/404.tsx
  52. +19
    -9
      react-ui/src/pages/CodeConfig/List/index.tsx
  53. +22
    -26
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less
  54. +18
    -9
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx
  55. +2
    -1
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.less
  56. +49
    -28
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  57. +54
    -31
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  58. +29
    -11
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  59. +2
    -0
      react-ui/src/pages/Dataset/components/CategoryItem/index.tsx
  60. +4
    -9
      react-ui/src/pages/Dataset/components/CategoryList/index.tsx
  61. +68
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.less
  62. +247
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  63. +5
    -60
      react-ui/src/pages/Dataset/components/ResourceIntro/index.less
  64. +224
    -134
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  65. +12
    -4
      react-ui/src/pages/Dataset/components/ResourceItem/index.tsx
  66. +4
    -0
      react-ui/src/pages/Dataset/components/ResourceList/index.less
  67. +53
    -31
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  68. +4
    -4
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  69. +18
    -136
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  70. +94
    -57
      react-ui/src/pages/Dataset/config.tsx
  71. +4
    -4
      react-ui/src/pages/Dataset/intro.tsx
  72. +1
    -2
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  73. +4
    -4
      react-ui/src/pages/Experiment/Comparison/index.tsx
  74. +4
    -1
      react-ui/src/pages/Experiment/Info/index.jsx
  75. +9
    -0
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  76. +2
    -0
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  77. +41
    -16
      react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx
  78. +6
    -1
      react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.tsx
  79. +95
    -77
      react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx
  80. +2
    -0
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx
  81. +2
    -0
      react-ui/src/pages/Experiment/index.jsx
  82. +5
    -3
      react-ui/src/pages/Mirror/Info/index.tsx
  83. +2
    -7
      react-ui/src/pages/Model/components/ModelEvolution/index.less
  84. +9
    -22
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  85. +72
    -59
      react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
  86. +2
    -0
      react-ui/src/pages/Model/components/NodeTooltips/index.less
  87. +23
    -23
      react-ui/src/pages/Model/components/NodeTooltips/index.tsx
  88. +4
    -4
      react-ui/src/pages/Model/intro.tsx
  89. +0
    -0
      react-ui/src/pages/ModelDeployment/CreateService/index.less
  90. +182
    -0
      react-ui/src/pages/ModelDeployment/CreateService/index.tsx
  91. +19
    -0
      react-ui/src/pages/ModelDeployment/CreateVersion/index.less
  92. +198
    -67
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  93. +67
    -119
      react-ui/src/pages/ModelDeployment/List/index.tsx
  94. +24
    -0
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.less
  95. +431
    -0
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  96. +1
    -1
      react-ui/src/pages/ModelDeployment/VersionInfo/index.less
  97. +30
    -19
      react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx
  98. +47
    -21
      react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
  99. +10
    -10
      react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.tsx
  100. +5
    -6
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx

+ 2
- 0
k8s/build-java.sh View File

@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash


baseDir="/home/somuns/ci4s" baseDir="/home/somuns/ci4s"


#判断$1是否为all,如果是,则编译所有模块,否则只编译management-platform模块 #判断$1是否为all,如果是,则编译所有模块,否则只编译management-platform模块
if [ "$1" == "all" ]; then if [ "$1" == "all" ]; then
buildDir=$baseDir buildDir=$baseDir


+ 7
- 6
k8s/build-node.sh View File

@@ -3,12 +3,13 @@
baseDir="/home/somuns/ci4s" baseDir="/home/somuns/ci4s"
cd ${baseDir}/react-ui cd ${baseDir}/react-ui


npm install

if [ $? -ne 0 ]; then
echo "Failed to install npm depend package"
exit 1
fi
#npm install -g npm@10.9.0
#npm install
#
#if [ $? -ne 0 ]; then
# echo "Failed to install npm depend package"
# exit 1
#fi




npm run build npm run build


+ 6
- 1
k8s/build.sh View File

@@ -43,18 +43,23 @@ cd ${baseDir}


# 拉取指定分支的最新代码 # 拉取指定分支的最新代码
echo "Checking out and pulling branch $branch..." echo "Checking out and pulling branch $branch..."

git stash
git checkout $branch git checkout $branch
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "切换到分支 $branch 失败,请检查分支名称是否正确!" echo "切换到分支 $branch 失败,请检查分支名称是否正确!"
exit 1 exit 1
fi fi


git stash
git pull origin $branch git pull origin $branch
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "拉取代码失败,请检查网络或联系管理员!" echo "拉取代码失败,请检查网络或联系管理员!"
exit 1 exit 1
fi fi


chmod +777 ${baseDir}/k8s/*.sh

# 创建目录 # 创建目录
mkdir -p ${baseDir}/k8s/dockerfiles/jar mkdir -p ${baseDir}/k8s/dockerfiles/jar
mkdir -p ${baseDir}/k8s/dockerfiles/html mkdir -p ${baseDir}/k8s/dockerfiles/html
@@ -134,7 +139,7 @@ fi


if [ "$service" == "all" ]; then if [ "$service" == "all" ]; then
# 编译前端 # 编译前端
compile_front
# compile_front


# 编译java # 编译java
compile_java "all" compile_java "all"


+ 1
- 1
k8s/deploy.sh View File

@@ -143,7 +143,7 @@ fi


if [ "$service" == "all" ]; then if [ "$service" == "all" ]; then
#部署前端 #部署前端
build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml"
# 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" build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml"
#部署认证中心 #部署认证中心


+ 1
- 1
k8s/dockerfiles/auth-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 131
- 60
k8s/dockerfiles/conf/nginx.conf View File

@@ -1,60 +1,131 @@
worker_processes 1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

server {
listen 8000;
server_name localhost;

location /api/{
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://ci4s-gateway-service.argo.svc:8082/;
}
location /label-studio {
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_pass http://label-studio-ls-app.label-data.svc:80/;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "ALLOW-FROM http://label-studio-ls-app.label-data.svc:80/";
}

location /api/v1/model/ {
proxy_pass http://pipeline-convert-service.argo.svc:80;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location / {
rewrite ^/prod-api/(.*)$ /$1 break;
root /home/ruoyi/projects/ruoyi-ui;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}

location @router {
rewrite ^.*$ /index.html last;
}

# 避免actuator暴露
if ($request_uri ~ "/actuator") {
return 403;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 20480m;
error_log /var/log/nginx/error.log debug;
server {
listen 8000;
server_name localhost;
location /api/{
# rewrite ^/prod-api/(.*)$ /$1 break;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://ci4s-gateway-service.argo.svc:8082/;
proxy_connect_timeout 500s; # 设置连接超时时间为 120 秒
proxy_read_timeout 500s; # 设置读取超时时间为 120 秒
proxy_send_timeout 500s; # 设置发送超时时间为 120 秒
}
location /label-studio/ {
# rewrite ^/label-studio/(.*)$ /$1 break;
proxy_pass http://label-studio-service.argo.svc:8080/projects/;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL;
}
location / {
rewrite ^/prod-api/(.*)$ /$1 break;
root /home/ruoyi/projects/ruoyi-ui;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
location /api/v1/model/ {
proxy_pass http://pipeline-convert-service.argo.svc:80;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# location /api/v1/realtimeStatus {
# proxy_pass http://argo-server.argo.svc:2746/api/v1/workflow-events/argo;
# proxy_set_header REMOTE-HOST $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# }
location /api/v1/tensorboard/show {
# 提取查询参数中的 `svc` 值
set $svc "";
if ($arg_svc) {
set $svc $arg_svc;
}
# 将请求转发到动态生成的内部服务地址
proxy_pass http://$svc.argo.svc:6006;
# 传递必要的头信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 对于 WebSocket 应用很重要
proxy_buffering off;
}
location /api/v1/realtimeStatus {
rewrite ^/api/v1/realtimeStatus(.*)$ /api/v1/workflow-events/argo$1 break;
proxy_pass https://argo-server.argo.svc:2746;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 保留查询参数
proxy_set_header X-Original-URI $request_uri;
# 禁用缓冲
proxy_buffering off;
# 增加超时时间
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 60s;
# 设置传递的请求头
# proxy_set_header Connection '';
# chunked_transfer_encoding off;
# 如果需要保留自定义头部
proxy_set_header Accept 'text/event-stream';
}
location /newlog/realtimeLog {
proxy_pass http://loki.loki-log.svc:3100/loki/api/v1/tail;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location @router {
rewrite ^.*$ /index.html last;
}
# 避免actuator暴露
if ($request_uri ~ "/actuator") {
return 403;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

+ 1
- 1
k8s/dockerfiles/file-dockerfile View File

@@ -1,5 +1,5 @@
# 基础镜像 # 基础镜像
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
#FROM openjdk:8-jre #FROM openjdk:8-jre
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi


+ 1
- 1
k8s/dockerfiles/gateway-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 1
- 1
k8s/dockerfiles/gen-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 1
- 1
k8s/dockerfiles/job-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 3
- 3
k8s/dockerfiles/managent-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
#FROM 172.20.32.187/ci4s/openjdk:8u162
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi


@@ -13,4 +13,4 @@ WORKDIR /home/ruoyi
# 复制jar文件到路径 # 复制jar文件到路径
COPY ./jar/management-platform.jar /home/ruoyi/management-platform.jar COPY ./jar/management-platform.jar /home/ruoyi/management-platform.jar
# 启动系统服务 # 启动系统服务
ENTRYPOINT ["java","-jar","management-platform.jar"]
ENTRYPOINT ["java","-jar","-Djdk.tls.client.protocols=TLSv1.2","management-platform.jar"]

+ 1
- 1
k8s/dockerfiles/system-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 1
- 1
k8s/dockerfiles/visual-dockerfile View File

@@ -1,6 +1,6 @@
# 基础镜像 # 基础镜像
#FROM openjdk:8-jre #FROM openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk:8-jre
FROM 172.20.32.187/ci4s/openjdk-dvc:2024829
# author # author
MAINTAINER ruoyi MAINTAINER ruoyi




+ 8
- 0
k8s/k8s-7management.yaml View File

@@ -18,6 +18,14 @@ spec:
image: 172.20.32.187/ci4s/managent:20240401 image: 172.20.32.187/ci4s/managent:20240401
ports: ports:
- containerPort: 9213 - containerPort: 9213
volumeMounts:
- name: resource
mountPath: /home/resource/
volumes:
- name: resource
hostPath:
path: /home/resource/
type: DirectoryOrCreate


--- ---
apiVersion: v1 apiVersion: v1


+ 20
- 3
k8s/template-yaml/deploy/k8s-7management.yaml View File

@@ -15,10 +15,21 @@ spec:
spec: spec:
containers: containers:
- name: ci4s-management-platform - name: ci4s-management-platform
image: 172.20.32.187/ci4s/managent:202406121003
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: ports:
- containerPort: 9213 - containerPort: 9213

volumeMounts:
- name: resource-volume
mountPath: /home/resource/
volumes:
- name: resource-volume
persistentVolumeClaim:
claimName: platform-data-pvc-nfs
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@@ -28,9 +39,15 @@ metadata:
spec: spec:
type: NodePort type: NodePort
ports: ports:
- port: 9213
- name: http
port: 9213
nodePort: 31208 nodePort: 31208
protocol: TCP protocol: TCP
- name: debug
nodePort: 34567
port: 5005
protocol: TCP
targetPort: 5005
selector: selector:
app: ci4s-management-platform app: ci4s-management-platform



+ 6
- 1
k8s/template-yaml/k8s-3nacos.yaml View File

@@ -36,6 +36,7 @@ spec:
ports: ports:
- containerPort: 8848 - containerPort: 8848
- containerPort: 9848 - containerPort: 9848
- containerPort: 9849
restartPolicy: Always restartPolicy: Always


--- ---
@@ -58,5 +59,9 @@ spec:
name: web name: web
- port: 9848 - port: 9848
targetPort: 9848 targetPort: 9848
nodePort: 31204
nodePort: 32203
name: podsa name: podsa
- port: 9849
targetPort: 9849
nodePort: 32204
name: tcp-9849

+ 19
- 2
k8s/template-yaml/k8s-7management.yaml View File

@@ -16,9 +16,20 @@ spec:
containers: containers:
- name: ci4s-management-platform - name: ci4s-management-platform
image: ${k8s-7management-image} image: ${k8s-7management-image}
env:
- name: TZ
value: Asia/Shanghai
- name: JAVA_TOOL_OPTIONS
value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
ports: ports:
- containerPort: 9213 - containerPort: 9213

volumeMounts:
- name: resource-volume
mountPath: /home/resource/
volumes:
- name: resource-volume
persistentVolumeClaim:
claimName: platform-data-pvc-nfs
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@@ -28,9 +39,15 @@ metadata:
spec: spec:
type: NodePort type: NodePort
ports: ports:
- port: 9213
- name: http
port: 9213
nodePort: 31208 nodePort: 31208
protocol: TCP protocol: TCP
- name: debug
nodePort: 31219
port: 5005
protocol: TCP
targetPort: 5005
selector: selector:
app: ci4s-management-platform app: ci4s-management-platform



+ 8
- 1
react-ui/.gitignore View File

@@ -40,4 +40,11 @@ screenshot


build build


pnpm-lock.yaml
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

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

@@ -150,9 +150,9 @@ export default defineConfig({
projectName: 'swagger', projectName: 'swagger',
}, },
], ],
mfsu: {
strategy: 'normal',
},
// mfsu: {
// strategy: 'normal',
// },
requestRecord: {}, requestRecord: {},
icons: {}, icons: {},
lessLoader: { lessLoader: {


+ 15
- 5
react-ui/config/routes.ts View File

@@ -221,14 +221,24 @@ export default [
component: './ModelDeployment/List', component: './ModelDeployment/List',
}, },
{ {
name: '模型部署详情',
path: 'info/:id',
component: './ModelDeployment/Info',
name: '服务详情',
path: 'serviceInfo/:id',
component: './ModelDeployment/ServiceInfo',
},
{
name: '服务版本详情',
path: 'versionInfo/:id',
component: './ModelDeployment/VersionInfo',
}, },
{ {
name: '创建推理服务', name: '创建推理服务',
path: 'create',
component: './ModelDeployment/Create',
path: 'createService',
component: './ModelDeployment/CreateService',
},
{
name: '新增服务版本',
path: 'addVersion/:id',
component: './ModelDeployment/CreateVersion',
}, },
], ],
}, },


+ 2
- 0
react-ui/src/app.tsx View File

@@ -22,6 +22,7 @@ export { requestConfig as request } from './requestConfig';
// const isDev = process.env.NODE_ENV === 'development'; // const isDev = process.env.NODE_ENV === 'development';
import { type GlobalInitialState } from '@/types'; import { type GlobalInitialState } from '@/types';
import { menuItemRender } from '@/utils/menuRender'; import { menuItemRender } from '@/utils/menuRender';
import ErrorBoundary from './components/ErrorBoundary';
import { gotoLoginPage } from './utils/ui'; import { gotoLoginPage } from './utils/ui';


/** /**
@@ -65,6 +66,7 @@ export async function getInitialState(): Promise<GlobalInitialState> {
// ProLayout 支持的api https://procomponents.ant.design/components/layout // ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RuntimeConfig['layout'] = ({ initialState }) => { export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
return { return {
ErrorBoundary: ErrorBoundary,
rightContentRender: false, rightContentRender: false,
waterMarkProps: { waterMarkProps: {
// content: initialState?.currentUser?.nickName, // content: initialState?.currentUser?.nickName,


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

Before After
Width: 1350  |  Height: 904  |  Size: 293 kB

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

Before After
Width: 950  |  Height: 583  |  Size: 154 kB Width: 950  |  Height: 583  |  Size: 114 kB

BIN
react-ui/src/assets/img/modal-code-config.png View File

Before After
Width: 60  |  Height: 60  |  Size: 3.1 kB

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

Before After
Width: 1425  |  Height: 1027  |  Size: 343 kB

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

Before After
Width: 45  |  Height: 45  |  Size: 852 B

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

Before After
Width: 45  |  Height: 45  |  Size: 1.1 kB

+ 52
- 0
react-ui/src/components/BasicInfo/index.less View File

@@ -0,0 +1,52 @@
.kf-basic-info {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px 40px;
align-items: flex-start;
width: 80%;
}

.kf-basic-info-item {
display: flex;
align-items: flex-start;
width: calc(50% - 20px);
font-size: 16px;
line-height: 1.6;

&__label {
position: relative;
flex: none;
color: @text-color-secondary;
text-align: justify;
text-align-last: justify;

&::after {
position: absolute;
content: ':';
}
}

&__list-value {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px 0;
}

&__value {
flex: 1;
margin-left: 16px;
white-space: pre-line;
word-break: break-all;
}

&__text {
color: @text-color;
}

&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
}
}

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

@@ -0,0 +1,98 @@
import { Link } from '@umijs/max';
import classNames from 'classnames';
import './index.less';

export type BasicInfoLink = {
value: string;
link?: string;
url?: string;
};

export type BasicInfoData = {
label: string;
value?: any;
format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined;
};

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

function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) {
return (
<div className={classNames('kf-basic-info', className)} style={style}>
{datas.map((item) => (
<BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} />
))}
</div>
);
}

type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth?: number;
};
function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) {
const { label, value, format } = data;
const formatValue = format ? format(value) : value;
let valueComponent = undefined;
if (Array.isArray(formatValue)) {
valueComponent = (
<div className="kf-basic-info-item__list-value">
{formatValue.map((item: BasicInfoLink) => (
<BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} />
))}
</div>
);
} else if (typeof formatValue === 'object' && formatValue) {
valueComponent = (
<BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} />
);
} else {
valueComponent = <BasicInfoItemValue value={formatValue} />;
}
return (
<div className="kf-basic-info-item" key={label}>
<div className="kf-basic-info-item__label" style={{ width: labelWidth }}>
{label}
</div>
{valueComponent}
</div>
);
}

type BasicInfoItemValueProps = {
value: string;
link?: string;
url?: string;
};

function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) {
if (url && value) {
return (
<a
className="kf-basic-info-item__value kf-basic-info-item__link"
href={url}
target="_blank"
rel="noopener noreferrer"
>
{value}
</a>
);
} else if (link && value) {
return (
<Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link">
{value}
</Link>
);
} else {
return (
<div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div>
);
}
}

export default BasicInfo;

+ 11
- 0
react-ui/src/components/CodeSelect/index.less View File

@@ -0,0 +1,11 @@
.kf-code-select {
position: relative;
display: flex;
align-items: center;

&__button {
position: absolute;
top: 0;
left: calc(100% + 10px);
}
}

+ 71
- 0
react-ui/src/components/CodeSelect/index.tsx View File

@@ -0,0 +1,71 @@
/*
* @Author: 赵伟
* @Date: 2024-10-08 15:36:08
* @Description: 代码配置选择表单组件
*/

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

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

type CodeSelectProps = ParameterInputProps;

function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) {
const selectResource = () => {
const { close } = openAntdModal(CodeSelectorModal, {
onOk: (res) => {
if (res) {
const { git_url, git_branch, code_repo_name } = res;
const jsonObj = {
code_path: git_url,
branch: git_branch,
};
const jsonObjStr = JSON.stringify(jsonObj);
const showValue = code_repo_name;
onChange?.({
value: jsonObjStr,
showValue,
fromSelect: true,
...jsonObj,
});
} else {
onChange?.({
value: undefined,
showValue: undefined,
fromSelect: false,
});
}
close();
},
});
};

return (
<div className="kf-code-select">
<ParameterInput
{...rest}
disabled={disabled}
value={value}
onChange={onChange}
onClick={selectResource}
></ParameterInput>
<Button
className="kf-code-select__button"
size="large"
type="link"
icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />}
disabled={disabled}
onClick={selectResource}
>
选择代码配置
</Button>
</div>
);
}

export default CodeSelect;

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


+ 78
- 0
react-ui/src/components/ErrorBoundary/index.tsx View File

@@ -0,0 +1,78 @@
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import { Button } from 'antd';
import { Component, ReactNode } from 'react';

interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode; // Optional fallback UI to show in case of error
}

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
};
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render shows the fallback UI
return { hasError: true, error };
}

// componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// // You can log the error to an error reporting service here
// console.error('Error caught by ErrorBoundary:', error.message, errorInfo.componentStack);
// }

render() {
if (this.state.hasError) {
return this.props.fallback || <ErrorBoundaryFallback error={this.state.error} />;
}
return this.props.children;
}
}

function ErrorBoundaryFallback({ error }: { error: Error | null }) {
const message = error && error instanceof Error ? error.message : 'Unknown error';
const errorMsg =
process.env.NODE_ENV === 'development' ? message : '非常抱歉,程序运行错误,\n我们会尽快修复。';
return (
<KFEmpty
style={{ height: '100vh' }}
type={EmptyType.NotFound}
title="出错了"
content={errorMsg}
footer={() => {
return (
<>
<Button
type="default"
onClick={() => {
window.history.pushState({}, '', '/');
window.location.reload();
}}
>
返回首页
</Button>
<Button
type="primary"
style={{ marginLeft: 20 }}
onClick={() => window.location.reload()}
>
刷新
</Button>
</>
);
}}
></KFEmpty>
);
}

export default ErrorBoundary;

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

@@ -9,6 +9,7 @@ import {
} from '@/utils/sessionStorage'; } from '@/utils/sessionStorage';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import './index.less'; import './index.less';


export enum IframePageType { export enum IframePageType {
@@ -61,7 +62,7 @@ function IframePage({ type, className, style }: IframePageProps) {


return ( return (
<div className={classNames('kf-iframe-page', className)} style={style}> <div className={classNames('kf-iframe-page', className)} style={style}>
{loading && <KFSpin />}
{loading && createPortal(<KFSpin size="large" />, document.body)}
<FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} /> <FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} />
</div> </div>
); );


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

@@ -0,0 +1,40 @@
.kf-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;

&__image {
width: 475px;
}

&__title {
margin-top: 15px;
color: @text-color;
font-weight: 500;
font-size: 30px;
text-align: center;
}

&__content {
max-width: 50%;
margin-top: 15px;
color: @text-color-secondary;
font-size: 15px;
white-space: pre-line;
text-align: center;
}

&__footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
margin-bottom: 30px;

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

+ 67
- 0
react-ui/src/components/KFEmpty/index.tsx View File

@@ -0,0 +1,67 @@
import { Button } from 'antd';
import classNames from 'classnames';
import './index.less';

export enum EmptyType {
NoData = 'NoData',
NotFound = 'NotFound',
Developing = 'Developing',
}

type EmptyProps = {
className?: string;
style?: React.CSSProperties;
type: EmptyType;
title?: string;
content?: string;
hasFooter?: boolean;
footer?: () => React.ReactNode;
buttonTitle?: string;
onRefresh?: () => void;
};

function getEmptyImage(type: EmptyType) {
switch (type) {
case EmptyType.NoData:
return require('@/assets/img/no-data.png');
case EmptyType.NotFound:
return require('@/assets/img/404.png');
case EmptyType.Developing:
return require('@/assets/img/missing-back.png');
}
}

function KFEmpty({
className,
style,
type,
title,
content,
hasFooter = true,
footer,
buttonTitle = '刷新',
onRefresh,
}: EmptyProps) {
const image = getEmptyImage(type);

return (
<div className={classNames('kf-empty', className)} style={style}>
<img className="kf-empty__image" src={image} draggable={false} alt="" />
<div className="kf-empty__title">{title}</div>
<div className="kf-empty__content">{content}</div>
{hasFooter && (
<div className="kf-empty__footer">
{footer ? (
footer()
) : (
<Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}>
{buttonTitle}
</Button>
)}
</div>
)}
</div>
);
}

export default KFEmpty;

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

@@ -4,12 +4,12 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 1000;
z-index: 1001;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(255, 255, 255, 0.5);
background-color: rgba(255, 255, 255, 0.3);


&__label { &__label {
margin-top: 20px; margin-top: 20px;


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

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-09-02 08:42:57
* @Description: 自定义 Spin
*/

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




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

@@ -1,16 +1,25 @@
.menu-icon-selector { .menu-icon-selector {
// grid 布局,每行显示 8 个图标 // grid 布局,每行显示 8 个图标
display: grid; display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(4, 80px);
gap: 20px;
justify-content: space-between;
width: 100%;


&__item { &__item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 80x;
height: 80px; height: 80px;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer; cursor: pointer;


&:hover {
border-color: @primary-color;
}

&__icon { &__icon {
display: block; display: block;
} }


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

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-04-17 16:59:42
* @Description: 菜单图标选择器
*/

import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import iconData from '@/iconfont/iconfont-menu.json'; import iconData from '@/iconfont/iconfont-menu.json';


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

@@ -18,7 +18,7 @@ type ModalTitleProps = {
function ModalTitle({ title, image, style, className }: ModalTitleProps) { function ModalTitle({ title, image, style, className }: ModalTitleProps) {
return ( return (
<div className={classNames('kf-modal-title', className)} style={style}> <div className={classNames('kf-modal-title', className)} style={style}>
{image && <img className={'kf-modal-title__image'} src={image} alt="" />}
{image && <img className={'kf-modal-title__image'} src={image} draggable={false} alt="" />}
{title} {title}
</div> </div>
); );


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

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 08:42:57
* @Description: 参数输入组件
*/

import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';


+ 11
- 3
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -14,7 +14,15 @@ const filterResourceStandard: SelectProps<string, ComputingResource>['filterOpti
}; };


// id 从 number 转换为 string // id 从 number 转换为 string
const convertId = (item: any) => ({ ...item, id: String(item.id) });
const convertId = (item: any) => ({
...item,
id: JSON.stringify({
id: `${item.id}`,
name: item.name,
identifier: item.identifier,
owner: item.owner,
}),
});


export type SelectPropsConfig = { export type SelectPropsConfig = {
getOptions: () => Promise<any>; // 获取下拉数据 getOptions: () => Promise<any>; // 获取下拉数据
@@ -29,7 +37,7 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
const res = await getDatasetList({ const res = await getDatasetList({
page: 0, page: 0,
size: 1000, size: 1000,
available_range: 0,
is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? []; return res?.data?.content?.map(convertId) ?? [];
}, },
@@ -44,7 +52,7 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
const res = await getModelList({ const res = await getModelList({
page: 0, page: 0,
size: 1000, size: 1000,
available_range: 0,
is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? []; return res?.data?.content?.map(convertId) ?? [];
}, },


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

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 08:42:57
* @Description: 参数选择组件
*/

import { PipelineNodeModelParameter } from '@/types'; import { PipelineNodeModelParameter } from '@/types';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Select } from 'antd'; import { Select } from 'antd';


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

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 数据集、模型、镜像选择表单组件
*/

import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import ResourceSelectorModal, { import ResourceSelectorModal, {
ResourceSelectorResponse, ResourceSelectorResponse,
@@ -11,6 +17,7 @@ import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less'; import './index.less';


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


type ResourceSelectProps = { type ResourceSelectProps = {
type: ResourceSelectorType; type: ResourceSelectorType;
@@ -21,7 +28,7 @@ const getSelectBtnIcon = (type: ResourceSelectorType) => {
return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />;
}; };


function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps) {
function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) {
const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>(
undefined, undefined,
); );
@@ -36,7 +43,7 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
onOk: (res) => { onOk: (res) => {
setSelectedResource(res); setSelectedResource(res);
if (res) { if (res) {
const { activeTab, id, name, version, path } = res;
const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
onChange?.({ onChange?.({
value: path, value: path,
@@ -49,8 +56,11 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
} else { } else {
const jsonObj = { const jsonObj = {
id, id,
name,
version, version,
path, path,
identifier,
owner,
}; };
const jsonObjStr = JSON.stringify(jsonObj); const jsonObjStr = JSON.stringify(jsonObj);
const showValue = `${name}:${version}`; const showValue = `${name}:${version}`;
@@ -83,6 +93,7 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
<div className="kf-resource-select"> <div className="kf-resource-select">
<ParameterInput <ParameterInput
{...rest} {...rest}
disabled={disabled}
value={value} value={value}
onChange={onChange} onChange={onChange}
onRemove={() => setSelectedResource(undefined)} onRemove={() => setSelectedResource(undefined)}
@@ -93,6 +104,7 @@ function ResourceSelect({ type, value, onChange, ...rest }: ResourceSelectProps)
size="large" size="large"
type="link" type="link"
icon={getSelectBtnIcon(type)} icon={getSelectBtnIcon(type)}
disabled={disabled}
onClick={selectResource} onClick={selectResource}
> >
{selectorTypeConfig[type].buttontTitle} {selectorTypeConfig[type].buttontTitle}


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

@@ -17,7 +17,7 @@ type SubAreaTitleProps = {
function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) {
return ( return (
<div className={classNames('kf-subarea-title', className)} style={style}> <div className={classNames('kf-subarea-title', className)} style={style}>
<img src={image} width={14} />
<img src={image} width={14} draggable={false} alt="" />
<span style={{ marginLeft: '8px' }}>{title}</span> <span style={{ marginLeft: '8px' }}>{title}</span>
</div> </div>
); );


+ 23
- 10
react-ui/src/enums/index.ts View File

@@ -44,8 +44,8 @@ export enum MirrorVersionStatus {
Failed = 'failed', // 构建中 Failed = 'failed', // 构建中
} }


// 模型部署状态
export enum ModelDeploymentStatus {
// 服务运行状态
export enum ServiceRunStatus {
Init = 'Init', // 启动中 Init = 'Init', // 启动中
Running = 'Running', // 运行中 Running = 'Running', // 运行中
Stopped = 'Stopped', // 已停止 Stopped = 'Stopped', // 已停止
@@ -53,14 +53,13 @@ export enum ModelDeploymentStatus {
Pending = 'Pending', // 挂起中 Pending = 'Pending', // 挂起中
} }


// 模型部署状态选项列表
export const modelDeploymentStatusOptions = [
{ label: '全部', value: '' },
{ label: '启动中', value: ModelDeploymentStatus.Init },
{ label: '运行中', value: ModelDeploymentStatus.Running },
{ label: '已停止', value: ModelDeploymentStatus.Stopped },
{ label: '失败', value: ModelDeploymentStatus.Failed },
{ label: '挂起中', value: ModelDeploymentStatus.Pending },
// 服务运行状态选项列表
export const serviceStatusOptions = [
{ label: '启动中', value: ServiceRunStatus.Init },
{ label: '运行中', value: ServiceRunStatus.Running },
{ label: '已停止', value: ServiceRunStatus.Stopped },
{ label: '失败', value: ServiceRunStatus.Failed },
{ label: '挂起中', value: ServiceRunStatus.Pending },
]; ];


// 开发环境编辑器状态 // 开发环境编辑器状态
@@ -71,3 +70,17 @@ export enum DevEditorStatus {
Failed = 'Failed', // 失败 Failed = 'Failed', // 失败
Unknown = 'Unknown', // 未启动 Unknown = 'Unknown', // 未启动
} }

export enum ServiceType {
Video = 'video',
Image = 'image',
Audio = 'audio',
Text = 'text',
}

export const serviceTypeOptions = [
{ label: '视频', value: ServiceType.Video },
{ label: '图像', value: ServiceType.Image },
{ label: '音频', value: ServiceType.Audio },
{ label: '文本', value: ServiceType.Text },
];

+ 3
- 0
react-ui/src/global.less View File

@@ -101,15 +101,18 @@ body {


::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px;
background: transparent; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
width: 8px; width: 8px;
height: 8px;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
border-radius: 99px; border-radius: 99px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
width: 8px; width: 8px;
height: 8px;
background: transparent; background: transparent;
} }
ul, ul,


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


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

@@ -116,6 +116,10 @@
} }
} }


.ant-input.ant-input-disabled {
height: 46px;
}

// 选择框高度为46px // 选择框高度为46px
.ant-select-single { .ant-select-single {
height: 46px; height: 46px;


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

@@ -1,18 +1,20 @@
import { history } from '@umijs/max';
import { Button, Result } from 'antd';
import React from 'react';
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import { useNavigate } from '@umijs/max';


const NoFoundPage: React.FC = () => (
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary" onClick={() => history.push('/')}>
Back Home
</Button>
}
/>
);
const NoFoundPage = () => {
const navigate = useNavigate();

return (
<KFEmpty
style={{ height: '100vh' }}
type={EmptyType.NotFound}
title="404"
content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'}
hasFooter={true}
buttonTitle="返回首页"
onRefresh={() => navigate('/')}
></KFEmpty>
);
};


export default NoFoundPage; export default NoFoundPage;

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

@@ -1,9 +1,10 @@
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig'; import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { App, Button, Empty, Input, Pagination, PaginationProps } from 'antd';
import { App, Button, Input, Pagination, PaginationProps } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal';
import CodeConfigItem from '../components/CodeConfigItem'; import CodeConfigItem from '../components/CodeConfigItem';
@@ -31,7 +32,7 @@ export type ResourceListRef = {
}; };


function CodeConfigList() { function CodeConfigList() {
const [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>({ const [pagination, setPagination] = useState<PaginationProps>({
current: 1, current: 1,
@@ -56,6 +57,9 @@ function CodeConfigList() {
if (res && res.data && res.data.content) { if (res && res.data && res.data.content) {
setDataList(res.data.content); setDataList(res.data.content);
setTotal(res.data.totalElements); setTotal(res.data.totalElements);
} else {
setDataList([]);
setTotal(0);
} }
}; };


@@ -117,7 +121,7 @@ function CodeConfigList() {
return ( return (
<div className={styles['code-config-list']}> <div className={styles['code-config-list']}>
<div className={styles['code-config-list__header']}> <div className={styles['code-config-list__header']}>
<span>数据总数:{total}个</span>
<span>数据总数:{total} 个</span>
<div> <div>
<Input.Search <Input.Search
placeholder="按代码仓库名称筛选" placeholder="按代码仓库名称筛选"
@@ -139,10 +143,10 @@ function CodeConfigList() {
</Button> </Button>
</div> </div>
</div> </div>
{dataList?.length !== 0 ? (
{dataList && dataList.length !== 0 && (
<> <>
<div className={styles['code-config-list__content']}> <div className={styles['code-config-list__content']}>
{dataList?.map((item) => (
{dataList.map((item) => (
<CodeConfigItem <CodeConfigItem
item={item} item={item}
key={item.id} key={item.id}
@@ -161,10 +165,16 @@ function CodeConfigList() {
{...pagination} {...pagination}
/> />
</> </>
) : (
<div className={styles['code-config-list__empty']}>
<Empty></Empty>
</div>
)}
{dataList && dataList.length === 0 && (
<KFEmpty
className={styles['code-config-list__empty']}
type={EmptyType.NoData}
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
/>
)} )}
</div> </div>
); );


+ 22
- 26
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less View File

@@ -14,35 +14,43 @@
} }


&__name { &__name {
position: relative;
display: inline-block;
height: 24px;
margin: 0 10px 0 0 !important;
margin-right: 10px;
margin-bottom: 0 !important;
color: @text-color; color: @text-color;
font-size: 16px; font-size: 16px;
} }


&__tag { &__tag {
padding: 4px;
color: @primary-color;
padding: 2px 11px;
font-size: 12px; font-size: 12px;
background-color: .addAlpha(@primary-color, 0.1) [];
border-radius: 4px;
border-radius: 1000px;

&--public {
color: @primary-color;
background-color: .addAlpha(@primary-color, 0.08) [];
border-color: .addAlpha(@primary-color, 0.5) [];
}

&--private {
color: @warning-color;
background-color: .addAlpha(@warning-color, 0.08) [];
border-color: .addAlpha(@warning-color, 0.5) [];
}
} }


&__url { &__url {
margin-bottom: 10px;
color: @text-color-secondary; color: @text-color-secondary;
font-size: 14px; font-size: 14px;
} }


&__description {
height: 44px;
&__branch {
margin-bottom: 20px; margin-bottom: 20px;
color: @text-color-secondary;
color: @text-color-tertiary;
font-size: 14px; font-size: 14px;
.multiLine(2);
} }


&__user,
&__time { &__time {
display: flex; display: flex;
flex: 0 1 content; flex: 0 1 content;
@@ -55,21 +63,9 @@
&:hover { &:hover {
border-color: @primary-color; border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);

.resource-item__name {
color: @primary-color;
}
} }
}


.resource-item__name {
&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(to right, rgba(22, 100, 255, 0.3) 0, rgba(22, 100, 255, 0) 100%);
content: '';
&:hover &__name {
color: @primary-color;
} }
} }

+ 18
- 9
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx View File

@@ -5,6 +5,7 @@ import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List'; import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { Button, Flex, Typography } from 'antd'; import { Button, Flex, Typography } from 'antd';
import classNames from 'classnames';
import styles from './index.less'; import styles from './index.less';


type CodeConfigItemProps = { type CodeConfigItemProps = {
@@ -24,10 +25,16 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps
> >
{item.code_repo_name} {item.code_repo_name}
</Typography.Paragraph> </Typography.Paragraph>
<div className={styles['code-config-item__tag']}>
<div
className={classNames(
styles['code-config-item__tag'],
item.code_repo_vis === AvailableRange.Public
? styles['code-config-item__tag--public']
: styles['code-config-item__tag--private'],
)}
>
{item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'}
</div> </div>

<Button <Button
type="text" type="text"
shape="circle" shape="circle"
@@ -54,20 +61,22 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps
<Typography.Paragraph <Typography.Paragraph
className={styles['code-config-item__url']} className={styles['code-config-item__url']}
ellipsis={{ tooltip: item.git_url }} ellipsis={{ tooltip: item.git_url }}
style={{ marginBottom: '8px' }}
> >
{item.git_url} {item.git_url}
</Typography.Paragraph> </Typography.Paragraph>
<div className={styles['code-config-item__url']} style={{ marginBottom: '20px' }}>
{item.git_branch}
</div>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
<Flex justify="space-between"> <Flex justify="space-between">
<div className={styles['code-config-item__time']}>
<img style={{ width: '17px', marginRight: '6px' }} src={creatByImg} alt="" />
<div className={styles['code-config-item__user']}>
<img
style={{ width: '17px', marginRight: '6px' }}
src={creatByImg}
alt=""
draggable={false}
/>
<span>{item.create_by}</span> <span>{item.create_by}</span>
</div> </div>
<div className={styles['code-config-item__time']}> <div className={styles['code-config-item__time']}>
<img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" />
<img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" draggable={false} />
<span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span> <span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span>
</div> </div>
</Flex> </Flex>


+ 2
- 1
react-ui/src/pages/Dataset/components/AddDatasetModal/index.less View File

@@ -1,6 +1,7 @@
.upload-tip { .upload-tip {
margin-top: 5px; margin-top: 5px;
color: @error-color;
color: @text-color-secondary;
font-size: 14px;
} }


.upload-button { .upload-button {


+ 49
- 28
react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx View File

@@ -1,9 +1,8 @@
import { getAccessToken } from '@/access'; import { getAccessToken } from '@/access';
import { DictValueEnumObj } from '@/components/DictTag';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { addDatesetAndVesion } from '@/services/dataset/index.js';
import { getDictSelectOption } from '@/services/system/dict';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { addDataset } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { import {
@@ -19,8 +18,7 @@ import {
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import { CategoryData } from '../../config';
import { useState } from 'react';
import styles from './index.less'; import styles from './index.less';


interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
@@ -31,15 +29,15 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {


function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]);
// const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]);


useEffect(() => {
getClusterOptions();
}, []);
// useEffect(() => {
// getClusterOptions();
// }, []);


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
action: '/api/mmp/dataset/upload',
action: resourceConfig[ResourceType.Dataset].uploadAction,
headers: { headers: {
Authorization: getAccessToken() || '', Authorization: getAccessToken() || '',
}, },
@@ -47,16 +45,16 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
}; };


// 获取集群版本数据 // 获取集群版本数据
const getClusterOptions = async () => {
const [res] = await to(getDictSelectOption('available_cluster'));
if (res) {
setClusterOptions(res);
}
};
// const getClusterOptions = async () => {
// const [res] = await to(getDictSelectOption('available_cluster'));
// if (res) {
// setClusterOptions(res);
// }
// };


// 上传请求 // 上传请求
const createDataset = async (params: any) => { const createDataset = async (params: any) => {
const [res] = await to(addDatesetAndVesion(params));
const [res] = await to(addDataset(params));
if (res) { if (res) {
message.success('创建成功'); message.success('创建成功');
onOk?.(); onOk?.();
@@ -69,6 +67,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
if (validateUploadFiles(fileList)) { if (validateUploadFiles(fileList)) {
const params = { const params = {
...omit(formData, ['fileList']), ...omit(formData, ['fileList']),
dataset_source: DataSource.Create,
dataset_version_vos: fileList.map((item) => { dataset_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {}; const data = item.response?.data?.[0] ?? {};
return { return {
@@ -94,7 +93,13 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
}} }}
destroyOnClose destroyOnClose
> >
<Form name="form" layout="vertical" onFinish={onFinish} autoComplete="off">
<Form
name="form"
layout="vertical"
onFinish={onFinish}
initialValues={{ is_public: false }}
autoComplete="off"
>
<Form.Item <Form.Item
label="数据集名称" label="数据集名称"
name="name" name="name"
@@ -106,7 +111,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
}, },
]} ]}
> >
<Input placeholder="请输入数据名称" showCount allowClear maxLength={64} />
<Input placeholder="请输入数据名称" showCount allowClear maxLength={50} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="数据集版本" label="数据集版本"
@@ -116,6 +121,18 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
required: true, required: true,
message: '请输入数据集版本', message: '请输入数据集版本',
}, },
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、下划线、点、横杠',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
}
return Promise.resolve();
},
},
]} ]}
> >
<Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} /> <Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} />
@@ -125,7 +142,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
allowClear allowClear
placeholder="请选择数据集分类" placeholder="请选择数据集分类"
options={typeList} options={typeList}
fieldNames={{ label: 'name', value: 'id' }}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
showSearch showSearch
/> />
@@ -135,14 +152,14 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
allowClear allowClear
placeholder="请选择研究方向/应用领域" placeholder="请选择研究方向/应用领域"
options={tagList} options={tagList}
fieldNames={{ label: 'name', value: 'id' }}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
showSearch showSearch
/> />
</Form.Item> </Form.Item>
<Form.Item label="集群版本" name="available_cluster">
{/* <Form.Item label="集群版本" name="available_cluster">
<Select allowClear placeholder="请选择集群版本" options={clusterOptions} /> <Select allowClear placeholder="请选择集群版本" options={clusterOptions} />
</Form.Item>
</Form.Item> */}
<Form.Item <Form.Item
label="数据集简介" label="数据集简介"
name="description" name="description"
@@ -156,15 +173,19 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
<Input.TextArea <Input.TextArea
placeholder="请输入数据集简介" placeholder="请输入数据集简介"
showCount showCount
maxLength={256}
maxLength={200}
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
allowClear allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item label="选择流水线" name="range">
<Form.Item
label="可见性"
name="is_public"
rules={[{ required: true, message: '请选择可见性' }]}
>
<Radio.Group> <Radio.Group>
<Radio value="0">仅自己可见</Radio>
<Radio value="1">工作空间可见</Radio>
<Radio value={false}>私有</Radio>
<Radio value={true}>公开</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -187,7 +208,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
> >
上传文件 上传文件
</Button> </Button>
<div className={styles['upload-tip']}>只允许上传.zip,.tgz格式文件</div>
<div className={styles['upload-tip']}>只允许上传 .zip 和 .tgz 格式文件</div>
</Upload> </Upload>
</Form.Item> </Form.Item>
</Form> </Form>


+ 54
- 31
react-ui/src/pages/Dataset/components/AddModelModal/index.tsx View File

@@ -1,7 +1,7 @@
import { getAccessToken } from '@/access'; import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData } from '@/pages/Dataset/config';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js'; import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
@@ -9,6 +9,7 @@ import {
Button, Button,
Form, Form,
Input, Input,
Radio,
Select, Select,
Upload, Upload,
UploadFile, UploadFile,
@@ -31,7 +32,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
action: '/api/mmp/models/upload',
action: resourceConfig[ResourceType.Model].uploadAction,
headers: { headers: {
Authorization: getAccessToken() || '', Authorization: getAccessToken() || '',
}, },
@@ -53,7 +54,8 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
if (validateUploadFiles(fileList)) { if (validateUploadFiles(fileList)) {
const params = { const params = {
...omit(formData, ['fileList']), ...omit(formData, ['fileList']),
models_version_vos: fileList.map((item) => {
model_source: DataSource.Create,
model_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {}; const data = item.response?.data?.[0] ?? {};
return { return {
file_name: data.fileName, file_name: data.fileName,
@@ -77,7 +79,13 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
form: 'form', form: 'form',
}} }}
> >
<Form name="form" layout="vertical" onFinish={onFinish} autoComplete="off">
<Form
name="form"
layout="vertical"
onFinish={onFinish}
autoComplete="off"
initialValues={{ is_public: false }}
>
<Form.Item <Form.Item
label="模型名称" label="模型名称"
name="name" name="name"
@@ -88,9 +96,8 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
}, },
]} ]}
> >
<Input placeholder="请输入模型名称" showCount allowClear maxLength={64} />
<Input placeholder="请输入模型名称" showCount allowClear maxLength={50} />
</Form.Item> </Form.Item>

<Form.Item <Form.Item
label="模型版本" label="模型版本"
name="version" name="version"
@@ -99,40 +106,28 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
required: true, required: true,
message: '请输入模型版本', message: '请输入模型版本',
}, },
]}
>
<Input placeholder="请输入模型版本" allowClear maxLength={64} />
</Form.Item>
<Form.Item
label="模型简介"
name="description"
rules={[
{ {
required: true,
message: '请输入模型简介',
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、下划线、点、横杠',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
}
return Promise.resolve();
},
}, },
]} ]}
> >
<Input.TextArea
placeholder="请输入模型简介"
showCount
maxLength={256}
autoSize={{ minRows: 2, maxRows: 6 }}
allowClear
/>
<Input placeholder="请输入模型版本" showCount allowClear maxLength={64} />
</Form.Item> </Form.Item>
{/* <Form.Item label="可见范围" name="available_range">
<Radio.Group>
<Radio value="0">仅自己可见</Radio>
<Radio value="1">工作空间可见</Radio>
</Radio.Group>
</Form.Item> */}
<Form.Item label="模型框架" name="model_type"> <Form.Item label="模型框架" name="model_type">
<Select <Select
allowClear allowClear
placeholder="请选择模型类型" placeholder="请选择模型类型"
options={typeList} options={typeList}
fieldNames={{ label: 'name', value: 'id' }}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
showSearch showSearch
/> />
@@ -142,11 +137,39 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
allowClear allowClear
placeholder="请选择模型标签" placeholder="请选择模型标签"
options={tagList} options={tagList}
fieldNames={{ label: 'name', value: 'id' }}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
showSearch showSearch
/> />
</Form.Item> </Form.Item>
<Form.Item
label="模型简介"
name="description"
rules={[
{
required: true,
message: '请输入模型简介',
},
]}
>
<Input.TextArea
placeholder="请输入模型简介"
maxLength={200}
autoSize={{ minRows: 2, maxRows: 6 }}
showCount
allowClear
/>
</Form.Item>
<Form.Item
label="可见性"
name="is_public"
rules={[{ required: true, message: '请选择可见性' }]}
>
<Radio.Group>
<Radio value={false}>私有</Radio>
<Radio value={true}>公开</Radio>
</Radio.Group>
</Form.Item>
<Form.Item <Form.Item
label="模型文件" label="模型文件"
name="fileList" name="fileList"


+ 29
- 11
react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx View File

@@ -1,7 +1,7 @@
import { getAccessToken } from '@/access'; import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui'; import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { import {
@@ -21,14 +21,16 @@ import styles from '../AddDatasetModal/index.less';
interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
resourceType: ResourceType; resourceType: ResourceType;
resourceId: number; resourceId: number;
initialName: string;
identifier: string;
resoureName: string;
onOk: () => void; onOk: () => void;
} }


function AddVersionModal({ function AddVersionModal({
resourceType, resourceType,
resourceId, resourceId,
initialName,
resoureName,
identifier,
onOk, onOk,
...rest ...rest
}: AddVersionModalProps) { }: AddVersionModalProps) {
@@ -58,17 +60,21 @@ function AddVersionModal({
const onFinish = (formData: any) => { const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? []; const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) { if (validateUploadFiles(fileList)) {
const otherParams = omit(formData, ['fileList']);
const params = fileList.map((item) => {
const version_vos = fileList.map((item) => {
const data = item.response?.data?.[0] ?? {}; const data = item.response?.data?.[0] ?? {};
return { return {
...otherParams,
[config.idParamKey]: resourceId,
file_name: data.fileName, file_name: data.fileName,
file_size: data.fileSize, file_size: data.fileSize,
url: data.url, url: data.url,
}; };
}); });
const params = {
id: resourceId,
identifier,
[config.filePropKey]: version_vos,
...omit(formData, 'fileList'),
[config.sourceParamKey]: DataSource.Create,
};
createDatasetVersion(params); createDatasetVersion(params);
} }
}; };
@@ -90,7 +96,7 @@ function AddVersionModal({
name="form" name="form"
layout="vertical" layout="vertical"
initialValues={{ initialValues={{
name: initialName,
name: resoureName,
}} }}
onFinish={onFinish} onFinish={onFinish}
autoComplete="off" autoComplete="off"
@@ -115,13 +121,25 @@ function AddVersionModal({
required: true, required: true,
message: `请输入${name}版本`, message: `请输入${name}版本`,
}, },
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、下划线、点、横杠',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
}
return Promise.resolve();
},
},
]} ]}
> >
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear /> <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"
name="description"
name="version_desc"
rules={[ rules={[
{ {
required: true, required: true,
@@ -132,7 +150,7 @@ function AddVersionModal({
<Input.TextArea <Input.TextArea
placeholder="请输入版本描述" placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={256}
maxLength={200}
showCount showCount
allowClear allowClear
/> />
@@ -158,7 +176,7 @@ function AddVersionModal({
上传文件 上传文件
</Button> </Button>
{resourceType === ResourceType.Dataset && ( {resourceType === ResourceType.Dataset && (
<div className={styles['upload-tip']}>只允许上传.zip格式文件</div>
<div className={styles['upload-tip']}>只允许上传 .zip 格式文件</div>
)} )}
</Upload> </Upload>
</Form.Item> </Form.Item>


+ 2
- 0
react-ui/src/pages/Dataset/components/CategoryItem/index.tsx View File

@@ -23,12 +23,14 @@ function CategoryItem({ resourceType, item, isSelected, onClick }: CategoryItemP
style={{ width: '22px' }} style={{ width: '22px' }}
src={`/assets/images/${config.prefix}/${item.path}.png`} src={`/assets/images/${config.prefix}/${item.path}.png`}
alt="" alt=""
draggable={false}
/> />
<img <img
className={styles['category-item__active-icon']} className={styles['category-item__active-icon']}
style={{ width: '22px' }} style={{ width: '22px' }}
src={`/assets/images/${config.prefix}/${item.path}-hover.png`} src={`/assets/images/${config.prefix}/${item.path}-hover.png`}
alt="" alt=""
draggable={false}
/> />
<span className={styles['category-item__name']}>{item.name}</span> <span className={styles['category-item__name']}>{item.name}</span>
</div> </div>


+ 4
- 9
react-ui/src/pages/Dataset/components/CategoryList/index.tsx View File

@@ -3,17 +3,12 @@ import { CategoryData, ResourceType, resourceConfig } from '../../config';
import CategoryItem from '../CategoryItem'; import CategoryItem from '../CategoryItem';
import styles from './index.less'; import styles from './index.less';


export type CategoryValue = {
dataType: number | undefined;
dataTag: number | undefined;
};

type CategoryProps = { type CategoryProps = {
resourceType: ResourceType; // 资源类型,数据集还是模型 resourceType: ResourceType; // 资源类型,数据集还是模型
typeList: CategoryData[]; typeList: CategoryData[];
tagList: CategoryData[]; tagList: CategoryData[];
activeType?: number;
activeTag?: number;
activeType?: string;
activeTag?: string;
onTypeSelect: (value: CategoryData) => void; onTypeSelect: (value: CategoryData) => void;
onTagSelect: (value: CategoryData) => void; onTagSelect: (value: CategoryData) => void;
onSearch: (value: string) => void; onSearch: (value: string) => void;
@@ -44,7 +39,7 @@ function CategoryList({
resourceType={resourceType} resourceType={resourceType}
item={item} item={item}
onClick={onTypeSelect} onClick={onTypeSelect}
isSelected={item.id === activeType}
isSelected={item.name === activeType}
></CategoryItem> ></CategoryItem>
))} ))}
</Flex> </Flex>
@@ -58,7 +53,7 @@ function CategoryList({
resourceType={resourceType} resourceType={resourceType}
item={item} item={item}
onClick={onTagSelect} onClick={onTagSelect}
isSelected={item.id === activeTag}
isSelected={item.name === activeTag}
></CategoryItem> ></CategoryItem>
))} ))}
</Flex> </Flex>


+ 68
- 0
react-ui/src/pages/Dataset/components/ResourceInfo/index.less View File

@@ -0,0 +1,68 @@
.resource-info {
height: 100%;

&__top {
width: 100%;
height: 125px;
margin-bottom: 10px;
padding: 20px 30px;
background-image: url(@/assets/img/dataset-intro-top.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

&__name {
margin-right: 10px;
color: @text-color;
font-weight: 500;
font-size: 20px;
.singleLine();
}

&__tag {
flex: none;
padding: 4px 10px;
color: @primary-color;
font-size: 14px;
background: .addAlpha(@primary-color, 0.1) [];
border-radius: 4px;
}

:global {
.ant-btn-dangerous {
background-color: transparent !important;
}
}
}

&__bottom {
position: relative;
height: calc(100% - 135px);
padding: 8px 30px 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

&__legend {
position: absolute;
top: 20px;
right: 30px;
}

:global {
.ant-tabs {
height: 100%;
.ant-tabs-content-holder {
height: 100%;
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
overflow-y: auto;
}
}
}
}
}
}
}

+ 247
- 0
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -0,0 +1,247 @@
/*
* @Author: 赵伟
* @Date: 2024-09-06 09:23:15
* @Description: 数据集、模型详情
*/

import KFIcon from '@/components/KFIcon';
import {
ResourceData,
ResourceType,
ResourceVersionData,
resourceConfig,
} from '@/pages/Dataset/config';
import GraphLegend from '@/pages/Model/components/GraphLegend';
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Flex, Select, Tabs } from 'antd';
import { useEffect, useState } from 'react';
import AddVersionModal from '../AddVersionModal';
import ResourceIntro from '../ResourceIntro';
import ResourceVersion from '../ResourceVersion';
import styles from './index.less';

// 这里值小写是因为值会写在 url 中
export enum ResourceInfoTabKeys {
Introduction = 'introduction', // 简介
Version = 'version', // 版本
Evolution = 'evolution', // 演化
}

type ResourceInfoProps = {
resourceType: ResourceType;
};

const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
const [info, setInfo] = useState<ResourceData>({} as ResourceData);
const locationParams = useParams();
const [searchParams] = useSearchParams();
const resourceId = Number(locationParams.id);
// 模型演化传入的 tab
const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction;
// 模型演化传入的版本
let versionParam = searchParams.get('version');
const name = searchParams.get('name') || '';
const owner = searchParams.get('owner') || '';
const identifier = searchParams.get('identifier') || '';
const [versionList, setVersionList] = useState<ResourceVersionData[]>([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const [activeTab, setActiveTab] = useState<string>(defaultTab);
const config = resourceConfig[resourceType];
const typeName = config.name; // 数据集/模型
const { message } = App.useApp();

useEffect(() => {
getVersionList();
}, [resourceId, owner, identifier]);

useEffect(() => {
if (version) {
getResourceDetail({
id: resourceId,
owner,
name,
identifier,
version,
});
}
}, [version]);

// 获取详情
const getResourceDetail = async (params: {
owner: string;
name: string;
id: number;
identifier: string;
version?: string;
}) => {
const request = config.getInfo;
const [res] = await to(request(params));
if (res && res.data) {
setInfo(res.data);
}
};

// 获取版本列表
const getVersionList = async () => {
const request = config.getVersions;
const [res] = await to(
request({
owner,
identifier,
}),
);
if (res && res.data && res.data.length > 0) {
setVersionList(res.data);
if (
versionParam &&
res.data.find((item: ResourceVersionData) => item.name === versionParam)
) {
setVersion(versionParam);
versionParam = null;
} else {
setVersion(res.data[0].name);
}
} else {
setVersion(undefined);
}
};

// 新建版本
const showModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType,
resourceId: resourceId,
resoureName: info.name,
identifier: info.identifier,
onOk: () => {
getVersionList();
close();
},
});
};

// 版本变化
const handleVersionChange = (value: string) => {
setVersion(value);
};

// 删除版本
const deleteVersion = async () => {
const request = config.deleteVersion;
const params = {
id: resourceId,
owner,
identifier,
relative_paths: info.relative_paths,
version,
};
const [res] = await to(request(params));
if (res) {
message.success('删除成功');
getVersionList();
}
};

// 处理删除
const hanldeDelete = () => {
modalConfirm({
title: '删除后,该版本将不可恢复',
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',
onOk: () => {
deleteVersion();
},
});
};

const items = [
{
key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>,
},
{
key: ResourceInfoTabKeys.Version,
label: `${typeName}文件`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: <ResourceVersion resourceType={resourceType} info={info}></ResourceVersion>,
},
];

if (resourceType === ResourceType.Model) {
items.push({
key: ResourceInfoTabKeys.Evolution,
label: `模型演化`,
icon: <KFIcon type="icon-moxingyanhua1" />,
children: (
<ModelEvolution
resourceId={resourceId}
version={version}
identifier={identifier}
isActive={activeTab === ResourceInfoTabKeys.Evolution}
onVersionChange={handleVersionChange}
></ModelEvolution>
),
});
}

const typePropertyName = config.typeParamKey as keyof ResourceData;
const tagPropertyName = config.tagParamKey as keyof ResourceData;

return (
<div className={styles['resource-info']}>
<div className={styles['resource-info__top']}>
<Flex align="center" gap={10} style={{ marginBottom: '20px' }}>
<div className={styles['resource-info__top__name']}>{info.name}</div>
{info[typePropertyName] && (
<div className={styles['resource-info__top__tag']}>
{(info[typePropertyName] as string) || '--'}
</div>
)}
{info[tagPropertyName] && (
<div className={styles['resource-info__top__tag']}>
{(info[tagPropertyName] as string) || '--'}
</div>
)}
</Flex>
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: 'auto', marginRight: 0 }}
onClick={hanldeDelete}
icon={<KFIcon type="icon-shanchu" />}
disabled={!version}
danger
>
删除版本
</Button>
</Flex>
</div>
<div className={styles['resource-info__bottom']}>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['resource-info__bottom__legend']}>
{activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />}
</div>
</div>
</div>
);
};
export default ResourceInfo;

+ 5
- 60
react-ui/src/pages/Dataset/components/ResourceIntro/index.less View File

@@ -1,65 +1,10 @@
.resource-intro { .resource-intro {
height: 100%;
&__top {
width: 100%;
margin-top: 24px;
&__basic {
width: 100%; width: 100%;
height: 110px;
margin-bottom: 10px;
padding: 20px 30px 0;
background-image: url(@/assets/img/dataset-intro-top.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

&__name {
margin-bottom: 12px;
color: @text-color;
font-size: 20px;
}

&__tag {
margin-right: 10px;
padding: 4px 10px;
color: @primary-color;
font-size: 14px;
background: rgba(22, 100, 255, 0.1);
border-radius: 4px;
}
} }

&__bottom {
height: calc(100% - 120px);
padding: 8px 30px 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

:global {
.ant-tabs {
height: 100%;
.ant-tabs-content-holder {
height: 100%;
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
overflow-y: auto;
}
}
}
}
}
}

&__title {
margin: 30px 0 10px;
color: @text-color;
font-weight: 500;
font-size: @font-size;
}

&__intro {
color: @text-color-secondary;
font-size: 14px;
&__usage {
width: 100%;
} }
} }

+ 224
- 134
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -1,156 +1,246 @@
import KFIcon from '@/components/KFIcon';
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max';
import { Flex, Tabs } from 'antd';
import { useEffect, useState } from 'react';
import { ResourceData, ResourceType, resourceConfig } from '../../config';
import ResourceVersion from '../ResourceVersion';
import BasicInfo, { BasicInfoData } from '@/components/BasicInfo';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import {
DataSource,
DatasetData,
ModelData,
ProjectDependency,
ResourceType,
TrainTask,
} from '@/pages/Dataset/config';
import styles from './index.less'; import styles from './index.less';


// 这里值小写是因为值会写在 url 中
export enum ResourceInfoTabKeys {
Introduction = 'introduction', // 简介
Version = 'version', // 版本
Evolution = 'evolution', // 演化
}

type ResourceIntroProps = { type ResourceIntroProps = {
resourceType: ResourceType; resourceType: ResourceType;
info: DatasetData | ModelData;
}; };


const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const [info, setInfo] = useState<ResourceData>({} as ResourceData);
const locationParams = useParams();
const [searchParams] = useSearchParams();
const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction;
let versionParam = searchParams.get('version');
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState<string | undefined>(undefined);
const [activeTab, setActiveTab] = useState<string>(defaultTab);
const resourceId = Number(locationParams.id);
const config = resourceConfig[resourceType];
const typeName = config.name; // 数据集/模型

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


// 获取详情
const getModelByDetail = async () => {
const request = config.getInfo;
const [res] = await to(request(resourceId));
if (res) {
setInfo(res.data);
}
};
const formatParams = (map?: Record<string, string>, space: string = '') => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => `${space}${key} : ${value}`)
.join('\n');
};


// 获取版本列表
const getVersionList = async () => {
const request = config.getVersions;
const [res] = await to(request(resourceId));
if (res && res.data && res.data.length > 0) {
setVersionList(
res.data.map((item: string) => {
return {
label: item,
value: item,
};
}),
);
if (versionParam && res.data.includes(versionParam)) {
setVersion(versionParam);
versionParam = null;
} else {
setVersion(res.data[0]);
const formatMetrics = (map?: Record<string, string>) => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => {
if (typeof value === 'object' && value !== null) {
return `${key} : \n${formatParams(value, ' ')}`;
} }
} else {
setVersion(undefined);
}
};
return `${key} : ${value}`;
})
.join('\n');
};


// 版本变化
const handleVersionChange = (value: string) => {
setVersion(value);
const getProjectUrl = (project?: ProjectDependency) => {
if (!project || !project.url || !project.branch) {
return undefined;
}
const { url, branch } = project;
if (url.endsWith('.git')) {
return `${url.substring(0, url.length - 4)}/tree/${branch}`;
}
};

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


const items = [
{
key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: (
<>
<div className={styles['resource-intro__title']}>简介</div>
<div className={styles['resource-intro__intro']}>{info.description}</div>
</>
),
},
{
key: ResourceInfoTabKeys.Version,
label: `${typeName}文件/版本`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: (
<ResourceVersion
resourceType={resourceType}
resourceId={resourceId}
resourceName={info.name}
isPublic={info.available_range === 1}
versionList={versionList}
version={version}
isActive={activeTab === ResourceInfoTabKeys.Version}
getVersionList={getVersionList}
onVersionChange={handleVersionChange}
></ResourceVersion>
),
},
];
const formatTrainTask = (task?: TrainTask) => {
if (!task) {
return undefined;
}
return {
value: task.name,
url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`,
};
};


if (resourceType === ResourceType.Model) {
items.push({
key: ResourceInfoTabKeys.Evolution,
label: `模型演化`,
icon: <KFIcon type="icon-moxingyanhua1" />,
children: (
<ModelEvolution
resourceId={resourceId}
versionList={versionList}
version={version}
isActive={activeTab === ResourceInfoTabKeys.Evolution}
onVersionChange={handleVersionChange}
></ModelEvolution>
),
});
const formatSource = (source?: string) => {
if (source === DataSource.Create) {
return '用户上传';
} else if (source === DataSource.HandExport) {
return '手动导入';
} else if (source === DataSource.AtuoExport) {
return '实验自动导入';
} }
return source;
};


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

const getModelDatas = (data: ModelData): BasicInfoData[] => [
{
label: '模型名称',
value: data.name,
},
{
label: '版本',
value: data.version,
},
{
label: '创建人',
value: data.create_by,
},
{
label: '更新时间',
value: data.update_time,
},
{
label: '训练镜像',
value: data.image,
},
{
label: '训练代码',
value: data.project_depency,
format: formatProject,
},
{
label: '训练数据集',
value: data.train_datasets,
format: formatDataset,
},
{
label: '测试数据集',
value: data.test_datasets,
format: formatDataset,
},
{
label: '参数',
value: data.params,
format: formatParams,
},
{
label: '指标',
value: data.metrics,
format: formatMetrics,
},
{
label: '模型来源',
value: data.model_source,
format: formatSource,
},
{
label: '训练任务',
value: data.train_task,
format: formatTrainTask,
},
{
label: '模型框架',
value: data.model_type,
},
{
label: '模型能力',
value: data.model_tag,
},
{
label: '模型描述',
value: data.description,
},
{
label: '版本描述',
value: data.version_desc,
},
];

function ResourceIntro({ resourceType, info }: ResourceIntroProps) {
const basicDatas: BasicInfoData[] =
resourceType === ResourceType.Dataset
? getDatasetDatas(info as DatasetData)
: getModelDatas(info as ModelData);


return ( return (
<div className={styles['resource-intro']}> <div className={styles['resource-intro']}>
<div className={styles['resource-intro__top']}>
<div className={styles['resource-intro__top__name']}>{info.name}</div>
<Flex align="center">
<div className={styles['resource-intro__top__tag']}>
{typeName} id:{info.id}
</div>
{info[infoTypePropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTypePropertyName] || '--'}
</div>
)}
{info[infoTagPropertyName] && (
<div className={styles['resource-intro__top__tag']}>
{info[infoTagPropertyName] || '--'}
</div>
)}
</Flex>
</div>
<div className={styles['resource-intro__bottom']}>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['resource-intro__basic']}>
<BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo>
</div> </div>
<SubAreaTitle
title="实例用法"
image={require('@/assets/img/usage-icon.png')}
style={{ margin: '40px 0 24px' }}
></SubAreaTitle>
<div
className={styles['resource-intro__usage']}
dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }}
></div>
</div> </div>
); );
};
}

export default ResourceIntro; export default ResourceIntro;

+ 12
- 4
react-ui/src/pages/Dataset/components/ResourceItem/index.tsx View File

@@ -39,12 +39,20 @@ function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps)
<div className={styles['resource-item__description']}>{item.description}</div> <div className={styles['resource-item__description']}>{item.description}</div>
<Flex justify="space-between"> <Flex justify="space-between">
<div className={styles['resource-item__time']}> <div className={styles['resource-item__time']}>
<img style={{ width: '17px', marginRight: '6px' }} src={creatByImg} alt="" />
<span>{item.create_by}</span>
<img
style={{ width: '17px', marginRight: '6px' }}
src={creatByImg}
draggable={false}
alt=""
/>
<span>{item.create_by ?? ''}</span>
</div> </div>
<div className={styles['resource-item__time']}> <div className={styles['resource-item__time']}>
<img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" />
<span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span>
<img style={{ width: '12px', marginRight: '5px' }} src={clock} draggable={false} alt="" />
<span>
{'最近更新: '}
{item.update_time ? formatDate(item.update_time, 'YYYY-MM-DD') : item.time_ago ?? ''}
</span>
</div> </div>
</Flex> </Flex>
</div> </div>


+ 4
- 0
react-ui/src/pages/Dataset/components/ResourceList/index.less View File

@@ -36,4 +36,8 @@
text-align: right; text-align: right;
} }
} }

&__empty {
flex: 1;
}
} }

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

@@ -1,3 +1,4 @@
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { CommonTabKeys } from '@/enums'; import { CommonTabKeys } from '@/enums';
import AddModelModal from '@/pages/Dataset/components/AddModelModal'; import AddModelModal from '@/pages/Dataset/components/AddModelModal';
@@ -6,6 +7,7 @@ import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max'; import { useNavigate } from '@umijs/max';
import { App, Button, Input, Pagination, PaginationProps } from 'antd'; import { App, Button, Input, Pagination, PaginationProps } from 'antd';
import { pick } from 'lodash';
import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config';
import AddDatasetModal from '../AddDatasetModal'; import AddDatasetModal from '../AddDatasetModal';
@@ -18,8 +20,8 @@ export type ResourceListRef = {


type ResourceListProps = { type ResourceListProps = {
resourceType: ResourceType; resourceType: ResourceType;
dataType?: number;
dataTag?: number;
dataType?: string;
dataTag?: string;
isPublic: boolean; isPublic: boolean;
typeList: CategoryData[]; typeList: CategoryData[];
tagList: CategoryData[]; tagList: CategoryData[];
@@ -43,7 +45,7 @@ function ResourceList(
ref: Ref<ResourceListRef>, ref: Ref<ResourceListRef>,
) { ) {
const navigate = useNavigate(); const navigate = useNavigate();
const [dataList, setDataList] = useState<ResourceData[]>([]);
const [dataList, setDataList] = useState<ResourceData[] | undefined>(undefined);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>( const [pagination, setPagination] = useState<PaginationProps>(
initialPagination ?? { initialPagination ?? {
@@ -71,7 +73,8 @@ function ResourceList(
}); });
setSearchText(''); setSearchText('');
setInputText(''); setInputText('');
setDataList([]);
setDataList(undefined);
setTotal(0);
}, },
}; };
}, },
@@ -80,12 +83,12 @@ function ResourceList(


// 获取数据请求 // 获取数据请求
const getDataList = async () => { const getDataList = async () => {
const params = {
const params: Record<string, any> = {
page: pagination.current! - 1, page: pagination.current! - 1,
size: pagination.pageSize, size: pagination.pageSize,
is_public: isPublic,
[config.typeParamKey]: dataType, [config.typeParamKey]: dataType,
[config.tagParamKey]: dataTag, [config.tagParamKey]: dataTag,
available_range: isPublic ? 1 : 0,
name: searchText !== '' ? searchText : undefined, name: searchText !== '' ? searchText : undefined,
}; };
const request = config.getList; const request = config.getList;
@@ -93,13 +96,16 @@ function ResourceList(
if (res && res.data && res.data.content) { if (res && res.data && res.data.content) {
setDataList(res.data.content); setDataList(res.data.content);
setTotal(res.data.totalElements); setTotal(res.data.totalElements);
} else {
setDataList([]);
setTotal(0);
} }
}; };


// 删除请求 // 删除请求
const deleteRecord = async (id: number) => {
const deleteRecord = async (params: { owner: string; identifier: string; repo_id?: number }) => {
const request = config.deleteRecord; const request = config.deleteRecord;
const [res] = await to(request(id));
const [res] = await to(request(params));
if (res) { if (res) {
getDataList(); getDataList();
message.success('删除成功'); message.success('删除成功');
@@ -116,7 +122,7 @@ function ResourceList(
modalConfirm({ modalConfirm({
title: config.deleteModalTitle, title: config.deleteModalTitle,
onOk: () => { onOk: () => {
deleteRecord(record.id);
deleteRecord(pick(record, ['owner', 'identifier', 'id']));
}, },
}); });
}; };
@@ -131,7 +137,9 @@ function ResourceList(
activeTag: dataTag, activeTag: dataTag,
}); });
const prefix = config.prefix; const prefix = config.prefix;
navigate(`/dataset/${prefix}/info/${record.id}`);
navigate(
`/dataset/${prefix}/info/${record.id}?name=${record.name}&owner=${record.owner}&identifier=${record.identifier}`,
);
}; };


// 分页切换 // 分页切换
@@ -158,7 +166,7 @@ function ResourceList(
return ( return (
<div className={styles['resource-list']}> <div className={styles['resource-list']}>
<div className={styles['resource-list__header']}> <div className={styles['resource-list__header']}>
<span>数据总数:{total}个</span>
<span>数据总数:{total} 个</span>
<div> <div>
<Input.Search <Input.Search
placeholder={`按${config.name}名称筛选`} placeholder={`按${config.name}名称筛选`}
@@ -182,26 +190,40 @@ function ResourceList(
)} )}
</div> </div>
</div> </div>
<div className={styles['resource-list__content']}>
{dataList?.map((item) => (
<ResourceItem
item={item}
key={item.id}
isPublic={isPublic}
onRemove={handleRemove}
onClick={handleClick}
></ResourceItem>
))}
</div>
<Pagination
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}
/>
{dataList && dataList.length > 0 && (
<>
<div className={styles['resource-list__content']}>
{dataList?.map((item) => (
<ResourceItem
item={item}
key={item.id}
isPublic={isPublic}
onRemove={handleRemove}
onClick={handleClick}
></ResourceItem>
))}
</div>
<Pagination
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}
/>
</>
)}
{dataList && dataList.length === 0 && (
<KFEmpty
className={styles['resource-list__empty']}
type={EmptyType.NoData}
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
/>
)}
</div> </div>
); );
} }


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

@@ -18,8 +18,8 @@ function ResourcePage({ resourceType }: ResourcePageProps) {
const [activeTab, setActiveTab] = useState<string>(cacheState?.activeTab ?? CommonTabKeys.Public); const [activeTab, setActiveTab] = useState<string>(cacheState?.activeTab ?? CommonTabKeys.Public);
const [typeList, setTypeList] = useState<CategoryData[]>([]); const [typeList, setTypeList] = useState<CategoryData[]>([]);
const [tagList, setTagList] = useState<CategoryData[]>([]); const [tagList, setTagList] = useState<CategoryData[]>([]);
const [activeType, setActiveType] = useState<number | undefined>(cacheState?.activeType);
const [activeTag, setActiveTag] = useState<number | undefined>(cacheState?.activeTag);
const [activeType, setActiveType] = useState<string | undefined>(cacheState?.activeType);
const [activeTag, setActiveTag] = useState<string | undefined>(cacheState?.activeTag);
const dataListRef = useRef<ResourceListRef>(null); const dataListRef = useRef<ResourceListRef>(null);
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];


@@ -34,12 +34,12 @@ function ResourcePage({ resourceType }: ResourcePageProps) {


// 选择类型 // 选择类型
const chooseType = (record: CategoryData) => { const chooseType = (record: CategoryData) => {
setActiveType((prev) => (prev === record.id ? undefined : record.id));
setActiveType((prev) => (prev === record.name ? undefined : record.name));
}; };


// 选择 Tag // 选择 Tag
const chooseTag = (record: CategoryData) => { const chooseTag = (record: CategoryData) => {
setActiveTag((prev) => (prev === record.id ? undefined : record.id));
setActiveTag((prev) => (prev === record.name ? undefined : record.name));
}; };


// 获取分类 // 获取分类


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

@@ -1,125 +1,41 @@
import CommonTableCell from '@/components/CommonTableCell'; import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell'; import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { useEffectWhen } from '@/hooks';
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
import { import {
ResourceData,
ResourceFileData, ResourceFileData,
ResourceType, ResourceType,
ResourceVersionData,
resourceConfig, resourceConfig,
} from '@/pages/Dataset/config'; } from '@/pages/Dataset/config';
import { downLoadZip } from '@/utils/downloadfile'; import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { App, Button, Flex, Select, Table } from 'antd';
import { useState } from 'react';
import { Button, Flex, Table } from 'antd';
import styles from './index.less'; import styles from './index.less';


type ResourceVersionProps = { type ResourceVersionProps = {
resourceType: ResourceType; resourceType: ResourceType;
resourceId: number;
resourceName: string;
isPublic: boolean;
versionList: ResourceVersionData[];
version?: string;
isActive: boolean;
getVersionList: () => void;
onVersionChange: (version: string) => void;
info: ResourceData;
}; };
function ResourceVersion({
resourceType,
resourceId,
resourceName,
isPublic,
versionList,
version,
isActive,
getVersionList,
onVersionChange,
}: ResourceVersionProps) {
const [fileList, setFileList] = useState<ResourceFileData[]>([]);
const { message } = App.useApp();
function ResourceVersion({ resourceType, info }: ResourceVersionProps) {
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];

// 获取版本文件列表
useEffectWhen(
() => {
if (version) {
getFileList(version);
} else {
setFileList([]);
}
},
[resourceId, version],
isActive,
);

// 获取版本下的文件列表
const getFileList = async (version: string) => {
const params = {
version,
[config.fileReqParamKey]: resourceId,
};
const request = config.getFiles;
const [res] = await to(request(params));
if (res) {
setFileList(res?.data?.content ?? []);
}
};

// 删除版本
const deleteVersion = async () => {
const request = config.deleteVersion;
const params = {
[config.idParamKey]: resourceId,
version,
};
const [res] = await to(request(params));
if (res) {
getVersionList();
message.success('删除成功');
}
};

// 新建版本
const showModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType,
resourceId: resourceId,
initialName: resourceName,
onOk: () => {
getVersionList();
close();
},
});
};

// 处理删除
const hanldeDelete = () => {
modalConfirm({
title: '删除后,该版本将不可恢复',
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',

onOk: () => {
deleteVersion();
},
});
};
const filePropKey = config.filePropKey as keyof ResourceData;
const fileList = (info[filePropKey] ?? []) as ResourceFileData[];
fileList.forEach((item) => (item.update_time = info.update_time));


// 全部导出 // 全部导出
const handleExport = async () => { const handleExport = async () => {
const url = config.downloadAllAction; const url = config.downloadAllAction;
downLoadZip(url, { models_id: resourceId, version });
downLoadZip(url, {
name: info.name,
id: info.id,
version: info.version,
identifier: info.identifier,
});
}; };


// 单个导出 // 单个导出
const downloadAlone = (record: ResourceFileData) => {
const downloadAlone = async (record: ResourceFileData) => {
const url = config.downloadSingleAction; const url = config.downloadSingleAction;
downLoadZip(`${url}/${record.id}`);
downLoadZip(url, { url: record.url });
}; };


const columns = [ const columns = [
@@ -142,12 +58,6 @@ function ResourceVersion({
</a> </a>
), ),
}, },
{
title: '版本号',
dataIndex: 'version',
key: 'version',
render: CommonTableCell(),
},
{ {
title: '文件大小', title: '文件大小',
dataIndex: 'file_size', dataIndex: 'file_size',
@@ -163,7 +73,7 @@ function ResourceVersion({
{ {
title: '操作', title: '操作',
dataIndex: 'option', dataIndex: 'option',
width: '100px',
width: 160,
key: 'option', key: 'option',
render: (_: any, record: ResourceFileData) => [ render: (_: any, record: ResourceFileData) => [
<Button <Button
@@ -183,32 +93,9 @@ function ResourceVersion({
<div className={styles['resource-version']}> <div className={styles['resource-version']}>
<Flex justify="space-between" align="center" style={{ margin: '30px 0' }}> <Flex justify="space-between" align="center" style={{ margin: '30px 0' }}>
<Flex align="center"> <Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={onVersionChange}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
</Flex>
<Flex align="center">
{!isPublic && (
<Button
type="default"
style={{ marginRight: '20px' }}
onClick={hanldeDelete}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
<Button <Button
type="default" type="default"
disabled={!version}
disabled={fileList.length === 0}
onClick={handleExport} onClick={handleExport}
icon={<KFIcon type="icon-xiazai" />} icon={<KFIcon type="icon-xiazai" />}
> >
@@ -216,12 +103,7 @@ function ResourceVersion({
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
<div style={{ marginBottom: '30px', fontSize: '15px' }}>
{fileList.length > 0 && fileList[0].description
? '版本描述:' + fileList[0].description
: null}
</div>
<Table columns={columns} dataSource={fileList} pagination={false} rowKey="id" />
<Table columns={columns} dataSource={fileList} pagination={false} rowKey="url" />
</div> </div>
); );
} }


+ 94
- 57
react-ui/src/pages/Dataset/config.tsx View File

@@ -1,20 +1,18 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { CommonTabKeys } from '@/enums'; import { CommonTabKeys } from '@/enums';
import { import {
addDatasetVersionDetail,
addModelsVersionDetail,
addDatasetVersion,
addModelVersion,
deleteDataset, deleteDataset,
deleteDatasetVersion, deleteDatasetVersion,
deleteModel, deleteModel,
deleteModelVersion, deleteModelVersion,
getDatasetById,
getDatasetInfo,
getDatasetList, getDatasetList,
getDatasetVersionIdList,
getDatasetVersionsById,
getModelById,
getDatasetVersionList,
getModelInfo,
getModelList, getModelList,
getModelVersionIdList,
getModelVersionsById,
getModelVersionList,
} from '@/services/dataset/index.js'; } from '@/services/dataset/index.js';
import type { TabsProps } from 'antd'; import type { TabsProps } from 'antd';


@@ -23,18 +21,24 @@ export enum ResourceType {
Dataset = 'Dataset', // 数据集 Dataset = 'Dataset', // 数据集
} }


export enum DataSource {
AtuoExport = 'auto_export', // 自动导出
HandExport = 'hand_export', // 手动导出
Create = 'add', // 新增
}

type ResourceTypeInfo = { type ResourceTypeInfo = {
getList: (params: any) => Promise<any>; // 获取资源列表 getList: (params: any) => Promise<any>; // 获取资源列表
getVersions: (params: any) => Promise<any>; // 获取版本列表 getVersions: (params: any) => Promise<any>; // 获取版本列表
getFiles: (params: any) => Promise<any>; // 获取版本下的文件列表
deleteRecord: (params: any) => Promise<any>; // 删除 deleteRecord: (params: any) => Promise<any>; // 删除
addVersion: (params: any) => Promise<any>; // 新增版本 addVersion: (params: any) => Promise<any>; // 新增版本
deleteVersion: (params: any) => Promise<any>; // 删除版本 deleteVersion: (params: any) => Promise<any>; // 删除版本
getInfo: (params: any) => Promise<any>; // 获取详情 getInfo: (params: any) => Promise<any>; // 获取详情
name: string; // 名称 name: string; // 名称
typeParamKey: string; // 类型参数名称,获取资源列表接口使用
tagParamKey: string; // 标签参数名称,获取资源列表接口使用
fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用
typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用
tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用
filePropKey: 'dataset_version_vos' | 'model_version_vos'; // 文件列表属性
sourceParamKey: 'dataset_source' | 'model_source'; // 来源参数名称
tabItems: TabsProps['items']; // tab 列表 tabItems: TabsProps['items']; // tab 列表
typeTitle: string; // 类型标题 typeTitle: string; // 类型标题
tagTitle: string; // 标签标题 tagTitle: string; // 标签标题
@@ -43,28 +47,25 @@ type ResourceTypeInfo = {
prefix: string; // 图片资源、详情 url 的前缀 prefix: string; // 图片资源、详情 url 的前缀
deleteModalTitle: string; // 删除弹框的title deleteModalTitle: string; // 删除弹框的title
addBtnTitle: string; // 新增按钮的title addBtnTitle: string; // 新增按钮的title
idParamKey: 'models_id' | 'dataset_id'; // 新建版本、删除版本接口,版本 id 的参数名称
uploadAction: string; // 上传接口 url uploadAction: string; // 上传接口 url
uploadAccept?: string; // 上传文件类型 uploadAccept?: string; // 上传文件类型
downloadAllAction: string; // 批量下载接口 url downloadAllAction: string; // 批量下载接口 url
downloadSingleAction: string; // 单个下载接口 url downloadSingleAction: string; // 单个下载接口 url
infoTypePropertyName: string; // 详情数据中,类型属性名称
infoTagPropertyName: string; // 详情数据中,标签属性名称
}; };


export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
[ResourceType.Dataset]: { [ResourceType.Dataset]: {
getList: getDatasetList, getList: getDatasetList,
getVersions: getDatasetVersionsById,
getFiles: getDatasetVersionIdList,
getVersions: getDatasetVersionList,
deleteRecord: deleteDataset, deleteRecord: deleteDataset,
addVersion: addDatasetVersionDetail,
addVersion: addDatasetVersion,
deleteVersion: deleteDatasetVersion, deleteVersion: deleteDatasetVersion,
getInfo: getDatasetById,
getInfo: getDatasetInfo,
name: '数据集', name: '数据集',
typeParamKey: 'data_type', typeParamKey: 'data_type',
tagParamKey: 'data_tag', tagParamKey: 'data_tag',
fileReqParamKey: 'dataset_id',
filePropKey: 'dataset_version_vos',
sourceParamKey: 'dataset_source',
tabItems: [ tabItems: [
{ {
key: CommonTabKeys.Public, key: CommonTabKeys.Public,
@@ -84,26 +85,23 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
prefix: 'dataset', prefix: 'dataset',
deleteModalTitle: '确定删除该条数据集实例吗?', deleteModalTitle: '确定删除该条数据集实例吗?',
addBtnTitle: '新建数据集', addBtnTitle: '新建数据集',
idParamKey: 'dataset_id',
uploadAction: '/api/mmp/dataset/upload',
uploadAction: '/api/mmp/newdataset/upload',
uploadAccept: '.zip,.tgz', uploadAccept: '.zip,.tgz',
downloadAllAction: '/api/mmp/dataset/downloadAllFilesl',
downloadSingleAction: '/api/mmp/dataset/download',
infoTypePropertyName: 'dataset_type_name',
infoTagPropertyName: 'dataset_tag_name',
downloadAllAction: '/api/mmp/newdataset/downloadAllFiles',
downloadSingleAction: '/api/mmp/newdataset/downloadSingleFile',
}, },
[ResourceType.Model]: { [ResourceType.Model]: {
getList: getModelList, getList: getModelList,
getVersions: getModelVersionsById,
getFiles: getModelVersionIdList,
getVersions: getModelVersionList,
deleteRecord: deleteModel, deleteRecord: deleteModel,
addVersion: addModelsVersionDetail,
addVersion: addModelVersion,
deleteVersion: deleteModelVersion, deleteVersion: deleteModelVersion,
getInfo: getModelById,
getInfo: getModelInfo,
name: '模型', name: '模型',
typeParamKey: 'model_type', typeParamKey: 'model_type',
tagParamKey: 'model_tag', tagParamKey: 'model_tag',
fileReqParamKey: 'models_id',
filePropKey: 'model_version_vos',
sourceParamKey: 'model_source',
tabItems: [ tabItems: [
{ {
key: CommonTabKeys.Public, key: CommonTabKeys.Public,
@@ -123,13 +121,10 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
prefix: 'model', prefix: 'model',
deleteModalTitle: '确定删除该条模型实例吗?', deleteModalTitle: '确定删除该条模型实例吗?',
addBtnTitle: '新建模型', addBtnTitle: '新建模型',
idParamKey: 'models_id',
uploadAction: '/api/mmp/models/upload',
uploadAction: '/api/mmp/newmodel/upload',
uploadAccept: undefined, uploadAccept: undefined,
downloadAllAction: '/api/mmp/models/downloadAllFiles',
downloadSingleAction: '/api/mmp/models/download_model',
infoTypePropertyName: 'model_type_name',
infoTagPropertyName: 'model_tag_name',
downloadAllAction: '/api/mmp/newmodel/downloadAllFiles',
downloadSingleAction: '/api/mmp/newmodel/downloadSingleFile',
}, },
}; };


@@ -141,36 +136,78 @@ export type CategoryData = {
path: string; path: string;
}; };


// 资源数据
export type ResourceData = {
// 数据集、模型列表数据
export interface ResourceData {
resourceType: ResourceType.Dataset | ResourceType.Model; // 用于 ts 类型判断
id: number; id: number;
name: string; name: string;
description: string;
create_by: string;
update_time: string;
available_range: number;
model_type_name?: string;
model_tag_name?: string;
dataset_type_name?: string;
dataset_tag_name?: string;
};
identifier: string;
owner: string;
version: string;
is_public: boolean;
description?: string;
create_by?: string;
update_time?: string;
time_ago?: string;
version_desc?: string;
usage?: string;
relative_paths?: string;
train_task?: TrainTask; // 训练任务
}

// 数据集数据
export interface DatasetData extends ResourceData {
resourceType: ResourceType.Dataset; // 用于区别类型
data_type?: string; // 数据集分类
data_tag?: string; // 研究方向
processing_code?: ProjectDependency; // 处理代码
dataset_source?: string; // 数据来源
dataset_version_vos?: ResourceFileData[];
}

// 模型数据
export interface ModelData extends ResourceData {
resourceType: ResourceType.Model; // 用于区别类型
model_type?: string; // 模型框架
model_tag?: string; // 模型能力
image?: string; // 训练镜像
code?: string; // 训练镜像
train_datasets?: DatasetData[]; // 训练数据集
test_datasets?: DatasetData[]; // 测试数据集
params?: Record<string, string>; // 参数
metrics?: Record<string, string>; // 指标
project_depency?: ProjectDependency; // 项目依赖
model_source?: string; // 模型来源
model_version_vos?: ResourceFileData[];
}


// 版本数据 // 版本数据
export type ResourceVersionData = { export type ResourceVersionData = {
label: string;
value: string;
name: string;
http_url: string;
tar_url: string;
zip_url: string;
}; };


// 版本文件数据 // 版本文件数据
export type ResourceFileData = { export type ResourceFileData = {
id: number;
file_name: string; file_name: string;
file_size: string; file_size: string;
description: string;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
url: string; url: string;
version: string;
update_time?: string;
};

// 训练任务
export type TrainTask = {
ins_id: number; // 实例id
name: string; // 实验名称
experiment_id: number; //实验 id
workflow_id: number; // 流水线 id
};

// 项目依赖
export type ProjectDependency = {
url: string; // 项目地址
name: string; // 项目名称
branch: string; // 分支
}; };

+ 4
- 4
react-ui/src/pages/Dataset/intro.tsx View File

@@ -1,8 +1,8 @@
import ResourceIntro from '@/pages/Dataset/components/ResourceIntro';
import ResourceInfo from '@/pages/Dataset/components/ResourceInfo';
import { ResourceType } from '@/pages/Dataset/config'; import { ResourceType } from '@/pages/Dataset/config';


function DatasetIntro() {
return <ResourceIntro resourceType={ResourceType.Dataset} />;
function DatasetInfo() {
return <ResourceInfo resourceType={ResourceType.Dataset} />;
} }


export default DatasetIntro;
export default DatasetInfo;

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

@@ -8,11 +8,11 @@ import KFRadio, { type KFRadioItem } from '@/components/KFRadio';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import ResourceSelect, { import ResourceSelect, {
requiredValidator, requiredValidator,
ResourceSelectorType,
type ParameterInputObject, type ParameterInputObject,
} from '@/components/ResourceSelect'; } from '@/components/ResourceSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal';
import { createEditorReq } from '@/services/developmentEnvironment'; import { createEditorReq } from '@/services/developmentEnvironment';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { useNavigate } from '@umijs/max'; import { useNavigate } from '@umijs/max';
@@ -90,7 +90,6 @@ function EditorCreate() {
<Form <Form
name="editor-create" name="editor-create"
labelCol={{ flex: '100px' }} labelCol={{ flex: '100px' }}
wrapperCol={{ flex: 1 }}
labelAlign="left" labelAlign="left"
form={form} form={form}
initialValues={{ computing_resource: ComputingResourceType.GPU }} initialValues={{ computing_resource: ComputingResourceType.GPU }}


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

@@ -34,7 +34,7 @@ function ExperimentComparison() {
// const [cacheState, setCacheState] = useCacheState(); // const [cacheState, setCacheState] = useCacheState();
// const [total, setTotal] = useState(0); // const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [loading, setLoading] = useState(false);
// const [loading, setLoading] = useState(false);
const { message } = App.useApp(); const { message } = App.useApp();
const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]);
// const [pagination, setPagination] = useState<TablePaginationConfig>( // const [pagination, setPagination] = useState<TablePaginationConfig>(
@@ -50,11 +50,11 @@ function ExperimentComparison() {


// 获取对比数据列表 // 获取对比数据列表
const getComparisonData = async () => { const getComparisonData = async () => {
setLoading(true);
// setLoading(true);
const request = const request =
comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq;
const [res] = await to(request(experimentId)); const [res] = await to(request(experimentId));
setLoading(false);
// setLoading(false);
if (res && res.data) { if (res && res.data) {
// const { content = [], totalElements = 0 } = res.data; // const { content = [], totalElements = 0 } = res.data;
setTableData(res.data); setTableData(res.data);
@@ -204,7 +204,7 @@ function ExperimentComparison() {
scroll={{ y: 'calc(100% - 55px)', x: '100%' }} scroll={{ y: 'calc(100% - 55px)', x: '100%' }}
pagination={false} pagination={false}
bordered={true} bordered={true}
loading={loading}
// loading={loading}
// pagination={{ // pagination={{
// ...pagination, // ...pagination,
// total: total, // total: total,


+ 4
- 1
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -353,7 +353,7 @@ function ExperimentText() {
fitView: true, fitView: true,
minZoom: 0.5, minZoom: 0.5,
maxZoom: 5, maxZoom: 5,
fitViewPadding: 300,
fitViewPadding: 200,
modes: { modes: {
default: [ default: [
// config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles // config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles
@@ -504,6 +504,9 @@ function ExperimentText() {
key={experimentNodeData.id} key={experimentNodeData.id}
open={propsDrawerOpen} open={propsDrawerOpen}
onClose={closePropsDrawer} onClose={closePropsDrawer}
pipelineId={Number(locationParams.workflowId)}
experimentId={experimentIns.experiment_id}
experimentName={experimentIns.experiment_name}
instanceId={experimentIns.id} instanceId={experimentIns.id}
instanceName={experimentIns.argo_ins_name} instanceName={experimentIns.argo_ins_name}
instanceNamespace={experimentIns.argo_ins_ns} instanceNamespace={experimentIns.argo_ins_ns}


+ 9
- 0
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -13,6 +13,9 @@ import styles from './index.less';
type ExperimentDrawerProps = { type ExperimentDrawerProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
pipelineId: number; // 流水线 id
experimentId: number; // 实验 id
experimentName: string; // 实验 name
instanceId: number; // 实验实例 id instanceId: number; // 实验实例 id
instanceName: string; // 实验实例 name instanceName: string; // 实验实例 name
instanceNamespace: string; // 实验实例 namespace instanceNamespace: string; // 实验实例 namespace
@@ -26,6 +29,9 @@ type ExperimentDrawerProps = {
const ExperimentDrawer = ({ const ExperimentDrawer = ({
open, open,
onClose, onClose,
pipelineId,
experimentId,
experimentName,
instanceId, instanceId,
instanceName, instanceName,
instanceNamespace, instanceNamespace,
@@ -64,6 +70,9 @@ const ExperimentDrawer = ({
label: '输出结果', label: '输出结果',
children: ( children: (
<ExperimentResult <ExperimentResult
pipelineId={pipelineId}
experimentId={experimentId}
experimentName={experimentName}
experimentInsId={instanceId} experimentInsId={instanceId}
pipelineNodeId={instanceNodeData.id} pipelineNodeId={instanceNodeData.id}
></ExperimentResult> ></ExperimentResult>


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

@@ -116,6 +116,8 @@ function ExperimentInstanceComponent({
<img <img
style={{ width: '17px', marginRight: '7px' }} style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} src={experimentStatusInfo[item.status as ExperimentStatus]?.icon}
draggable={false}
alt=""
/> />
<span <span
style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }}


+ 41
- 16
react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx View File

@@ -1,3 +1,4 @@
import { ResourceType } from '@/pages/Dataset/config';
import { getNodeResult } from '@/services/experiment/index.js'; import { getNodeResult } from '@/services/experiment/index.js';
import { downLoadZip } from '@/utils/downloadfile'; import { downLoadZip } from '@/utils/downloadfile';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
@@ -8,6 +9,9 @@ import ExportModelModal from '../ExportModelModal';
import styles from './index.less'; import styles from './index.less';


type ExperimentResultProps = { type ExperimentResultProps = {
pipelineId: number; // 流水线 id
experimentId: number; // 实验 id
experimentName: string; // 实验 name
experimentInsId: number; // 实验实例 id experimentInsId: number; // 实验实例 id
pipelineNodeId: string; // 流水线节点 id pipelineNodeId: string; // 流水线节点 id
}; };
@@ -22,9 +26,21 @@ type ExperimentResultData = {
}[]; }[];
}; };


function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultProps) {
function ExperimentResult({
pipelineId,
experimentId,
experimentName,
experimentInsId,
pipelineNodeId,
}: ExperimentResultProps) {
const { message } = App.useApp(); const { message } = App.useApp();
const [experimentResults, setExperimentResults] = useState<ExperimentResultData[]>([]); const [experimentResults, setExperimentResults] = useState<ExperimentResultData[]>([]);
const resourceType: ResourceType | undefined = pipelineNodeId.startsWith('general-data-process')
? ResourceType.Dataset
: pipelineNodeId.startsWith('model-train') ||
pipelineNodeId.startsWith('distributed-model-train')
? ResourceType.Model
: undefined;


useEffect(() => { useEffect(() => {
getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId }); getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId });
@@ -33,8 +49,11 @@ function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultP
// 获取实验结果 // 获取实验结果
const getExperimentResult = async (params: any) => { const getExperimentResult = async (params: any) => {
const [res] = await to(getNodeResult(params)); const [res] = await to(getNodeResult(params));
if (res && res.data) {
setExperimentResults(res.data);
if (res && res.data && Array.isArray(res.data)) {
const data = res.data.filter((item: ExperimentResultData) => item.value.length > 0);
setExperimentResults(data);
} else {
setExperimentResults([]);
} }
}; };


@@ -43,9 +62,15 @@ function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultP
downLoadZip(`/api/mmp/minioStorage/download`, { path }); downLoadZip(`/api/mmp/minioStorage/download`, { path });
}; };


// 导出到模型
const exportToModel = (path: string) => {
// 导出到数据集、模型
const exportToResource = (path: string) => {
const { close } = openAntdModal(ExportModelModal, { const { close } = openAntdModal(ExportModelModal, {
resourceType: resourceType!,
pipelineId,
experimentId,
experimentName,
experimentInsId,
pipelineNodeId,
path, path,
onOk: () => { onOk: () => {
message.success('导出成功'); message.success('导出成功');
@@ -72,17 +97,17 @@ function ExperimentResult({ experimentInsId, pipelineNodeId }: ExperimentResultP
> >
下载 下载
</Button> </Button>
<Button
size="small"
type="link"
onClick={() => {
exportToModel(item.path);
}}
>
导出到模型库
</Button>
{/* <a style={{ marginRight: '10px' }}>导出到模型库</a>
<a style={{ marginRight: '10px' }}>导出到数据集</a> */}
{resourceType && (
<Button
size="small"
type="link"
onClick={() => {
exportToResource(item.path);
}}
>
导出到{resourceType === ResourceType.Model ? '模型' : '数据集'}
</Button>
)}
</div> </div>
<div style={{ margin: '15px 0' }} className={styles['experiment-result__item__file']}> <div style={{ margin: '15px 0' }} className={styles['experiment-result__item__file']}>
<span>文件名称</span> <span>文件名称</span>


+ 6
- 1
react-ui/src/pages/Experiment/components/ExperimentStatusCell/index.tsx View File

@@ -14,7 +14,12 @@ function ExperimentStatusCell(status?: ExperimentStatus | null) {
} }
return ( return (
<div className={styles['experiment-status-cell']}> <div className={styles['experiment-status-cell']}>
<img style={{ width: '17px', marginRight: '7px' }} src={statusInfo[status]?.icon} />
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo[status]?.icon}
draggable={false}
alt=""
/>
<span <span
style={{ color: statusInfo[status]?.color }} style={{ color: statusInfo[status]?.color }}
className={styles['experiment-status-cell__label']} className={styles['experiment-status-cell__label']}


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

@@ -1,47 +1,51 @@
import editExperimentIcon from '@/assets/img/edit-experiment.png'; import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { type ResourceData } from '@/pages/Dataset/config';
import { import {
addModelsVersionDetail,
exportModelReq,
getModelList,
getModelVersionsById,
} from '@/services/dataset';
DataSource,
ResourceType,
ResourceVersionData,
resourceConfig,
type ResourceData,
} from '@/pages/Dataset/config';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { Form, Input, ModalProps, Select } from 'antd'; import { Form, Input, ModalProps, Select } from 'antd';
import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styles from './index.less'; import styles from './index.less';


type FormData = { type FormData = {
models_id: string;
id: number;
version: string; version: string;
description: string;
};

type ExportModelResponce = {
fileName: string;
fileSize: string;
url: string;
};

type CreateModelVersionParams = FormData & {
file_name: string;
file_size: string;
url: string;
// name: string;
version_desc: string;
}; };


interface ExportModelModalProps extends Omit<ModalProps, 'onOk'> { interface ExportModelModalProps extends Omit<ModalProps, 'onOk'> {
path: string;
resourceType: ResourceType;
pipelineId: number; // 流水线 id
experimentId: number; // 实验 id
experimentName: string; // 实验 name
experimentInsId: number; // 实验实例 id
pipelineNodeId: string; // 流水线节点 id
path: string; // 文件路径
onOk: () => void; onOk: () => void;
} }


function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
function ExportModelModal({
resourceType,
pipelineId,
experimentId,
experimentName,
experimentInsId,
pipelineNodeId,
path,
onOk,
...rest
}: ExportModelModalProps) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [models, setModels] = useState<ResourceData[]>([]);
const [versions, setVersions] = useState<string[]>([]);
const [uuid] = useState(Date.now());
const [resources, setResources] = useState<ResourceData[]>([]);
const [versions, setVersions] = useState<ResourceVersionData[]>([]);
const config = resourceConfig[resourceType];


const layout = { const layout = {
labelCol: { span: 24 }, labelCol: { span: 24 },
@@ -49,43 +53,57 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
}; };


useEffect(() => { useEffect(() => {
requestModelList();
requestResourceList();
}, []); }, []);


// 模型版本tooltip
// 获取选中的数据集、模型
const getSelectedResource = (id: number | undefined) => {
if (id) {
return resources.find((item) => item.id === id);
}
return undefined;
};

// 版本 tooltip
const getTooltip = () => { const getTooltip = () => {
const id = form.getFieldValue('models_id');
const name = models.find((item) => item.id === id)?.name ?? '';
const id = form.getFieldValue('id');
const resource = getSelectedResource(id);
const name = resource?.name ?? '';
const versionNames = versions.map((item: ResourceVersionData) => item.name).join('、');
const tooltip = const tooltip =
versions.length > 0 ? `${name}有以下版本:\n${versions.join('、')}\n注意不能重复` : undefined;
versions.length > 0 ? `${name}有以下版本:\n${versionNames}\n注意不能重复` : undefined;
return tooltip; return tooltip;
}; };


// 处理模型名称变化
const handleModelChange = (id: number | undefined) => {
// 处理数据集、模型选择变化
const handleResourceChange = (id: number | undefined) => {
if (id) { if (id) {
getModelVersions(id);
getRecourceVersions(id);
} else { } else {
setVersions([]); setVersions([]);
} }
}; };


// 获取模型列表
const requestModelList = async () => {
// 获取数据集、模型列表
const requestResourceList = async () => {
const params = { const params = {
page: 0, page: 0,
size: 1000, size: 1000,
available_range: 0, // 个人
is_public: false, // 个人
}; };
const [res] = await to(getModelList(params));
const [res] = await to(config.getList(params));
if (res && res.data) { if (res && res.data) {
setModels(res.data.content || []);
setResources(res.data.content || []);
} }
}; };


// 获取模型版本列表
const getModelVersions = async (id: number) => {
const [res] = await to(getModelVersionsById(id));
// 获取数据集、模型版本列表
const getRecourceVersions = async (id: number) => {
const resource = getSelectedResource(id);
if (!resource) {
return;
}
const [res] = await to(config.getVersions(pick(resource, ['identifier', 'owner'])));
if (res && res.data) { if (res && res.data) {
setVersions(res.data); setVersions(res.data);
} }
@@ -93,32 +111,32 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {


// 提交 // 提交
const hanldeFinish = (formData: FormData) => { const hanldeFinish = (formData: FormData) => {
exportToModel(formData);
exportToResource(formData);
}; };


// 导出到模型
const exportToModel = async (formData: FormData) => {
// 导出到数据集、模型
const exportToResource = async (formData: FormData) => {
const id = form.getFieldValue('id');
const resource = getSelectedResource(id);
const params = { const params = {
uuid: String(uuid),
path,
...formData,
identifier: resource?.identifier,
name: resource?.name,
[config.sourceParamKey]: DataSource.HandExport,
train_task: {
workflow_id: pipelineId,
experiment_id: experimentId,
name: experimentName,
ins_id: experimentInsId,
task_id: pipelineNodeId,
},
[config.filePropKey]: [
{
url: path,
},
],
}; };
const [res] = await to(exportModelReq(params));
if (res && res.data) {
const files = res.data as ExportModelResponce[];
const params: CreateModelVersionParams[] = files.map((item) => ({
...formData,
file_name: item.fileName,
file_size: item.fileSize,
url: item.url,
}));

createModelVersion(params);
}
};

// 创建模型版本
const createModelVersion = async (params: CreateModelVersionParams[]) => {
const [res] = await to(addModelsVersionDetail(params));
const [res] = await to(config.addVersion(params));
if (res) { if (res) {
onOk(); onOk();
} }
@@ -127,7 +145,7 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
return ( return (
<KFModal <KFModal
{...rest} {...rest}
title="导出到模型库"
title={`导出到${config.name}`}
image={editExperimentIcon} image={editExperimentIcon}
okButtonProps={{ okButtonProps={{
htmlType: 'submit', htmlType: 'submit',
@@ -147,20 +165,20 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
labelWrap labelWrap
> >
<Form.Item <Form.Item
label="模型名称"
name="models_id"
rules={[{ required: true, message: '请选择模型' }]}
label={`${config.name}名称`}
name="id"
rules={[{ required: true, message: `请选择${config.name}` }]}
> >
<Select <Select
placeholder="请选择模型"
onChange={handleModelChange}
options={models}
placeholder={`请选择${config.name}`}
onChange={handleResourceChange}
options={resources}
fieldNames={{ label: 'name', value: 'id' }} fieldNames={{ label: 'name', value: 'id' }}
allowClear allowClear
></Select> ></Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="模型版本"
label={`${config.name}版本`}
name="version" name="version"
tooltip={ tooltip={
getTooltip() getTooltip()
@@ -172,11 +190,11 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
: undefined : undefined
} }
rules={[ rules={[
{ required: true, message: '请输入模型版本' },
{ required: true, message: `请输入${config.name}版本` },
{ {
validator: (_, value) => { validator: (_, value) => {
if (value && versions.includes(value)) {
return Promise.reject('模型版本已存在');
if (value && versions.map((item) => item.name).includes(value)) {
return Promise.reject(`${config.name}版本已存在`);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@@ -184,11 +202,11 @@ function ExportModelModal({ path, onOk, ...rest }: ExportModelModalProps) {
}, },
]} ]}
> >
<Input placeholder="请输入模型版本" maxLength={64} showCount allowClear />
<Input placeholder={`请输入${config.name}版本`} maxLength={64} showCount allowClear />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"
name="description"
name="version_desc"
rules={[{ required: true, message: '请输入版本描述' }]} rules={[{ required: true, message: '请输入版本描述' }]}
> >
<Input.TextArea <Input.TextArea


+ 2
- 0
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx View File

@@ -70,6 +70,8 @@ function TensorBoardStatusCell({
className={styles['tensorBoard-status__icon']} className={styles['tensorBoard-status__icon']}
src={statusConfig[status].icon} src={statusConfig[status].icon}
onClick={onClick} onClick={onClick}
draggable={false}
alt=""
/> />
)} )}
</> </>


+ 2
- 0
react-ui/src/pages/Experiment/index.jsx View File

@@ -385,6 +385,8 @@ function Experiment() {
style={{ width: '17px', marginRight: '6px' }} style={{ width: '17px', marginRight: '6px' }}
key={index} key={index}
src={experimentStatusInfo[item].icon} src={experimentStatusInfo[item].icon}
draggable={false}
alt=""
/> />
); );
}) })


+ 5
- 3
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -8,6 +8,7 @@ import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { MirrorVersionStatus } from '@/enums';
import { useDomSize } from '@/hooks'; import { useDomSize } from '@/hooks';
import { useCacheState } from '@/hooks/pageCacheState'; import { useCacheState } from '@/hooks/pageCacheState';
import { import {
@@ -36,7 +37,7 @@ import { useEffect, useMemo, useState } from 'react';
import MirrorStatusCell from '../components/MirrorStatusCell'; import MirrorStatusCell from '../components/MirrorStatusCell';
import styles from './index.less'; import styles from './index.less';


type MirrorInfoData = {
export type MirrorInfoData = {
name?: string; name?: string;
description?: string; description?: string;
version_count?: string; version_count?: string;
@@ -44,13 +45,14 @@ type MirrorInfoData = {
image_type?: number; image_type?: number;
}; };


type MirrorVersionData = {
export type MirrorVersionData = {
id: number; id: number;
version: string; version: string;
url: string; url: string;
status: string;
status: MirrorVersionStatus;
file_size: string; file_size: string;
create_time: string; create_time: string;
tag_name: string;
}; };


function MirrorInfo() { function MirrorInfo() {


+ 2
- 7
react-ui/src/pages/Model/components/ModelEvolution/index.less View File

@@ -1,16 +1,11 @@
.model-evolution { .model-evolution {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-x: hidden;
background-color: white; background-color: white;


&__top {
padding: 30px 0;
color: @text-color;
font-size: @font-size-content;
}

&__graph { &__graph {
height: calc(100% - 92px);
height: calc(100%);
background-color: @background-color; background-color: @background-color;
background-image: url(@/assets/img/pipeline-canvas-bg.png); background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%; background-size: 100% 100%;


+ 9
- 22
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -5,15 +5,12 @@
*/ */


import { useEffectWhen } from '@/hooks'; import { useEffectWhen } from '@/hooks';
import { ResourceVersionData } from '@/pages/Dataset/config';
import { getModelAtlasReq } from '@/services/dataset/index.js'; import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6, { G6GraphEvent, Graph, INode } from '@antv/g6'; import G6, { G6GraphEvent, Graph, INode } from '@antv/g6';
// @ts-ignore
import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import GraphLegend from '../GraphLegend';

import NodeTooltips from '../NodeTooltips'; import NodeTooltips from '../NodeTooltips';
import styles from './index.less'; import styles from './index.less';
import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils'; import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils';
@@ -29,7 +26,7 @@ import {


type modeModelEvolutionProps = { type modeModelEvolutionProps = {
resourceId: number; resourceId: number;
versionList: ResourceVersionData[];
identifier: string;
version?: string; version?: string;
isActive: boolean; isActive: boolean;
onVersionChange: (version: string) => void; onVersionChange: (version: string) => void;
@@ -38,7 +35,7 @@ type modeModelEvolutionProps = {
let graph: Graph; let graph: Graph;
function ModelEvolution({ function ModelEvolution({
resourceId, resourceId,
versionList,
identifier,
version, version,
isActive, isActive,
onVersionChange, onVersionChange,
@@ -147,14 +144,15 @@ function ModelEvolution({
// 更加缩放,调整 tooltip 位置 // 更加缩放,调整 tooltip 位置
const offsetX = (nodeWidth * zoom) / 4; const offsetX = (nodeWidth * zoom) / 4;
const offsetY = (nodeHeight * zoom) / 4; const offsetY = (nodeHeight * zoom) / 4;
point.x += offsetX;


const canvasWidth = graphRef.current!.clientWidth; const canvasWidth = graphRef.current!.clientWidth;
if (point.x + 300 > canvasWidth) {
point.x = canvasWidth - 300;
if (point.x + 300 > canvasWidth + 30) {
point.x = canvasWidth + 30 - 300;
} }


setHoverNodeData(model); setHoverNodeData(model);
setNodeToolTipX(point.x + offsetX);
setNodeToolTipX(point.x);
setNodeToolTipY(graphRef.current!.clientHeight - point.y + offsetY); setNodeToolTipY(graphRef.current!.clientHeight - point.y + offsetY);
setShowNodeTooltip(true); setShowNodeTooltip(true);
}); });
@@ -217,7 +215,8 @@ function ModelEvolution({
// 获取模型依赖 // 获取模型依赖
const getModelAtlas = async () => { const getModelAtlas = async () => {
const params = { const params = {
current_model_id: resourceId,
id: resourceId,
identifier,
version, version,
}; };
const [res] = await to(getModelAtlasReq(params)); const [res] = await to(getModelAtlasReq(params));
@@ -249,18 +248,6 @@ function ModelEvolution({


return ( return (
<div className={styles['model-evolution']}> <div className={styles['model-evolution']}>
<Flex align="center" className={styles['model-evolution__top']}>
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
allowClear
onChange={onVersionChange}
options={versionList}
/>
<GraphLegend style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegend>
</Flex>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>
{(showNodeTooltip || enterTooltip) && ( {(showNodeTooltip || enterTooltip) && (
<NodeTooltips <NodeTooltips


+ 72
- 59
react-ui/src/pages/Model/components/ModelEvolution/utils.tsx View File

@@ -1,3 +1,4 @@
import { TrainTask } from '@/pages/Dataset/config';
import { changePropertyName, fittingString } from '@/utils'; import { changePropertyName, fittingString } from '@/utils';
import { EdgeConfig, GraphData, LayoutConfig, NodeConfig, TreeGraphData, Util } from '@antv/g6'; import { EdgeConfig, GraphData, LayoutConfig, NodeConfig, TreeGraphData, Util } from '@antv/g6';
// @ts-ignore // @ts-ignore
@@ -31,16 +32,12 @@ export type Rect = {
height: number; height: number;
}; };


export type TrainTask = {
ins_id: number;
name: string;
task_id: string;
};

export interface TrainDataset extends NodeConfig { export interface TrainDataset extends NodeConfig {
dataset_id: number;
dataset_name: string;
dataset_version: string;
repo_id: number;
name: string;
version: string;
identifier: string;
owner: string;
model_type: NodeType.TestDataset | NodeType.TrainDataset; model_type: NodeType.TestDataset | NodeType.TrainDataset;
} }


@@ -51,34 +48,33 @@ export interface ProjectDependency extends NodeConfig {
model_type: NodeType.Project; model_type: NodeType.Project;
} }


export type ModalDetail = {
export type ModelMeta = {
train_datasets?: TrainDataset[];
test_datasets?: TrainDataset[];
project_depency?: ProjectDependency;
train_task?: TrainTask;
name: string; name: string;
available_range: number;
file_name: string;
file_size: string;
description: string;
model_type_name: string;
model_tag_name: string;
version: string;
model_source: string;
model_type: string;
create_time: string; create_time: string;
file_size: string;
is_public: boolean;
}; };


export interface ModelDepsAPIData { export interface ModelDepsAPIData {
current_model_id: number;
repo_id: number;
model_name: string;
version: string; version: string;
workflow_id: number; workflow_id: number;
exp_ins_id: number; exp_ins_id: number;
model_type: NodeType.Children | NodeType.Current | NodeType.Parent; model_type: NodeType.Children | NodeType.Current | NodeType.Parent;
current_model_name: string;
project_dependency?: ProjectDependency;
test_dataset: TrainDataset[];
train_dataset: TrainDataset[];
train_task: TrainTask;
model_version_dependcy_vo: ModalDetail;
children_models: ModelDepsAPIData[];
parent_models: ModelDepsAPIData[];
model_meta: ModelMeta;
child_model_list: ModelDepsAPIData[];
parent_model_vo?: ModelDepsAPIData;
} }


export interface ModelDepsData extends Omit<ModelDepsAPIData, 'children_models'>, TreeGraphData {
export interface ModelDepsData extends Omit<ModelDepsAPIData, 'child_model_list'>, TreeGraphData {
children: ModelDepsData[]; children: ModelDepsData[];
expanded: boolean; // 是否展开 expanded: boolean; // 是否展开
level: number; // 层级,从 0 开始 level: number; // 层级,从 0 开始
@@ -92,8 +88,11 @@ export function normalizeChildren(data: ModelDepsData[]) {
item.model_type = NodeType.Children; item.model_type = NodeType.Children;
item.expanded = false; item.expanded = false;
item.level = 0; item.level = 0;
item.datasetLen = item.train_dataset.length + item.test_dataset.length;
item.id = `$M_${item.current_model_id}_${item.version}`;
item.datasetLen = getDatasetLen(
item.model_meta.train_datasets,
item.model_meta.test_datasets,
);
item.id = `$M_${item.repo_id}_${item.version}`;
item.label = getLabel(item); item.label = getLabel(item);
item.style = getStyle(NodeType.Children); item.style = getStyle(NodeType.Children);
normalizeChildren(item.children); normalizeChildren(item.children);
@@ -104,16 +103,17 @@ export function normalizeChildren(data: ModelDepsData[]) {
// 获取 label // 获取 label
export function getLabel(node: ModelDepsData | ModelDepsAPIData) { export function getLabel(node: ModelDepsData | ModelDepsAPIData) {
return ( return (
fittingString(
`${node.model_version_dependcy_vo.name ?? ''}`,
nodeWidth - labelPadding,
nodeFontSize,
) +
fittingString(`${node.model_name ?? ''}`, nodeWidth - labelPadding, nodeFontSize) +
'\n' + '\n' +
fittingString(`${node.version}`, nodeWidth - labelPadding, nodeFontSize) fittingString(`${node.version}`, nodeWidth - labelPadding, nodeFontSize)
); );
} }


// 获取数据集数量
export function getDatasetLen(train?: TrainDataset[], test?: TrainDataset[]) {
return (train?.length || 0) + (test?.length || 0);
}

// 获取 style // 获取 style
export function getStyle(model_type: NodeType) { export function getStyle(model_type: NodeType) {
let fill = ''; let fill = '';
@@ -148,41 +148,43 @@ export function getStyle(model_type: NodeType) {
export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData { export function normalizeTreeData(apiData: ModelDepsAPIData): ModelDepsData {
// 将 children_models 转换成 children // 将 children_models 转换成 children
let normalizedData = changePropertyName(apiData, { let normalizedData = changePropertyName(apiData, {
children_models: 'children',
child_model_list: 'children',
}) as ModelDepsData; }) as ModelDepsData;


// 设置当前模型的数据 // 设置当前模型的数据
normalizedData.model_type = NodeType.Current; normalizedData.model_type = NodeType.Current;
normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
normalizedData.id = `$M_${normalizedData.repo_id}_${normalizedData.version}`;
normalizedData.label = getLabel(normalizedData); normalizedData.label = getLabel(normalizedData);
normalizedData.style = getStyle(NodeType.Current); normalizedData.style = getStyle(NodeType.Current);
normalizedData.expanded = true; normalizedData.expanded = true;
normalizedData.datasetLen =
normalizedData.train_dataset.length + normalizedData.test_dataset.length;
normalizedData.datasetLen = getDatasetLen(
normalizedData.model_meta.train_datasets,
normalizedData.model_meta.test_datasets,
);
normalizeChildren(normalizedData.children as ModelDepsData[]); normalizeChildren(normalizedData.children as ModelDepsData[]);
normalizedData.level = 0; normalizedData.level = 0;


// 将 parent_models 转换成树形结构 // 将 parent_models 转换成树形结构
let parent_models = normalizedData.parent_models || [];
while (parent_models.length > 0) {
const parent = parent_models[0];
let parent_model = normalizedData.parent_model_vo;
while (parent_model) {
const parent = parent_model;
normalizedData = { normalizedData = {
...parent, ...parent,
expanded: false, expanded: false,
level: 0, level: 0,
datasetLen: parent.train_dataset.length + parent.test_dataset.length,
datasetLen: getDatasetLen(parent.model_meta.train_datasets, parent.model_meta.test_datasets),
model_type: NodeType.Parent, model_type: NodeType.Parent,
id: `$M_${parent.current_model_id}_${parent.version}`,
id: `$M_${parent.repo_id}_${parent.version}`,
label: getLabel(parent), label: getLabel(parent),
style: getStyle(NodeType.Parent), style: getStyle(NodeType.Parent),
children: [ children: [
{ {
...normalizedData, ...normalizedData,
parent_models: [],
parent_model: null,
}, },
], ],
}; };
parent_models = normalizedData.parent_models || [];
parent_model = normalizedData.parent_model_vo;
} }
return normalizedData; return normalizedData;
} }
@@ -195,11 +197,12 @@ export function getGraphData(data: ModelDepsData, hierarchyNodes: ModelDepsData[
getWidth: () => nodeWidth, getWidth: () => nodeWidth,
getVGap: (node: NodeConfig) => { getVGap: (node: NodeConfig) => {
const model = node as ModelDepsData; const model = node as ModelDepsData;
const { model_type, expanded, project_dependency } = model;
const { model_type, expanded, model_meta } = model;
const { project_depency } = model_meta;
if (model_type === NodeType.Current || model_type === NodeType.Parent) { if (model_type === NodeType.Current || model_type === NodeType.Parent) {
return vGap / 2; return vGap / 2;
} }
const selfGap = expanded && project_dependency?.url ? nodeHeight + vGap : 0;
const selfGap = expanded && project_depency?.url ? nodeHeight + vGap : 0;
const nextNode = getSameHierarchyNextNode(model, hierarchyNodes); const nextNode = getSameHierarchyNextNode(model, hierarchyNodes);
if (!nextNode) { if (!nextNode) {
return vGap / 2; return vGap / 2;
@@ -254,28 +257,35 @@ const addDatasetDependency = (
nodes: NodeConfig[], nodes: NodeConfig[],
edges: EdgeConfig[], edges: EdgeConfig[],
) => { ) => {
const { train_dataset, test_dataset, id } = data;
train_dataset.forEach((item) => {
item.id = `$DTrain_${id}_${item.dataset_id}_${item.dataset_version}`;
const { repo_id, model_meta } = data;
const { train_datasets, test_datasets } = model_meta;
train_datasets?.forEach((item) => {
if (!item.repo_id) {
item.repo_id = item.id;
}
item.id = `$DTrain_${repo_id}_${item.repo_id}_${item.version}`;
item.model_type = NodeType.TrainDataset; item.model_type = NodeType.TrainDataset;
item.style = getStyle(NodeType.TrainDataset); item.style = getStyle(NodeType.TrainDataset);
}); });
test_dataset.forEach((item) => {
item.id = `$DTest_${id}_${item.dataset_id}_${item.dataset_version}`;
test_datasets?.forEach((item) => {
if (!item.repo_id) {
item.repo_id = item.id;
}
item.id = `$DTest_${repo_id}_${item.repo_id}_${item.version}`;
item.model_type = NodeType.TestDataset; item.model_type = NodeType.TestDataset;
item.style = getStyle(NodeType.TestDataset); item.style = getStyle(NodeType.TestDataset);
}); });


datasetNodes.length = 0; datasetNodes.length = 0;
const len = train_dataset.length + test_dataset.length;
[...train_dataset, ...test_dataset].forEach((item, index) => {
const len = getDatasetLen(train_datasets, test_datasets);
[...(train_datasets ?? []), ...(test_datasets ?? [])].forEach((item, index) => {
const node = { ...item }; const node = { ...item };
node.type = 'ellipse'; node.type = 'ellipse';
node.size = [ellipseWidth, nodeHeight]; node.size = [ellipseWidth, nodeHeight];
node.label = node.label =
fittingString(node.dataset_name, ellipseWidth - labelPadding, nodeFontSize) +
fittingString(node.name, ellipseWidth - labelPadding, nodeFontSize) +
'\n' + '\n' +
fittingString(node.dataset_version, ellipseWidth - labelPadding, nodeFontSize);
fittingString(node.version, ellipseWidth - labelPadding, nodeFontSize);


const half = len / 2 - 0.5; const half = len / 2 - 0.5;
node.x = currentNode.x! - (half - index) * (ellipseWidth + datasetHGap); node.x = currentNode.x! - (half - index) * (ellipseWidth + datasetHGap);
@@ -299,10 +309,11 @@ const addProjectDependency = (
nodes: NodeConfig[], nodes: NodeConfig[],
edges: EdgeConfig[], edges: EdgeConfig[],
) => { ) => {
const { project_dependency, id } = data;
if (project_dependency?.url) {
const node = { ...project_dependency };
node.id = `$P_${id}_${node.url}_${node.branch}`;
const { repo_id, model_meta } = data;
const { project_depency } = model_meta;
if (project_depency?.url) {
const node = { ...project_depency };
node.id = `$P_${repo_id}_${node.url}_${node.branch}`;
node.model_type = NodeType.Project; node.model_type = NodeType.Project;
node.type = 'rect'; node.type = 'rect';
node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize); node.label = fittingString(node.name, nodeWidth - labelPadding, nodeFontSize);
@@ -322,6 +333,7 @@ const addProjectDependency = (
} }
}; };


/*
// 判断两个矩形是否相交 // 判断两个矩形是否相交
function isRectanglesOverlap(rect1: Rect, rect2: Rect) { function isRectanglesOverlap(rect1: Rect, rect2: Rect) {
const a2x = rect1.x + rect1.width / 2; const a2x = rect1.x + rect1.width / 2;
@@ -366,6 +378,7 @@ function adjustDatasetPosition(node: NodeConfig) {
}); });
} }
} }
*/


// 层级遍历树结构 // 层级遍历树结构
export function traverseHierarchically(data: ModelDepsData | undefined): ModelDepsData[] { export function traverseHierarchically(data: ModelDepsData | undefined): ModelDepsData[] {


+ 2
- 0
react-ui/src/pages/Model/components/NodeTooltips/index.less View File

@@ -2,6 +2,7 @@
position: absolute; position: absolute;
bottom: -100px; bottom: -100px;
left: -300px; left: -300px;
z-index: 10;
width: 300px; width: 300px;
padding: 10px; padding: 10px;
background: white; background: white;
@@ -50,6 +51,7 @@
flex: 1; flex: 1;
min-width: 0; min-width: 0;
font-weight: 500; font-weight: 500;
word-break: break-all;


&:hover { &:hover {
text-decoration: underline @underline-color; text-decoration: underline @underline-color;


+ 23
- 23
react-ui/src/pages/Model/components/NodeTooltips/index.tsx View File

@@ -1,4 +1,4 @@
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { useNavigate } from '@umijs/max'; import { useNavigate } from '@umijs/max';
import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils'; import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils';
@@ -14,9 +14,9 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
const navigate = useNavigate(); const navigate = useNavigate();


const gotoExperimentPage = () => { const gotoExperimentPage = () => {
if (data.train_task?.ins_id) {
if (data.model_meta.train_task?.ins_id) {
const { origin } = location; const { origin } = location;
const url = `${origin}/pipeline/experiment/instance/${data.workflow_id}/${data.train_task.ins_id}`;
const url = `${origin}/pipeline/experiment/instance/${data.model_meta.train_task.workflow_id}/${data.model_meta.train_task.ins_id}`;
window.open(url, '_blank'); window.open(url, '_blank');
} }
}; };
@@ -25,10 +25,10 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
if (data.model_type === NodeType.Current) { if (data.model_type === NodeType.Current) {
return; return;
} }
if (data.current_model_id === resourceId) {
if (data.repo_id === resourceId) {
onVersionChange?.(data.version); onVersionChange?.(data.version);
} else { } else {
const path = `/dataset/model/info/${data.current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${data.version}`;
const path = `/dataset/model/info/${data.repo_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${data.version}&name=${data.model_name}&owner=${data.owner}&identifier=${data.identifier}`;
navigate(path); navigate(path);
} }
}; };
@@ -40,12 +40,10 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型名称:</span> <span className={styles['node-tooltips__row__title']}>模型名称:</span>
{data.model_type === NodeType.Current ? ( {data.model_type === NodeType.Current ? (
<span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.name || '--'}
</span>
<span className={styles['node-tooltips__row__value']}>{data.model_name || '--'}</span>
) : ( ) : (
<ValueLink <ValueLink
value={data.model_version_dependcy_vo?.name}
value={data.model_name}
className={styles['node-tooltips__row__link']} className={styles['node-tooltips__row__link']}
nullClassName={styles['node-tooltips__row__value']} nullClassName={styles['node-tooltips__row__value']}
onClick={gotoModelPage} onClick={gotoModelPage}
@@ -59,25 +57,25 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型框架:</span> <span className={styles['node-tooltips__row__title']}>模型框架:</span>
<span className={styles['node-tooltips__row__value']}> <span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.model_type_name || '--'}
{data.model_meta.model_type || '--'}
</span> </span>
</div> </div>
<div className={styles['node-tooltips__row']}>
{/* <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型大小:</span> <span className={styles['node-tooltips__row__title']}>模型大小:</span>
<span className={styles['node-tooltips__row__value']}> <span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.file_size || '--'}
{data.model_meta.file_size || '--'}
</span> </span>
</div>
</div> */}
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>创建时间:</span> <span className={styles['node-tooltips__row__title']}>创建时间:</span>
<span className={styles['node-tooltips__row__value']}> <span className={styles['node-tooltips__row__value']}>
{formatDate(data.model_version_dependcy_vo?.create_time)}
{formatDate(data.model_meta.create_time || '--')}
</span> </span>
</div> </div>
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>模型权限:</span> <span className={styles['node-tooltips__row__title']}>模型权限:</span>
<span className={styles['node-tooltips__row__value']}> <span className={styles['node-tooltips__row__value']}>
{data.model_version_dependcy_vo?.available_range === 1 ? '公开' : '私有'}
{data.model_meta.is_public ? '公开' : '私有'}
</span> </span>
</div> </div>
</div> </div>
@@ -86,7 +84,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>训练任务:</span> <span className={styles['node-tooltips__row__title']}>训练任务:</span>
<ValueLink <ValueLink
value={data.train_task?.name}
value={data.model_meta.train_task?.name}
className={styles['node-tooltips__row__link']} className={styles['node-tooltips__row__link']}
nullClassName={styles['node-tooltips__row__value']} nullClassName={styles['node-tooltips__row__value']}
onClick={gotoExperimentPage} onClick={gotoExperimentPage}
@@ -100,7 +98,7 @@ function ModelInfo({ resourceId, data, onVersionChange }: ModelInfoProps) {
function DatasetInfo({ data }: { data: TrainDataset }) { function DatasetInfo({ data }: { data: TrainDataset }) {
const gotoDatasetPage = () => { const gotoDatasetPage = () => {
const { origin } = location; const { origin } = location;
const url = `${origin}/dataset/dataset/info/${data.dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${data.dataset_version}`;
const url = `${origin}/dataset/dataset/info/${data.repo_id}?tab=${ResourceInfoTabKeys.Version}&version=${data.version}&name=${data.name}&owner=${data.owner}&identifier=${data.identifier}`;
window.open(url, '_blank'); window.open(url, '_blank');
}; };


@@ -111,7 +109,7 @@ function DatasetInfo({ data }: { data: TrainDataset }) {
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>数据集名称:</span> <span className={styles['node-tooltips__row__title']}>数据集名称:</span>
<ValueLink <ValueLink
value={data.dataset_name}
value={data.name}
className={styles['node-tooltips__row__link']} className={styles['node-tooltips__row__link']}
nullClassName={styles['node-tooltips__row__value']} nullClassName={styles['node-tooltips__row__value']}
onClick={gotoDatasetPage} onClick={gotoDatasetPage}
@@ -119,9 +117,7 @@ function DatasetInfo({ data }: { data: TrainDataset }) {
</div> </div>
<div className={styles['node-tooltips__row']}> <div className={styles['node-tooltips__row']}>
<span className={styles['node-tooltips__row__title']}>数据集版本:</span> <span className={styles['node-tooltips__row__title']}>数据集版本:</span>
<span className={styles['node-tooltips__row__value']}>
{data.dataset_version || '--'}
</span>
<span className={styles['node-tooltips__row__value']}>{data.version || '--'}</span>
</div> </div>
</div> </div>
</> </>
@@ -130,8 +126,12 @@ function DatasetInfo({ data }: { data: TrainDataset }) {


function ProjectInfo({ data }: { data: ProjectDependency }) { function ProjectInfo({ data }: { data: ProjectDependency }) {
const gotoProjectPage = () => { const gotoProjectPage = () => {
const { url } = data;
window.open(url, '_blank');
const { url, branch } = data;
let projectUrl = url;
if (url.endsWith('.git')) {
projectUrl = `${url.substring(0, url.length - 4)}/tree/${branch}`;
}
window.open(projectUrl, '_blank');
}; };


return ( return (


+ 4
- 4
react-ui/src/pages/Model/intro.tsx View File

@@ -1,8 +1,8 @@
import ResourceIntro from '@/pages/Dataset/components/ResourceIntro';
import ResourceInfo from '@/pages/Dataset/components/ResourceInfo';
import { ResourceType } from '@/pages/Dataset/config'; import { ResourceType } from '@/pages/Dataset/config';


function ModelIntro() {
return <ResourceIntro resourceType={ResourceType.Model} />;
function ModelInfo() {
return <ResourceInfo resourceType={ResourceType.Model} />;
} }


export default ModelIntro;
export default ModelInfo;

react-ui/src/pages/ModelDeployment/Create/index.less → react-ui/src/pages/ModelDeployment/CreateService/index.less View File


+ 182
- 0
react-ui/src/pages/ModelDeployment/CreateService/index.tsx View File

@@ -0,0 +1,182 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建推理服务
*/

import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys, serviceTypeOptions } from '@/enums';
import { createServiceReq, updateServiceReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import {
getSessionStorageItem,
removeSessionStorageItem,
serviceInfoKey,
} from '@/utils/sessionStorage';
import { useNavigate } from '@umijs/max';
import { App, Button, Col, Form, Input, Row, Select } from 'antd';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import { ServiceData, ServiceOperationType } from '../types';
import styles from './index.less';

// 表单数据
export type FormData = {
service_name: string; // 服务名称
service_type: string; // 服务类型
description: string; // 描述
};

function CreateService() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [operationType, setOperationType] = useState(ServiceOperationType.Create);
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const { message } = App.useApp();

useEffect(() => {
const res = getSessionStorageItem(serviceInfoKey, true);
if (res) {
setOperationType(res.operationType);
setServiceInfo(res);
form.setFieldsValue(pick(res, ['service_name', 'service_type', 'description']));
}
return () => {
removeSessionStorageItem(serviceInfoKey);
};
}, []);

// 创建、更新服务
const createService = async (formData: FormData) => {
const request =
operationType === ServiceOperationType.Create ? createServiceReq : updateServiceReq;
const params =
operationType === ServiceOperationType.Create
? formData
: {
id: serviceInfo?.id,
...formData,
};
const [res] = await to(request(params));
if (res) {
message.success('操作成功');
navigate(-1);
}
};

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

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

const disabled = operationType !== ServiceOperationType.Create;
const title = operationType === ServiceOperationType.Create ? '创建推理服务' : '更新推理服务';

return (
<div className={styles['model-deployment-create']}>
<PageTitle title={title}></PageTitle>
<div className={styles['model-deployment-create__content']}>
<div>
<Form
name="model-deployment-create"
labelCol={{ flex: '100px' }}
labelAlign="left"
form={form}
initialValues={{ upload_type: CommonTabKeys.Public }}
onFinish={handleSubmit}
size="large"
autoComplete="off"
>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="服务名称"
name="service_name"
rules={[
{
required: true,
message: '请输入服务名称',
},
]}
>
<Input
placeholder="请输入服务名称"
disabled={disabled}
maxLength={30}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="服务类型"
name="service_type"
rules={[
{
required: true,
message: '请选择服务类型',
},
]}
>
<Select placeholder="请选择服务类型" options={serviceTypeOptions} allowClear />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={20}>
<Form.Item
label="描  述"
name="description"
rules={[
{
required: true,
message: '请输入描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述,最长128字符"
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>

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

export default CreateService;

+ 19
- 0
react-ui/src/pages/ModelDeployment/CreateVersion/index.less View File

@@ -0,0 +1,19 @@
.create-service-version {
height: 100%;

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

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

react-ui/src/pages/ModelDeployment/Create/index.tsx → react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -1,108 +1,158 @@
/* /*
* @Author: 赵伟 * @Author: 赵伟
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 创建模型部署
* @Description: 创建服务版本
*/ */
import CodeSelect from '@/components/CodeSelect';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import ResourceSelect, { import ResourceSelect, {
requiredValidator, requiredValidator,
ResourceSelectorType,
type ParameterInputObject, type ParameterInputObject,
} from '@/components/ResourceSelect'; } from '@/components/ResourceSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import { ResourceSelectorType } from '@/pages/Pipeline/components/ResourceSelectorModal';
import { import {
createModelDeploymentReq,
restartModelDeploymentReq,
updateModelDeploymentReq,
createServiceVersionReq,
getServiceInfoReq,
updateServiceVersionReq,
} from '@/services/modelDeployment'; } from '@/services/modelDeployment';
import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils';
import { changePropertyName } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { import {
getSessionStorageItem, getSessionStorageItem,
modelDeploymentInfoKey,
removeSessionStorageItem, removeSessionStorageItem,
serviceVersionInfoKey,
} from '@/utils/sessionStorage'; } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { PlusOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd'; import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd';
import { omit, pick } from 'lodash'; import { omit, pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ModelDeploymentData, ModelDeploymentOperationType } from '../types';
import { ServiceData, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';


// 表单数据 // 表单数据
export type FormData = { export type FormData = {
serviceName: string; // 服务名称
service_name: string; // 服务名称
version: string; // 服务版本
description: string; // 描述 description: string; // 描述
model: ParameterInputObject; // 模型 model: ParameterInputObject; // 模型
image: ParameterInputObject; // 镜像 image: ParameterInputObject; // 镜像
code_config: ParameterInputObject; // 代码
resource: string; // 资源规格 resource: string; // 资源规格
replicas: string; // 副本数量 replicas: string; // 副本数量
modelPath: string; // 模型路径
env: { key: string; value: string }[]; // 环境变量
mount_path: string; // 模型路径
env_variables: { key: string; value: string }[]; // 环境变量
}; };


function ModelDeploymentCreate() {
function CreateServiceVersion() {
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [resourceStandardList, filterResourceStandard] = useComputingResource(); const [resourceStandardList, filterResourceStandard] = useComputingResource();
const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create);
const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>(
undefined,
);
const [operationType, setOperationType] = useState(ServiceOperationType.Create);
const { message } = App.useApp(); const { message } = App.useApp();
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined);
const params = useParams();
const id = params.id;


useEffect(() => { useEffect(() => {
const res = getSessionStorageItem(modelDeploymentInfoKey, true);
const res: (ServiceVersionData & { operationType: ServiceOperationType }) | undefined =
getSessionStorageItem(serviceVersionInfoKey, true);
if (res) { if (res) {
setOperationType(res.operationType); setOperationType(res.operationType);
setModelDeploymentInfo(res);
const formData = underscoreToCamelCase(res) as FormData;
setVersionInfo(res);
let model, codeConfig, envVariables;
if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
}
if (res.code_config && typeof res.code_config === 'object') {
codeConfig = changePropertyName(res.code_config, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value
codeConfig.value = codeConfig.showValue;
}
if (res.env_variables && typeof res.env_variables === 'object') {
envVariables = Object.entries(res.env_variables).map(([key, value]) => ({
key,
value,
}));
}

const formData = {
...omit(res, 'model', 'code_config', 'env_variables'),
model: model,
code_config: codeConfig,
env_variables: envVariables,
};
form.setFieldsValue(formData); form.setFieldsValue(formData);
} }
return () => { return () => {
removeSessionStorageItem(modelDeploymentInfoKey);
removeSessionStorageItem(serviceVersionInfoKey);
}; };
}, []); }, []);


// 创建
const createModelDeployment = async (formData: FormData) => {
const envList = formData['env'] ?? [];
useEffect(() => {
getServiceInfo();
}, []);

// 获取服务详情
const getServiceInfo = async () => {
const [res] = await to(getServiceInfoReq(id));
if (res && res.data) {
setServiceInfo(res.data);
form.setFieldsValue({
service_name: res.data.service_name,
});
}
};

// 创建版本
const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables'] ?? [];
const image = formData['image']; const image = formData['image'];
const model = formData['model']; const model = formData['model'];
const env = envList.reduce((acc, cur) => {
const codeConfig = formData['code_config'];
const envVariables = envList.reduce((acc, cur) => {
acc[cur.key] = cur.value; acc[cur.key] = cur.value;
return acc; return acc;
}, {} as Record<string, string>); }, {} as Record<string, string>);


// 根据后台要求,修改表单数据 // 根据后台要求,修改表单数据
const object = camelCaseToUnderscore({
...omit(formData, ['replicas', 'env', 'image', 'model']),
const object = {
...omit(formData, ['replicas', 'env_variables', 'image', 'model', 'code_config']),
replicas: Number(formData.replicas), replicas: Number(formData.replicas),
env,
env_variables: envVariables,
image: image.value, image: image.value,
model: pick(model, ['id', 'version', 'path', 'showValue']),
});
model: changePropertyName(
pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
{ showValue: 'show_value' },
),
code_config: changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), {
showValue: 'show_value',
}),
service_id: serviceInfo?.id,
};


const params = const params =
operationType === ModelDeploymentOperationType.Create
operationType === ServiceOperationType.Create
? object ? object
: { : {
...pick(modelDeploymentInfo, ['service_id', 'service_ins_id']),
update_model: {
...pick(object, ['description', 'env', 'replicas', 'resource', 'image']),
},
id: versionInfo?.id,
rerun: operationType === ServiceOperationType.Restart ? true : false,
deployment_name: versionInfo?.deployment_name,
...object,
}; };


let request = createModelDeploymentReq;
if (operationType === ModelDeploymentOperationType.Restart) {
request = restartModelDeploymentReq;
} else if (operationType === ModelDeploymentOperationType.Update) {
request = updateModelDeploymentReq;
}
const request =
operationType === ServiceOperationType.Create
? createServiceVersionReq
: updateServiceVersionReq;

const [res] = await to(request(params)); const [res] = await to(request(params));
if (res) { if (res) {
message.success('操作成功'); message.success('操作成功');
@@ -112,7 +162,7 @@ function ModelDeploymentCreate() {


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


// 取消 // 取消
@@ -120,25 +170,27 @@ function ModelDeploymentCreate() {
navigate(-1); navigate(-1);
}; };


const disabled = operationType !== ModelDeploymentOperationType.Create;
const disabled = operationType !== ServiceOperationType.Create;
let buttonText = '新建'; let buttonText = '新建';
if (operationType === ModelDeploymentOperationType.Update) {
let title = '新增服务版本';
if (operationType === ServiceOperationType.Update) {
title = '更新服务版本';
buttonText = '更新'; buttonText = '更新';
} else if (operationType === ModelDeploymentOperationType.Restart) {
} else if (operationType === ServiceOperationType.Restart) {
title = '重启服务版本';
buttonText = '重启'; buttonText = '重启';
} }


return ( return (
<div className={styles['model-deployment-create']}>
<PageTitle title="创建推理服务"></PageTitle>
<div className={styles['model-deployment-create__content']}>
<div className={styles['create-service-version']}>
<PageTitle title={title}></PageTitle>
<div className={styles['create-service-version__content']}>
<div> <div>
<Form <Form
name="model-deployment-create"
name="create-service-version"
labelCol={{ flex: '100px' }} labelCol={{ flex: '100px' }}
labelAlign="left" labelAlign="left"
form={form} form={form}
initialValues={{ upload_type: CommonTabKeys.Public }}
onFinish={handleSubmit} onFinish={handleSubmit}
size="large" size="large"
autoComplete="off" autoComplete="off"
@@ -152,7 +204,7 @@ function ModelDeploymentCreate() {
<Col span={10}> <Col span={10}>
<Form.Item <Form.Item
label="服务名称" label="服务名称"
name="serviceName"
name="service_name"
rules={[ rules={[
{ {
required: true, required: true,
@@ -162,8 +214,34 @@ function ModelDeploymentCreate() {
> >
<Input <Input
placeholder="请输入服务名称" placeholder="请输入服务名称"
disabled={disabled}
maxLength={30} maxLength={30}
disabled
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="服务版本"
name="version"
rules={[
{
required: true,
message: '请输入服务版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、下划线、点、横杠',
},
]}
>
<Input
placeholder="请输入服务版本"
maxLength={30}
disabled={disabled}
showCount showCount
allowClear allowClear
/> />
@@ -173,18 +251,18 @@ function ModelDeploymentCreate() {
<Row gutter={8}> <Row gutter={8}>
<Col span={20}> <Col span={20}>
<Form.Item <Form.Item
label="描  述"
label="版本描述"
name="description" name="description"
rules={[ rules={[
{ {
required: true, required: true,
message: '请输入描述',
message: '请输入版本描述',
}, },
]} ]}
> >
<Input.TextArea <Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述,最长128字符"
placeholder="请输入版本描述,最长128字符"
maxLength={128} maxLength={128}
showCount showCount
allowClear allowClear
@@ -238,6 +316,29 @@ function ModelDeploymentCreate() {
placeholder="请选择镜像" placeholder="请选择镜像"
canInput={false} canInput={false}
size="large" size="large"
disabled={disabled}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="代码配置"
name="code_config"
rules={[
{
validator: requiredValidator,
message: '请选择代码配置',
},
]}
required
>
<CodeSelect
placeholder="请选择代码配置"
canInput={false}
size="large"
disabled={disabled}
/> />
</Form.Item> </Form.Item>
</Col> </Col>
@@ -292,12 +393,16 @@ function ModelDeploymentCreate() {
<Col span={10}> <Col span={10}>
<Form.Item <Form.Item
label="挂载路径" label="挂载路径"
name="modelPath"
name="mount_path"
rules={[ rules={[
{ {
required: true, required: true,
message: '请输入模型挂载路径', message: '请输入模型挂载路径',
}, },
{
pattern: /^\/[a-zA-Z0-9._/-]+$/,
message: '请输入正确的挂载绝对路径',
},
]} ]}
> >
<Input <Input
@@ -311,15 +416,22 @@ function ModelDeploymentCreate() {
</Col> </Col>
</Row> </Row>


<Form.List name="env">
<Form.List name="env_variables">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
<Row gutter={8}> <Row gutter={8}>
<Col span={10}> <Col span={10}>
<Form.Item label="环境变量"> <Form.Item label="环境变量">
<Button type="link" style={{ padding: '0' }} onClick={() => add()}>
添加环境变量
</Button>
{fields.length === 0 ? (
<Button
type="link"
style={{ padding: '0' }}
onClick={() => add()}
disabled={disabled}
>
添加环境变量
</Button>
) : null}
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
@@ -329,9 +441,16 @@ function ModelDeploymentCreate() {
{...restField} {...restField}
name={[name, 'key']} name={[name, 'key']}
style={{ flex: 1 }} style={{ flex: 1 }}
rules={[{ required: true, message: '请输入变量名' }]}
rules={[
{ required: true, message: '请输入变量名' },
{
pattern: /^[a-zA-Z_][a-zA-Z0-9_-]*$/,
message:
'变量名只支持字母、数字、下划线、中横线且开头必须是字母或下划线',
},
]}
> >
<Input placeholder="请输入变量名" />
<Input placeholder="请输入变量名" disabled={disabled} />
</Form.Item> </Form.Item>
<span style={{ marginBottom: '24px' }}>=</span> <span style={{ marginBottom: '24px' }}>=</span>
<Form.Item <Form.Item
@@ -340,15 +459,17 @@ function ModelDeploymentCreate() {
style={{ flex: 1 }} style={{ flex: 1 }}
rules={[{ required: true, message: '请输入变量值' }]} rules={[{ required: true, message: '请输入变量值' }]}
> >
<Input placeholder="请输入变量值" />
<Input placeholder="请输入变量值" disabled={disabled} />
</Form.Item> </Form.Item>
<Button <Button
type="link" type="link"
style={{ marginBottom: '24px' }} style={{ marginBottom: '24px' }}
icon={<KFIcon type="icon-shanchu" font={16} />} icon={<KFIcon type="icon-shanchu" font={16} />}
disabled={disabled}
onClick={() => { onClick={() => {
modalConfirm({ modalConfirm({
content: '是否确认删除?',
title: '删除',
content: '是否确认要删除该环境变量?',
onOk: () => { onOk: () => {
remove(name); remove(name);
}, },
@@ -357,10 +478,20 @@ function ModelDeploymentCreate() {
></Button> ></Button>
</Flex> </Flex>
))} ))}
{fields.length > 0 ? (
<Button
type="link"
style={{ padding: '0', margin: '-24px 0 24px' }}
onClick={() => add()}
icon={<PlusOutlined />}
disabled={disabled}
>
环境变量
</Button>
) : null}
</> </>
)} )}
</Form.List> </Form.List>

<Form.Item wrapperCol={{ offset: 0, span: 16 }}> <Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
{buttonText} {buttonText}
@@ -381,4 +512,4 @@ function ModelDeploymentCreate() {
); );
} }


export default ModelDeploymentCreate;
export default CreateServiceVersion;

+ 67
- 119
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -1,22 +1,18 @@
/* /*
* @Author: 赵伟 * @Author: 赵伟
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 模型部署列表
* @Description: 模型部署服务列表
*/ */
import CommonTableCell from '@/components/CommonTableCell'; import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell'; import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import { ModelDeploymentStatus, modelDeploymentStatusOptions } from '@/enums';
import { serviceTypeOptions } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState'; import { useCacheState } from '@/hooks/pageCacheState';
import {
deleteModelDeploymentReq,
getModelDeploymentListReq,
stopModelDeploymentReq,
} from '@/services/modelDeployment';
import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modelDeploymentInfoKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { serviceInfoKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max'; import { useNavigate } from '@umijs/max';
import { import {
@@ -31,20 +27,20 @@ import {
} from 'antd'; } from 'antd';
import { type SearchProps } from 'antd/es/input'; import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames'; import classNames from 'classnames';
import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell';
import { ModelDeploymentData, ModelDeploymentOperationType } from '../types';
import { ServiceData, ServiceOperationType } from '../types';
import styles from './index.less'; import styles from './index.less';


const allServiceTypeOptions = [{ label: '全部', value: '' }, ...serviceTypeOptions];

function ModelDeployment() { function ModelDeployment() {
const navigate = useNavigate(); const navigate = useNavigate();
const { message } = App.useApp(); const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState(); const [cacheState, setCacheState] = useCacheState();
const [searchStatus, setSearchStatus] = useState(cacheState?.searchStatus ?? '');
const [serviceType, setServiceType] = useState(cacheState?.serviceType ?? '');
const [searchText, setSearchText] = useState(cacheState?.searchText); const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText); const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<ModelDeploymentData[]>([]);
const [tableData, setTableData] = useState<ServiceData[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>( const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? { cacheState?.pagination ?? {
@@ -54,29 +50,28 @@ function ModelDeployment() {
); );


useEffect(() => { useEffect(() => {
getModelDeploymentList();
}, [pagination, searchText, searchStatus]);
getServiceList();
}, [pagination, searchText, serviceType]);


// 获取模型部署列表
const getModelDeploymentList = async () => {
// 获取模型部署服务列表
const getServiceList = async () => {
const params: Record<string, any> = { const params: Record<string, any> = {
page: pagination.current!,
page: pagination.current! - 1,
size: pagination.pageSize, size: pagination.pageSize,
service_name: searchText, service_name: searchText,
status: searchStatus,
service_type: serviceType,
}; };
const [res] = await to(getModelDeploymentListReq(params));
const [res] = await to(getServiceListReq(params));
if (res && res.data) { if (res && res.data) {
const { service_list = [], total = 0 } = res.data;
setTableData(service_list);
setTotal(total);
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
} }
}; };


// 删除模型部署 // 删除模型部署
const deleteModelDeploy = async (record: ModelDeploymentData) => {
const params = pick(record, ['service_id', 'service_ins_id']);
const [res] = await to(deleteModelDeploymentReq(params));
const deleteService = async (record: ServiceData) => {
const [res] = await to(deleteServiceReq(record.id));
if (res) { if (res) {
message.success('删除成功'); message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据 // 如果是一页的唯一数据,删除时,请求第一页的数据
@@ -88,54 +83,31 @@ function ModelDeployment() {
current: 1, current: 1,
})); }));
} else { } else {
getModelDeploymentList();
getServiceList();
} }
} }
}; };


// 停止模型部署
const stopModelDeploy = async (record: ModelDeploymentData) => {
const params = pick(record, ['service_id', 'service_ins_id']);
const [res] = await to(stopModelDeploymentReq(params));
if (res) {
message.success('操作成功');
getModelDeploymentList();
}
};

// 搜索 // 搜索
const onSearch: SearchProps['onSearch'] = (value) => { const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value); setSearchText(value);
}; };


// 处理删除 // 处理删除
const handleModelDeployDelete = (record: ModelDeploymentData) => {
const handleServiceDelete = (record: ServiceData) => {
modalConfirm({ modalConfirm({
title: '删除后,该模型部署将不可恢复',
title: '删除后,该服务将不可恢复',
content: '是否确认删除?', content: '是否确认删除?',
onOk: () => { onOk: () => {
deleteModelDeploy(record);
deleteService(record);
}, },
}); });
}; };


// 处理停止
const handleModelDeployStop = async (record: ModelDeploymentData) => {
modalConfirm({
content: '是否确认停止?',
onOk: () => {
stopModelDeploy(record);
},
});
};

// 创建、更新、重启模型部署
const createModelDeployment = (
type: ModelDeploymentOperationType,
record?: ModelDeploymentData,
) => {
// 创建、更新服务
const createService = (type: ServiceOperationType, record?: ServiceData) => {
setSessionStorageItem( setSessionStorageItem(
modelDeploymentInfoKey,
serviceInfoKey,
{ {
...record, ...record,
operationType: type, operationType: type,
@@ -146,23 +118,23 @@ function ModelDeployment() {
setCacheState({ setCacheState({
pagination, pagination,
searchText, searchText,
searchStatus,
serviceType: serviceType,
}); });


navigate(`/modelDeployment/create`);
navigate(`/modelDeployment/createService`);
}; };


// 查看详情 // 查看详情
const toDetail = (record: ModelDeploymentData) => {
setSessionStorageItem(modelDeploymentInfoKey, record, true);
const toDetail = (record: ServiceData) => {
setSessionStorageItem(serviceInfoKey, record, true);


setCacheState({ setCacheState({
pagination, pagination,
searchText, searchText,
searchStatus,
serviceType: serviceType,
}); });


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


// 分页切换 // 分页切换
@@ -173,7 +145,7 @@ function ModelDeployment() {
// console.log(pagination, filters, sorter, action); // console.log(pagination, filters, sorter, action);
}; };


const columns: TableProps<ModelDeploymentData>['columns'] = [
const columns: TableProps<ServiceData>['columns'] = [
{ {
title: '序号', title: '序号',
dataIndex: 'index', dataIndex: 'index',
@@ -197,23 +169,23 @@ function ModelDeployment() {
}, },
}, },
{ {
title: '型',
dataIndex: ['model', 'show_value'],
key: 'model',
title: '服务类型',
dataIndex: 'service_type_name',
key: 'service_type_name',
width: '20%', width: '20%',
render: CommonTableCell(), render: CommonTableCell(),
}, },
{ {
title: '状态',
dataIndex: 'status',
key: 'status',
title: '版本数量',
dataIndex: 'version_count',
key: 'version_count',
width: '20%', width: '20%',
render: ModelDeploymentStatusCell,
render: CommonTableCell(),
}, },
{ {
title: '创建人',
dataIndex: 'created_by',
key: 'created_by',
title: '服务描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(), render: CommonTableCell(),
width: '20%', width: '20%',
}, },
@@ -227,44 +199,28 @@ function ModelDeployment() {
{ {
title: '操作', title: '操作',
dataIndex: 'operation', dataIndex: 'operation',
width: 250,
width: 300,
key: 'operation', key: 'operation',
render: (_: any, record: ModelDeploymentData) => (
render: (_: any, record: ServiceData) => (
<div> <div>
<Button <Button
type="link" type="link"
size="small" size="small"
key="edit" key="edit"
icon={<KFIcon type="icon-bianji" />} icon={<KFIcon type="icon-bianji" />}
onClick={() => createModelDeployment(ModelDeploymentOperationType.Update, record)}
onClick={() => createService(ServiceOperationType.Update, record)}
> >
更新
编辑
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-xiangqing" />}
onClick={() => toDetail(record)}
>
查看详情
</Button> </Button>
{(record.status === ModelDeploymentStatus.Failed ||
record.status === ModelDeploymentStatus.Stopped) && (
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => createModelDeployment(ModelDeploymentOperationType.Restart, record)}
>
重启
</Button>
)}
{(record.status === ModelDeploymentStatus.Running ||
record.status === ModelDeploymentStatus.Init ||
record.status === ModelDeploymentStatus.Pending) && (
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => handleModelDeployStop(record)}
>
停止
</Button>
)}
<ConfigProvider <ConfigProvider
theme={{ theme={{
token: { token: {
@@ -277,7 +233,7 @@ function ModelDeployment() {
size="small" size="small"
key="remove" key="remove"
icon={<KFIcon type="icon-shanchu" />} icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleModelDeployDelete(record)}
onClick={() => handleServiceDelete(record)}
> >
删除 删除
</Button> </Button>
@@ -289,11 +245,11 @@ function ModelDeployment() {


return ( return (
<div className={styles['model-deployment']}> <div className={styles['model-deployment']}>
<PageTitle title="模型列表"></PageTitle>
<PageTitle title="服务列表"></PageTitle>
<div className={styles['model-deployment__content']}> <div className={styles['model-deployment__content']}>
<div className={styles['model-deployment__content__filter']}> <div className={styles['model-deployment__content__filter']}>
<Input.Search <Input.Search
placeholder="按模型服务名称筛选"
placeholder="按服务名称筛选"
onSearch={onSearch} onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)} onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }} style={{ width: 300 }}
@@ -303,27 +259,19 @@ function ModelDeployment() {
<Select <Select
style={{ width: 100, marginLeft: '20px' }} style={{ width: 100, marginLeft: '20px' }}
placeholder="请选择" placeholder="请选择"
onChange={(value) => setSearchStatus(value)}
options={modelDeploymentStatusOptions}
value={searchStatus}
onChange={(value) => setServiceType(value ?? '')}
options={allServiceTypeOptions}
value={serviceType}
allowClear allowClear
></Select> ></Select>
<Button <Button
style={{ marginLeft: '20px' }}
style={{ marginLeft: 'auto', marginRight: '20px' }}
type="default" type="default"
onClick={() => createModelDeployment(ModelDeploymentOperationType.Create)}
onClick={() => createService(ServiceOperationType.Create)}
icon={<KFIcon type="icon-xinjian2" />} icon={<KFIcon type="icon-xinjian2" />}
> >
创建推理服务 创建推理服务
</Button> </Button>
<Button
style={{ marginRight: 0, marginLeft: 'auto' }}
type="default"
onClick={getModelDeploymentList}
icon={<KFIcon type="icon-shuaxin" />}
>
刷新
</Button>
</div> </div>
<div <div
className={classNames( className={classNames(
@@ -343,7 +291,7 @@ function ModelDeployment() {
showTotal: () => `共${total}条`, showTotal: () => `共${total}条`,
}} }}
onChange={handleTableChange} onChange={handleTableChange}
rowKey="service_id"
rowKey="id"
/> />
</div> </div>
</div> </div>


+ 24
- 0
react-ui/src/pages/ModelDeployment/ServiceInfo/index.less View File

@@ -0,0 +1,24 @@
.service-info {
height: 100%;
&__content {
display: flex;
flex-direction: column;
height: calc(100% - 60px);
margin-top: 10px;
padding: 20px 30px 0;
background-color: white;
border-radius: 10px;

&__filter {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
}

&__table {
flex: 1;
margin-top: 24px;
}
}
}

+ 431
- 0
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -0,0 +1,431 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 模型部署列表
*/
import BasicInfo from '@/components/BasicInfo';
import CommonTableCell from '@/components/CommonTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useComputingResource } from '@/hooks/resource';
import {
deleteServiceVersionReq,
getServiceInfoReq,
getServiceVersionsReq,
stopServiceVersionReq,
} from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { serviceVersionInfoKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui';
import { useNavigate, useParams } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Select,
Table,
Tooltip,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ServiceRunStatusCell from '../components/ModelDeployStatusCell';
import { ServiceData, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less';

const allServiceStatusOptions = [{ label: '全部', value: '' }, ...serviceStatusOptions];

function ServiceInfo() {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [serviceStatus, setServiceStatus] = useState(cacheState?.serviceStatus ?? '');
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<ServiceVersionData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);
const params = useParams();
const id = params.id;
const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
const basicInfo = [
{
label: '服务名称',
value: serviceInfo?.service_name,
},
{
label: '服务描述',
value: serviceInfo?.description,
},
{
label: '版本数量',
value: serviceInfo?.version_count,
},
{
label: '创建时间',
value: serviceInfo?.create_time,
format: formatDate,
},
];
const getResourceDescription = useComputingResource()[2];

useEffect(() => {
getServiceInfo();
}, []);

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

// 获取服务详情
const getServiceInfo = async () => {
const [res] = await to(getServiceInfoReq(id));
if (res && res.data) {
setServiceInfo(res.data);
}
};

// 获取服务版本列表
const getServiceVersions = async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
version: searchText,
run_state: serviceStatus,
service_id: id,
};
const [res] = await to(getServiceVersionsReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

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

// 停止模型部署
const stopServiceVersion = async (record: ServiceVersionData) => {
const [res] = await to(stopServiceVersionReq(record.id));
if (res) {
message.success('操作成功');
getServiceVersions();
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 处理删除
const handleServiceVersionDelete = (record: ServiceVersionData) => {
modalConfirm({
title: '删除后,该服务版本将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteServiceVersion(record);
},
});
};

// 处理停止
const handleServiceVersionStop = async (record: ServiceVersionData) => {
modalConfirm({
title: '停止',
content: '是否确认停止该服务?',
onOk: () => {
stopServiceVersion(record);
},
});
};

// 创建、更新、重启模型部署
const createServiceVersion = (type: ServiceOperationType, record?: ServiceVersionData) => {
setSessionStorageItem(
serviceVersionInfoKey,
{
...record,
operationType: type,
},
true,
);

setCacheState({
pagination,
searchText,
serviceStatus: serviceStatus,
});

navigate(`/modelDeployment/addVersion/${id}`);
};

// 查看详情
const toDetail = (record: ServiceVersionData) => {
setSessionStorageItem(serviceVersionInfoKey, record, true);

setCacheState({
pagination,
searchText,
serviceStatus: serviceStatus,
});

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

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

const columns: TableProps<ServiceVersionData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render(_text, _record, index) {
return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>;
},
},
{
title: '服务版本',
dataIndex: 'version',
key: 'version',
width: '20%',
render: CommonTableCell(),
},
{
title: '模型版本',
dataIndex: 'model',
key: 'model',
width: '20%',
render: (_text: string, record: ServiceVersionData) => (
<Tooltip
title={record.model.show_value}
placement="topLeft"
overlayStyle={{ maxWidth: '400px' }}
>
<span>{record.model.show_value}</span>
</Tooltip>
),
ellipsis: { showTitle: false },
},
{
title: '状态',
dataIndex: 'run_state',
key: 'run_state',
width: '20%',
render: ServiceRunStatusCell,
},
{
title: '版本镜像',
dataIndex: 'image',
key: 'image',
width: '20%',
render: CommonTableCell(true),
ellipsis: { showTitle: false },
},
{
title: '副本数量',
dataIndex: 'replicas',
key: 'replicas',
render: CommonTableCell(),
width: '20%',
},
{
title: '资源规格',
dataIndex: 'resource',
key: 'resource',
width: '20%',
render: (resource: string) => (
<Tooltip
title={getResourceDescription(resource)}
placement="topLeft"
overlayStyle={{ maxWidth: '400px' }}
>
<span>{resource ? getResourceDescription(resource) : '--'}</span>
</Tooltip>
),
ellipsis: { showTitle: false },
},
{
title: '操作',
dataIndex: 'operation',
width: 320,
key: 'operation',
render: (_: any, record: ServiceVersionData) => (
<div>
<Button
type="link"
size="small"
key="info"
icon={<KFIcon type="icon-xiangqing" />}
onClick={() => toDetail(record)}
>
详情
</Button>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createServiceVersion(ServiceOperationType.Update, record)}
>
更新
</Button>
{(record.run_state === ServiceRunStatus.Failed ||
record.run_state === ServiceRunStatus.Stopped) && (
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => createServiceVersion(ServiceOperationType.Restart, record)}
>
重启
</Button>
)}
{(record.run_state === ServiceRunStatus.Running ||
record.run_state === ServiceRunStatus.Init ||
record.run_state === ServiceRunStatus.Pending) && (
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => handleServiceVersionStop(record)}
>
停止
</Button>
)}

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

return (
<div className={styles['service-info']}>
<PageTitle title="服务详情"></PageTitle>
<div className={styles['service-info__content']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px', flex: 'none' }}
></SubAreaTitle>
<BasicInfo datas={basicInfo} labelWidth={66} style={{ flex: 'none' }}></BasicInfo>
<SubAreaTitle
title="服务版本"
image={require('@/assets/img/service-version.png')}
style={{ margin: '40px 0 26px', flex: 'none' }}
></SubAreaTitle>
<div className={styles['service-info__content__filter']}>
<Input.Search
placeholder="按服务版本筛选"
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
allowClear
/>
<Select
style={{ width: 100, marginLeft: '20px' }}
placeholder="请选择"
onChange={(value) => setServiceStatus(value ?? '')}
options={allServiceStatusOptions}
value={serviceStatus}
allowClear
></Select>
<Button
style={{ marginRight: '20px', marginLeft: 'auto' }}
type="default"
onClick={() => createServiceVersion(ServiceOperationType.Create)}
icon={<KFIcon type="icon-xinjian2" />}
>
新增版本
</Button>
<Button
style={{ marginRight: 0 }}
type="default"
onClick={getServiceVersions}
icon={<KFIcon type="icon-shuaxin" />}
>
刷新
</Button>
</div>
<div
className={classNames('vertical-scroll-table', styles['service-info__content__table'])}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ServiceInfo;

react-ui/src/pages/ModelDeployment/Info/index.less → react-ui/src/pages/ModelDeployment/VersionInfo/index.less View File

@@ -1,4 +1,4 @@
.model-deployment-info {
.service-version-info {
height: 100%; height: 100%;


&__content { &__content {

react-ui/src/pages/ModelDeployment/Info/index.tsx → react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx View File

@@ -1,19 +1,20 @@
/* /*
* @Author: 赵伟 * @Author: 赵伟
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 镜像详情
* @Description: 服务版本详情
*/ */
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { useSessionStorage } from '@/hooks/sessionStorage';
import { modelDeploymentInfoKey } from '@/utils/sessionStorage';
import { getServiceVersionInfoReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
import { Tabs, type TabsProps } from 'antd'; import { Tabs, type TabsProps } from 'antd';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import BasicInfo from '../components/BasicInfo'; import BasicInfo from '../components/BasicInfo';
import ServerLog from '../components/ServerLog'; import ServerLog from '../components/ServerLog';
import UserGuide from '../components/UserGuide'; import UserGuide from '../components/UserGuide';
import { ModelDeploymentData } from '../types';
import { ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';


export enum ModelDeploymentTabKey { export enum ModelDeploymentTabKey {
@@ -22,13 +23,23 @@ export enum ModelDeploymentTabKey {
Log = 'Log', // 服务日志 Log = 'Log', // 服务日志
} }


function ModelDeploymentInfo() {
function ServiceVersionInfo() {
const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict); const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict);
const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>(
modelDeploymentInfoKey,
true,
undefined,
);
const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined);
const params = useParams();
const id = params.id;

useEffect(() => {
getServiceVersionInfo();
}, []);

// 获取服务版本详情
const getServiceVersionInfo = async () => {
const [res] = await to(getServiceVersionInfoReq(id));
if (res && res.data) {
setVersionInfo(res.data);
}
};


const tabItems = [ const tabItems = [
{ {
@@ -40,13 +51,13 @@ function ModelDeploymentInfo() {
key: ModelDeploymentTabKey.Guide, key: ModelDeploymentTabKey.Guide,
label: '调用指南', label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />, icon: <KFIcon type="icon-tiaoyongzhinan" />,
children: <UserGuide info={modelDeployementInfo}></UserGuide>,
children: <UserGuide info={versionInfo}></UserGuide>,
}, },
{ {
key: ModelDeploymentTabKey.Log, key: ModelDeploymentTabKey.Log,
label: '服务日志', label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />, icon: <KFIcon type="icon-fuwurizhi" />,
children: <ServerLog info={modelDeployementInfo}></ServerLog>,
children: <ServerLog info={versionInfo}></ServerLog>,
}, },
]; ];


@@ -56,16 +67,16 @@ function ModelDeploymentInfo() {
}; };


return ( return (
<div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle>
<div className={styles['model-deployment-info__content']}>
<div className={styles['service-version-info']}>
<PageTitle title="服务版本详情"></PageTitle>
<div className={styles['service-version-info__content']}>
<SubAreaTitle <SubAreaTitle
title="基本信息" title="基本信息"
image={require('@/assets/img/mirror-basic.png')} image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }} style={{ marginBottom: '26px' }}
></SubAreaTitle> ></SubAreaTitle>
<BasicInfo info={modelDeployementInfo} />
<div className={styles['model-deployment-info__content__tabs']}>
<BasicInfo info={versionInfo} />
<div className={styles['service-version-info__content__tabs']}>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} /> <Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</div> </div>
</div> </div>
@@ -73,4 +84,4 @@ function ModelDeploymentInfo() {
); );
} }


export default ModelDeploymentInfo;
export default ServiceVersionInfo;

+ 47
- 21
react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx View File

@@ -1,12 +1,13 @@
import LabelValue from '@/components/LabelValue'; import LabelValue from '@/components/LabelValue';
import { useComputingResource } from '@/hooks/resource'; import { useComputingResource } from '@/hooks/resource';
import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
import { ServiceVersionData } from '@/pages/ModelDeployment/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { Link } from '@umijs/max';
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import ModelDeploymentStatusCell from '../ModelDeployStatusCell';
import ServiceRunStatusCell from '../ModelDeployStatusCell';


type BasicInfoProps = { type BasicInfoProps = {
info?: ModelDeploymentData;
info?: ServiceVersionData;
}; };


function BasicInfo({ info }: BasicInfoProps) { function BasicInfo({ info }: BasicInfoProps) {
@@ -14,42 +15,75 @@ function BasicInfo({ info }: BasicInfoProps) {


// 格式化环境变量 // 格式化环境变量
const formatEnvText = () => { const formatEnvText = () => {
if (!info?.env) {
if (!info?.env_variables) {
return '--'; return '--';
} }
const env = info.env;
const env = info.env_variables;
return Object.entries(env) return Object.entries(env)
.map(([key, value]) => `${key}: ${value}`) .map(([key, value]) => `${key}: ${value}`)
.join('\n'); .join('\n');
}; };


const formatCodeConfig = () => {
if (info && info.code_config) {
const url = `${info.code_config.code_path}/tree/${info.code_config.branch}`;
return (
<a href={url} target="_blank" rel="noreferrer">
{info?.code_config?.show_value}
</a>
);
}
return undefined;
};

const formatResource = () => {
if (info && info.resource) {
return getResourceDescription(info.resource);
}
return undefined;
};

const formatModel = () => {
if (info && info.model) {
const model = info.model;
const path = `/dataset/model/info/${model.id}?version=${model.version}&name=${model.name}&owner=${model.owner}&identifier=${model.identifier}`;
return <Link to={path}>{info?.model?.show_value}</Link>;
}
return undefined;
};

return ( return (
<div> <div>
<Row gutter={40} style={{ marginBottom: '20px' }}> <Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}> <Col span={10}>
<LabelValue label="服务名称:" value={info?.service_name}></LabelValue> <LabelValue label="服务名称:" value={info?.service_name}></LabelValue>
</Col> </Col>
<Col span={10}>
<LabelValue label="版本名称:" value={info?.version}></LabelValue>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<LabelValue label="代码配置" value={formatCodeConfig()}></LabelValue>
</Col>
<Col span={10}> <Col span={10}>
<LabelValue label="镜  像:" value={info?.image}></LabelValue> <LabelValue label="镜  像:" value={info?.image}></LabelValue>
</Col> </Col>
</Row> </Row>
<Row gutter={40} style={{ marginBottom: '20px' }}> <Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}> <Col span={10}>
<LabelValue
label="状  态:"
value={ModelDeploymentStatusCell(info?.status)}
></LabelValue>
<LabelValue label="状  态:" value={ServiceRunStatusCell(info?.run_state)}></LabelValue>
</Col> </Col>
<Col span={10}> <Col span={10}>
<LabelValue label="模  型:" value={info?.model?.show_value}></LabelValue>
<LabelValue label="模  型:" value={formatModel()}></LabelValue>
</Col> </Col>
</Row> </Row>
<Row gutter={40} style={{ marginBottom: '20px' }}> <Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}> <Col span={10}>
<LabelValue label="创建人:" value={info?.created_by}></LabelValue>
<LabelValue label="资源规格:" value={formatResource()}></LabelValue>
</Col> </Col>
<Col span={10}> <Col span={10}>
<LabelValue label="挂载路径:" value={info?.model_path}></LabelValue>
<LabelValue label="挂载路径:" value={info?.mount_path}></LabelValue>
</Col> </Col>
</Row> </Row>
<Row gutter={40} style={{ marginBottom: '20px' }}> <Row gutter={40} style={{ marginBottom: '20px' }}>
@@ -68,19 +102,11 @@ function BasicInfo({ info }: BasicInfoProps) {
<LabelValue label="更新时间:" value={formatDate(info?.update_time)}></LabelValue> <LabelValue label="更新时间:" value={formatDate(info?.update_time)}></LabelValue>
</Col> </Col>
</Row> </Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Row gutter={40}>
<Col span={10}> <Col span={10}>
<LabelValue label="环境变量:" value={formatEnvText()}></LabelValue> <LabelValue label="环境变量:" value={formatEnvText()}></LabelValue>
</Col> </Col>
<Col span={10}> <Col span={10}>
<LabelValue
label="资源规格:"
value={info?.resource ? getResourceDescription(info.resource) : '--'}
></LabelValue>
</Col>
</Row>
<Row gutter={40}>
<Col span={18}>
<LabelValue label="描  述:" value={info?.description}></LabelValue> <LabelValue label="描  述:" value={info?.description}></LabelValue>
</Col> </Col>
</Row> </Row>


+ 10
- 10
react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.tsx View File

@@ -3,42 +3,42 @@
* @Date: 2024-04-18 18:35:41 * @Date: 2024-04-18 18:35:41
* @Description: 模型部署状态 * @Description: 模型部署状态
*/ */
import { ModelDeploymentStatus } from '@/enums';
import { ServiceRunStatus } from '@/enums';
import styles from './index.less'; import styles from './index.less';


export type ModelDeploymentStatusInfo = {
export type ServiceRunStatusInfo = {
text: string; text: string;
classname: string; classname: string;
}; };


export const statusInfo: Record<ModelDeploymentStatus, ModelDeploymentStatusInfo> = {
[ModelDeploymentStatus.Init]: {
export const statusInfo: Record<ServiceRunStatus, ServiceRunStatusInfo> = {
[ServiceRunStatus.Init]: {
text: '启动中', text: '启动中',
classname: styles['model-deployment-status-cell'], classname: styles['model-deployment-status-cell'],
}, },
[ModelDeploymentStatus.Running]: {
[ServiceRunStatus.Running]: {
classname: styles['model-deployment-status-cell--running'], classname: styles['model-deployment-status-cell--running'],
text: '运行中', text: '运行中',
}, },
[ModelDeploymentStatus.Stopped]: {
[ServiceRunStatus.Stopped]: {
classname: styles['model-deployment-status-cell--stopped'], classname: styles['model-deployment-status-cell--stopped'],
text: '已停止', text: '已停止',
}, },
[ModelDeploymentStatus.Failed]: {
[ServiceRunStatus.Failed]: {
classname: styles['model-deployment-status-cell--error'], classname: styles['model-deployment-status-cell--error'],
text: '失败', text: '失败',
}, },
[ModelDeploymentStatus.Pending]: {
[ServiceRunStatus.Pending]: {
classname: styles['model-deployment-status-cell--pending'], classname: styles['model-deployment-status-cell--pending'],
text: '挂起中', text: '挂起中',
}, },
}; };


function ModelDeploymentStatusCell(status?: ModelDeploymentStatus | null) {
function ServiceRunStatusCell(status?: ServiceRunStatus | null) {
if (status === null || status === undefined || !statusInfo[status]) { if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>; return <span>--</span>;
} }
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>; return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
} }


export default ModelDeploymentStatusCell;
export default ServiceRunStatusCell;

+ 5
- 6
react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx View File

@@ -1,10 +1,9 @@
import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
import { getModelDeploymentLogReq } from '@/services/modelDeployment';
import { ServiceVersionData } from '@/pages/ModelDeployment/types';
import { getServiceVersionLogReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { DoubleRightOutlined } from '@ant-design/icons'; import { DoubleRightOutlined } from '@ant-design/icons';
import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; import { Button, DatePicker, type TimeRangePickerProps } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styles from './index.less'; import styles from './index.less';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
@@ -28,7 +27,7 @@ type LogData = {
}; };


type ServerLogProps = { type ServerLogProps = {
info?: ModelDeploymentData;
info?: ServiceVersionData;
}; };


function ServerLog({ info }: ServerLogProps) { function ServerLog({ info }: ServerLogProps) {
@@ -64,9 +63,9 @@ function ServerLog({ info }: ServerLogProps) {
const params = { const params = {
start_time: logTime[0], start_time: logTime[0],
end_time: logTime[1], end_time: logTime[1],
...pick(info, ['service_id', 'service_ins_id']),
id: info.id,
}; };
const [res] = await to(getModelDeploymentLogReq(params));
const [res] = await to(getServiceVersionLogReq(params));
if (res && res.data) { if (res && res.data) {
setLogData((prev) => [...prev, res.data]); setLogData((prev) => [...prev, res.data]);
setHasMore(!!res.data.log_content); setHasMore(!!res.data.log_content);


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

Loading…
Cancel
Save