Browse Source

Merge remote-tracking branch 'origin/dev' into dev-active_learn

dev-active_learn
chenzhihang 10 months ago
parent
commit
79f5aa0c82
100 changed files with 1977 additions and 1073 deletions
  1. +2
    -0
      .gitignore
  2. +2
    -2
      k8s/build.sh
  3. +1
    -1
      k8s/build_and_deploy.sh
  4. +2
    -2
      k8s/deploy.sh
  5. +15
    -1
      k8s/dockerfiles/conf/nginx.conf
  6. +0
    -36
      k8s/k8s-10gen.yaml
  7. +0
    -36
      k8s/k8s-11visual.yaml
  8. +0
    -36
      k8s/k8s-12front.yaml
  9. +0
    -62
      k8s/k8s-3nacos.yaml
  10. +0
    -36
      k8s/k8s-4gateway.yaml
  11. +0
    -36
      k8s/k8s-5auth.yaml
  12. +0
    -36
      k8s/k8s-6system.yaml
  13. +0
    -44
      k8s/k8s-7management.yaml
  14. +0
    -36
      k8s/k8s-8file.yaml
  15. +0
    -36
      k8s/k8s-9job.yaml
  16. +0
    -36
      k8s/template-yaml/deploy/k8s-12front.yaml
  17. +0
    -53
      k8s/template-yaml/deploy/k8s-7management.yaml
  18. +43
    -0
      k8s/template-yaml/k8s-13oauth2.yaml
  19. +3
    -3
      k8s/template-yaml/k8s-3nacos.yaml
  20. +22
    -0
      k8s/template-yaml/k8s-7management.yaml
  21. +68
    -0
      k8s/template-yaml/rolebindings.yaml
  22. +2
    -1
      react-ui/.eslintignore
  23. +7
    -1
      react-ui/.eslintrc.js
  24. +2
    -7
      react-ui/.gitignore
  25. +1
    -0
      react-ui/.nvmrc
  26. +16
    -0
      react-ui/.storybook/babel-plugin-auto-css-modules.js
  27. +19
    -0
      react-ui/.storybook/blocks/StoryName.tsx
  28. +117
    -0
      react-ui/.storybook/main.ts
  29. +6
    -0
      react-ui/.storybook/manager.ts
  30. +19
    -0
      react-ui/.storybook/mock/umijs.mock.tsx
  31. +92
    -0
      react-ui/.storybook/mock/websocket.mock.js
  32. +112
    -0
      react-ui/.storybook/preview.tsx
  33. +19
    -0
      react-ui/.storybook/storybook.css
  34. +7
    -0
      react-ui/.storybook/theme.ts
  35. +27
    -0
      react-ui/.storybook/tsconfig.json
  36. +20
    -0
      react-ui/.storybook/typings.d.ts
  37. +1
    -1
      react-ui/config/config.ts
  38. +1
    -1
      react-ui/config/defaultSettings.ts
  39. +76
    -39
      react-ui/config/routes.ts
  40. +31
    -0
      react-ui/package.json
  41. BIN
      react-ui/public/favicon-cc.ico
  42. BIN
      react-ui/public/favicon-cl.ico
  43. BIN
      react-ui/public/favicon.ico
  44. +307
    -0
      react-ui/public/mockServiceWorker.js
  45. +9
    -6
      react-ui/src/app.tsx
  46. BIN
      react-ui/src/assets/img/logo-cc.png
  47. BIN
      react-ui/src/assets/img/logo-cl.png
  48. BIN
      react-ui/src/assets/img/logo.png
  49. BIN
      react-ui/src/assets/img/popover-bg.png
  50. +86
    -0
      react-ui/src/components/BasicInfo/BasicInfoItem.tsx
  51. +58
    -0
      react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx
  52. +0
    -113
      react-ui/src/components/BasicInfo/components.tsx
  53. +0
    -48
      react-ui/src/components/BasicInfo/format.ts
  54. +26
    -2
      react-ui/src/components/BasicInfo/index.less
  55. +35
    -6
      react-ui/src/components/BasicInfo/index.tsx
  56. +2
    -2
      react-ui/src/components/BasicInfo/types.ts
  57. +10
    -3
      react-ui/src/components/BasicTableInfo/index.less
  58. +10
    -10
      react-ui/src/components/BasicTableInfo/index.tsx
  59. +0
    -0
      react-ui/src/components/CodeConfigItem/index.less
  60. +0
    -0
      react-ui/src/components/CodeConfigItem/index.tsx
  61. +36
    -13
      react-ui/src/components/CodeSelect/index.tsx
  62. +49
    -0
      react-ui/src/components/CodeSelectorModal/index.less
  63. +25
    -21
      react-ui/src/components/CodeSelectorModal/index.tsx
  64. +41
    -0
      react-ui/src/components/ConfigInfo/index.tsx
  65. +0
    -9
      react-ui/src/components/DisabledInput/index.less
  66. +0
    -20
      react-ui/src/components/DisabledInput/index.tsx
  67. +19
    -0
      react-ui/src/components/FormInfo/index.less
  68. +74
    -0
      react-ui/src/components/FormInfo/index.tsx
  69. +13
    -5
      react-ui/src/components/FullScreenFrame/index.tsx
  70. +23
    -22
      react-ui/src/components/IFramePage/index.tsx
  71. +2
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.less
  72. +7
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.tsx
  73. +1
    -1
      react-ui/src/components/InfoGroup/index.less
  74. +12
    -3
      react-ui/src/components/InfoGroup/index.tsx
  75. +1
    -1
      react-ui/src/components/KFEmpty/index.less
  76. +16
    -6
      react-ui/src/components/KFEmpty/index.tsx
  77. +8
    -2
      react-ui/src/components/KFIcon/index.tsx
  78. +0
    -0
      react-ui/src/components/KFModal/KFModalTitle.less
  79. +5
    -1
      react-ui/src/components/KFModal/KFModalTitle.tsx
  80. +6
    -3
      react-ui/src/components/KFModal/index.tsx
  81. +12
    -4
      react-ui/src/components/KFRadio/index.tsx
  82. +11
    -5
      react-ui/src/components/KFSpin/index.tsx
  83. +0
    -19
      react-ui/src/components/LabelValue/index.less
  84. +0
    -20
      react-ui/src/components/LabelValue/index.tsx
  85. +1
    -2
      react-ui/src/components/MenuIconSelector/index.less
  86. +3
    -0
      react-ui/src/components/MenuIconSelector/index.tsx
  87. +8
    -1
      react-ui/src/components/PageTitle/index.tsx
  88. +18
    -5
      react-ui/src/components/ParameterInput/index.less
  89. +32
    -8
      react-ui/src/components/ParameterInput/index.tsx
  90. +4
    -22
      react-ui/src/components/ParameterSelect/config.tsx
  91. +69
    -34
      react-ui/src/components/ParameterSelect/index.tsx
  92. +67
    -31
      react-ui/src/components/ResourceSelect/index.tsx
  93. +0
    -0
      react-ui/src/components/ResourceSelectorModal/config.tsx
  94. +7
    -7
      react-ui/src/components/ResourceSelectorModal/index.less
  95. +78
    -62
      react-ui/src/components/ResourceSelectorModal/index.tsx
  96. +7
    -5
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  97. +2
    -5
      react-ui/src/components/RightContent/index.tsx
  98. +9
    -2
      react-ui/src/components/SubAreaTitle/index.tsx
  99. +3
    -0
      react-ui/src/components/TableColTitle/index.less
  100. +32
    -0
      react-ui/src/components/TableColTitle/index.tsx

+ 2
- 0
.gitignore View File

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


# web # web
**/node_modules **/node_modules

*storybook.log

+ 2
- 2
k8s/build.sh View File

@@ -13,7 +13,7 @@ show_help() {
echo echo
echo "Options:" echo "Options:"
echo " -b Branch to deploy, default is master" echo " -b Branch to deploy, default is master"
echo " -s Service to deploy (manage-front, manage, front, all, default is manage-front)"
echo " -s Service to deploy (manage-front, manage, front, all, system, default is manage-front)"
echo " -h Show this help message" echo " -h Show this help message"
} }


@@ -54,7 +54,7 @@ compile_front() {
# 编译前端 # 编译前端
docker run -v ${baseDir}:${baseDir} \ docker run -v ${baseDir}:${baseDir} \
-e http_proxy=http://172.20.32.253:3128 -e https_proxy=http://172.20.32.253:3128 \ -e http_proxy=http://172.20.32.253:3128 -e https_proxy=http://172.20.32.253:3128 \
172.20.32.187/ci4s/node:16.16.0 ${baseDir}/k8s/build-node.sh
172.20.32.187/tempimagefile/node:18.16.0 ${baseDir}/k8s/build-node.sh
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "编译失败,请检查代码!" echo "编译失败,请检查代码!"
exit 1 exit 1


+ 1
- 1
k8s/build_and_deploy.sh View File

@@ -19,7 +19,7 @@ show_help() {
echo echo
echo "Options:" echo "Options:"
echo " -b Branch to deploy, default: master" echo " -b Branch to deploy, default: master"
echo " -s Service to deploy (manage-front, manage, front, all, default: manage-front)"
echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)"
echo " -e Environment (e.g., dev, test, default: dev)" echo " -e Environment (e.g., dev, test, default: dev)"
echo " -h Show this help message" echo " -h Show this help message"
} }


+ 2
- 2
k8s/deploy.sh View File

@@ -10,7 +10,7 @@ show_help() {
echo "Usage: $0 [-s service] [-e environment]" echo "Usage: $0 [-s service] [-e environment]"
echo echo
echo "Options:" echo "Options:"
echo " -s Service to deploy (manage-front, manage, front, all default: manage-front)"
echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)"
echo " -e Environment (e.g., dev, test, default: dev)" echo " -e Environment (e.g., dev, test, default: dev)"
echo " -h Show this help message" echo " -h Show this help message"
} }
@@ -43,7 +43,7 @@ fi


# 根据环境设置 IP 地址 # 根据环境设置 IP 地址
if [ "$env" == "dev" ]; then if [ "$env" == "dev" ]; then
remote_ip="172.20.32.181"
remote_ip="172.20.32.197"
elif [ "$env" == "test" ]; then elif [ "$env" == "test" ]; then
remote_ip="172.20.32.185" remote_ip="172.20.32.185"
else else


+ 15
- 1
k8s/dockerfiles/conf/nginx.conf View File

@@ -29,11 +29,25 @@ http {
location /label-studio/ { location /label-studio/ {
# rewrite ^/label-studio/(.*)$ /$1 break; # rewrite ^/label-studio/(.*)$ /$1 break;
proxy_pass http://label-studio-service.argo.svc:8080/projects/;
proxy_pass http://label-studio-service.argo.svc:9000/projects/;
proxy_hide_header X-Frame-Options; proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL; add_header X-Frame-Options ALLOWALL;
} }
location /minio/ {
# rewrite ^/label-studio/(.*)$ /$1 break;
proxy_pass http://juicefs-s3-gateway.juicefs.svc:9000/;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL;
}
location /neo4j/ {
# rewrite ^/label-studio/(.*)$ /$1 break;
proxy_pass http://172.20.20.88:7474/;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options ALLOWALL;
}
location / { location / {
rewrite ^/prod-api/(.*)$ /$1 break; rewrite ^/prod-api/(.*)$ /$1 break;
root /home/ruoyi/projects/ruoyi-ui; root /home/ruoyi/projects/ruoyi-ui;


+ 0
- 36
k8s/k8s-10gen.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-gen-deployment
namespace: ci4s-test
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-gen
template:
metadata:
labels:
app: ci4s-gen
spec:
containers:
- name: ci4s-gen
image: ci4s-gen:v1.0
ports:
- containerPort: 9202

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-gen-service
namespace: ci4s-test
spec:
type: NodePort
ports:
- port: 9202
nodePort: 31211
protocol: TCP
selector:
app: ci4s-gen


+ 0
- 36
k8s/k8s-11visual.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-visual-deployment
namespace: ci4s-test
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-visual
template:
metadata:
labels:
app: ci4s-visual
spec:
containers:
- name: ci4s-visual
image: ci4s-visual:v1.0
ports:
- containerPort: 9100

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-visual-service
namespace: ci4s-test
spec:
type: NodePort
ports:
- port: 9100
nodePort: 31212
protocol: TCP
selector:
app: ci4s-visual


+ 0
- 36
k8s/k8s-12front.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-front-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-front
template:
metadata:
labels:
app: ci4s-front
spec:
containers:
- name: ci4s-front
image: 172.20.32.187/ci4s/ci4s-front:20240401
ports:
- containerPort: 8000

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-front-service
namespace: argo
spec:
type: NodePort
ports:
- port: 8000
nodePort: 31213
protocol: TCP
selector:
app: ci4s-front


+ 0
- 62
k8s/k8s-3nacos.yaml View File

@@ -1,62 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: argo
name: nacos-ci4s
labels:
app: nacos-ci4s
spec:
replicas: 1
selector:
matchLabels:
app: nacos-ci4s
template:
metadata:
labels:
app: nacos-ci4s
spec:
containers:
- name: nacos-ci4s
image: nacos/nacos-server:v2.2.0
env:
- name: SPRING_DATASOURCE_PLATFORM
value: mysql
- name: MODE
value: standalone
- name: MYSQL_SERVICE_HOST
value: mysql.argo.svc
- name: MYSQL_SERVICE_PORT
value: "3306"
- name: MYSQL_SERVICE_DB_NAME
value: nacos-ci4s-config
- name: MYSQL_SERVICE_USER
value: root
- name: MYSQL_SERVICE_PASSWORD
value: qazxc123456.
ports:
- containerPort: 8848
- containerPort: 9848
restartPolicy: Always

---

apiVersion: v1
kind: Service
metadata:
namespace: argo
name: nacos-ci4s
labels:
app: nacos-ci4s
spec:
type: NodePort
selector:
app: nacos-ci4s
ports:
- port: 8848
targetPort: 8848
nodePort: 31203
name: web
- port: 9848
targetPort: 9848
nodePort: 31204
name: podsa

+ 0
- 36
k8s/k8s-4gateway.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-gateway-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-gateway
template:
metadata:
labels:
app: ci4s-gateway
spec:
containers:
- name: ci4s-gateway
image: 172.20.32.187/ci4s/ci4s-gateway:20240401
ports:
- containerPort: 8082

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-gateway-service
namespace: argo
spec:
type: NodePort
ports:
- port: 8082
nodePort: 31205
protocol: TCP
selector:
app: ci4s-gateway


+ 0
- 36
k8s/k8s-5auth.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-auth-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-auth
template:
metadata:
labels:
app: ci4s-auth
spec:
containers:
- name: ci4s-auth
image: 172.20.32.187/ci4s/ci4s-auth:20240401
ports:
- containerPort: 9200

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-auth-service
namespace: argo
spec:
type: NodePort
ports:
- port: 9200
nodePort: 31206
protocol: TCP
selector:
app: ci4s-auth


+ 0
- 36
k8s/k8s-6system.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-system-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-system
template:
metadata:
labels:
app: ci4s-system
spec:
containers:
- name: ci4s-system
image: 172.20.32.187/ci4s/ci4s-system:20240401
ports:
- containerPort: 9201

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-system-service
namespace: argo
spec:
type: NodePort
ports:
- port: 9201
nodePort: 31207
protocol: TCP
selector:
app: ci4s-system


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

@@ -1,44 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-management-platform-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-management-platform
template:
metadata:
labels:
app: ci4s-management-platform
spec:
containers:
- name: ci4s-management-platform
image: 172.20.32.187/ci4s/managent:20240401
ports:
- containerPort: 9213
volumeMounts:
- name: resource
mountPath: /home/resource/
volumes:
- name: resource
hostPath:
path: /home/resource/
type: DirectoryOrCreate

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-management-platform-service
namespace: argo
spec:
type: NodePort
ports:
- port: 9213
nodePort: 31208
protocol: TCP
selector:
app: ci4s-management-platform


+ 0
- 36
k8s/k8s-8file.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-file-deployment
namespace: ci4s-test
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-file
template:
metadata:
labels:
app: ci4s-file
spec:
containers:
- name: ci4s-file
image: ci4s-file:v1.0
ports:
- containerPort: 9300

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-file-service
namespace: ci4s-test
spec:
type: NodePort
ports:
- port: 9300
nodePort: 31209
protocol: TCP
selector:
app: ci4s-file


+ 0
- 36
k8s/k8s-9job.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-job-deployment
namespace: ci4s-test
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-job
template:
metadata:
labels:
app: ci4s-job
spec:
containers:
- name: ci4s-job
image: ci4s-job:v1.0
ports:
- containerPort: 9203

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-job-service
namespace: ci4s-test
spec:
type: NodePort
ports:
- port: 9203
nodePort: 31210
protocol: TCP
selector:
app: ci4s-job


+ 0
- 36
k8s/template-yaml/deploy/k8s-12front.yaml View File

@@ -1,36 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-front-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-front
template:
metadata:
labels:
app: ci4s-front
spec:
containers:
- name: ci4s-front
image: 172.20.32.187/ci4s/ci4s-front:202406120836
ports:
- containerPort: 8000

---
apiVersion: v1
kind: Service
metadata:
name: ci4s-front-service
namespace: argo
spec:
type: NodePort
ports:
- port: 8000
nodePort: 31213
protocol: TCP
selector:
app: ci4s-front


+ 0
- 53
k8s/template-yaml/deploy/k8s-7management.yaml View File

@@ -1,53 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-management-platform-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-management-platform
template:
metadata:
labels:
app: ci4s-management-platform
spec:
containers:
- name: ci4s-management-platform
image: 172.20.32.187/ci4s/ci4s-managent:202409201355
env:
- name: TZ
value: Asia/Shanghai
- name: JAVA_TOOL_OPTIONS
value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
ports:
- containerPort: 9213
volumeMounts:
- name: resource-volume
mountPath: /home/resource/
volumes:
- name: resource-volume
persistentVolumeClaim:
claimName: platform-data-pvc-nfs
---
apiVersion: v1
kind: Service
metadata:
name: ci4s-management-platform-service
namespace: argo
spec:
type: NodePort
ports:
- name: http
port: 9213
nodePort: 31208
protocol: TCP
- name: debug
nodePort: 34567
port: 5005
protocol: TCP
targetPort: 5005
selector:
app: ci4s-management-platform


+ 43
- 0
k8s/template-yaml/k8s-13oauth2.yaml View File

@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ci4s-oauth2-authenticator-deployment
namespace: argo
spec:
replicas: 1
selector:
matchLabels:
app: ci4s-oauth2-authenticator
template:
metadata:
labels:
app: ci4s-oauth2-authenticator
spec:
containers:
- name: ci4s-oauth2-authenticator
image: 172.20.32.187/ci4s/spring-oauth2-authenticator:latest
env:
- name: DB_URL
value: mysql.argo.svc:3306
- name: DB_USERNAME
value: root
- name: DB_PASSWORD
value: qazxc123456.
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: ci4s-oauth2-authenticator-service
namespace: argo
spec:
type: NodePort
ports:
- name: http
port: 8080
nodePort: 31080
protocol: TCP
selector:
app: ci4s-oauth2-authenticator


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

@@ -17,7 +17,7 @@ spec:
spec: spec:
containers: containers:
- name: nacos-ci4s - name: nacos-ci4s
image: nacos/nacos-server:v2.2.0
image: 172.20.32.187/ci4s/nacos-server:v2.2.0
env: env:
- name: SPRING_DATASOURCE_PLATFORM - name: SPRING_DATASOURCE_PLATFORM
value: mysql value: mysql
@@ -38,8 +38,8 @@ spec:
- containerPort: 9848 - containerPort: 9848
- containerPort: 9849 - containerPort: 9849
initContainers: initContainers:
- name: init-mydb
image: busybox:1.31
- name: init-mydb-check
image: 172.20.32.187/ci4s/busybox:1.31
command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ]
restartPolicy: Always restartPolicy: Always




+ 22
- 0
k8s/template-yaml/k8s-7management.yaml View File

@@ -16,6 +16,8 @@ spec:
containers: containers:
- name: ci4s-management-platform - name: ci4s-management-platform
image: ${k8s-7management-image} image: ${k8s-7management-image}
securityContext:
privileged: true
env: env:
- name: TZ - name: TZ
value: Asia/Shanghai value: Asia/Shanghai
@@ -27,10 +29,30 @@ spec:
- name: resource-volume - name: resource-volume
mountPath: /home/resource/ mountPath: /home/resource/
subPath: mini-model-platform-data subPath: mini-model-platform-data
mountPropagation: Bidirectional
volumes: volumes:
- name: resource-volume - name: resource-volume
hostPath: hostPath:
path: /platform-data path: /platform-data
initContainers:
- name: init-fs-check
image: 172.20.32.187/ci4s/ci4s-managent:202502141722
securityContext:
privileged: true
volumeMounts:
- name: resource-volume
mountPath: /home/resource/
subPath: mini-model-platform-data
mountPropagation: Bidirectional
command: [ "/bin/sh", "-c" ]
args:
- |
mounted=$(findmnt /home/resource/ | grep 'fuse.juicefs')
if [ -z "$mounted" ]; then
echo "/platform-data not mounted";
exit 1
fi
restartPolicy: Always
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service


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

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

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

+ 2
- 1
react-ui/.eslintignore View File

@@ -5,4 +5,5 @@
public public
dist dist
.umi .umi
mock
mock
/src/iconfont/

+ 7
- 1
react-ui/.eslintrc.js View File

@@ -1,10 +1,16 @@
module.exports = { module.exports = {
extends: [require.resolve('@umijs/lint/dist/config/eslint')],
extends: [
require.resolve('@umijs/lint/dist/config/eslint'),
'plugin:react/recommended',
"plugin:react-hooks/recommended"
],
globals: { globals: {
page: true, page: true,
REACT_APP_ENV: true, REACT_APP_ENV: true,
}, },
rules: { rules: {
'@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off',
'react/react-in-jsx-scope': 'off',
'react/display-name': 'off'
}, },
}; };

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

@@ -41,10 +41,5 @@ 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

*storybook.log

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

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

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

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

+ 19
- 0
react-ui/.storybook/blocks/StoryName.tsx View File

@@ -0,0 +1,19 @@
import { Of, useOf } from '@storybook/blocks';

/**
* A block that displays the story name or title from the of prop
* - if a story reference is passed, it renders the story name
* - if a meta reference is passed, it renders the stories' title
* - if nothing is passed, it defaults to the primary story
*/
export const StoryName = ({ of }: { of?: Of }) => {
const resolvedOf = useOf(of || 'story', ['story', 'meta']);
switch (resolvedOf.type) {
case 'story': {
return <h3 className="css-wzniqs">{resolvedOf.story.name}</h3>;
}
case 'meta': {
return <h3 className="css-wzniqs">{resolvedOf.preparedMeta.title}</h3>;
}
}
};

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

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

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

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

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

+ 6
- 0
react-ui/.storybook/manager.ts View File

@@ -0,0 +1,6 @@
import { addons } from '@storybook/manager-api';
import theme from './theme';

addons.setConfig({
theme: theme,
});

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

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

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

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

+ 92
- 0
react-ui/.storybook/mock/websocket.mock.js View File

@@ -0,0 +1,92 @@
export const createWebSocketMock = () => {
class WebSocketMock {
constructor(url) {
this.url = url;
this.readyState = WebSocket.OPEN;
this.listeners = {};
this.count = 0;

console.log("Mock WebSocket connected to:", url);

// 模拟服务器推送消息
this.intervalId = setInterval(() => {
this.count += 1;
if (this.count > 5) {
this.count = 0;
clearInterval(this.intervalId);
return;
}
this.sendMessage(JSON.stringify(logStreamData));
}, 3000);
}

sendMessage(data) {
if (this.listeners["message"]) {
this.listeners["message"].forEach((callback) => callback({ data }));
}
}

addEventListener(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}

removeEventListener(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);
}
}

close() {
this.readyState = WebSocket.CLOSED;
console.log("Mock WebSocket closed");
}
}

return WebSocketMock;
};

export const logStreamData = {
streams: [
{
stream: {
workflows_argoproj_io_completed: 'false',
workflows_argoproj_io_workflow: 'workflow-p2ddj',
container: 'init',
filename:
'/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log',
job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653',
namespace: 'argo',
pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653',
stream: 'stderr',
},
values: [
[
'1742179591969785990',
'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n',
],
],
},
{
stream: {
filename:
'/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log',
job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653',
namespace: 'argo',
pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653',
stream: 'stderr',
workflows_argoproj_io_completed: 'false',
workflows_argoproj_io_workflow: 'workflow-p2ddj',
container: 'init',
},
values: [
[
'1742179591973414064',
'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n',
],
],
},
],
};

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

@@ -0,0 +1,112 @@
import '@/global.less';
import '@/overrides.less';
import themes from '@/styles/theme.less';
import type { Preview } from '@storybook/react';
import { App, ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { createWebSocketMock } from './mock/websocket.mock';
import './storybook.css';

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

// 替换全局 WebSocket 为 Mock 版本
// @ts-ignore
global.WebSocket = createWebSocketMock();

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

export default preview;

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

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

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

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

+ 7
- 0
react-ui/.storybook/theme.ts View File

@@ -0,0 +1,7 @@
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: '组件库文档',
brandUrl: 'https://storybook.js.org/docs',
brandTarget: '_blank',
});

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

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

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

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

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

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

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


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

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


+ 76
- 39
react-ui/config/routes.ts View File

@@ -44,7 +44,7 @@ export default [
{ {
name: 'login', name: 'login',
path: '/user/login', path: '/user/login',
component: './User/Login/login',
component: process.env.NO_SSO ? './User/Login/login' : './User/Login',
}, },
], ],
}, },
@@ -181,6 +181,42 @@ export default [
}, },
], ],
}, },
{
name: '超参数自动寻优',
path: 'hyperparameter',
routes: [
{
name: '超参数寻优',
path: '',
component: './HyperParameter/List/index',
},
{
name: '实验详情',
path: 'info/:id',
component: './HyperParameter/Info/index',
},
{
name: '创建实验',
path: 'create',
component: './HyperParameter/Create/index',
},
{
name: '编辑实验',
path: 'edit/:id',
component: './HyperParameter/Create/index',
},
{
name: '复制实验',
path: 'copy/:id',
component: './HyperParameter/Create/index',
},
{
name: '实验实例详情',
path: 'instance/:autoMLId/:id',
component: './HyperParameter/Instance/index',
},
],
},
], ],
}, },
{ {
@@ -255,60 +291,61 @@ export default [
}, },
], ],
}, },
],
},
{
name: '模型部署',
path: '/modelDeployment',
routes: [
{ {
name: '模型部署', name: '模型部署',
path: '',
component: './ModelDeployment/List',
},
{
name: '创建推理服务',
path: 'createService',
component: './ModelDeployment/CreateService',
},
{
name: '编辑推理服务',
path: 'editService/:serviceId',
component: './ModelDeployment/CreateService',
},
{
name: '服务详情',
path: 'serviceInfo/:serviceId',
path: 'modelDeployment',
routes: [ routes: [
{ {
name: '服务详情',
name: '模型部署',
path: '', path: '',
component: './ModelDeployment/ServiceInfo',
component: './ModelDeployment/List',
}, },
{ {
name: '新增服务版本',
path: 'createVersion',
component: './ModelDeployment/CreateVersion',
name: '创建推理服务',
path: 'createService',
component: './ModelDeployment/CreateService',
}, },
{ {
name: '更新服务版本',
path: 'updateVersion',
component: './ModelDeployment/CreateVersion',
name: '编辑推理服务',
path: 'editService/:serviceId',
component: './ModelDeployment/CreateService',
}, },
{ {
name: '重启服务版本',
path: 'restartVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '服务版本详情',
path: 'versionInfo/:id',
component: './ModelDeployment/VersionInfo',
name: '服务详情',
path: 'serviceInfo/:serviceId',
routes: [
{
name: '服务详情',
path: '',
component: './ModelDeployment/ServiceInfo',
},
{
name: '新增服务版本',
path: 'createVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '更新服务版本',
path: 'updateVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '重启服务版本',
path: 'restartVersion',
component: './ModelDeployment/CreateVersion',
},
{
name: '服务版本详情',
path: 'versionInfo/:id',
component: './ModelDeployment/VersionInfo',
},
],
}, },
], ],
}, },
], ],
}, },

{ {
name: '应用开发', name: '应用开发',
path: '/appsDeployment', path: '/appsDeployment',


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

@@ -8,6 +8,7 @@
"build": "max build", "build": "max build",
"deploy": "npm run build && npm run gh-pages", "deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev", "dev": "npm run start:dev",
"dev-no-sso": "cross-env NO_SSO=true npm run start:dev",
"docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
"docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
@@ -36,6 +37,10 @@
"start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"storybook": "storybook dev -p 6006",
"storybook-build": "storybook build",
"storybook-docs": "storybook dev --docs",
"storybook-docs-build": "storybook build --docs",
"test": "jest", "test": "jest",
"test:coverage": "npm run jest -- --coverage", "test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u", "test:update": "npm run jest -- -u",
@@ -83,6 +88,19 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/pro-cli": "^3.1.0", "@ant-design/pro-cli": "^3.1.0",
"@chromatic-com/storybook": "~3.2.4",
"@storybook/addon-essentials": "~8.5.3",
"@storybook/addon-interactions": "~8.5.3",
"@storybook/addon-onboarding": "~8.5.3",
"@storybook/addon-styling-webpack": "~1.0.1",
"@storybook/addon-webpack5-compiler-babel": "~3.0.5",
"@storybook/addon-webpack5-compiler-swc": "~2.0.0",
"@storybook/blocks": "~8.5.3",
"@storybook/manager-api": "~8.6.0",
"@storybook/react": "~8.5.3",
"@storybook/react-webpack5": "~8.5.3",
"@storybook/test": "~8.5.3",
"@storybook/theming": "~8.6.0",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@types/antd": "^1.0.0", "@types/antd": "^1.0.0",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
@@ -96,15 +114,23 @@
"@umijs/max": "^4.0.66", "@umijs/max": "^4.0.66",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-plugin-react-hooks": "~5.2.0",
"eslint-plugin-storybook": "~0.11.2",
"express": "^4.18.2", "express": "^4.18.2",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.5.0",
"less": "~4.2.2",
"less-loader": "~12.2.0",
"lint-staged": "^13.2.0", "lint-staged": "^13.2.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"msw": "~2.7.0",
"msw-storybook-addon": "~2.0.4",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"storybook": "~8.5.3",
"swagger-ui-dist": "^4.18.2", "swagger-ui-dist": "^4.18.2",
"ts-loader": "~9.5.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"umi-presets-pro": "^2.0.0" "umi-presets-pro": "^2.0.0"
@@ -140,5 +166,10 @@
"CNAME", "CNAME",
"create-umi" "create-umi"
] ]
},
"msw": {
"workerDirectory": [
"public"
]
} }
} }

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

Before After

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

Before After

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

Before After

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

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

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

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

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

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

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

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

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

if (!client) {
return
}

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

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

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

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

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

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

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

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

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

break
}
}
})

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

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

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

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

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

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

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

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

return response
}

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

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

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

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

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

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

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

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

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

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

return fetch(requestClone, { headers })
}

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

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

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

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

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

return passthrough()
}

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

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

resolve(event.data)
}

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

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

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

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

return mockedResponse
}

+ 9
- 6
react-ui/src/app.tsx View File

@@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css'; import '../public/fonts/font.css';
import { getAccessToken } from './access'; import { getAccessToken } from './access';
import './dayjsConfig'; import './dayjsConfig';
import './global.less';
import { removeAllPageCacheState } from './hooks/pageCacheState'; import { removeAllPageCacheState } from './hooks/pageCacheState';
import { import {
getRemoteMenu, getRemoteMenu,
@@ -41,7 +40,7 @@ export async function getInitialState(): Promise<GlobalInitialState> {
roleNames: response.user.roles, roleNames: response.user.roles,
} as API.CurrentUser; } as API.CurrentUser;
} catch (error) { } catch (error) {
console.error('1111', error);
console.error('getInitialState', error);
gotoLoginPage(); gotoLoginPage();
} }
return undefined; return undefined;
@@ -169,7 +168,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => {
} }
}; };


export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => {
export const patchRoutes: RuntimeConfig['patchRoutes'] = () => {
//console.log('patchRoutes', e); //console.log('patchRoutes', e);
}; };


@@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => {
defaultColor: themes['textColor'], defaultColor: themes['textColor'],
defaultHoverBg: 'rgba(22, 100, 255, 0.06)', defaultHoverBg: 'rgba(22, 100, 255, 0.06)',
defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)',
defaultHoverColor: '#3F7FFF ',
defaultHoverColor: '#3F7FFF',
defaultActiveBg: 'rgba(22, 100, 255, 0.12)', defaultActiveBg: 'rgba(22, 100, 255, 0.12)',
defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)',
defaultActiveColor: themes['primaryColor'], defaultActiveColor: themes['primaryColor'],
@@ -228,11 +227,12 @@ export const antd: RuntimeAntdConfig = (memo) => {
}; };
memo.theme.components.Select = { memo.theme.components.Select = {
singleItemHeightLG: 46, singleItemHeightLG: 46,
optionSelectedColor: themes['primaryColor'],
}; };
memo.theme.components.Table = { memo.theme.components.Table = {
headerBg: 'rgba(242, 244, 247, 0.36)', headerBg: 'rgba(242, 244, 247, 0.36)',
headerBorderRadius: 4, headerBorderRadius: 4,
rowSelectedBg: 'rgba(22, 100, 255, 0.05)',
// rowSelectedBg: 'rgba(22, 100, 255, 0.05)', 固定列时,横向滑动导致重叠
}; };
memo.theme.components.Tabs = { memo.theme.components.Tabs = {
titleFontSize: 16, titleFontSize: 16,
@@ -245,9 +245,12 @@ export const antd: RuntimeAntdConfig = (memo) => {
linkColor: 'rgba(29, 29, 32, 0.7)', linkColor: 'rgba(29, 29, 32, 0.7)',
separatorColor: 'rgba(29, 29, 32, 0.7)', separatorColor: 'rgba(29, 29, 32, 0.7)',
}; };
memo.theme.components.Tree = {
directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)',
};


memo.theme.cssVar = true; memo.theme.cssVar = true;
// memo.theme.hashed = false;
memo.theme.hashed = false;


memo.appConfig = { memo.appConfig = {
message: { message: {


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

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

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

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

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

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

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

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

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

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

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

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

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

export default BasicInfoItem;

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

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

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

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

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

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

export default BasicInfoItemValue;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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


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


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


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

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


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

+ 10
- 3
react-ui/src/components/BasicTableInfo/index.less View File

@@ -4,7 +4,7 @@
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
width: 100%; width: 100%;
border: 1px solid @border-color-base;
border: 1px solid @border-color;
border-bottom: none; border-bottom: none;
border-radius: 4px; border-radius: 4px;


@@ -12,7 +12,7 @@
display: flex; display: flex;
align-items: stretch; align-items: stretch;
width: 25%; width: 25%;
border-bottom: 1px solid @border-color-base;
border-bottom: 1px solid @border-color;


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

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

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

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


type BasicTableInfoProps = {
datas: BasicInfoData[];
className?: string;
style?: React.CSSProperties;
labelWidth: number;
};
export type BasicTableInfoProps = Omit<BasicInfoProps, 'labelAlign' | 'threeColumns'>;


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


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


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


+ 36
- 13
react-ui/src/components/CodeSelect/index.tsx View File

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


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


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


type CodeSelectProps = ParameterInputProps; type CodeSelectProps = ParameterInputProps;


function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) {
/** 代码配置选择表单组件 */
function CodeSelect({
value,
size,
disabled,
className,
style,
onChange,
...rest
}: CodeSelectProps) {
// 选择代码配置
const selectResource = () => { const selectResource = () => {
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { git_url, git_branch, code_repo_name } = res;
const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } =
res;
const jsonObj = { const jsonObj = {
id,
name: code_repo_name,
code_path: git_url, code_path: git_url,
branch: git_branch, branch: git_branch,
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
}; };
const jsonObjStr = JSON.stringify(jsonObj); const jsonObjStr = JSON.stringify(jsonObj);
const showValue = code_repo_name;
onChange?.({ onChange?.({
value: jsonObjStr, value: jsonObjStr,
showValue,
showValue: code_repo_name,
fromSelect: true, fromSelect: true,
...jsonObj, ...jsonObj,
}); });
} else { } else {
onChange?.({
value: undefined,
showValue: undefined,
fromSelect: false,
});
onChange?.(undefined);
} }
close(); close();
}, },
}); });
}; };


// 删除
const handleRemove = () => {
onChange?.(undefined);
};

return ( return (
<div className="kf-code-select">
<div className={classNames('kf-code-select', className)} style={style}>
<ParameterInput <ParameterInput
{...rest} {...rest}
size={size}
disabled={disabled} disabled={disabled}
value={value} value={value}
onChange={onChange} onChange={onChange}
onClick={selectResource} onClick={selectResource}
onRemove={handleRemove}
></ParameterInput> ></ParameterInput>
<Button <Button
className="kf-code-select__button" className="kf-code-select__button"
size="large"
size={size}
type="link" type="link"
icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />} icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />}
disabled={disabled} disabled={disabled}


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

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

&__search {
width: 100%;
}

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

&__empty {
padding-top: 40px;
}

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

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

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

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

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


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


export { type CodeConfigData }; export { type CodeConfigData };


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


/** 选择代码配置的弹窗,推荐使用函数的方式打开 */
function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
const [dataList, setDataList] = useState<CodeConfigData[]>([]); const [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -32,23 +33,23 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
const [inputText, setInputText] = useState<string | undefined>(undefined); const [inputText, setInputText] = useState<string | undefined>(undefined);


useEffect(() => { useEffect(() => {
// 获取数据请求
const getDataList = async () => {
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
code_repo_name: searchText || undefined,
};
const [res] = await to(getCodeConfigListReq(params));
if (res && res.data && res.data.content) {
setDataList(res.data.content);
setTotal(res.data.totalElements);
}
};

getDataList(); getDataList();
}, [pagination, searchText]); }, [pagination, searchText]);


// 获取数据请求
const getDataList = async () => {
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
code_repo_name: searchText || undefined,
};
const [res] = await to(getCodeConfigListReq(params));
if (res && res.data && res.data.content) {
setDataList(res.data.content);
setTotal(res.data.totalElements);
}
};

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

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

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

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

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

export default ConfigInfo;

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

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

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

@@ -1,20 +0,0 @@
import { Typography } from 'antd';
import styles from './index.less';

type DisabledInputProps = {
value?: any;
valuePropName?: string;
};

function DisabledInput({ value, valuePropName }: DisabledInputProps) {
const data = valuePropName ? value[valuePropName] : value;
return (
<div className={styles['disabled-input']}>
<Typography.Text ellipsis={{ tooltip: data }} style={{ color: 'inherit' }}>
{data}
</Typography.Text>
</div>
);
}

export default DisabledInput;

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

@@ -0,0 +1,19 @@
.form-info {
min-height: 32px;
padding: 4px 11px;
color: @text-disabled-color;
font-size: @font-size-input;
background-color: rgba(0, 0, 0, 0.04);
border: 1px solid #d9d9d9;
border-radius: 6px;

.ant-typography {
margin: 0 !important;
}
}

.form-info--multiline {
.ant-typography {
white-space: pre-wrap;
}
}

+ 74
- 0
react-ui/src/components/FormInfo/index.tsx View File

@@ -0,0 +1,74 @@
import { formatEnum } from '@/utils/format';
import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames';
import './index.less';

type FormInfoProps = {
/** 值 */
value?: any;
/** 如果 `value` 是对象,取对象的哪个属性作为值 */
valuePropName?: string;
/** 是否是多行文本 */
textArea?: boolean;
/** 是否是下拉框 */
select?: boolean;
/** 下拉框数据 */
options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames'];
/** 自定义类名 */
className?: string;
/** 自定义样式 */
style?: React.CSSProperties;
};

/**
* 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容
*/
function FormInfo({
value,
valuePropName,
textArea = false,
select = false,
options,
fieldNames,
className,
style,
}: FormInfoProps) {
let showValue = value;
if (value && typeof value === 'object' && valuePropName) {
showValue = value[valuePropName];
} else if (select === true && options) {
let _options: SelectProps['options'] = options;
if (fieldNames) {
_options = options.map((v) => {
return {
...v,
label: fieldNames.label && v[fieldNames.label],
value: fieldNames.value && v[fieldNames.value],
options: fieldNames.options && v[fieldNames.options],
};
});
}
showValue = formatEnum(_options)(value);
}

return (
<div
className={classNames(
'form-info',
{
'form-info--multiline': textArea,
},
className,
)}
style={style}
>
<Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}>
{showValue}
</Typography.Paragraph>
</div>
);
}

export default FormInfo;

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

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


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


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


+ 23
- 22
react-ui/src/components/IFramePage/index.tsx View File

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


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


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


/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */
function IframePage({ type, className, style }: IframePageProps) { function IframePage({ type, className, style }: IframePageProps) {
const [iframeUrl, setIframeUrl] = useState(''); const [iframeUrl, setIframeUrl] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);

useEffect(() => { useEffect(() => {
requestIframeUrl();
return () => {
if (type === IframePageType.DevEnv) {
SessionStorage.removeItem(SessionStorage.editorUrlKey);
const requestIframeUrl = async () => {
setLoading(true);
const [res] = await to(getRequestAPI(type)());
if (res && res.data) {
setIframeUrl(res.data);
} else {
setLoading(false);
} }
}; };
}, []);
const requestIframeUrl = async () => {
setLoading(true);
const [res] = await to(getRequestAPI(type)());
if (res && res.data) {
setIframeUrl(res.data);
} else {
setLoading(false);
}
};

requestIframeUrl();
}, [type]);


const hideLoading = () => { const hideLoading = () => {
setLoading(false); setLoading(false);
@@ -66,7 +67,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 && createPortal(<KFSpin size="large" />, document.body)} {loading && createPortal(<KFSpin size="large" />, document.body)}
<FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} />
<FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} />
</div> </div>
); );
} }


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

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


&::after { &::after {
position: absolute; position: absolute;

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

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


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


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

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

@@ -4,7 +4,7 @@
&__content { &__content {
padding: 20px @content-padding; padding: 20px @content-padding;
background-color: white; background-color: white;
border: 1px solid @border-color-base;
border: 1px solid @border-color;
border-top: none; border-top: none;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }


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

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


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


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


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

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


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


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

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


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


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


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


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


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

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


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


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


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


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

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


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


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



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

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


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


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

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


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

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


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


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


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


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

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


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


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

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


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

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

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

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

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

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

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

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

export default LabelValue;

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

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


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

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


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


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


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


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

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


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

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


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

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


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


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


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

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

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


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




+ 32
- 8
react-ui/src/components/ParameterInput/index.tsx View File

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


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


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


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


@@ -50,7 +67,7 @@ function ParameterInput({
allowClear, allowClear,
className, className,
style, style,
size = 'middle',
size,
disabled = false, disabled = false,
id, id,
...rest ...rest
@@ -64,10 +81,17 @@ function ParameterInput({
const placeholder = valueObj?.placeholder || rest?.placeholder; const placeholder = valueObj?.placeholder || rest?.placeholder;
const InputComponent = textArea ? Input.TextArea : Input; const InputComponent = textArea ? Input.TextArea : Input;
const { status } = Form.Item.useStatus(); const { status } = Form.Item.useStatus();
const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize;


// 删除 // 删除
const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
e.stopPropagation(); e.stopPropagation();
if (onRemove) {
onRemove();
return;
}

onChange?.({ onChange?.({
...valueObj, ...valueObj,
value: undefined, value: undefined,
@@ -77,7 +101,6 @@ function ParameterInput({
expandedKeys: [], expandedKeys: [],
checkedKeys: [], checkedKeys: [],
}); });
onRemove?.();
}; };


return ( return (
@@ -87,7 +110,8 @@ function ParameterInput({
id={id} id={id}
className={classNames( className={classNames(
'parameter-input', 'parameter-input',
{ 'parameter-input--large': size === 'large' },
{ 'parameter-input--large': mySize === 'large' },
{ 'parameter-input--small': mySize === 'small' },
{ [`parameter-input--${status}`]: status }, { [`parameter-input--${status}`]: status },
className, className,
)} )}
@@ -110,7 +134,7 @@ function ParameterInput({
<InputComponent <InputComponent
{...rest} {...rest}
id={id} id={id}
size={size}
size={mySize}
className={className} className={className}
style={style} style={style}
placeholder={placeholder} placeholder={placeholder}


+ 4
- 22
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -1,21 +1,10 @@
import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource';
import { ServiceData } from '@/pages/ModelDeployment/types'; import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment'; import { getServiceListReq } from '@/services/modelDeployment';
import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { type SelectProps } from 'antd'; import { type SelectProps } from 'antd';
import { pick } from 'lodash'; import { pick } from 'lodash';


// 过滤资源规格
const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
input: string,
option?: ComputingResource,
) => {
return (
option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false
);
};

// id 从 number 转换为 string // id 从 number 转换为 string
const convertId = (item: any) => ({ const convertId = (item: any) => ({
...item, ...item,
@@ -86,17 +75,10 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
}, },
resource: { resource: {
getOptions: async () => { getOptions: async () => {
const res = await getComputingResourceReq({
page: 0,
size: 1000,
resource_type: '',
});
return res?.data?.content ?? [];
},
fieldNames: {
label: 'description',
value: 'standard',
// 不需要这个函数
return [];
}, },
fieldNames: resourceFieldNames,
filterOption: filterResourceStandard as SelectProps['filterOption'], filterOption: filterResourceStandard as SelectProps['filterOption'],
}, },
}; };

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

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


import { PipelineNodeModelParameter } from '@/types';
import { useComputingResource } from '@/hooks/resource';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Select } from 'antd';
import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import FormInfo from '../FormInfo';
import { paramSelectConfig } from './config'; import { paramSelectConfig } from './config';


type ParameterSelectProps = {
value?: PipelineNodeModelParameter;
onChange?: (value: PipelineNodeModelParameter) => void;
disabled?: boolean;
export type ParameterSelectObject = {
value: any;
[key: string]: any;
}; };


function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) {
const [options, setOptions] = useState([]);
const valueNonNullable = value ?? ({} as PipelineNodeModelParameter);
const { item_type } = valueNonNullable;
const propsConfig = paramSelectConfig[item_type];
export interface ParameterSelectProps extends SelectProps {
/** 类型 */
dataType: 'dataset' | 'model' | 'service' | 'resource';
/** 是否只是展示信息 */
display?: boolean;
/** 值,支持对象,对象必须包含 value */
value?: string | ParameterSelectObject;
/** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void;
}

/** 参数选择器,支持资源规格、数据集、模型、服务 */
function ParameterSelect({
dataType,
display = false,
value,
onChange,
...rest
}: ParameterSelectProps) {
const [options, setOptions] = useState<SelectProps['options']>([]);
const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;
const [resourceStandardList] = useComputingResource();


useEffect(() => { useEffect(() => {
// 获取下拉数据
const getSelectOptions = async () => {
if (!propsConfig) {
return;
}
const getOptions = propsConfig.getOptions;
const [res] = await to(getOptions());
if (res) {
setOptions(res);
}
};

getSelectOptions(); getSelectOptions();
}, []);
}, [propsConfig]);


const hangleChange = (e: string) => {
onChange?.({
...valueNonNullable,
value: e,
});
};
const selectOptions = dataType === 'resource' ? resourceStandardList : options;


// 获取下拉数据
const getSelectOptions = async () => {
if (!propsConfig) {
return;
}
const getOptions = propsConfig.getOptions;
const [res] = await to(getOptions());
if (res) {
setOptions(res);
const handleChange = (text: string) => {
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: text,
});
} else {
onChange?.(text);
} }
}; };


// 只用于展示,FormInfo 组件带有 Tooltip
if (display) {
return (
<FormInfo
select
value={valueText}
options={selectOptions}
fieldNames={propsConfig?.fieldNames}
></FormInfo>
);
}

return ( return (
<Select <Select
placeholder={valueNonNullable.placeholder}
{...rest}
filterOption={propsConfig?.filterOption} filterOption={propsConfig?.filterOption}
options={options}
options={selectOptions}
fieldNames={propsConfig?.fieldNames} fieldNames={propsConfig?.fieldNames}
value={valueNonNullable.value}
optionFilterProp={propsConfig.optionFilterProp}
onChange={hangleChange}
disabled={disabled}
optionFilterProp={propsConfig?.optionFilterProp}
value={valueText}
onChange={handleChange}
showSearch showSearch
allowClear allowClear
/> />


+ 67
- 31
react-ui/src/components/ResourceSelect/index.tsx View File

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


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


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


// 获取选择数据集、模型后面按钮 icon
// 获取选择数据集、模型、镜像后面按钮 icon
const getSelectBtnIcon = (type: ResourceSelectorType) => { 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, disabled, ...rest }: ResourceSelectProps) {
const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>(
undefined,
);
/** 数据集、模型、镜像选择表单组件 */
function ResourceSelect({
type,
value,
size,
disabled,
className,
style,
onChange,
...rest
}: ResourceSelectProps) {
const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize;
let selectedResource: ResourceSelectorResponse | undefined = undefined;
if (
value &&
typeof value === 'object' &&
value.activeTab &&
value.id &&
value.name &&
value.version &&
value.path &&
(type === ResourceSelectorType.Mirror || (value.identifier && value.owner))
) {
selectedResource = pick(value, [
'activeTab',
'id',
'identifier',
'name',
'owner',
'version',
'path',
]) as ResourceSelectorResponse;
}


// 选择数据集、模型、镜像
const selectResource = () => { const selectResource = () => {
const resource = selectedResource;
const { close } = openAntdModal(ResourceSelectorModal, { const { close } = openAntdModal(ResourceSelectorModal, {
type, type,
defaultExpandedKeys: resource ? [resource.id] : [],
defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
defaultActiveTab: resource?.activeTab,
defaultExpandedKeys: selectedResource ? [selectedResource.id] : [],
defaultCheckedKeys: selectedResource
? [`${selectedResource.id}-${selectedResource.version}`]
: [],
defaultActiveTab: selectedResource?.activeTab,
onOk: (res) => { onOk: (res) => {
setSelectedResource(res);
if (res) { if (res) {
const { activeTab, id, name, version, path, identifier, owner } = res; const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
@@ -50,8 +87,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
showValue: path, showValue: path,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
expandedKeys: [`${id}`],
checkedKeys: [`${id}-${version}`],
id,
name,
version,
path,
}); });
} else { } else {
const jsonObj = { const jsonObj = {
@@ -69,39 +108,36 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
expandedKeys: [`${id}`],
checkedKeys: [`${id}-${version}`],
...jsonObj, ...jsonObj,
}); });
} }
} else { } else {
onChange?.({
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
onChange?.(undefined);
} }
close(); close();
}, },
}); });
}; };


// 删除
const handleRemove = () => {
onChange?.(undefined);
};

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


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


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

@@ -22,8 +22,8 @@
height: 398px; height: 398px;
margin-right: 15px; margin-right: 15px;
padding: 15px; padding: 15px;
background-color: @background-color-primary;
border: 1px solid @border-color;
background-color: rgba(22, 100, 255, 0.03);
border: 1px solid rgba(22, 100, 255, 0.3);
border-radius: 8px; border-radius: 8px;


&__search { &__search {
@@ -31,7 +31,7 @@
padding-left: 0; padding-left: 0;
background-color: transparent; background-color: transparent;
border-width: 0; border-width: 0;
border-bottom: 1px solid @border-color-secondary;
border-bottom: 1px solid rgba(22, 100, 255, 0.1);
border-radius: 0; border-radius: 0;
} }


@@ -45,8 +45,8 @@
width: calc(100% - 488px - 15px); width: calc(100% - 488px - 15px);
height: 398px; height: 398px;
padding: 15px; padding: 15px;
background-color: @background-color-primary;
border: 1px solid @border-color;
background-color: rgba(22, 100, 255, 0.03);
border: 1px solid rgba(22, 100, 255, 0.3);
border-radius: 8px; border-radius: 8px;


&__title { &__title {
@@ -56,7 +56,7 @@
color: @text-color; color: @text-color;
font-size: @font-size; font-size: @font-size;
line-height: 46px; line-height: 46px;
border-bottom: 1px solid @border-color-secondary;
border-bottom: 1px solid rgba(22, 100, 255, 0.1);
} }
&__files { &__files {
height: calc(100% - 75px); height: calc(100% - 75px);
@@ -68,7 +68,7 @@
color: @text-color-secondary; color: @text-color-secondary;
font-size: 13px; font-size: 13px;
word-break: break-all; word-break: break-all;
background: @background-color-gray;
background: rgba(4, 3, 3, 0.06);
border-radius: 4px; border-radius: 4px;
} }
} }

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

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


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


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


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


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


/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */
function ResourceSelectorModal({ function ResourceSelectorModal({
type, type,
defaultExpandedKeys = [], defaultExpandedKeys = [],
@@ -69,7 +78,7 @@ function ResourceSelectorModal({
onOk, onOk,
...rest ...rest
}: ResourceSelectorModalProps) { }: ResourceSelectorModalProps) {
const [activeTab, setActiveTab] = useState<string>(defaultActiveTab);
const [activeTab, setActiveTab] = useState<CommonTabKeys>(defaultActiveTab);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
@@ -82,16 +91,7 @@ function ResourceSelectorModal({
const treeRef = useRef<TreeRef>(null); const treeRef = useRef<TreeRef>(null);
const config = selectorTypeConfig[type]; const config = selectorTypeConfig[type];


useEffect(() => {
setExpandedKeys([]);
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionPath('');
setSearchText('');
getTreeData();
}, [activeTab, type]);

// 搜索
const treeData = useMemo( const treeData = useMemo(
() => () =>
originTreeData.filter((v) => originTreeData.filter((v) =>
@@ -100,19 +100,45 @@ function ResourceSelectorModal({
[originTreeData, searchText], [originTreeData, searchText],
); );


// 获取数据集\模型\镜像列表
const getTreeData = async () => {
const isPublic = activeTab === CommonTabKeys.Private ? false : true;
const [res] = await to(config.getList(isPublic));
if (res) {
setOriginTreeData(res);
useEffect(() => {
// 获取数据集\模型\镜像列表
const getTreeData = async () => {
const isPublic = activeTab === CommonTabKeys.Private ? false : true;
const [res] = await to(config.getList(isPublic));
if (res) {
setOriginTreeData(res);


// 恢复上一次的 Expand 操作
restoreLastExpand();
} else {
setOriginTreeData([]);
}
};
// 恢复上一次的 Expand 操作
setFirstLoadList(true);
} else {
setOriginTreeData([]);
}
};

setExpandedKeys([]);
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionPath('');
setSearchText('');
getTreeData();
}, [activeTab, config]);

useEffect(() => {
// 恢复上一次的 Expand 操作
// 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys
// fisrtLoadList 标志位
const restoreLastExpand = () => {
if (firstLoadList && Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length > 0) {
setExpandedKeys(defaultExpandedKeys);
// 延时滑动到 defaultExpandedKeys,不然不会加载 defaultExpandedKeys,不然不会加载版本
setTimeout(() => {
treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' });
}, 100);
}
};
restoreLastExpand();
}, [firstLoadList, defaultExpandedKeys]);


// 获取数据集\模型\镜像版本列表 // 获取数据集\模型\镜像版本列表
const getVersions = async (parentId: string, parentNode: any) => { const getVersions = async (parentId: string, parentNode: any) => {
@@ -127,10 +153,10 @@ function ResourceSelectorModal({
setLoadedKeys((prev) => prev.concat(parentId)); setLoadedKeys((prev) => prev.concat(parentId));
} }


// 恢复上一次的 Check 操作
// 恢复上一次的 Check 操作,需要延时以便 TreeData 更新完
setTimeout(() => { setTimeout(() => {
restoreLastCheck(parentId, res); restoreLastCheck(parentId, res);
}, 300);
}, 100);
} else { } else {
setExpandedKeys([]); setExpandedKeys([]);
return Promise.reject(error); return Promise.reject(error);
@@ -149,7 +175,7 @@ function ResourceSelectorModal({
} }
}; };


// 动态加载 tree children
// 展开时,动态加载 tree children
const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { const onLoadData = ({ key, children, ...rest }: TreeDataNode) => {
if (children) { if (children) {
return Promise.resolve(); return Promise.resolve();
@@ -178,42 +204,25 @@ function ResourceSelectorModal({
} }
}; };


// 恢复上一次的 Expand 操作
// 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys
// fisrtLoadList 标志位
const restoreLastExpand = () => {
if (!firstLoadList && defaultExpandedKeys.length > 0) {
setTimeout(() => {
setExpandedKeys(defaultExpandedKeys);
setFirstLoadList(true);
setTimeout(() => {
treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' });
}, 100);
}, 0);
}
};

// 恢复上一次的 Check 操作 // 恢复上一次的 Check 操作
// 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口
// fisrtLoadVersions 标志位 // fisrtLoadVersions 标志位
const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => {
if (!firstLoadVersions && defaultCheckedKeys.length > 0) {
if (!firstLoadVersions && Array.isArray(defaultCheckedKeys) && defaultCheckedKeys.length > 0) {
const last = defaultCheckedKeys[0] as string; const last = defaultCheckedKeys[0] as string;
const { id } = getIdAndVersion(last); const { id } = getIdAndVersion(last);
// 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致
if (id === parentId) { if (id === parentId) {
setCheckedKeys(defaultCheckedKeys);
const parentNode = versions.find((v) => v.key === last);
getFiles(last, parentNode);
setFirstLoadVersions(true);
setTimeout(() => { setTimeout(() => {
setCheckedKeys(defaultCheckedKeys);
const parentNode = versions.find((v) => v.key === last);
getFiles(last, parentNode);
setFirstLoadVersions(true);
setTimeout(() => {
treeRef?.current?.scrollTo({
key: defaultCheckedKeys[0],
align: 'bottom',
});
}, 100);
}, 0);
treeRef?.current?.scrollTo({
key: defaultCheckedKeys[0],
align: 'bottom',
});
}, 100);
} }
} }
}; };
@@ -234,7 +243,7 @@ function ResourceSelectorModal({
version, version,
identifier, identifier,
owner, owner,
activeTab: activeTab as CommonTabKeys,
activeTab: activeTab,
}; };
onOk?.(res); onOk?.(res);
} else { } else {
@@ -255,7 +264,7 @@ function ResourceSelectorModal({
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
items={tabItems} items={tabItems}
onChange={setActiveTab}
onChange={(e) => setActiveTab(e as CommonTabKeys)}
className={styles['model-tabs']} className={styles['model-tabs']}
/> />
<div className={styles['model-selector']}> <div className={styles['model-selector']}>
@@ -267,7 +276,14 @@ function ResourceSelectorModal({
variant="borderless" variant="borderless"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
prefix={
<KFIcon
type="icon-sousuo"
color="rgba(22,100,255,0.4)"
style={{ height: '15px' }}
/>
}
// prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
/> />
<Tree <Tree
ref={treeRef} ref={treeRef}

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

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


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

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


<ProBreadcrumb></ProBreadcrumb> <ProBreadcrumb></ProBreadcrumb>


{/* <KFBreadcrumb /> */}

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


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

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


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


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


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

@@ -0,0 +1,3 @@
.ant-table .ant-table-cell .kf-table-col-title {
margin-bottom: 0;
}

+ 32
- 0
react-ui/src/components/TableColTitle/index.tsx View File

@@ -0,0 +1,32 @@
/*
* @Author: 赵伟
* @Date: 2025-03-11 10:52:23
* @Description: 用于内容可变的表格类标题
*/

import { Typography } from 'antd';
import classNames from 'classnames';
import './index.less';

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

function TableColTitle({ title, className, style }: TableColTitleProps) {
return (
<Typography.Paragraph
ellipsis={{ tooltip: title }}
className={classNames('kf-table-col-title', className)}
style={style}
>
{title}
</Typography.Paragraph>
);
}

export default TableColTitle;

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

Loading…
Cancel
Save