Compare commits

...

240 Commits

Author SHA1 Message Date
  zhaowei f1c7d0ad15 fix:注释掉修改头像 7 months ago
  zhaowei 431903b1de feat: 领域知识图谱菜单移动到多形态资源库 7 months ago
  zhaowei 4220ad1323 feat: 调整用户头像的大小 7 months ago
  zhaowei 6f9354ddc0 feat: 重新设计个人中心 7 months ago
  zhaowei 03136fe4c2 feat: 重新设计个人中心 7 months ago
  zhaowei 4fe48f99d1 feat: app 启动优化 7 months ago
  zhaowei 8fda69e19a feat: 开发环境添加代码配置 7 months ago
  zhaowei 3a3f98d0f1 feat: 代码选择分页数组 7 months ago
  zhaowei e24f5c4876 feat: 代码选择分页数量为18 7 months ago
  zhaowei 29cfff97ec feat: 代码选择分页数量为21 7 months ago
  zhaowei cff33a8e62 feat: 代码选择分页数量为20 7 months ago
  zhaowei 2fdbf62b2b feat: 代码配置选择回显选中的 7 months ago
  zhaowei 5fb95438bd feat: 修复token失效之后,重新登录,创建表单成功后返回到登录界面的问题 7 months ago
  zhaowei 3518879e12 feat: 选择模型添加版本描述 7 months ago
  zhaowei e91aade6af feat: 修改使用指南 7 months ago
  zhaowei b11b4d9f78 fix: 数据集、模型版本不能是origin 7 months ago
  zhaowei e8af394523 fix: 超参数寻优添加可视化对比iframe 8 months ago
  zhaowei eafca94b60 fix: 导出到数据集添加is_public参数 8 months ago
  zhaowei cd4071149b fix: 导出到数据集添加owner参数 8 months ago
  zhaowei f11582bc64 fix: 数据集和模型回退时分页没有设置 8 months ago
  zhaowei fddb63d293 fix: 流水线模板配置参数修改,历史实验实例配置参数变换 8 months ago
  zhaowei fd7f0008c8 Merge branch 'dev-check' into dev-zw 8 months ago
  zhaowei 531beedac6 chore: merge 8 months ago
  zhaowei 0e8efbb692 fix: 工作空间添加代码配置和服务数量 8 months ago
  chenzhihang 742be0bb03 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  zhaowei 512aa61b05 fix: 调整工作空间快速开始按钮的偏移 8 months ago
  chenzhihang b6273098c6 新增代码,服务统计 8 months ago
  zhaowei 921bc1d49d fix: 调整工作空间样式 8 months ago
  zhaowei 78eb6ee12f fix: 修复控制参数转成object的问题 8 months ago
  cp3hnu 57a0ec1040 Merge pull request '合并' (#258) from dev-zw into dev-check 8 months ago
  cp3hnu 154c834223 Merge pull request '合并' (#257) from dev-zw into dev-check 8 months ago
  zhaowei 560ff3411c fix: 实验隐藏流水线控制参数 8 months ago
  zhaowei bd269cffbf fix: 流水线隐藏参数 8 months ago
  cp3hnu 6646aff405 Merge pull request '合并' (#256) from dev-zw into dev-check 8 months ago
  zhaowei 1b497e1743 fix: 自动机器学习添加算法描述 8 months ago
  cp3hnu ff69531b22 Merge pull request '合并' (#255) from dev-zw into dev-check 8 months ago
  chenzhihang 399d611607 优化用户 8 months ago
  chenzhihang 8e024c9813 优化用户 8 months ago
  chenzhihang 5c9f59887c 优化实验 8 months ago
  chenzhihang b3e5eb08e9 优化实验 8 months ago
  chenzhihang 6efe25a3f0 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 548016091e 优化实验 8 months ago
  cp3hnu 5d61ade863 Merge pull request '合并' (#254) from dev-zw into dev-check 8 months ago
  zhaowei 458621fe91 fix: 自主机器学习改为自动机器学习 8 months ago
  chenzhihang 55b49047c3 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 597e16f81f 优化实验 8 months ago
  zhaowei aef75f4a37 fix: 修改角色 8 months ago
  cp3hnu 153b7de3ec Merge pull request '合并' (#253) from dev-zw into dev-check 8 months ago
  chenzhihang 4f415719f8 优化主动学习 8 months ago
  chenzhihang aa410acdfe 优化 8 months ago
  chenzhihang 4252f1710d 优化项目分页查询 8 months ago
  chenzhihang 23af257179 优化项目分页查询 8 months ago
  chenzhihang f0b70feca7 优化项目分页查询 8 months ago
  chenzhihang b014c9ce92 优化项目分页查询 8 months ago
  chenzhihang 976fb1dce5 优化项目分页查询 8 months ago
  chenzhihang 3a5845b623 优化项目分页查询 8 months ago
  chenzhihang b811bb51cd 优化项目分页查询 8 months ago
  chenzhihang 90c958b974 优化项目分页查询 8 months ago
  chenzhihang 19a4d6aed3 优化项目分页查询 8 months ago
  chenzhihang 6e27e5da0d 优化项目分页查询 8 months ago
  chenzhihang 3d0ea6603f 优化项目分页查询 8 months ago
  chenzhihang dc7e8dc801 优化项目分页查询 8 months ago
  chenzhihang 8e1f5fc587 优化 8 months ago
  chenzhihang 746314e5d2 优化 8 months ago
  chenzhihang d169d3d9db 优化 8 months ago
  chenzhihang 7a939f3f29 优化 8 months ago
  chenzhihang dc6cf41651 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 17c1c86043 测试 8 months ago
  cp3hnu a46a948760 Merge pull request '合并' (#252) from dev-zw into dev-check 8 months ago
  zhaowei 40ca029363 fix:自动机器学习添加mlp算法 8 months ago
  zhaowei 767a208732 fix:工作空间实验运行时长动态变化 8 months ago
  cp3hnu b314476ac3 Merge pull request '合并' (#251) from dev-zw into dev-check 8 months ago
  chenzhihang 39c0c2c01a 优化 8 months ago
  chenzhihang 873dd0ed5e Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 2a101d7f47 优化 8 months ago
  cp3hnu bcff86269b Merge pull request '合并' (#250) from dev-zw into dev-check 8 months ago
  zhaowei b2b74686ca fix: 自动机器学习创建时间改为更新时间 8 months ago
  chenzhihang 316cca31a4 优化排序 8 months ago
  zhaowei d1c41934b0 fix: 预测有两个loading 8 months ago
  zhaowei 274b8612e9 fix: 流水线模型部署服务版本验证 8 months ago
  chenzhihang df7460a01b Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 48d46a0723 优化用户 8 months ago
  zhaowei 8f72953683 fix: 服务日志样式错误 8 months ago
  cp3hnu 62bd8049ba Merge pull request '合并' (#249) from dev-zw into dev-check 8 months ago
  zhaowei 1ec43a60cf fix: 添加预测加载状态 8 months ago
  cp3hnu c42cf77939 Merge pull request '合并' (#248) from dev-zw into dev-check 8 months ago
  chenzhihang 85c8a3e6dc Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 96bf3351db 优化积分扣除结束 8 months ago
  cp3hnu b2ac5877d0 Merge pull request '合并' (#247) from dev-zw into dev-check 8 months ago
  zhaowei 0bace90a23 fix: 全局参数删除脱敏的配置 8 months ago
  cp3hnu 4c73d4339d Merge branch 'dev-zw' of code.gitlink.org.cn:ci4s/ci4sManagement-cloud into dev-zw 8 months ago
  cp3hnu ef9a78b167 fix: 最近更新时间 8 months ago
  chenzhihang 68ad21fadf 优化 8 months ago
  chenzhihang 696f939295 优化 8 months ago
  chenzhihang cc98a699d4 优化 8 months ago
  chenzhihang c11c728c71 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 31334c3a11 优化 8 months ago
  cp3hnu 3066853aeb Merge pull request '合并' (#246) from dev-zw into dev-check 8 months ago
  zhaowei f3f9846dff fix: 模型指标对比图错误 8 months ago
  cp3hnu 7715ee272d Merge pull request '合并' (#245) from dev-zw into dev-check 8 months ago
  zhaowei 65c588ac8a fix: 集成模型数量>=1 8 months ago
  zhaowei 34e2b8bb05 fix: mlp 显示成tablenet 8 months ago
  chenzhihang ff07d4c4a8 优化用户 8 months ago
  chenzhihang 6ad1910cb0 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 879c2e5802 优化更新ray 8 months ago
  chenzhihang 50dce4003f 优化用户 8 months ago
  cp3hnu e4385544b1 Merge pull request '合并dev-zw' (#244) from dev-zw into dev-check 8 months ago
  cp3hnu 4a6f4d2120 fix: 退出登录获取label-studio地址 8 months ago
  cp3hnu 8ef236f5b4 feat: 修改服务调用指南 8 months ago
  chenzhihang a5914e67c8 优化 8 months ago
  chenzhihang cccb2ae8c2 测试登录 8 months ago
  chenzhihang e5978fde0c 测试登录 8 months ago
  chenzhihang e76a0d7544 测试登录 8 months ago
  chenzhihang d19f0cfa82 优化 8 months ago
  chenzhihang d3508e6eba Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang b42a63f209 优化 8 months ago
  cp3hnu 603b943699 Merge pull request '合并dev' (#243) from dev into dev-check 8 months ago
  cp3hnu c8fc258089 Merge pull request '合并dev-zw' (#242) from dev-zw into dev 8 months ago
  cp3hnu 53fe983462 fix: 用户管理界面无法退出登录 8 months ago
  chenzhihang 1b63a74ce0 优化 8 months ago
  chenzhihang c4a3275358 优化 8 months ago
  chenzhihang fc026e9d15 优化 8 months ago
  chenzhihang 9d3e0ad5f3 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang a2555f6ac3 优化 8 months ago
  cp3hnu fefc545dad Merge pull request '合并dev' (#241) from dev into dev-check 8 months ago
  cp3hnu b419ab9485 Merge pull request '合并dev-zw' (#240) from dev-zw into dev 8 months ago
  cp3hnu 16d4b476f2 fix: 分配用户创建时间为null 8 months ago
  cp3hnu b8049721df fix: 退出登录两次 8 months ago
  chenzhihang 272ec6ef97 优化 8 months ago
  chenzhihang 46eff25e01 优化 8 months ago
  chenzhihang dcc591cacd Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 4da7042f61 优化 8 months ago
  chenzhihang 236ac4da0f 优化 8 months ago
  cp3hnu 2b0c11525b test: 添加 giturl 的测试 8 months ago
  cp3hnu 67a2b41240 Merge pull request '合并dev' (#239) from dev into dev-check 8 months ago
  cp3hnu 7de2295191 Merge pull request '合并dev-zw' (#238) from dev-zw into dev 8 months ago
  cp3hnu 0ac624ed2a fix: 实验无法查看更多 8 months ago
  chenzhihang 277ed0a710 优化积分扣除结束 8 months ago
  chenzhihang 2aaa8a9fe7 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 19b797189d 优化积分扣除 8 months ago
  cp3hnu 2d5538251e Merge pull request '合并dev' (#237) from dev into dev-check 8 months ago
  cp3hnu b142d5985d Merge pull request '合并dev-zw' (#236) from dev-zw into dev 8 months ago
  cp3hnu 14a9629167 fix: 添加iframe 加载失败日志 8 months ago
  chenzhihang 48fdf61dff 优化积分扣除 8 months ago
  chenzhihang 48851a2d4b 优化积分查询 8 months ago
  cp3hnu 5fe010f52d fix: 服务只有运行中才显示预测 8 months ago
  cp3hnu e6f74dc513 Merge pull request '合并dev' (#235) from dev into dev-check 8 months ago
  cp3hnu e4ffcea914 Merge pull request '合并dev' (#234) from dev-zw into dev 8 months ago
  cp3hnu 44673f39ed fix: 实验状态不同步 8 months ago
  chenzhihang 88227a85cb 优化运行开发环境 8 months ago
  chenzhihang a0d17e6dd3 优化查询pod状态 8 months ago
  chenzhihang adf3b8d02a 优化查询pod状态 8 months ago
  chenzhihang 596aa80315 优化查询pod状态 8 months ago
  chenzhihang 7b146651c7 优化创建pod 8 months ago
  chenzhihang eb50e76a54 优化实验状态查询 8 months ago
  chenzhihang a2f1c0532b 优化dvc 8 months ago
  chenzhihang 694f142b3f 优化积分扣除,优化dvc 8 months ago
  chenzhihang cdceefcb24 优化实验状态查询 8 months ago
  chenzhihang 1cfbd5185c 优化积分扣除 8 months ago
  chenzhihang bee9f43762 优化 8 months ago
  chenzhihang aa3909a047 优化 8 months ago
  chenzhihang 111ade2f49 优化项目排序 8 months ago
  chenzhihang 6670aa0658 优化项目排序 8 months ago
  chenzhihang 6748c46d6f 优化项目排序 8 months ago
  chenzhihang d6ed84ac59 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 5a7a188478 优化积分更新 8 months ago
  cp3hnu 5d641cb61b Merge pull request '合并dev' (#233) from dev into dev-check 8 months ago
  chenzhihang 6809f62b9c 优化 8 months ago
  cp3hnu 8110739002 Merge pull request '合并dev-zw' (#232) from dev-zw into dev 8 months ago
  cp3hnu 1fd40f927c fix: 用户账号支持4-15位 8 months ago
  chenzhihang b02d15662f Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 20b1e78df7 优化 8 months ago
  cp3hnu d7ddcefb96 fix: 自动机器学习创建时间有误 8 months ago
  cp3hnu 1f08241ca9 Merge pull request '合并dev' (#231) from dev into dev-check 8 months ago
  cp3hnu b4873208c2 Merge pull request '合并dev-zw' (#230) from dev-zw into dev 8 months ago
  chenzhihang 25381c26ae 优化 8 months ago
  chenzhihang 567263b11a Merge remote-tracking branch 'origin/dev' into dev-check 8 months ago
  chenzhihang cdb3fafd14 Merge branch 'dev' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev 8 months ago
  chenzhihang 25dec31eaa 优化 8 months ago
  cp3hnu 5b8a10006d feat: 删除机器人 8 months ago
  zhaowei b0d8a19975 feat: 快速开始删除开发环境 8 months ago
  cp3hnu f5d4158532 Merge pull request '合并dev' (#229) from dev into dev-check 8 months ago
  cp3hnu dcb73eacf9 Merge pull request '合并dev-zw' (#228) from dev-zw into dev 8 months ago
  fanshuai 84dbb1e8fb 修改BUG 【开发环境】新创建开发环境处于列表底部,未处于列表前列 8 months ago
  cp3hnu 95cfbefda4 feat: 自动机器学习运行&获取tensorboard状态 8 months ago
  fanshuai ae86b2a835 Merge remote-tracking branch 'origin/dev-check' into dev-check 8 months ago
  fanshuai 68ee173591 修改BUG 【工作空间】AI资产卡片数据统计错误 8 months ago
  chenzhihang 71edeb6922 优化状态更新 8 months ago
  chenzhihang a4b82fb9f5 优化状态更新 8 months ago
  cp3hnu 9a89988e95 fix: 删除“构建中”状态镜像版本,构建成功/失败状态返回后重新显示在列表 8 months ago
  cp3hnu b9f4c48ea6 fix: 位于大于筛选结果的页码,点击左侧边栏筛选,页面提示暂无数据 8 months ago
  chenzhihang 8270406d3c 优化http请求 8 months ago
  chenzhihang 870fbce684 优化查询代码配置bug 8 months ago
  cp3hnu 7a4852908b fix: 添加镜像版本描述 8 months ago
  chenzhihang ce0d898af8 优化积分扣除 8 months ago
  chenzhihang b5fd8eb031 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang b9b0db8442 优化异常提示 8 months ago
  chenzhihang a2cf8fe75e 优化 8 months ago
  cp3hnu 6bb5a9e8e6 Merge pull request '合并dev' (#227) from dev into dev-check 8 months ago
  cp3hnu aa980985d5 Merge pull request '合并dev-zw' (#226) from dev-zw into dev 8 months ago
  cp3hnu b4d99f8e5c fix: 代码配置30202改为30203 8 months ago
  chenzhihang 5d49e2d1b5 优化镜像版本描述 8 months ago
  chenzhihang 20ce4e4758 优化镜像版本描述 8 months ago
  chenzhihang b994fb3f31 优化 8 months ago
  chenzhihang c0320a2a68 优化 8 months ago
  chenzhihang 41c2faf9cb 优化 8 months ago
  cp3hnu e92ac40694 fix: 列表运行时长和详情运行时长不一致 8 months ago
  chenzhihang 45990fa2b7 优化 8 months ago
  cp3hnu 9c62812424 fix: 列表运行时长和详情运行时长不一致 8 months ago
  chenzhihang d1928d702b 优化 8 months ago
  chenzhihang eea826de2c 优化构建镜像 8 months ago
  chenzhihang 55fd9d7271 优化代码配置 8 months ago
  chenzhihang 3905841db7 优化状态更新 8 months ago
  chenzhihang 0a7be4e261 优化结束时间 8 months ago
  chenzhihang 47f2c80a00 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 9def5a4d7a 优化状态更新 8 months ago
  cp3hnu 7fc016c3a0 Merge pull request '合并dev' (#225) from dev into dev-check 8 months ago
  cp3hnu 260812125a Merge pull request '合并dev-zw' (#224) from dev-zw into dev 8 months ago
  cp3hnu 22f85fb3ae fix: 实验取流水线节点 8 months ago
  chenzhihang 765f37ab7c 优化主动学习 8 months ago
  chenzhihang 57296d364e 优化主动学习 8 months ago
  chenzhihang 04a9f8f125 优化主动学习 8 months ago
  chenzhihang 640c49f507 优化 8 months ago
  chenzhihang 85c92e8fae 优化主动学习 8 months ago
  chenzhihang 7b51ed5bc6 优化主动学习 8 months ago
  chenzhihang b78b0075cd 优化主动学习 8 months ago
  chenzhihang e4dd26c87d 优化 8 months ago
  chenzhihang f5f3aca3e5 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang 5aa9cdc62f 优化 8 months ago
  cp3hnu 5d60751a66 feat: 内嵌tesorboard 8 months ago
  cp3hnu 0e54f6e95d Merge pull request '合并dev' (#223) from dev into dev-check 8 months ago
  cp3hnu bc18fb14c2 Merge pull request '合并dev-zw' (#222) from dev-zw into dev 8 months ago
  cp3hnu c103375c8c fix: 自动机器学习日志一片空白 8 months ago
  cp3hnu ebd1d8680a fix: 全选时可以选中运行的实验实例 8 months ago
  chenzhihang ac95c975a5 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 8 months ago
  chenzhihang c16c9d75a3 Merge remote-tracking branch 'origin/dev' into dev-check 8 months ago
  cp3hnu b6f0ffbfe1 style: 调整样式 8 months ago
  cp3hnu 039ad81fc7 Merge pull request '合并dev' (#221) from dev into dev-check 8 months ago
  cp3hnu e553a21c2f Merge pull request '合并dev-zw' (#220) from dev-zw into dev 8 months ago
  zhaowei c719141676 feat: 验收 8 months ago
100 changed files with 2252 additions and 1339 deletions
Split View
  1. +12
    -1
      k8s/template-yaml/k8s-5auth.yaml
  2. +39
    -17
      react-ui/config/routes.ts
  3. BIN
      react-ui/public/assets/材料科研软件平台使用文档-v1.0.pdf
  4. BIN
      react-ui/public/assets/材料科研软件平台使用文档.pdf
  5. +26
    -49
      react-ui/src/app.tsx
  6. +22
    -3
      react-ui/src/components/CodeConfigItem/index.less
  7. +38
    -11
      react-ui/src/components/CodeConfigItem/index.tsx
  8. +39
    -3
      react-ui/src/components/CodeSelect/index.tsx
  9. +1
    -0
      react-ui/src/components/CodeSelectorModal/index.less
  10. +84
    -17
      react-ui/src/components/CodeSelectorModal/index.tsx
  11. +45
    -14
      react-ui/src/components/IFramePage/index.tsx
  12. +25
    -13
      react-ui/src/components/ResourceSelectorModal/config.tsx
  13. +16
    -1
      react-ui/src/components/ResourceSelectorModal/index.less
  14. +24
    -8
      react-ui/src/components/ResourceSelectorModal/index.tsx
  15. +22
    -7
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  16. +10
    -1
      react-ui/src/components/RunDuration/index.tsx
  17. +3
    -3
      react-ui/src/enums/index.ts
  18. +22
    -10
      react-ui/src/hooks/useSSE.ts
  19. +8
    -10
      react-ui/src/pages/ActiveLearn/Instance/index.tsx
  20. +5
    -9
      react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx
  21. +14
    -4
      react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx
  22. +4
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts
  23. +7
    -2
      react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx
  24. +1
    -1
      react-ui/src/pages/Authorize/index.tsx
  25. +11
    -9
      react-ui/src/pages/AutoML/Instance/index.tsx
  26. +1
    -1
      react-ui/src/pages/AutoML/List/index.tsx
  27. +44
    -9
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  28. +6
    -67
      react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx
  29. +78
    -0
      react-ui/src/pages/AutoML/components/CreateForm/utils.ts
  30. +0
    -4
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less
  31. +16
    -8
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx
  32. +35
    -32
      react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx
  33. +14
    -3
      react-ui/src/pages/AutoML/components/ExperimentList/config.ts
  34. +126
    -61
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  35. +7
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/empty.tsx
  36. +11
    -0
      react-ui/src/pages/AutoML/components/ExperimentLog/index.less
  37. +4
    -1
      react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx
  38. +18
    -16
      react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx
  39. +8
    -0
      react-ui/src/pages/AutoML/components/ExperimentVisualResult/index.less
  40. +69
    -17
      react-ui/src/pages/AutoML/components/ExperimentVisualResult/index.tsx
  41. +1
    -0
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less
  42. +4
    -2
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  43. +6
    -4
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  44. +4
    -2
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  45. +4
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.less
  46. +1
    -3
      react-ui/src/pages/Dataset/components/ResourceItem/index.tsx
  47. +7
    -0
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  48. +3
    -1
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  49. +1
    -0
      react-ui/src/pages/Dataset/config.tsx
  50. +9
    -0
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  51. +78
    -15
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  52. +1
    -1
      react-ui/src/pages/Docs/index.tsx
  53. +1
    -1
      react-ui/src/pages/Experiment/Comparison/index.tsx
  54. +102
    -54
      react-ui/src/pages/Experiment/Info/index.jsx
  55. +12
    -0
      react-ui/src/pages/Experiment/Tensorboard/index.tsx
  56. +11
    -18
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  57. +0
    -4
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less
  58. +15
    -10
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx
  59. +35
    -32
      react-ui/src/pages/Experiment/components/ExperimentInstanceList/instance.tsx
  60. +60
    -50
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  61. +13
    -1
      react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx
  62. +4
    -1
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  63. +1
    -4
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
  64. +189
    -136
      react-ui/src/pages/Experiment/index.jsx
  65. +12
    -0
      react-ui/src/pages/HyperParameter/Aim/index.tsx
  66. +1
    -1
      react-ui/src/pages/HyperParameter/Info/index.tsx
  67. +6
    -8
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  68. +5
    -1
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
  69. +7
    -2
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  70. +5
    -54
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
  71. +8
    -0
      react-ui/src/pages/Mirror/Create/index.less
  72. +29
    -6
      react-ui/src/pages/Mirror/Create/index.tsx
  73. +6
    -3
      react-ui/src/pages/Mirror/Info/index.tsx
  74. +13
    -9
      react-ui/src/pages/Model/components/MetricsChart/index.tsx
  75. +1
    -1
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  76. +29
    -26
      react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx
  77. +9
    -1
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.less
  78. +9
    -2
      react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx
  79. +1
    -1
      react-ui/src/pages/Pipeline/Info/index.jsx
  80. +2
    -2
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  81. +158
    -94
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  82. +2
    -2
      react-ui/src/pages/Pipeline/index.jsx
  83. +1
    -1
      react-ui/src/pages/System/Role/authUser.tsx
  84. +1
    -1
      react-ui/src/pages/System/Role/components/UserSelectorModal.tsx
  85. +2
    -2
      react-ui/src/pages/System/Role/index.tsx
  86. +5
    -8
      react-ui/src/pages/System/User/components/DeptTree.tsx
  87. +2
    -3
      react-ui/src/pages/System/User/edit.tsx
  88. +0
    -60
      react-ui/src/pages/User/Center/Center.less
  89. +94
    -90
      react-ui/src/pages/User/Center/components/BaseInfo/index.tsx
  90. +60
    -51
      react-ui/src/pages/User/Center/components/ResetPassword/index.tsx
  91. +64
    -0
      react-ui/src/pages/User/Center/index.less
  92. +98
    -88
      react-ui/src/pages/User/Center/index.tsx
  93. +1
    -1
      react-ui/src/pages/User/Login/login.tsx
  94. +12
    -1
      react-ui/src/pages/Workspace/components/AssetsManagement/index.less
  95. +16
    -8
      react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx
  96. +4
    -4
      react-ui/src/pages/Workspace/components/ExperimentTable/index.less
  97. +8
    -29
      react-ui/src/pages/Workspace/components/ExperimentTable/index.tsx
  98. +76
    -0
      react-ui/src/pages/Workspace/components/ExperimentTable/instance.tsx
  99. +21
    -21
      react-ui/src/pages/Workspace/components/QuickStart/index.tsx
  100. +7
    -7
      react-ui/src/pages/Workspace/components/TotalStatistics/index.less

+ 12
- 1
k8s/template-yaml/k8s-5auth.yaml View File

@@ -18,6 +18,11 @@ spec:
image: ${k8s-5auth-image}
ports:
- containerPort: 9200
env:
- name: TZ
value: Asia/Shanghai
- name: JAVA_TOOL_OPTIONS
value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"

---
apiVersion: v1
@@ -28,9 +33,15 @@ metadata:
spec:
type: NodePort
ports:
- port: 9200
- name: http
port: 9200
nodePort: 31206
protocol: TCP
- name: debug
nodePort: 31221
port: 5005
protocol: TCP
targetPort: 5005
selector:
app: ci4s-auth


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

@@ -141,12 +141,23 @@ export default [
{
name: '实验对比',
path: 'compare',
component: './Experiment/Comparison/index',
routes: [
{
name: '实验对比',
path: '',
component: './Experiment/Comparison/index',
},
{
name: '可视化对比',
path: 'compare-visual',
component: './Experiment/Aim/index',
},
],
},
{
name: '实验可视化对比',
path: 'compare-visual',
component: './Experiment/Aim/index',
name: '可视化',
path: 'visual',
component: './Experiment/Tensorboard/index',
},
],
},
@@ -218,7 +229,18 @@ export default [
{
name: '实验实例详情',
path: 'instance/:experimentId/:id',
component: './HyperParameter/Instance/index',
routes: [
{
name: '实验实例详情',
path: '',
component: './HyperParameter/Instance/index',
},
{
name: '可视化对比',
path: 'compare-visual',
component: './HyperParameter/Aim/index',
},
],
},
],
},
@@ -395,6 +417,18 @@ export default [
},
],
},
{
name: '知识图谱',
path: 'knowledge',
routes: [
{
name: '知识图谱',
path: '',
key: 'knowledge',
component: './Knowledge/index',
},
],
},
],
},
{
@@ -561,18 +595,6 @@ export default [
},
],
},
{
name: '知识图谱',
path: '/knowledge',
routes: [
{
name: '知识图谱',
path: '',
key: 'knowledge',
component: './Knowledge/index',
},
],
},
{
path: '*',
layout: false,


BIN
react-ui/public/assets/材料科研软件平台使用文档-v1.0.pdf View File


BIN
react-ui/public/assets/材料科研软件平台使用文档.pdf View File


+ 26
- 49
react-ui/src/app.tsx View File

@@ -21,6 +21,7 @@ import {
} from './services/session';
import './styles/menu.less';
import { needAuth } from './utils';
import { closeAllModals } from './utils/modal';
import { gotoLoginPage } from './utils/ui';
export { requestConfig as request } from './requestConfig';

@@ -29,15 +30,14 @@ export { requestConfig as request } from './requestConfig';
*/
export async function getInitialState(): Promise<GlobalInitialState> {
const fetchUserInfo = async () => {
globalGetSeverTime();
try {
globalGetSeverTime();
const response = await getUserInfo();
return {
...response.user,
avatar: response.user.avatar || require('@/assets/img/avatar-default.png'),
permissions: response.permissions,
roles: response.roles,
roleNames: response.user.roles,
roleNames: response.roles,
} as API.CurrentUser;
} catch (error) {
console.error('getInitialState', error);
@@ -46,11 +46,8 @@ export async function getInitialState(): Promise<GlobalInitialState> {
return undefined;
};

// 如果不是登录页面,执行
const { location } = history;

// console.log('getInitialState', needAuth(location.pathname));
if (needAuth(location.pathname)) {
const token = getAccessToken();
if (token) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
@@ -71,9 +68,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
return {
ErrorBoundary: ErrorBoundary,
rightContentRender: false,
waterMarkProps: {
// content: initialState?.currentUser?.nickName,
},
menu: {
locale: false,
// 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
@@ -84,45 +78,9 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
if (!initialState?.currentUser?.userId) {
return [];
}
// console.log('get menus')
// initialState.currentUser 中包含了所有用户信息
// console.log('get routers')
// setInitialState((preInitialState) => ({
// ...preInitialState,
// menus,
// }));
return getRemoteMenu();
},
},
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && needAuth(location.pathname)) {
gotoLoginPage();
}
},
layoutBgImgList: [
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
left: 85,
bottom: 100,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
bottom: -68,
right: -45,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
bottom: 0,
left: 0,
width: '331px',
},
],
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
childrenRender: (children) => {
// 增加一个 loading 的状态
// if (initialState?.loading) return <PageLoading />;
@@ -159,9 +117,26 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
};

export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => {
// console.log('onRouteChange');

// 路由切换时,尤其是回退时,关闭打开的弹框
closeAllModals();

const { location } = e;
const token = getAccessToken();
// 没有 token,跳转到登录页面
if (!token && needAuth(location.pathname)) {
gotoLoginPage();
return;
}

// 有 token, 登录页面直接跳转到首页
if (token && !needAuth(location.pathname)) {
history.push('/');
}

const menus = getRemoteMenu();
// console.log('onRouteChange', menus);
// 没有菜单,刷新页面
if (menus === null && needAuth(location.pathname)) {
history.go(0);
}
@@ -179,10 +154,12 @@ export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => {
export function render(oldRender: () => void) {
// console.log('render');
const token = getAccessToken();
if (!token || token?.length === 0) {
if (!token) {
oldRender();
return;
}

// 有 token,获取路由
getRoutersInfo()
.then((res) => {
setRemoteMenu(res);


+ 22
- 3
react-ui/src/components/CodeConfigItem/index.less View File

@@ -1,11 +1,22 @@
.code-config-item {
position: relative;
width: calc(25% - 7.5px);
width: calc(33.33% - 7px);
padding: 15px;
background-color: .addAlpha(@primary-color, 0.04) [];
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;

&__checkbox {
flex: 1;
min-width: 0;

:global {
.ant-checkbox + span {
flex: 1;
min-width: 0;
}
}
}

&__name {
margin-right: 8px;
@@ -38,6 +49,8 @@
margin-bottom: 10px !important;
color: @text-color-secondary;
font-size: 13px;
cursor: pointer;
word-break: break-all;
}

&__branch {
@@ -46,11 +59,17 @@
}

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

&--active {
border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);
}

&:hover &__name {
&--active &__name {
color: @primary-color;
}


}

+ 38
- 11
react-ui/src/components/CodeConfigItem/index.tsx View File

@@ -1,25 +1,51 @@
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { Flex, Typography } from 'antd';
import { getGitUrl } from '@/utils';
import { Checkbox, Flex, Typography } from 'antd';
import { type CheckboxChangeEvent } from 'antd/es/checkbox';
import classNames from 'classnames';
import { useState } from 'react';
import styles from './index.less';

type CodeConfigItemProps = {
item: CodeConfigData;
onClick?: (item: CodeConfigData) => void;
checked: boolean;
onChange?: (item: CodeConfigData, checked: boolean) => void;
};

function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
function CodeConfigItem({ item, checked, onChange }: CodeConfigItemProps) {
const [isEllipsis, setIsEllipsis] = useState(false);

const openProject = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
const { git_url, git_branch } = item;
const url = getGitUrl(git_url, git_branch);
window.open(url, '_blank');
};

const handleChange = (e: CheckboxChangeEvent) => {
onChange?.(item, e.target.checked);
};

return (
<div className={styles['code-config-item']} onClick={() => onClick?.(item)}>
<div
id={`code-config-item-${item.id}`}
className={classNames(styles['code-config-item'], {
[styles['code-config-item--active']]: checked,
})}
>
<Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}>
<Typography.Paragraph
className={styles['code-config-item__name']}
ellipsis={{ tooltip: item.code_repo_name }}
<Checkbox
className={styles['code-config-item__checkbox']}
checked={checked}
onChange={handleChange}
>
{item.code_repo_name}
</Typography.Paragraph>
<Typography.Paragraph
className={styles['code-config-item__name']}
ellipsis={{ tooltip: item.code_repo_name }}
>
{item.code_repo_name}
</Typography.Paragraph>
</Checkbox>
<div
className={classNames(
styles['code-config-item__tag'],
@@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
className={styles['code-config-item__url']}
ellipsis={{
rows: 2,
tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip
onEllipsis: (ellipsis) => setIsEllipsis(ellipsis),
tooltip: isEllipsis ? item.git_url : false,
onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), // 必须这样,不然不能省略
}}
onClick={openProject}
>
{item.git_url}
</Typography.Paragraph>


+ 39
- 3
react-ui/src/components/CodeSelect/index.tsx View File

@@ -18,7 +18,19 @@ export {
type ParameterInputValue,
} from '../ParameterInput';

type CodeSelectProps = ParameterInputProps;
export type CodeSelectProps = ParameterInputProps;

// 服务的需要的代码配置数据格式
export type ServerCodeData = {
id: number;
name: string;
code_path: string;
branch: string;
username: string;
password: string;
ssh_private_key: string;
is_public: boolean;
};

/** 代码配置选择表单组件 */
function CodeSelect({
@@ -32,11 +44,34 @@ function CodeSelect({
}: CodeSelectProps) {
// 选择代码配置
const selectResource = () => {
const codeData = value as ServerCodeData;
const defaultSelected =
value && typeof value === 'object'
? {
id: codeData.id,
code_repo_name: codeData.name,
git_url: codeData.code_path,
git_branch: codeData.branch,
git_user_name: codeData.username,
git_password: codeData.password,
ssh_key: codeData.ssh_private_key,
is_public: codeData.is_public,
}
: undefined;
const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected: defaultSelected,
onOk: (res) => {
if (res) {
const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } =
res;
const {
id,
code_repo_name,
git_url,
git_branch,
git_user_name,
git_password,
ssh_key,
is_public,
} = res;
const jsonObj = {
id,
name: code_repo_name,
@@ -45,6 +80,7 @@ function CodeSelect({
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
is_public,
};
const jsonObjStr = JSON.stringify(jsonObj);
onChange?.({


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

@@ -17,6 +17,7 @@
margin-bottom: 30px;
overflow-x: hidden;
overflow-y: auto;
padding-bottom: 10px;
}

&__empty {


+ 84
- 17
react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -7,7 +7,8 @@
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { getCodeConfigListReq } from '@/services/codeConfig';
import { getCodeConfigListReq, getCodeConfigPageNumReq } from '@/services/codeConfig';
import { CustomPartial } from '@/types';
import { to } from '@/utils/promise';
import type { ModalProps, PaginationProps } from 'antd';
import { Empty, Input, Pagination } from 'antd';
@@ -17,24 +18,68 @@ import './index.less';

export { type CodeConfigData };

export type SelectCodeData = CustomPartial<
CodeConfigData,
| 'id'
| 'code_repo_name'
| 'git_url'
| 'git_branch'
| 'git_user_name'
| 'git_password'
| 'ssh_key'
| 'is_public'
>;

export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> {
onOk?: (params: CodeConfigData | undefined) => void;
defaultSelected?: SelectCodeData;
onOk?: (params: SelectCodeData | undefined) => void;
}

/** 选择代码配置的弹窗,推荐使用函数的方式打开 */
function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
function CodeSelectorModal({ defaultSelected, onOk, ...rest }: CodeSelectorModalProps) {
const DefaultPageSize = 18;
const [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>({
current: 1,
pageSize: 20,
});
const [searchText, setSearchText] = useState<string | undefined>(undefined);
const [inputText, setInputText] = useState<string | undefined>(undefined);
const [selected, setSelected] = useState(defaultSelected);
const [isScrolled, setIsScrolled] = useState(false);
const [pagination, setPagination] = useState<PaginationProps>({
current: defaultSelected?.id ? 0 : 1, // 为 0 时,不请求,等待接口返回选中的代码配置在第几页
pageSize: DefaultPageSize,
});

useEffect(() => {
const getCodeConfigPageNum = async (id: number, size: number) => {
const [res] = await to(
getCodeConfigPageNumReq(id, {
size,
}),
);
if (res) {
setPagination({
current: typeof res.data === 'number' ? Math.max(0, res.data) + 1 : 1,
pageSize: DefaultPageSize,
});
} else {
setPagination({
current: 1,
pageSize: DefaultPageSize,
});
}
};
if (defaultSelected?.id) {
getCodeConfigPageNum(defaultSelected?.id, DefaultPageSize);
}
}, [defaultSelected?.id]);

useEffect(() => {
// 获取数据请求
const getDataList = async () => {
// 为 0 时,不请求,等待接口返回选中的代码配置在第几页
if (pagination.current === 0) {
return;
}
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
@@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
getDataList();
}, [pagination, searchText]);

useEffect(() => {
if (dataList.length > 0 && !isScrolled && defaultSelected?.id) {
const selectedItem = document.getElementById(`code-config-item-${defaultSelected?.id}`);
if (selectedItem) {
selectedItem.scrollIntoView();
}
setIsScrolled(true);
}
}, [isScrolled, dataList, defaultSelected?.id]);

// 搜索
const handleSearch = (value: string) => {
setSearchText(value);
@@ -59,8 +114,12 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
}));
};

const handleClick = (item: CodeConfigData) => {
onOk?.(item);
const handleChange = (item: CodeConfigData, checked: boolean) => {
if (checked) {
setSelected(item);
} else {
setSelected(undefined);
}
};

// 分页切换
@@ -77,7 +136,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
title="选择代码配置"
image={require('@/assets/img/modal-code-config.png')}
width={920}
footer={null}
onOk={() => onOk?.(selected)}
destroyOnClose
>
<div className="kf-code-selector-modal">
@@ -93,23 +152,31 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
prefix={
<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 ? (
<>
<div className="kf-code-selector-modal__content">
{dataList?.map((item) => (
<CodeConfigItem item={item} key={item.id} onClick={handleClick} />
<CodeConfigItem
item={item}
key={item.id}
checked={item.id === selected?.id}
onChange={handleChange}
/>
))}
</div>
<Pagination
align="center"
align="end"
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
defaultPageSize={DefaultPageSize}
pageSizeOptions={[
DefaultPageSize,
2 * DefaultPageSize,
3 * DefaultPageSize,
4 * DefaultPageSize,
5 * DefaultPageSize,
]}
showQuickJumper
onChange={handlePageChange}
{...pagination}


+ 45
- 14
react-ui/src/components/IFramePage/index.tsx View File

@@ -1,12 +1,11 @@
import FullScreenFrame from '@/components/FullScreenFrame';
import KFSpin from '@/components/KFSpin';
import { getKnowledgeGraphUrl, getLabelStudioUrl } from '@/services/developmentEnvironment';
import Loading from '@/utils/loading';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { FloatButton } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import './index.less';

export enum IframePageType {
@@ -54,9 +53,13 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {

type IframePageProps = {
/** 子系统 */
type: IframePageType;
type?: IframePageType;
/** url */
url?: string;
/** 是否可以在页签上打开 */
openInTab?: boolean;
/** 是否显示加载 */
showLoading?: boolean;
/** 自定义样式类名 */
className?: string;
/** 自定义样式 */
@@ -64,32 +67,60 @@ type IframePageProps = {
};

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

useEffect(() => {
const requestIframeUrl = async () => {
setLoading(true);
const requestIframeUrl = async (type: IframePageType) => {
if (showLoading) {
Loading.show();
}
const [res] = await to(getRequestAPI(type)());
if (res && res.data) {
setIframeUrl(res.data);
} else {
setLoading(false);
if (showLoading) {
Loading.hide();
}
}
};

requestIframeUrl();
}, [type]);
if (type) {
requestIframeUrl(type);
} else if (url) {
if (showLoading) {
Loading.show();
}

setIframeUrl(url);
}
}, [type, url, showLoading]);

const handleLoad = () => {
if (showLoading) {
Loading.hide();
}
};

const hideLoading = () => {
setLoading(false);
const handleError = (error?: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
console.log('error', error);
if (showLoading) {
Loading.hide();
}
};

return (
<div className={classNames('kf-iframe-page', className)} style={style}>
{loading && createPortal(<KFSpin size="large" />, document.body)}
<FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} />
{/* {loading && createPortal(<KFSpin size="large" />, document.body)} */}
{iframeUrl && <FullScreenFrame url={iframeUrl} onLoad={handleLoad} onError={handleError} />}
{openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />}
</div>
);


+ 25
- 13
react-ui/src/components/ResourceSelectorModal/config.tsx View File

@@ -1,8 +1,8 @@
import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png';
import { AvailableRange, CommonTabKeys } from '@/enums';
import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums';
import { DatasetData, ModelData, ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { MirrorVersionData } from '@/pages/Mirror/Info';
import { MirrorData } from '@/pages/Mirror/List';
import {
@@ -24,11 +24,11 @@ export enum ResourceSelectorType {
}

// 数据集、模型列表转为树形结构
const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => {
const convertDatasetToTreeData = (list: ResourceData[], isPublic: boolean): TreeDataNode[] => {
return list.map((v) => ({
...v,
key: `${v.id}`,
title: v.name,
title: isPublic ? `${v.name} (${v.owner})` : v.name,
isLeaf: false,
checkable: false,
}));
@@ -71,6 +71,7 @@ const convertMirrorVersionToTreeData = (
key: `${parentId}-${item.id}`,
isLeaf: true,
checkable: true,
description: item.description,
}));
};

@@ -106,7 +107,7 @@ export class DatasetSelector implements SelectorTypeInfo {
const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) {
const list = res.data.content || [];
return convertDatasetToTreeData(list);
return convertDatasetToTreeData(list, isPublic);
} else {
return Promise.reject('获取数据集列表失败');
}
@@ -125,11 +126,16 @@ export class DatasetSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getDatasetInfo(params);
if (res && res.data) {
const path = res.data.relative_paths || '';
const list = res.data.dataset_version_vos || [];
const dataset = res.data as DatasetData;
const {
relative_paths: path = '',
dataset_version_vos: list = [],
version_desc: versionDesc = '',
} = dataset;
return {
path,
content: list,
versionDesc,
};
} else {
return Promise.reject('获取数据集文件列表失败');
@@ -158,7 +164,7 @@ export class ModelSelector implements SelectorTypeInfo {
const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) {
const list = res.data.content || [];
return convertDatasetToTreeData(list);
return convertDatasetToTreeData(list, isPublic);
} else {
return Promise.reject('获取模型列表失败');
}
@@ -177,11 +183,17 @@ export class ModelSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getModelInfo(params);
if (res && res.data) {
const path = res.data.relative_paths || '';
const list = res.data.model_version_vos || [];
const model = res.data as ModelData;
const {
relative_paths: path = '',
model_version_vos: list = [],
version_desc: versionDesc = '',
} = model;

return {
path,
content: list,
versionDesc,
};
} else {
return Promise.reject('获取模型文件列表失败');
@@ -224,8 +236,7 @@ export class MirrorSelector implements SelectorTypeInfo {
image_id: parentKey,
page: 0,
size: 2000,
status: 'available',
state: 1,
status: MirrorVersionStatus.Available,
});
if (res && res.data) {
const list = res.data.content || [];
@@ -236,7 +247,7 @@ export class MirrorSelector implements SelectorTypeInfo {
}

async getFiles(_parentKey: string, parentNode: MirrorVersionData) {
const { url } = parentNode;
const { url, description } = parentNode;
return {
path: url,
content: [
@@ -245,6 +256,7 @@ export class MirrorSelector implements SelectorTypeInfo {
file_name: `${url}`,
},
],
versionDesc: description,
};
}
}


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

@@ -65,7 +65,7 @@
border-bottom: 1px solid rgba(22, 100, 255, 0.1);
}
&__files {
height: calc(100% - 75px);
height: calc(100% - 61px);
overflow-y: auto;

&__file {
@@ -76,7 +76,22 @@
word-break: break-all;
background: rgba(4, 3, 3, 0.06);
border-radius: 4px;

&:last-child {
margin-bottom: 0;
}
}
}
&__desc {
margin-bottom: 10px;
padding: 10px;
overflow-y: auto;
color: @text-color-secondary;
font-size: 13px;
word-break: break-all;
background: rgba(4, 3, 3, 0.06);
border-radius: 4px;
max-height: calc(100% - 61px);
}
}
}

+ 24
- 8
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -84,6 +84,7 @@ function ResourceSelectorModal({
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFileData[]>([]);
const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined);
const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState('');
const [firstLoadList, setFirstLoadList] = useState(false);
@@ -119,6 +120,7 @@ function ResourceSelectorModal({
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionDesc(undefined);
setVersionPath('');
setSearchText('');
getTreeData();
@@ -169,9 +171,11 @@ function ResourceSelectorModal({
if (res) {
setVersionPath(res.path);
setFiles(res.content);
setVersionDesc(res.versionDesc);
} else {
setVersionPath('');
setFiles([]);
setVersionDesc(undefined);
}
};

@@ -201,6 +205,7 @@ function ResourceSelectorModal({
} else {
setVersionPath('');
setFiles([]);
setVersionDesc(undefined);
}
};

@@ -253,8 +258,9 @@ function ResourceSelectorModal({

const title = `选择${config.name}`;
const palceholder = `请输入${config.name}名称`;
const fileLen = files.length > 0 ? `(${files.length})` : '';
const fileTitle =
type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`;
type === ResourceSelectorType.Mirror ? '镜像地址' : `${config.name}版本文件${fileLen}`;
const tabItems = config.tabItems;
const titleImg = config.modalIcon;

@@ -312,14 +318,24 @@ function ResourceSelectorModal({
/>
</div>
<div className={styles['model-selector__right']}>
<div className={styles['model-selector__right__title']}>{fileTitle}</div>
<div className={styles['model-selector__right__files']}>
{files.map((v) => (
<div key={v.url} className={styles['model-selector__right__files__file']}>
{v.file_name}
</div>
))}
<div style={{ height: '50%' }}>
<div className={styles['model-selector__right__title']}>{fileTitle}</div>
<div className={styles['model-selector__right__files']}>
{files.map((v) => (
<div key={v.url} className={styles['model-selector__right__files__file']}>
{v.file_name}
</div>
))}
</div>
</div>
{versionDesc && (
<div style={{ height: '50%' }}>
<div
className={styles['model-selector__right__title']}
>{`${config.name}版本描述`}</div>
<div className={styles['model-selector__right__desc']}>{versionDesc}</div>
</div>
)}
</div>
</div>
</div>


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

@@ -1,14 +1,16 @@
import { clearSessionToken } from '@/access';
import DefaultAvatar from '@/assets/img/avatar-default.png';
import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types';
import { sleep } from '@/utils/promise';
import { sleep, to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage, oauthLogout } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { history, useModel } from '@umijs/max';
import { useModel, useNavigate } from '@umijs/max';
import { Avatar, Spin } from 'antd';
import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react';
@@ -55,24 +57,37 @@ const AvatarLogo = () => {
},
};
});
return <Avatar size="small" className={avatarClassName} src={currentUser?.avatar} alt="avatar" />;
return (
<Avatar
size="small"
className={avatarClassName}
src={currentUser?.avatar || DefaultAvatar}
alt="avatar"
/>
);
};

const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
const navigate = useNavigate();
/**
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
oauthLogout('http://172.20.32.197:31209/oauth/logout');
const [res] = await to(getLabelStudioUrl());
if (res && res.data) {
oauthLogout(`${res.data}/oauth/logout`);
}
// 至少 1 秒后跳转,希望子系统能完成注销
await Promise.all([logout(), sleep(1000)]);
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage();
// 退出 oauth2
const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
if (clientInfo) {
const { logoutUri } = clientInfo;
location.replace(logoutUri);
} else {
gotoLoginPage();
}
};
const actionClassName = useEmotionCss(({ token }) => {
@@ -102,9 +117,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
loginOut();
return;
}
history.push(`/account/${key}`);
navigate(`/account/${key}`);
},
[setInitialState],
[setInitialState, navigate],
);

const loading = (


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

@@ -10,13 +10,21 @@ type RunDurationProps = {
};
function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) {
const [now] = useServerTime();
const [currentTime, setCurrentTime] = useState<Date>(now());
const [currentTime, setCurrentTime] = useState<Date>(finishTime ? new Date(finishTime) : now());

// console.log(
// 'currentTime',
// new Date(createTime ?? 0),
// currentTime,
// (currentTime.getTime() - new Date(createTime ?? 0).getTime()) / 1000,
// );

// 定时刷新耗时
useEffect(() => {
if (finishTime) {
setCurrentTime(new Date(finishTime));
} else {
setCurrentTime(now());
const timer = setInterval(() => {
setCurrentTime(now());
}, 1000);
@@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr
};
}
}, [finishTime, now]);

return (
<span className={className} style={style}>
{elapsedTime(createTime, currentTime)}


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

@@ -33,7 +33,7 @@ export enum TensorBoardStatus {
Unknown = 'Unknown', // 未知
Pending = 'Pending', // 启动中
Running = 'Running', // 运行中
Terminated = 'Terminated', // 未启动或者已终止
Terminated = 'Terminated', // 未启动
Failed = 'Failed', // 失败
}

@@ -95,8 +95,8 @@ export enum AutoMLType {

export const autoMLTypeOptions = [
{ label: '表格', value: AutoMLType.Table },
{ label: '文本分类', value: AutoMLType.Text },
{ label: '视频分类', value: AutoMLType.Video },
{ label: '文本', value: AutoMLType.Text },
{ label: '视频', value: AutoMLType.Video },
];

// 自动化任务类型


+ 22
- 10
react-ui/src/hooks/useSSE.ts View File

@@ -1,11 +1,24 @@
import { parseJsonText } from '@/utils';
import { useEffect } from 'react';
import { ExperimentStatus } from '@/enums';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { useEffect } from 'react';

export type MessageHandler = (experimentInsId: number, status: string, finishedAt: string, nodes: Record<string, NodeStatus>) => void
export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: string, namespace: string, onMessage: MessageHandler) => {
const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running
export type MessageHandler = (
experimentId: number,
experimentInsId: number,
status: string,
finishTime: string,
nodes: Record<string, NodeStatus>,
) => void;
export const useSSE = (
experimentId: number,
experimentInsId: number,
status: ExperimentStatus,
name: string,
namespace: string,
onMessage: MessageHandler,
) => {
const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running;
useEffect(() => {
if (isRunning) {
const { origin } = location;
@@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name:
const dataJson = parseJsonText(data);
const statusData = dataJson?.result?.object?.status;
if (statusData) {
const { finishedAt, phase, nodes } = statusData;
onMessage(experimentInsId, phase, finishedAt, nodes);
const { finishedAt, phase, nodes } = statusData;
onMessage(experimentId, experimentInsId, phase, finishedAt, nodes);
}
};

@@ -33,8 +46,7 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name:

return () => {
evtSource.close();
}
};
}
}, [experimentInsId, isRunning, name, namespace, onMessage]);
}, [experimentId, experimentInsId, isRunning, name, namespace, onMessage]);
};

+ 8
- 10
react-ui/src/pages/ActiveLearn/Instance/index.tsx View File

@@ -51,13 +51,12 @@ function ActiveLearnInstance() {
const [res] = await to(getActiveLearnInsReq(instanceId));
if (res && res.data) {
const info = res.data as ActiveLearnInstanceData;
const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info;
const { param, node_status, argo_ins_name, argo_ins_ns, status } = info;
// 解析配置参数
const paramJson = parseJsonText(param);
if (paramJson) {
setExperimentInfo({
...paramJson.data,
create_time,
});
}

@@ -69,7 +68,7 @@ function ActiveLearnInstance() {
return;
}

// 进行节点状态
// 设置总 workflow 状态
const nodeStatusJson = parseJsonText(node_status);
if (nodeStatusJson) {
setNodes(nodeStatusJson);
@@ -106,18 +105,17 @@ function ActiveLearnInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
// 设置节点
setNodes(nodes);

// 设置总 workflow 状态
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 设置工作流状态
if (workflowStatus) {
setWorkflowStatus(workflowStatus);

// 实验结束,关闭 SSE
// 实验结束,关闭 SSE,获取实验实例结果
if (
workflowStatus.phase !== ExperimentStatus.Pending &&
workflowStatus.phase !== ExperimentStatus.Running
@@ -152,8 +150,8 @@ function ActiveLearnInstance() {
<ActiveLearnBasic
className={styles['active-learn-instance__basic']}
info={experimentInfo}
runStatus={workflowStatus}
instanceStatus={instanceInfo?.status}
workflowStatus={workflowStatus}
instanceStatus={instanceInfo?.status as ExperimentStatus}
isInstance
/>
),
@@ -181,7 +179,7 @@ function ActiveLearnInstance() {
},
{
key: TabKeys.History,
label: '训练列表',
label: '运行列表',
icon: <KFIcon type="icon-Trialliebiao" />,
children: (
<ExperimentHistory


+ 5
- 9
react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx View File

@@ -28,14 +28,14 @@ type BasicInfoProps = {
info?: ActiveLearnData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus;
};

function BasicInfo({
info,
className,
runStatus,
workflowStatus,
instanceStatus,
isInstance = false,
}: BasicInfoProps) {
@@ -154,7 +154,7 @@ function BasicInfo({
value: info.dataset_py,
},
{
label: '数据集类名',
label: '数据集处理类名',
value: info.dataset_class_name,
},
{
@@ -212,12 +212,8 @@ function BasicInfo({

return (
<div className={classNames(styles['active-learn-basic'], className)}>
{isInstance && runStatus && (
<ExperimentRunBasic
create_time={info?.create_time}
runStatus={runStatus}
instanceStatus={instanceStatus}
/>
{isInstance && workflowStatus && (
<ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} />
)}
{!isInstance && (
<ConfigInfo


+ 14
- 4
react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx View File

@@ -17,6 +17,11 @@ import {

function ExecuteConfig() {
const form = Form.useFormInstance();
const task_type = Form.useWatch('task_type', form);
const queryStrategiesOptions =
task_type === AutoMLTaskType.Classification
? queryStrategies.slice(0, 2)
: queryStrategies.slice(2);
return (
<>
<SubAreaTitle
@@ -101,16 +106,16 @@ function ExecuteConfig() {
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集类名"
label="数据集处理类名"
name="dataset_class_name"
rules={[
{
required: true,
message: '请输入数据集类名',
message: '请输入数据集处理类名',
},
]}
>
<Input placeholder="请输入数据集类名" maxLength={64} showCount allowClear />
<Input placeholder="请输入数据集处理类名" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
@@ -488,7 +493,12 @@ function ExecuteConfig() {
},
]}
>
<Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear />
<Select
placeholder="请选择查询策略"
options={queryStrategiesOptions}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>


+ 4
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts View File

@@ -87,4 +87,8 @@ export const queryStrategies = [
label: 'upper_confidence_bound',
value: 'upper_confidence_bound',
},
{
label: 'probability_of_improvement',
value: 'probability_of_improvement',
},
];

+ 7
- 2
react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx View File

@@ -1,5 +1,6 @@
import { ExperimentStatus } from '@/enums';
import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types';
import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty';
import LogList from '@/pages/Experiment/components/LogList';
import { NodeStatus } from '@/types';
import { Tabs } from 'antd';
@@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
{trainCloneNodeStatus ? (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
@@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList>
) : (
<EmptyLog />
)}
</div>
),
@@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
{hpoNodeStatus ? (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
@@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList>
) : (
<EmptyLog />
)}
</div>
),


+ 1
- 1
react-ui/src/pages/Authorize/index.tsx View File

@@ -36,7 +36,7 @@ function Authorize() {
setSessionToken(access_token, access_token, expires_in);
message.success('登录成功!');
await fetchUserInfo();
history.push(redirect || '/');
history.replace(redirect || '/');
}
}, [fetchUserInfo, redirect, code]);



+ 11
- 9
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -51,7 +51,7 @@ function AutoMLInstance() {
const [res] = await to(getExperimentInsReq(instanceId));
if (res && res.data) {
const info = res.data as AutoMLInstanceData;
const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time, type } = info;
const { param, node_status, argo_ins_name, argo_ins_ns, status, type } = info;

setType(type);
// 解析配置参数
@@ -59,7 +59,6 @@ function AutoMLInstance() {
if (paramJson) {
setAutoMLInfo({
...paramJson.data,
create_time,
type,
});
}
@@ -95,7 +94,10 @@ function AutoMLInstance() {
};

const setupSSE = (name: string, namespace: string) => {
const { origin } = location;
let { origin } = location;
if (process.env.NODE_ENV === 'development') {
origin = 'http://172.20.32.235:31213';
}
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
@@ -110,17 +112,17 @@ function AutoMLInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
// 设置节点
setNodes(nodes);

// 设置总 workflow 状态
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

if (workflowStatus) {
setWorkflowStatus(workflowStatus);

// 实验结束,关闭 SSE
// 实验结束,关闭 SSE,获取实验实例结果
if (
workflowStatus.phase !== ExperimentStatus.Pending &&
workflowStatus.phase !== ExperimentStatus.Running
@@ -155,8 +157,8 @@ function AutoMLInstance() {
<AutoMLBasic
className={styles['auto-ml-instance__basic']}
info={autoMLInfo}
runStatus={workflowStatus}
instanceStatus={instanceInfo?.status}
workflowStatus={workflowStatus}
instanceStatus={instanceInfo?.status as ExperimentStatus}
isInstance
/>
),
@@ -202,7 +204,7 @@ function AutoMLInstance() {
}
: {
key: TabKeys.History,
label: '试验列表',
label: '运行列表',
icon: <KFIcon type="icon-Trialliebiao" />,
children: (
<ExperimentHistory


+ 1
- 1
react-ui/src/pages/AutoML/List/index.tsx View File

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 自机器学习列表
* @Description: 自机器学习列表
*/

import ExperimentList, { ExperimentListType } from '../components/ExperimentList';


+ 44
- 9
react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx View File

@@ -7,10 +7,21 @@ import {
autoMLTaskTypeOptions,
} from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import {
classificationAlgorithms,
featureAlgorithms,
regressorAlgorithms,
} from '@/pages/AutoML/components/CreateForm/utils';
import { AutoMLData } from '@/pages/AutoML/types';
import { type NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format';
import {
formatBoolean,
formatDataset,
formatDate,
formatEnum,
type EnumOptions,
} from '@/utils/format';
import classNames from 'classnames';
import { useMemo } from 'react';
import ExperimentRunBasic from '../ExperimentRunBasic';
@@ -21,6 +32,7 @@ const formatOptimizeMode = (value: boolean) => {
return value ? '越大越好' : '越小越好';
};

// 格式化权重
const formatMetricsWeight = (value: string) => {
if (!value) {
return '--';
@@ -34,18 +46,33 @@ const formatMetricsWeight = (value: string) => {
.join('\n');
};

// 格式化算法
const formatAlgorithm = (algorithms: EnumOptions[]) => {
return (value: string) => {
if (!value) {
return '--';
}
const list = value
.split(',')
.filter((v) => v !== '')
.map((v) => v.trim());
return list.map((v) => formatEnum(algorithms)(v)).join(',');
};
};

type AutoMLBasicProps = {
info?: AutoMLData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus;
instanceCreateTime?: string;
};

function AutoMLBasic({
info,
className,
runStatus,
workflowStatus,
instanceStatus,
isInstance = false,
}: AutoMLBasicProps) {
@@ -95,10 +122,12 @@ function AutoMLBasic({
{
label: '特征预处理算法',
value: info.include_feature_preprocessor,
format: formatAlgorithm(featureAlgorithms),
},
{
label: '排除的特征预处理算法',
value: info.exclude_feature_preprocessor,
format: formatAlgorithm(featureAlgorithms),
},
{
label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法',
@@ -106,6 +135,11 @@ function AutoMLBasic({
info.task_type === AutoMLTaskType.Regression
? info.include_regressor
: info.include_classifier,
format: formatAlgorithm(
info.task_type === AutoMLTaskType.Regression
? regressorAlgorithms
: classificationAlgorithms,
),
},
{
label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法',
@@ -113,6 +147,11 @@ function AutoMLBasic({
info.task_type === AutoMLTaskType.Regression
? info.exclude_regressor
: info.exclude_classifier,
format: formatAlgorithm(
info.task_type === AutoMLTaskType.Regression
? regressorAlgorithms
: classificationAlgorithms,
),
},
{
label: '集成方式',
@@ -292,12 +331,8 @@ function AutoMLBasic({

return (
<div className={classNames(styles['auto-ml-basic'], className)}>
{isInstance && runStatus && (
<ExperimentRunBasic
create_time={info?.create_time}
runStatus={runStatus}
instanceStatus={instanceStatus}
/>
{isInstance && workflowStatus && (
<ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} />
)}
{!isInstance && (
<ConfigInfo


+ 6
- 67
react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx View File

@@ -8,69 +8,7 @@ import {
autoMLTaskTypeOptions,
} from '@/enums';
import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd';

// 分类算法
const classificationAlgorithms = [
'adaboost',
'bernoulli_nb',
'decision_tree',
'extra_trees',
'gaussian_nb',
'gradient_boosting',
'k_nearest_neighbors',
'lda',
'liblinear_svc',
'libsvm_svc',
'mlp',
'multinomial_nb',
'passive_aggressive',
'qda',
'random_forest',
'sgd',
'LightGBMClassification',
'XGBoostClassification',
'StackingClassification',
].map((name) => ({ label: name, value: name }));

// 回归算法
const regressorAlgorithms = [
'adaboost',
'ard_regression',
'decision_tree',
'extra_trees',
'gaussian_process',
'gradient_boosting',
'k_nearest_neighbors',
'liblinear_svr',
'libsvm_svr',
'mlp',
'random_forest',
'sgd',
'LightGBMRegression',
'XGBoostRegression',
].map((name) => ({ label: name, value: name }));

// 特征预处理算法
const featureAlgorithms = [
'densifier',
'extra_trees_preproc_for_classification',
'extra_trees_preproc_for_regression',
'fast_ica',
'feature_agglomeration',
'kernel_pca',
'kitchen_sinks',
'liblinear_svc_preprocessor',
'no_preprocessing',
'nystroem_sampler',
'pca',
'polynomial',
'random_trees_embedding',
'select_percentile_classification',
'select_percentile_regression',
'select_rates_classification',
'select_rates_regression',
'truncatedSVD',
].map((name) => ({ label: name, value: name }));
import { classificationAlgorithms, featureAlgorithms, regressorAlgorithms } from './utils';

// 分类指标
export const classificationMetrics = [
@@ -280,9 +218,9 @@ function ExecuteConfig() {
<Form.Item
label="集成模型数量"
name="ensemble_size"
tooltip="集成模型数量,如果设置为0,则没有集成。默认50"
tooltip="集成模型数量,必须是大于等于1的整数,默认50"
>
<InputNumber placeholder="请输入集成模型数量" min={0} precision={0} />
<InputNumber placeholder="请输入集成模型数量" min={1} precision={0} />
</Form.Item>
</Col>
</Row>
@@ -292,7 +230,7 @@ function ExecuteConfig() {
<Form.Item
label="集成最佳模型数量"
name="ensemble_nbest"
tooltip="仅集成最佳的N个模型"
tooltip="仅集成最佳的N个模型,必须是大于等于1的整数"
>
<InputNumber placeholder="请输入集成最佳模型数量" min={1} precision={0} />
</Form.Item>
@@ -419,6 +357,7 @@ function ExecuteConfig() {
<Form.Item
label="交叉验证折数"
name="folds"
tooltip="交叉验证折数必须是大于等于2的整数"
rules={[
{
required: true,
@@ -426,7 +365,7 @@ function ExecuteConfig() {
},
]}
>
<InputNumber placeholder="请输入交叉验证折数" min={1} precision={0} />
<InputNumber placeholder="请输入交叉验证折数" min={2} precision={0} />
</Form.Item>
</Col>
</Row>


+ 78
- 0
react-ui/src/pages/AutoML/components/CreateForm/utils.ts View File

@@ -0,0 +1,78 @@
// 分类算法
export const classificationAlgorithms = [
{ label: 'adaboost (自适应提升算法)', value: 'adaboost' },
{ label: 'bernoulli_nb (伯努利朴素贝叶斯)', value: 'bernoulli_nb' },
{ label: 'decision_tree (决策树)', value: 'decision_tree' },
{ label: 'extra_trees (极端随机树)', value: 'extra_trees' },
{ label: 'gaussian_nb (高斯朴素贝叶斯)', value: 'gaussian_nb' },
{ label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' },
{ label: 'k_nearest_neighbors (k近邻)', value: 'k_nearest_neighbors' },
{ label: 'lda (线性判别分析)', value: 'lda' },
{ label: 'liblinear_svc (liblinear支持向量分类)', value: 'liblinear_svc' },
{ label: 'libsvm_svc (libsvm支持向量分类)', value: 'libsvm_svc' },
{ label: 'mlp (多层感知器)', value: 'mlp' },
{ label: 'multinomial_nb (多项式朴素贝叶斯)', value: 'multinomial_nb' },
{ label: 'passive_aggressive (被动攻击算法)', value: 'passive_aggressive' },
{ label: 'qda (二次判别式分析)', value: 'qda' },
{ label: 'random_forest (随机森林)', value: 'random_forest' },
{ label: 'sgd (随机梯度下降)', value: 'sgd' },
{ label: 'tablenet (表格网络)', value: 'tablenet' },
{ label: 'LightGBMClassification (轻量梯度提升机分类)', value: 'LightGBMClassification' },
{ label: 'XGBoostClassification (极端梯度提升机分类)', value: 'XGBoostClassification' },
{ label: 'StackingClassification (堆叠泛化)', value: 'StackingClassification' },
];

// 回归算法
export const regressorAlgorithms = [
{ label: 'adaboost (自适应提升算法)', value: 'adaboost' },
{ label: 'ard_regression (自动相关性确定回归)', value: 'ard_regression' },
{ label: 'decision_tree (决策树)', value: 'decision_tree' },
{ label: 'extra_trees (极端随机树)', value: 'extra_trees' },
{ label: 'gaussian_process (高斯过程回归)', value: 'gaussian_process' },
{ label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' },
{ label: 'k_nearest_neighbors (梯度提升)', value: 'k_nearest_neighbors' },
{ label: 'liblinear_svr (liblinear支持向量回归)', value: 'liblinear_svr' },
{ label: 'libsvm_svr (libsvm支持向量回归)', value: 'libsvm_svr' },
{ label: 'mlp (多层感知器)', value: 'mlp' },
{ label: 'random_forest (随机森林)', value: 'random_forest' },
{ label: 'sgd (随机梯度下降)', value: 'sgd' },
{ label: 'LightGBMRegression (轻量梯度提升机回归)', value: 'LightGBMRegression' },
{ label: 'XGBoostRegression (极端梯度提升机回归)', value: 'XGBoostRegression' },
];

// 特征预处理算法
export const featureAlgorithms = [
{ label: 'densifier (数据增稠)', value: 'densifier' },
{
label: 'extra_trees_preproc_for_classification (分类任务极端随机树)',
value: 'extra_trees_preproc_for_classification',
},
{
label: 'extra_trees_preproc_for_regression (回归任务极端随机树)',
value: 'extra_trees_preproc_for_regression',
},
{ label: 'fast_ica (快速独立成分分析)', value: 'fast_ica' },
{ label: 'feature_agglomeration (特征聚合)', value: 'feature_agglomeration' },
{ label: 'kernel_pca (核主成分分析)', value: 'kernel_pca' },
{ label: 'kitchen_sinks (随机特征映射)', value: 'kitchen_sinks' },
{ label: 'liblinear_svc_preprocessor (线性svc预处理器)', value: 'liblinear_svc_preprocessor' },
{ label: 'no_preprocessing (无预处理)', value: 'no_preprocessing' },
{ label: 'nystroem_sampler (尼斯特罗姆采样器)', value: 'nystroem_sampler' },
{ label: 'pca (主成分分析)', value: 'pca' },
{ label: 'polynomial (多项式特征扩展)', value: 'polynomial' },
{ label: 'random_trees_embedding (随机森林特征嵌入)', value: 'random_trees_embedding' },
{
label: 'select_percentile_classification (基于百分位的分类特征选择)',
value: 'select_percentile_classification',
},
{
label: 'select_percentile_regression (基于百分位的回归特征选择)',
value: 'select_percentile_regression',
},
{
label: 'select_rates_classification (基于比率的分类特征选择)',
value: 'select_rates_classification',
},
{ label: 'select_rates_regression (基于比率的回归特征选择)', value: 'select_rates_regression' },
{ label: 'truncatedSVD (截断奇异值分解)', value: 'truncatedSVD' },
];

+ 0
- 4
react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.less View File

@@ -54,10 +54,6 @@
display: flex;
align-items: center;
width: 200px;

.statusIcon {
visibility: visible;
}
}
}



+ 16
- 8
react-ui/src/pages/AutoML/components/ExperimentInstanceList/index.tsx View File

@@ -34,7 +34,14 @@ function ExperimentInstanceList({
}: ExperimentInstanceListProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInsList?.map((item) => item.id) || [];
return (
experimentInsList
?.filter(
(item) =>
item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending,
)
.map((item) => item.id) || []
);
}, [experimentInsList]);
const [
selectedIns,
@@ -126,7 +133,12 @@ function ExperimentInstanceList({
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}>
<Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox>
<Checkbox
checked={checked}
indeterminate={indeterminate}
disabled={allIntanceIds.length === 0}
onChange={checkAll}
></Checkbox>
</div>
<div className={styles.index}>序号</div>
<div className={styles.description}>运行时长</div>
@@ -171,12 +183,8 @@ function ExperimentInstanceList({
{index + 1}
</a>
<ExperimentInstanceComponent
create_time={item.create_time}
finish_time={item.finish_time}
status={item.status as ExperimentStatus}
argo_ins_name={item.argo_ins_name}
argo_ins_ns={item.argo_ins_ns}
experimentInsId={item.id}
experimentId={item[config['idInsProperty'] as keyof ExperimentInstance] as number}
instance={item}
></ExperimentInstanceComponent>
<div className={styles.operation}>
<Button


+ 35
- 32
react-ui/src/pages/AutoML/components/ExperimentInstanceList/instance.tsx View File

@@ -2,67 +2,70 @@ import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums';
import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance, NodeStatus } from '@/types';
import { ExperimentCompleted } from '@/utils/constant';
import { formatDate } from '@/utils/date';
import { getWorkflowStatus } from '@/utils/experiment';
import { Typography } from 'antd';
import React, { useCallback } from 'react';
import styles from './index.less';

type ExperimentInstanceProps = {
create_time?: string;
finish_time?: string;
status: ExperimentStatus;
argo_ins_name: string;
argo_ins_ns: string;
experimentInsId: number;
type ExperimentInstanceComponentProps = {
experimentId: number;
instance: ExperimentInstance;
};

function ExperimentInstance({
create_time,
finish_time,
status,
argo_ins_name,
argo_ins_ns,
experimentInsId,
}: ExperimentInstanceProps) {
function ExperimentInstanceComponent({ experimentId, instance }: ExperimentInstanceComponentProps) {
const { id, argo_ins_name, argo_ins_ns, node_status } = instance;
const workflowStatus = getWorkflowStatus(node_status) as NodeStatus | undefined;
const status = instance.status as ExperimentStatus;
const createTime = workflowStatus?.startedAt;
const finishTime = workflowStatus?.finishedAt;
const statusInfo = experimentStatusInfo[status];
const handleSSEMessage: MessageHandler = useCallback(
(experimentInsId: number, status: string, finish_time: string) => {
(experimentId: number, experimentInsId: number, status: string, finishTime: string) => {
window.postMessage({
type: ExperimentCompleted,
payload: {
id: experimentInsId,
experimentId,
experimentInsId,
status,
finish_time,
finishTime,
},
});
},
[],
);
useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage);
useSSE(experimentId, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage);

return (
<React.Fragment>
<div className={styles.description}>
<RunDuration createTime={create_time} finishTime={finish_time} />
<RunDuration createTime={createTime} finishTime={finishTime} />
</div>
<div className={styles.startTime}>
<Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}>
{formatDate(create_time)}
<Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}>
{formatDate(createTime)}
</Typography.Text>
</div>
<div className={styles.statusBox}>
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[status]?.icon}
draggable={false}
alt=""
/>
<span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}>
{experimentStatusInfo[status]?.label}
</span>
{statusInfo ? (
<>
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo.icon}
draggable={false}
alt=""
/>
<span style={{ color: statusInfo.color }}>{statusInfo.label}</span>
</>
) : (
'--'
)}
</div>
</React.Fragment>
);
}

export default ExperimentInstance;
export default ExperimentInstanceComponent;

+ 14
- 3
react-ui/src/pages/AutoML/components/ExperimentList/config.ts View File

@@ -8,6 +8,7 @@ import {
batchDeleteActiveLearnInsReq,
deleteActiveLearnInsReq,
deleteActiveLearnReq,
editActiveLearnInsReq,
getActiveLearnInsListReq,
getActiveLearnListReq,
runActiveLearnReq,
@@ -17,6 +18,7 @@ import {
batchDeleteExperimentInsReq,
deleteAutoMLReq,
deleteExperimentInsReq,
editExperimentInsReq,
getAutoMLListReq,
getExperimentInsListReq,
runAutoMLReq,
@@ -26,6 +28,7 @@ import {
batchDeleteRayInsReq,
deleteRayInsReq,
deleteRayReq,
editRayInsReq,
getRayInsListReq,
getRayListReq,
runRayReq,
@@ -39,18 +42,20 @@ export enum ExperimentListType {
}

type ExperimentListInfo = {
getListReq: (params: any) => Promise<any>; // 获取列表
getInsListReq: (params: any) => Promise<any>; // 获取实例列表
getListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取列表
getInsListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取实例列表
deleteRecordReq: (params: any) => Promise<any>; // 删除
runRecordReq: (params: any) => Promise<any>; // 运行
deleteInsReq: (params: any) => Promise<any>; // 删除实例
batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例
stopInsReq: (params: any) => Promise<any>; // 终止实例
editInsReq: (params: any) => Promise<any>; // 编辑实例
title: string; // 标题
pathPrefix: string; // 路由路径前缀
idProperty: string; // ID属性
nameProperty: string; // 名称属性
descProperty: string; // 描述属性
idInsProperty: string; // 实例返回的ID属性
};

export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = {
@@ -62,11 +67,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo
deleteInsReq: deleteExperimentInsReq,
batchDeleteInsReq: batchDeleteExperimentInsReq,
stopInsReq: stopExperimentInsReq,
title: '自主机器学习',
editInsReq: editExperimentInsReq,
title: '自动机器学习',
pathPrefix: 'automl',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'machineLearnId',
idInsProperty: 'machine_learn_id',
},
[ExperimentListType.HyperParameter]: {
getListReq: getRayListReq,
@@ -76,11 +83,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo
deleteInsReq: deleteRayInsReq,
batchDeleteInsReq: batchDeleteRayInsReq,
stopInsReq: stopRayInsReq,
editInsReq: editRayInsReq,
title: '超参数自动寻优',
pathPrefix: 'hyperparameter',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'rayId',
idInsProperty: 'ray_id',
},
[ExperimentListType.ActiveLearn]: {
getListReq: getActiveLearnListReq,
@@ -90,10 +99,12 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo
deleteInsReq: deleteActiveLearnInsReq,
batchDeleteInsReq: batchDeleteActiveLearnInsReq,
stopInsReq: stopActiveLearnInsReq,
editInsReq: editActiveLearnInsReq,
title: '自动学习',
pathPrefix: 'active-learn',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'activeLearnId',
idInsProperty: 'active_learn_id',
},
};

+ 126
- 61
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -30,7 +30,7 @@ import {
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import ExperimentInstanceList from '../ExperimentInstanceList';
import { ExperimentListType, experimentListConfig } from './config';
import styles from './index.less';
@@ -52,7 +52,6 @@ function ExperimentList({ type }: ExperimentListProps) {
const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const [now] = useServerTime();
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
@@ -60,38 +59,37 @@ function ExperimentList({ type }: ExperimentListProps) {
},
);
const config = experimentListConfig[type];
const timerRef = useRef<ReturnType<typeof window.setTimeout> | undefined>();

// 获取自主机器学习或超参数自动优化列表
const getAutoMLList = useCallback(async () => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
[config.nameProperty]: searchText || undefined,
};
const request = config.getListReq;
const [res] = await to(request(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
}, [pagination, searchText, config]);
const [now] = useServerTime();

useEffect(() => {
getAutoMLList();
}, [getAutoMLList]);
// 获取实验列表
const getExperimentList = useCallback(
async (skipLoading: boolean = false) => {
const params: Record<string, any> = {
page: pagination.current! - 1,
size: pagination.pageSize,
[config.nameProperty]: searchText || undefined,
};
const request = config.getListReq;
const [res] = await to(request(params, skipLoading));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
},
[pagination, searchText, config],
);

// 获取实验实例列表
const getExperimentInsList = useCallback(
async (recordId: number, page: number, size: number) => {
async (recordId: number, page: number, size: number, skipLoading: boolean = false) => {
const params = {
[config.idProperty]: recordId,
page: page,
size: size,
};
const request = config.getInsListReq;
const [res] = await to(request(params));
const [res] = await to(request(params, skipLoading));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
try {
@@ -111,59 +109,115 @@ function ExperimentList({ type }: ExperimentListProps) {

// 刷新实验列表状态,
// TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = useCallback(() => {
getAutoMLList();
}, [getAutoMLList]);
const refreshExperimentList = useCallback(
(skipLoading: boolean = false) => {
getExperimentList(skipLoading);
},
[getExperimentList],
);

// 刷新实验实例列表
const refreshExperimentIns = useCallback(
(experimentId: number) => {
(experimentId: number, skipLoading: boolean = false) => {
const length = experimentInsList.length;
getExperimentInsList(experimentId, 0, length);
getExperimentInsList(experimentId, 0, length, skipLoading);
},
[getExperimentInsList, experimentInsList],
);

// 新增,删除版本时,重置分页,然后刷新版本列表
// 更新实验实例状态
const editExperimentIns = useCallback(
async (
experimentId: number,
experimentInsId: number,
status: ExperimentStatus,
argo_ins_name: string,
argo_ins_ns: string,
) => {
const params = {
[config.idInsProperty]: experimentId,
id: experimentInsId,
status: status,
argo_ins_name,
argo_ins_ns,
};
const request = config.editInsReq;
const [res] = await to(request(params));
if (res && res.data) {
refreshExperimentIns(experimentId, true);
refreshExperimentList(true);
}
},
[config, refreshExperimentIns, refreshExperimentList],
);

// 获取实验列表
useEffect(() => {
getExperimentList();
}, [getExperimentList]);

// expandedRowKeys 变化
useEffect(() => {
if (expandedRowKeys.length > 0) {
getExperimentInsList(expandedRowKeys[0], 0, 5);
refreshExperimentList();
}
}, [expandedRowKeys, getExperimentInsList, refreshExperimentList]);

// 实验实例状态变化
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data;
if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;

// 修改实例的状态和结束时间
setExperimentInsList((prev) =>
prev.map((v) =>
v.id === id
? {
...v,
status: status,
finish_time: finish_time,
}
: v,
),
const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);

if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
if (
!currentIns ||
currentIns.status === ExperimentStatus.Terminated ||
currentIns.status === status
) {
return;
}

timerRef.current = setTimeout(() => {
refreshExperimentList();
}, 10000);
// refreshExperimentList(true);
// refreshExperimentIns(experimentId);
editExperimentIns(
experimentId,
experimentInsId,
status,
currentIns.argo_ins_name,
currentIns.argo_ins_ns,
);

// 修改实例的状态和结束时间
// setExperimentInsList((prev) =>
// prev.map((v) =>
// v.id === experimentInsId
// ? {
// ...v,
// status: status,
// finish_time: finishTime,
// }
// : v,
// ),
// );
}
};

window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}
};
}, [refreshExperimentList]);
}, [experimentInsList, editExperimentIns]);

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
@@ -207,6 +261,7 @@ function ExperimentList({ type }: ExperimentListProps) {
setCacheState({
pagination,
searchText,
expandedRowKeys,
});

if (record) {
@@ -225,6 +280,7 @@ function ExperimentList({ type }: ExperimentListProps) {
setCacheState({
pagination,
searchText,
expandedRowKeys,
});

navigate(`info/${record.id}`);
@@ -237,8 +293,8 @@ function ExperimentList({ type }: ExperimentListProps) {
if (res) {
message.success('运行成功');
setExpandedRowKeys([record.id]);
refreshExperimentList();
getExperimentInsList(record.id, 0, 5);
// getExperimentInsList(record.id, 0, 5);
// refreshExperimentList();
}
};

@@ -248,8 +304,8 @@ function ExperimentList({ type }: ExperimentListProps) {
setExperimentInsList([]);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0, 5);
refreshExperimentList();
// getExperimentInsList(record.id, 0, 5);
// refreshExperimentList();
} else {
setExpandedRowKeys([]);
}
@@ -257,6 +313,11 @@ function ExperimentList({ type }: ExperimentListProps) {

// 跳转到实验实例详情
const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => {
setCacheState({
pagination,
searchText,
expandedRowKeys,
});
navigate(`instance/${autoML.id}/${record.id}`);
};

@@ -269,8 +330,7 @@ function ExperimentList({ type }: ExperimentListProps) {

// 实验实例终止
const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => {
// 刷新实验列表
refreshExperimentList();
// 修改实例的状态和结束时间
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIns.id) {
@@ -283,6 +343,11 @@ function ExperimentList({ type }: ExperimentListProps) {
return item;
});
});
// 刷新实验列表和实例列表
refreshExperimentList(true);
if (expandedRowKeys.length > 0) {
refreshExperimentIns(expandedRowKeys[0]);
}
};
// --------------------------- Table ---------------------------
// 分页切换
@@ -330,7 +395,7 @@ function ExperimentList({ type }: ExperimentListProps) {
},
...diffColumns,
{
title: '创建时间',
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
width: '20%',


+ 7
- 0
react-ui/src/pages/AutoML/components/ExperimentLog/empty.tsx View File

@@ -0,0 +1,7 @@
import styles from './index.less';

function EmptyLog() {
return <div className={styles['empty-log']}>暂无日志</div>;
}

export default EmptyLog;

+ 11
- 0
react-ui/src/pages/AutoML/components/ExperimentLog/index.less View File

@@ -5,3 +5,14 @@
height: 100%;
}
}

.empty-log {
height: 100%;
padding: 15px;
color: white;
font-size: 14px;
white-space: pre-line;
text-align: center;
word-break: break-all;
background: #19253b;
}

+ 4
- 1
react-ui/src/pages/AutoML/components/ExperimentLog/index.tsx View File

@@ -2,6 +2,7 @@ import { ExperimentStatus } from '@/enums';
import { AutoMLInstanceData } from '@/pages/AutoML/types';
import LogList from '@/pages/Experiment/components/LogList';
import { NodeStatus } from '@/types';
import EmptyLog from './empty';
import styles from './index.less';

const NodePrefix = 'auto-ml';
@@ -19,7 +20,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
return (
<div className={styles['experiment-log']}>
<div className={styles['experiment-log__log']}>
{nodeStatus && (
{nodeStatus ? (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
@@ -28,6 +29,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={nodeStatus.startedAt}
instanceNodeStatus={nodeStatus.phase as ExperimentStatus}
></LogList>
) : (
<EmptyLog />
)}
</div>
</div>


+ 18
- 16
react-ui/src/pages/AutoML/components/ExperimentRunBasic/index.tsx View File

@@ -3,59 +3,61 @@ import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { type NodeStatus } from '@/types';
import { getExperimentInstanceStatus } from '@/utils/experiment';
import { formatDate } from '@/utils/format';
import { Flex } from 'antd';
import { useMemo } from 'react';

type ExperimentRunBasicProps = {
create_time?: string;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus;
};

function ExperimentRunBasic({ create_time, runStatus, instanceStatus }: ExperimentRunBasicProps) {
function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) {
const instanceDatas = useMemo(() => {
if (!runStatus) {
return [];
}

const status =
instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase;
const status = getExperimentInstanceStatus(instanceStatus as ExperimentStatus, workflowStatus);
const statusInfo = experimentStatusInfo[status];

return [
{
label: '启动时间',
value: formatDate(create_time),
value: formatDate(workflowStatus?.startedAt),
},
{
label: '执行时长',
value: <RunDuration createTime={create_time} finishTime={runStatus.finishedAt} />,
value: (
<RunDuration
createTime={workflowStatus?.startedAt}
finishTime={workflowStatus?.finishedAt}
/>
),
},
{
label: '状态',
value: (
value: statusInfo ? (
<Flex align="center">
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo?.icon}
src={statusInfo.icon}
draggable={false}
alt=""
/>
<div
style={{
color: statusInfo?.color,
color: statusInfo.color,
fontSize: '15px',
lineHeight: 1.6,
}}
>
{statusInfo?.label}
{statusInfo.label}
</div>
</Flex>
) : (
'--'
),
},
];
}, [runStatus, create_time, instanceStatus]);
}, [workflowStatus, instanceStatus]);

return (
<ConfigInfo


+ 8
- 0
react-ui/src/pages/AutoML/components/ExperimentVisualResult/index.less View File

@@ -0,0 +1,8 @@
.experiment-visual {
width: 100%;
height: 100%;

&__empty {
height: 100%;
}
}

+ 69
- 17
react-ui/src/pages/AutoML/components/ExperimentVisualResult/index.tsx View File

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

import IframePage, { IframePageType } from '@/components/IFramePage';
import { runTensorBoardReq } from '@/services/experiment/index.js';
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFSpin from '@/components/KFSpin';
import { TensorBoardStatus } from '@/enums';
import { getTensorBoardStatusReq, runTensorBoardReq } from '@/services/experiment/index.js';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { useEffect, useState } from 'react';
import { LoadingOutlined } from '@ant-design/icons';
import { useCallback, useEffect, useState } from 'react';
import styles from './index.less';

type TensorBoardProps = {
namespace?: string;
@@ -16,28 +21,75 @@ type TensorBoardProps = {
};

function ExperimentVisualResult({ namespace, path }: TensorBoardProps) {
const [tensorboardUrl, setTensorboardUrl] = useState('');
useEffect(() => {
// 运行 TensorBoard
const runTensorBoard = async () => {
const params = {
namespace: namespace,
path: path,
};
const [res] = await to(runTensorBoardReq(params));
if (res && res.data) {
const url = res.data;
SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url);
setTensorboardUrl(url);
const [tensorboardUrl, setTensorboardUrl] = useState<string | undefined | null>(undefined);
const [status, setStatus] = useState<TensorBoardStatus | undefined>(undefined);

// 获取 TensorBoard 状态
const getTensorBoardStatus = useCallback(async () => {
const params = {
namespace: namespace,
path: path,
};
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
const status = res.data.status as TensorBoardStatus | undefined;
setStatus(res.data.status);
if (!status || status === TensorBoardStatus.Pending) {
setTimeout(() => {
getTensorBoardStatus();
}, 5000);
}
}
}, [namespace, path]);

// 运行 TensorBoard
const runTensorBoard = useCallback(async () => {
const params = {
namespace: namespace,
path: path,
};
const [res] = await to(runTensorBoardReq(params));
if (res && res.data) {
const url = res.data;
SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url);
setTensorboardUrl(url);
getTensorBoardStatus();
} else {
setTensorboardUrl(null);
}
}, [namespace, path, getTensorBoardStatus]);

useEffect(() => {
if (namespace && path) {
runTensorBoard();
}
}, [namespace, path]);
}, [namespace, path, runTensorBoard]);

return <>{tensorboardUrl && <IframePage type={IframePageType.TensorBoard}></IframePage>}</>;
if (tensorboardUrl === null || status === TensorBoardStatus.Failed) {
return (
<div className={styles['experiment-visual']}>
<KFEmpty
className={styles['experiment-visual__empty']}
type={EmptyType.NoData}
title="运行可视化失败"
buttonTitle="重新运行"
onButtonClick={runTensorBoard}
/>
</div>
);
} else if (status === TensorBoardStatus.Pending) {
return (
<div className={styles['experiment-visual']}>
<KFSpin indicator={<LoadingOutlined spin />} size="large" />
</div>
);
} else if (status === TensorBoardStatus.Running) {
return (
<div className={styles['experiment-visual']}>
<IframePage type={IframePageType.TensorBoard}></IframePage>
</div>
);
}
}

export default ExperimentVisualResult;

+ 1
- 0
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less View File

@@ -89,6 +89,7 @@
margin-bottom: 15px !important;
color: @text-color;
font-size: 14px;
word-break: break-all;
}

&__branch {


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

@@ -118,12 +118,14 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '数据集版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
return Promise.reject(`数据集版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`数据集版本不能为 origin`);
}
return Promise.resolve();
},


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

@@ -109,12 +109,14 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '模型版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
return Promise.reject(`模型版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`模型版本不能为 origin`);
}
return Promise.resolve();
},
@@ -126,7 +128,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Form.Item label="模型框架" name="model_type">
<Select
allowClear
placeholder="请选择模型类型"
placeholder="请选择模型框架"
options={typeList}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
@@ -136,7 +138,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Form.Item label="模型能力" name="model_tag">
<Select
allowClear
placeholder="请选择模型标签"
placeholder="请选择模型能力"
options={tagList}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"


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

@@ -132,12 +132,14 @@ function AddVersionModal({
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
return Promise.reject(`${name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${name}版本不能为 origin`);
}
return Promise.resolve();
},


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

@@ -42,6 +42,10 @@
border-radius: 4px;
cursor: pointer;

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

&--praised {
color: @primary-color;
}


+ 1
- 3
react-ui/src/pages/Dataset/components/ResourceItem/index.tsx View File

@@ -14,9 +14,7 @@ type ResourceItemProps = {
};

function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps) {
const timeAgo = `更新于${
item.update_time ? formatDate(item.update_time, 'YYYY-MM-DD') : item.time_ago ?? ''
}`;
const timeAgo = `最近更新:${formatDate(item.full_last_update_time, 'YYYY-MM-DD HH:mm')}`;
const create_by = item.create_by ?? '';
return (
<div className={styles['resource-item']} onClick={() => onClick(item)}>


+ 7
- 0
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -16,6 +16,7 @@ import styles from './index.less';

export type ResourceListRef = {
reset: () => void;
resetPage: () => void;
};

type ResourceListProps = {
@@ -97,6 +98,12 @@ function ResourceList(
setDataList(undefined);
setTotal(0);
},
resetPage: () => {
setPagination((prev) => ({
...prev,
current: 1,
}));
},
};
},
[],


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

@@ -56,11 +56,13 @@ function ResourcePage({ resourceType }: ResourcePageProps) {

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

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

@@ -96,7 +98,7 @@ function ResourcePage({ resourceType }: ResourcePageProps) {
dataType={activeType}
dataTag={activeTag}
initialSearchText={cacheState?.searchText}
initialPagination={cacheState?.initialPagination}
initialPagination={cacheState?.pagination}
setCacheState={setCacheState}
></ResourceList>
</Flex>


+ 1
- 0
react-ui/src/pages/Dataset/config.tsx View File

@@ -164,6 +164,7 @@ export interface ResourceData {
train_task?: TrainTask; // 训练任务
praises_count: number; // 点赞数
praised: boolean; // 是否点赞
full_last_update_time: string; // 完整的更新时间
}

// 数据集数据


+ 9
- 0
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -3,6 +3,7 @@
* @Date: 2024-04-16 13:58:08
* @Description: 创建开发环境
*/
import CodeSelect from '@/components/CodeSelect';
import KFIcon from '@/components/KFIcon';
import KFRadio, { type KFRadioItem } from '@/components/KFRadio';
import PageTitle from '@/components/PageTitle';
@@ -187,6 +188,14 @@ function EditorCreate() {
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item label="代码配置" name="code_config">
<CodeSelect placeholder="请选择代码配置" canInput={false} size="large" />
</Form.Item>
</Col>
</Row>

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit">
确定


+ 78
- 15
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -17,6 +17,12 @@ import {
} from '@/services/developmentEnvironment';
import themes from '@/styles/theme.less';
import { parseJsonText } from '@/utils';
import {
formatCodeConfig,
formatDataset,
formatModel,
type SelectedCodeConfig,
} from '@/utils/format';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
@@ -49,6 +55,7 @@ export type EditorData = {
dataset?: string | DatasetData;
model?: string | ModelData;
image?: string;
code_config?: string | SelectedCodeConfig;
};

function EditorList() {
@@ -78,6 +85,8 @@ function EditorList() {
item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null;
item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null;
item.image = typeof item.image === 'string' ? parseJsonText(item.image) : null;
item.code_config =
typeof item.code_config === 'string' ? parseJsonText(item.code_config) : null;
});
setTableData(content);
setTotal(totalElements);
@@ -159,13 +168,54 @@ function EditorList() {
};

// 跳转编辑器页面
const gotoEditorPage = (e: React.MouseEvent, record: EditorData) => {
const gotoEditorPage = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation();
SessionStorage.setItem(SessionStorage.editorUrlKey, record.url);
navigate(`/developmentEnvironment/editor`);

setCacheState({
pagination,
});

SessionStorage.setItem(SessionStorage.editorUrlKey, record.url);
navigate(`/developmentEnvironment/editor`);
};

// 去数据集
const gotoDataset = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation();

const dataset = record.dataset as DatasetData;
const link = formatDataset(dataset)?.link;
if (link) {
setCacheState({
pagination,
});
navigate(link);
}
};

// 去模型
const gotoModel = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation();

const model = record.model as ModelData;
const link = formatModel(model)?.link;
if (link) {
setCacheState({
pagination,
});
navigate(link);
}
};

// 打开代码配置仓库
const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation();

const codeConfig = record.code_config as SelectedCodeConfig;
const url = formatCodeConfig(codeConfig)?.url;
if (url) {
window.open(url, '_blank');
}
};

// 分页切换
@@ -185,11 +235,11 @@ function EditorList() {
title: '编辑器名称',
dataIndex: 'name',
key: 'name',
width: '16%',
width: '12%',
render: (text, record, index) =>
record.url && record.status === DevEditorStatus.Running
? tableCellRender<EditorData>(true, TableCellValueType.Link, {
onClick: (record, e) => gotoEditorPage(e, record),
onClick: gotoEditorPage,
})(text, record, index)
: tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index),
},
@@ -197,14 +247,14 @@ function EditorList() {
title: '计算资源',
dataIndex: 'computing_resource',
key: 'computing_resource',
width: '12%',
width: '11%',
render: tableCellRender(),
},
{
title: '资源规格',
dataIndex: 'computing_resource_id',
key: 'computing_resource_id',
width: '12%',
width: '11%',
render: tableCellRender(true, TableCellValueType.Custom, {
format: getResourceDescription,
}),
@@ -213,42 +263,55 @@ function EditorList() {
title: '数据集',
dataIndex: ['dataset', 'showValue'],
key: 'dataset',
width: '12%',
render: tableCellRender(true),
width: '11%',
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoDataset,
}),
},
{
title: '模型',
dataIndex: ['model', 'showValue'],
key: 'model',
width: '12%',
render: tableCellRender(true),
width: '11%',
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoModel,
}),
},
{
title: '代码配置',
dataIndex: ['code_config', 'showValue'],
key: 'code_config',
width: '11%',
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoCodeConfig,
}),
},
{
title: '镜像',
dataIndex: ['image', 'showValue'],
key: 'image',
width: '12%',
width: '11%',
render: tableCellRender(true),
},
{
title: '创建者',
dataIndex: 'update_by',
key: 'update_by',
width: '12%',
width: '11%',
render: tableCellRender(true),
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: '12%',
width: '11%',
render: tableCellRender(true, TableCellValueType.Date),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
width: 100,
render: EditorStatusCell,
},
{


+ 1
- 1
react-ui/src/pages/Docs/index.tsx View File

@@ -2,7 +2,7 @@ const Docs = () => {
return (
<iframe
style={{ width: '100%', height: '100%', border: 0 }}
src={'/assets/材料科研软件平台使用文档.pdf'}
src={'/assets/材料科研软件平台使用文档-v1.0.pdf'}
></iframe>
);
};


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

@@ -77,7 +77,7 @@ function ExperimentComparison() {
const url = res.data;
// window.open(url, '_blank');
SessionStorage.setItem(SessionStorage.aimUrlKey, url);
navigate('../compare-visual');
navigate('compare-visual');
}
};



+ 102
- 54
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -3,10 +3,10 @@ import { ExperimentStatus } from '@/enums';
import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible';
import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString, parseJsonText } from '@/utils';
import { formatDate } from '@/utils/date';
import { getExperimentInstanceStatus } from '@/utils/experiment';
import { to } from '@/utils/promise';
import G6, { Util } from '@antv/g6';
import { Button } from 'antd';
@@ -18,10 +18,12 @@ import { experimentStatusInfo } from '../status';
import styles from './index.less';

let graph = null;
const NodePrefix = 'workflow';

function ExperimentText() {
const [experimentIns, setExperimentIns] = useState(undefined);
const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined);
const [workflowStatus, setWorkflowStatus] = useState(undefined);
const graphRef = useRef();
const workflowRef = useRef();
const locationParams = useParams(); // 新版本获取路由参数接口
@@ -32,10 +34,12 @@ function ExperimentText() {
const evtSourceRef = useRef();
const width = 110;
const height = 36;
const status = getExperimentInstanceStatus(experimentIns?.status, workflowStatus);
const statusInfo = experimentStatusInfo[status];

useEffect(() => {
initGraph();
getWorkflow();
getExperimentInstance();

return () => {
if (evtSourceRef.current) {
@@ -61,54 +65,83 @@ function ExperimentText() {
}, []);

// 获取流水线模版
const getWorkflow = async () => {
const [res] = await to(getWorkflowById(locationParams.workflowId));
if (res && res.data && res.data.dag) {
try {
const dag = JSON.parse(res.data.dag);
dag.nodes.forEach((item) => {
item.in_parameters = JSON.parse(item.in_parameters);
item.out_parameters = JSON.parse(item.out_parameters);
item.control_strategy = JSON.parse(item.control_strategy);
item.imgName = item.img.slice(0, item.img.length - 4);
});
workflowRef.current = dag;
getExperimentInstance();
} catch (error) {
// JSON.parse 错误
console.error('JSON.parse error: ', error);
}
}
};
// const getWorkflow = async () => {
// const [res] = await to(getWorkflowById(locationParams.workflowId));
// if (res && res.data && res.data.dag) {
// try {
// const dag = JSON.parse(res.data.dag);
// dag.nodes.forEach((item) => {
// item.in_parameters = JSON.parse(item.in_parameters);
// item.out_parameters = JSON.parse(item.out_parameters);
// item.control_strategy = JSON.parse(item.control_strategy);
// item.imgName = item.img.slice(0, item.img.length - 4);
// });
// workflowRef.current = dag;
// getExperimentInstance();
// } catch (error) {
// // JSON.parse 错误
// console.error('JSON.parse error: ', error);
// }
// }
// };

// 获取实验实例
const getExperimentInstance = async () => {
const [res] = await to(getExperimentIns(locationParams.id));
if (res && res.data && workflowRef.current) {
if (res && res.data) {
setExperimentIns(res.data);
const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data;
const workflowData = workflowRef.current;
const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time, dag } = res.data;
if (!dag) {
return;
}

const workflow = parseJsonText(dag);
const experimentStatusObjs = parseJsonText(nodes_status);
workflowData.nodes.forEach((item) => {
const experimentNode = experimentStatusObjs?.[item.id];
updateWorkflowNode(item, experimentNode);
if (!workflow || !workflow.nodes) {
return;
}

workflow.nodes.forEach((item) => {
item.in_parameters = parseJsonText(item.in_parameters);
item.out_parameters = parseJsonText(item.out_parameters);
item.control_strategy = parseJsonText(item.control_strategy);
item.imgName = item.img.slice(0, item.img.length - 4);
});
workflowRef.current = workflow;

if (experimentStatusObjs) {
// 更新各个节点
workflow.nodes.forEach((item) => {
const experimentNode = experimentStatusObjs[item.id];
updateWorkflowNode(item, experimentNode);
});

// 设置 workflow 总状态
Object.keys(experimentStatusObjs).some((key) => {
if (key.startsWith(NodePrefix)) {
const tempWorkflowStatus = experimentStatusObjs[key];
setWorkflowStatus(tempWorkflowStatus);
return true;
}
return false;
});
}

// 绘制图
getGraphData(workflowData, true);
getGraphData(workflow, true);

if (status === ExperimentStatus.Pending) {
// 如果状态是 Pending, 打开第一个节点
const node = workflowData.nodes[0];
const node = workflow.nodes[0];
if (node) {
setExperimentNodeData(node);
openPropsDrawer();
}
} else if (status === ExperimentStatus.Running) {
// 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,则打开第一个节点
// 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点
const node =
workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ??
workflowData.nodes[0];
workflow.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running || item.experimentStatus === ExperimentStatus.Pending) ??
workflow.nodes[0];
if (node) {
setExperimentNodeData(node);
openPropsDrawer();
@@ -135,23 +168,36 @@ function ExperimentText() {
return;
}
try {
const dataJson = JSON.parse(data);
const dataJson = parseJsonText(data);
const statusData = dataJson?.result?.object?.status;
if (!statusData) {
return;
}
const { startedAt, finishedAt, phase, nodes = {} } = statusData;
setExperimentIns((prev) => ({
...prev,
finish_time: finishedAt,
status: phase,
}));
const { finishedAt, phase, nodes = {} } = statusData;

// 更新实验实例状态和结束时间
// setExperimentIns((prev) => ({
// ...prev,
// finish_time: finishedAt,
// status: phase,
// }));

// 设置总 workflow 状态
const tempWorkflowStatus = Object.values(nodes).find((node) =>
node.displayName.startsWith(NodePrefix),
);
if (tempWorkflowStatus) {
setWorkflowStatus(tempWorkflowStatus);
}

// 更新各个节点
const workflowData = workflowRef.current;
workflowData.nodes.forEach((item) => {
const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id);
updateWorkflowNode(item, experimentNode);
});

// 绘制图
getGraphData(workflowData, false);

// 更新打开的抽屉数据
@@ -177,6 +223,7 @@ function ExperimentText() {
evtSourceRef.current = evtSource;
};

// 更新各个节点
function updateWorkflowNode(workflowNode, statusNode) {
if (!statusNode) {
return;
@@ -471,29 +518,30 @@ function ExperimentText() {
<div className={styles['pipeline-container']}>
<div className={styles['pipeline-container__top']}>
<div className={styles['pipeline-container__top__info']}>
启动时间:{formatDate(experimentIns?.create_time)}
启动时间:{formatDate(workflowStatus?.startedAt)}
</div>
<div className={styles['pipeline-container__top__info']}>
执行时长:
<RunDuration
createTime={experimentIns?.create_time}
finishTime={experimentIns?.finish_time}
createTime={workflowStatus?.startedAt}
finishTime={workflowStatus?.finishedAt}
/>
</div>
<div className={styles['pipeline-container__top__info']}>
状态:
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
marginRight: '6px',
backgroundColor: experimentStatusInfo[experimentIns?.status]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}>
{experimentStatusInfo[experimentIns?.status]?.label}
</span>
{statusInfo ? (
<>
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo.icon}
draggable={false}
alt=""
/>
<span style={{ color: statusInfo.color }}>{statusInfo.label}</span>
</>
) : (
'--'
)}
</div>
<Button
className={styles['pipeline-container__top__param-button']}


+ 12
- 0
react-ui/src/pages/Experiment/Tensorboard/index.tsx View File

@@ -0,0 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2025-03-31 16:38:59
* @Description: 实验可视化 Tensorboard
*/

import IframePage, { IframePageType } from '@/components/IFramePage';

function TensorboardPage() {
return <IframePage type={IframePageType.TensorBoard}></IframePage>;
}
export default TensorboardPage;

+ 11
- 18
react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx View File

@@ -30,7 +30,7 @@ interface Workflow {
}

// 根据参数设置输入组件
export const getParamComponent = (paramType: number, isSensitive?: number): JSX.Element => {
export const getParamComponent = (paramType: number): JSX.Element => {
// 防止后台返回不是 number 类型
if (Number(paramType) === 3) {
return (
@@ -40,9 +40,9 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX.
</Radio.Group>
);
}
if (isSensitive && Number(isSensitive) === 1) {
return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />;
}
// if (isSensitive && Number(isSensitive) === 1) {
// return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />;
// }
return <Input placeholder="请输入值" allowClear />;
};

@@ -95,8 +95,8 @@ function AddExperimentModal({
};

const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 20 },
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};

const paramLayout = {
@@ -181,13 +181,13 @@ function AddExperimentModal({
/>
</Form.Item>
<Form.Item
label="选择流水线"
label="选择流水线模板"
name="workflow_id"
rules={[{ required: true, message: '请选择流水线' }]}
rules={[{ required: true, message: '请选择流水线模板' }]}
>
<Select
disabled={workflowDisabled}
placeholder="请选择流水线"
placeholder="请选择流水线模板"
onChange={handleWorkflowChange}
>
{Array.isArray(workflowList)
@@ -202,11 +202,7 @@ function AddExperimentModal({
</Select>
</Form.Item>
{globalParam.length > 0 && (
<Form.Item
label="运行参数"
tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示"
{...tailLayout}
>
<Form.Item label="运行参数" tooltip="展示关联的流水线的参数" {...tailLayout}>
<div className={styles.global_param_item}>
<Form.List name="global_param">
{(fields) =>
@@ -219,10 +215,7 @@ function AddExperimentModal({
name={[name, 'param_value']}
rules={getParamRules(globalParam[name]['param_type'], true)}
>
{getParamComponent(
globalParam[name]['param_type'],
globalParam[name]['is_sensitive'],
)}
{getParamComponent(globalParam[name]['param_type'])}
</Form.Item>
))
}


+ 0
- 4
react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.less View File

@@ -55,10 +55,6 @@
display: flex;
align-items: center;
width: 160px;

.statusIcon {
visibility: visible;
}
}
}



+ 15
- 10
react-ui/src/pages/Experiment/components/ExperimentInstanceList/index.tsx View File

@@ -39,7 +39,14 @@ function ExperimentInstanceList({
}: ExperimentInstanceListProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInsList?.map((item) => item.id) || [];
return (
experimentInsList
?.filter(
(item) =>
item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending,
)
.map((item) => item.id) || []
);
}, [experimentInsList]);
const [
selectedIns,
@@ -127,7 +134,12 @@ function ExperimentInstanceList({
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}>
<Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox>
<Checkbox
checked={checked}
indeterminate={indeterminate}
disabled={allIntanceIds.length === 0}
onChange={checkAll}
></Checkbox>
</div>
<div className={styles.index}>序号</div>
<div className={styles.tensorBoard}>可视化</div>
@@ -185,14 +197,7 @@ function ExperimentInstanceList({
)}
</div>

<ExperimentInstanceComponent
create_time={item.create_time}
finish_time={item.finish_time}
status={item.status as ExperimentStatus}
argo_ins_name={item.argo_ins_name}
argo_ins_ns={item.argo_ins_ns}
experimentInsId={item.id}
></ExperimentInstanceComponent>
<ExperimentInstanceComponent instance={item}></ExperimentInstanceComponent>

<div className={styles.operation}>
<Button


+ 35
- 32
react-ui/src/pages/Experiment/components/ExperimentInstanceList/instance.tsx View File

@@ -2,69 +2,72 @@ import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums';
import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance, NodeStatus } from '@/types';
import { ExperimentCompleted } from '@/utils/constant';
import { formatDate } from '@/utils/date';
import { getWorkflowStatus } from '@/utils/experiment';
import { Typography } from 'antd';
import React, { useCallback } from 'react';
import styles from './index.less';

type ExperimentInstanceProps = {
create_time?: string;
finish_time?: string;
status: ExperimentStatus;
argo_ins_name: string;
argo_ins_ns: string;
experimentInsId: number;
type ExperimentInstanceComponentProps = {
instance: ExperimentInstance;
};

function ExperimentInstance({
create_time,
finish_time,
status,
argo_ins_name,
argo_ins_ns,
experimentInsId,
}: ExperimentInstanceProps) {
function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentProps) {
const { id, experiment_id, argo_ins_name, argo_ins_ns, nodes_status, create_time, finish_time } =
instance;
const workflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined;
const status = instance.status as ExperimentStatus;
const createTime = workflowStatus?.startedAt ?? create_time;
const finishTime = workflowStatus?.finishedAt ?? finish_time;
const statusInfo = experimentStatusInfo[status];

const handleSSEMessage: MessageHandler = useCallback(
(experimentInsId: number, status: string, finish_time: string) => {
(experimentId: number, experimentInsId: number, status: string, finishTime: string) => {
window.postMessage({
type: ExperimentCompleted,
payload: {
id: experimentInsId,
experimentId,
experimentInsId,
status,
finish_time,
finishTime,
},
});
},
[],
);
useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage);
useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage);

return (
<React.Fragment>
<div className={styles.description}>
<div style={{ width: '50%' }}>
<RunDuration createTime={create_time} finishTime={finish_time} />
<RunDuration createTime={createTime} finishTime={finishTime} />
</div>
<div style={{ width: '50%' }} className={styles.startTime}>
<Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}>
{formatDate(create_time)}
<Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}>
{formatDate(createTime)}
</Typography.Text>
</div>
</div>
<div className={styles.statusBox}>
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[status]?.icon}
draggable={false}
alt=""
/>
<span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}>
{experimentStatusInfo[status]?.label}
</span>
{statusInfo ? (
<>
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo.icon}
draggable={false}
alt=""
/>
<span style={{ color: statusInfo.color }}>{statusInfo.label}</span>
</>
) : (
'--'
)}
</div>
</React.Fragment>
);
}

export default ExperimentInstance;
export default ExperimentInstanceComponent;

+ 60
- 50
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -11,9 +11,15 @@ type ExperimentParameterProps = {

function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
// 控制策略
const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
);
// const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map(
// ([key, value]) => ({ key, value }),
// );
const nodeId = nodeData.id;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');

// 输入参数
const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
@@ -74,54 +80,58 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
>
<FormInfo />
</Form.Item>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<FormInfo />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<FormInfo />
</Form.Item>
{hasTaskInfo && (
<>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<FormInfo />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<FormInfo />
</Form.Item>

<Form.Item label="启动命令" name="command">
<FormInfo textArea />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请输入资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" display />
</Form.Item>
<Form.Item label="挂载路径" name="mount_path">
<FormInfo />
</Form.Item>
<Form.Item label="环境变量" name="env_variables">
<FormInfo textArea />
</Form.Item>
{controlStrategyList.map((item) => (
<Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}>
<FormInfo valuePropName="showValue" />
</Form.Item>
))}
<Form.Item label="启动命令" name="command">
<FormInfo textArea />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请输入资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" display />
</Form.Item>
{/* <Form.Item label="挂载路径" name="mount_path">
<FormInfo />
</Form.Item> */}
<Form.Item label="环境变量" name="env_variables">
<FormInfo textArea />
</Form.Item>
{/* {controlStrategyList.map((item) => (
<Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}>
<FormInfo valuePropName="showValue" />
</Form.Item>
))} */}
</>
)}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}


+ 13
- 1
react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx View File

@@ -121,6 +121,8 @@ function ExportModelModal({
const params = {
...formData,
identifier: resource?.identifier,
owner: resource?.owner,
is_public: resource?.is_public,
name: resource?.name,
[config.sourceParamKey]: DataSource.HandExport,
train_task: {
@@ -174,6 +176,8 @@ function ExportModelModal({
onChange={handleResourceChange}
options={resources}
fieldNames={{ label: 'name', value: 'id' }}
optionFilterProp="name"
showSearch
allowClear
></Select>
</Form.Item>
@@ -191,9 +195,17 @@ function ExportModelModal({
}
rules={[
{ required: true, message: `请输入${config.name}版本` },
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: `${config.name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{
validator: (_, value) => {
if (value && versions.map((item) => item.name).includes(value)) {
if (value === 'master') {
return Promise.reject(`${config.name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${config.name}版本不能为 origin`);
} else if (value && versions.map((item) => item.name).includes(value)) {
return Promise.reject(`${config.name}版本已存在`);
} else {
return Promise.resolve();


+ 4
- 1
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -54,7 +54,10 @@ function LogGroup({
useEffect(() => {
// 建立 socket 连接
const setupSockect = () => {
const { host } = location;
let { host } = location;
if (process.env.NODE_ENV === 'development') {
host = '172.20.32.235:31213';
}
const socket = new WebSocket(
`ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`,
);


+ 1
- 4
react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx View File

@@ -47,10 +47,7 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
name={[name, 'param_value']}
label={getParamLabel(globalParam[name])}
>
{getParamComponent(
globalParam[name]['param_type'],
globalParam[name]['is_sensitive'],
)}
{getParamComponent(globalParam[name]['param_type'])}
</Form.Item>
))
}


+ 189
- 136
react-ui/src/pages/Experiment/index.jsx View File

@@ -2,8 +2,10 @@ import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState';
import { useServerTime } from '@/hooks/useServerTime';
import {
deleteExperimentById,
editExperimentInsReq,
getExperiment,
getExperimentById,
getQueryByExperimentId,
@@ -17,6 +19,7 @@ import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { ExperimentCompleted } from '@/utils/constant';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd';
@@ -28,7 +31,6 @@ import AddExperimentModal from './components/AddExperimentModal';
import ExperimentInstanceList from './components/ExperimentInstanceList';
import styles from './index.less';
import { experimentStatusInfo } from './status';
import { useServerTime } from '@/hooks/useServerTime';

// 定时器
const timerIds = new Map();
@@ -39,7 +41,7 @@ function Experiment() {
const [workflowList, setWorkflowList] = useState([]);
const [experimentId, setExperimentId] = useState(null);
const [experimentInsList, setExperimentInsList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState(null);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [total, setTotal] = useState(0);
const [isAdd, setIsAdd] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -59,29 +61,137 @@ function Experiment() {
const timerRef = useRef();

// 获取实验列表
const getExperimentList = useCallback(async () => {
const getExperimentList = useCallback(
async (skipLoading = false) => {
const params = {
page: pagination.current - 1,
size: pagination.pageSize,
name: searchText || undefined,
};
const [res] = await to(getExperiment(params, skipLoading));
if (res && res.data && Array.isArray(res.data.content)) {
setExperimentList(
res.data.content.map((item) => {
return { ...item, key: item.id };
}),
);

setTotal(res.data.totalElements);
}
},
[pagination, searchText],
);

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = useCallback(
(skipLoading = false) => {
getExperimentList(skipLoading);
},
[getExperimentList],
);

// 获取 TensorBoard 状态
const getTensorBoardStatus = useCallback(async (experimentIn) => {
const params = {
page: pagination.current - 1,
size: pagination.pageSize,
name: searchText || undefined,
namespace: experimentIn.nodes_result.tensorboard_log.namespace,
path: experimentIn.nodes_result.tensorboard_log.path,
pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
};
const [res] = await to(getExperiment(params));
if (res && res.data && Array.isArray(res.data.content)) {
setExperimentList(
res.data.content.map((item) => {
return { ...item, key: item.id };
}),
);

setTotal(res.data.totalElements);
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
...item,
tensorBoardStatus: res.data.status,
tensorboardUrl: res.data.url,
};
}
return item;
});
});

let timerId = timerIds.get(experimentIn.id);
if (timerId) {
clearTimeout(timerId);
timerIds.delete(experimentIn.id);
}
timerId = setTimeout(() => {
getTensorBoardStatus(experimentIn);
}, 10 * 1000);
timerIds.set(experimentIn.id, timerId);
}
}, [pagination, searchText]);
}, []);

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = useCallback(() => {
getExperimentList();
}, [getExperimentList]);
// 获取实验实例列表
const getExperimentInsList = useCallback(
async (experimentId, page, size = 5, skipLoading = false) => {
const params = {
experimentId: experimentId,
page: page,
size: size,
};
const [res, error] = await to(getQueryByExperimentId(params, skipLoading));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
try {
const list = content.map((v) => {
const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
return {
...v,
nodes_result,
};
});
if (page === 0) {
setExperimentInsList(list);
clearExperimentInTimers();
} else {
setExperimentInsList((prev) => [...prev, ...list]);
}
setExperimentInsTotal(totalElements);
// 获取 TensorBoard 状态
list.forEach((item) => {
if (item.nodes_result?.tensorboard_log) {
getTensorBoardStatus(item);
}
});
} catch (error) {
console.error('JSON parse error: ', error);
}
}
},
[getTensorBoardStatus],
);

// 刷新实验实例列表
const refreshExperimentIns = useCallback(
(experimentId, skipLoading = false) => {
const length = experimentInsList.length;
getExperimentInsList(experimentId, 0, length, skipLoading);
},
[experimentInsList, getExperimentInsList],
);

// 更新实验状态
const editExperimentIns = useCallback(
async (experimentId, experimentInsId, status, argo_ins_name, argo_ins_ns) => {
const params = {
experiment_id: experimentId,
id: experimentInsId,
status: status,
argo_ins_name,
argo_ins_ns,
};
const [res, error] = await to(editExperimentInsReq(params));
if (res && res.data) {
refreshExperimentIns(experimentId, true);
refreshExperimentList(true);
}
},
[refreshExperimentIns, refreshExperimentList],
);

// 获取流水线列表
useEffect(() => {
@@ -104,52 +214,66 @@ function Experiment() {
clearExperimentInTimers();
};
}, []);

// 获取实验列表
useEffect(() => {
getExperimentList();
}, [getExperimentList]);

// 新增,删除版本时,重置分页,然后刷新版本列表
// 更新实验实例状态
useEffect(() => {
const handleMessage = (e) => {
const { type, payload } = e.data;
if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;

// 修改实例的状态和结束时间
setExperimentInsList((prev) =>
prev.map((v) =>
v.id === id
? {
...v,
status: status,
finish_time: finish_time,
}
: v,
),
const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);

if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
if (
!currentIns ||
currentIns.status === ExperimentStatus.Terminated ||
currentIns.status === status
) {
return;
}

timerRef.current = setTimeout(() => {
refreshExperimentList();
}, 10000);
editExperimentIns(
experimentId,
experimentInsId,
status,
currentIns.argo_ins_name,
currentIns.argo_ins_ns,
);

// refreshExperimentList(true);
// refreshExperimentIns(experimentId);

// 修改实例的状态和结束时间
// setExperimentInsList((prev) =>
// prev.map((v) =>
// v.id === experimentInsId
// ? {
// ...v,
// status: status,
// finish_time: finishTime,
// }
// : v,
// ),
// );
}
};

window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}
};
}, [refreshExperimentList]);
}, [experimentInsList, editExperimentIns]);

// 搜索
const onSearch = (value) => {
@@ -160,44 +284,6 @@ function Experiment() {
}));
};

// 获取实验实例列表
const getQueryByExperiment = async (experimentId, page, size = 5) => {
const params = {
experimentId: experimentId,
page: page,
size: size,
};
const [res, error] = await to(getQueryByExperimentId(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setExpandedRowKeys(experimentId);
try {
const list = content.map((v) => {
const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
return {
...v,
nodes_result,
};
});
if (page === 0) {
setExperimentInsList(list);
clearExperimentInTimers();
} else {
setExperimentInsList((prev) => [...prev, ...list]);
}
setExperimentInsTotal(totalElements);
// 获取 TensorBoard 状态
list.forEach((item) => {
if (item.nodes_result?.tensorboard_log) {
getTensorBoardStatus(item);
}
});
} catch (error) {
console.error('JSON parse error: ', error);
}
}
};

// 运行 TensorBoard
const runTensorBoard = async (experimentIn) => {
const params = {
@@ -217,49 +303,16 @@ function Experiment() {
}
};

// 获取 TensorBoard 状态
const getTensorBoardStatus = async (experimentIn) => {
const params = {
namespace: experimentIn.nodes_result.tensorboard_log.namespace,
path: experimentIn.nodes_result.tensorboard_log.path,
pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
};
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
...item,
tensorBoardStatus: res.data.status,
tensorboardUrl: res.data.url,
};
}
return item;
});
});

let timerId = timerIds.get(experimentIn.id);
if (timerId) {
clearTimeout(timerId);
timerIds.delete(experimentIn.id);
}
timerId = setTimeout(() => {
getTensorBoardStatus(experimentIn);
}, 10 * 1000);
timerIds.set(experimentIn.id, timerId);
}
};

// 展开实例
const expandChange = (e, record) => {
const expandChange = (expanded, record) => {
clearExperimentInTimers();
setExperimentInsList([]);
if (record.id === expandedRowKeys) {
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id, 0, 5);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0, 5);
refreshExperimentList();
} else {
setExpandedRowKeys([]);
}
};

@@ -337,8 +390,9 @@ function Experiment() {
const [res] = await to(runExperiments(id));
if (res) {
message.success('运行成功');
setExpandedRowKeys([id]);
refreshExperimentList();
getQueryByExperiment(id, 0, 5);
getExperimentInsList(id, 0, 5);
}
};

@@ -372,14 +426,16 @@ function Experiment() {
experimentIn.tensorBoardStatus === TensorBoardStatus.Running &&
experimentIn.tensorboardUrl
) {
window.open(experimentIn.tensorboardUrl, '_blank');
const url = experimentIn.tensorboardUrl;
SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url);
navigateToUrl(`/pipeline/experiment/visual`);
// window.open(experimentIn.tensorboardUrl, '_blank');
}
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
// 修改实例的状态和结束时间
setExperimentInsList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
@@ -392,6 +448,9 @@ function Experiment() {
return item;
});
});
// 刷新实验列表和实例列表
refreshExperimentList(true);
refreshExperimentIns(experimentIn.experiment_id);
};

// 实验对比菜单
@@ -413,16 +472,10 @@ function Experiment() {
};
};

// 刷新实验实例列表
const refreshExperimentIns = (experimentId) => {
const length = experimentInsList.length;
getQueryByExperiment(experimentId, 0, length);
};

// 加载更多实验实例
const loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5);
getQueryByExperiment(expandedRowKeys, page, 5);
getExperimentInsList(expandedRowKeys[0], page, 5);
};

// 处理删除
@@ -607,7 +660,7 @@ function Experiment() {
></ExperimentInstanceList>
),
onExpand: expandChange,
expandedRowKeys: [expandedRowKeys],
expandedRowKeys: expandedRowKeys,
}}
/>
</div>


+ 12
- 0
react-ui/src/pages/HyperParameter/Aim/index.tsx View File

@@ -0,0 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2025-03-31 16:38:59
* @Description: 实验对比 Aim
*/

import IframePage, { IframePageType } from '@/components/IFramePage';

function AimPage() {
return <IframePage type={IframePageType.Aim}></IframePage>;
}
export default AimPage;

+ 1
- 1
react-ui/src/pages/HyperParameter/Info/index.tsx View File

@@ -1,7 +1,7 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 自机器学习详情
* @Description: 自机器学习详情
*/
import PageTitle from '@/components/PageTitle';
import { getRayInfoReq } from '@/services/hyperParameter';


+ 6
- 8
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -51,7 +51,7 @@ function HyperParameterInstance() {
const [res] = await to(getRayInsReq(instanceId));
if (res && res.data) {
const info = res.data as HyperParameterInstanceData;
const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info;
const { param, node_status, argo_ins_name, argo_ins_ns, status } = info;
// 解析配置参数
const paramJson = parseJsonText(param).data;
if (paramJson) {
@@ -72,7 +72,6 @@ function HyperParameterInstance() {
}
setExperimentInfo({
...paramJson,
create_time,
});
}

@@ -121,18 +120,17 @@ function HyperParameterInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
// 节点
// 设置节点
setNodes(nodes);

// 设置总 workflow 状态
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 设置工作流状态
if (workflowStatus) {
setWorkflowStatus(workflowStatus);

// 实验结束,关闭 SSE
// 实验结束,关闭 SSE,获取实验实例结果
if (
workflowStatus.phase !== ExperimentStatus.Pending &&
workflowStatus.phase !== ExperimentStatus.Running
@@ -167,8 +165,8 @@ function HyperParameterInstance() {
<HyperParameterBasic
className={styles['hyper-parameter-instance__basic']}
info={experimentInfo}
runStatus={workflowStatus}
instanceStatus={instanceInfo?.status}
workflowStatus={workflowStatus}
instanceStatus={instanceInfo?.status as ExperimentStatus}
isInstance
/>
),


+ 5
- 1
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx View File

@@ -3,7 +3,9 @@ import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell';
import { HyperParameterTrial } from '@/pages/HyperParameter/types';
import { getExpMetricsReq } from '@/services/hyperParameter';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { useNavigate } from '@umijs/max';
import { App, Button, Table, type TableProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
@@ -36,6 +38,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {};
const paramsNames = Object.keys(config);
const metricNames = Object.keys(metricAnalysis);
const navigate = useNavigate();

const trialColumns: TableProps<HyperParameterTrial>['columns'] = [
{
@@ -160,7 +163,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
const [res] = await to(getExpMetricsReq(selectedRowKeys));
if (res && res.data) {
const url = res.data;
window.open(url, '_blank');
SessionStorage.setItem(SessionStorage.aimUrlKey, url);
navigate('compare-visual');
}
};



+ 7
- 2
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx View File

@@ -1,4 +1,5 @@
import { ExperimentStatus } from '@/enums';
import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty';
import LogList from '@/pages/Experiment/components/LogList';
import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
import { NodeStatus } from '@/types';
@@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
{trainCloneNodeStatus ? (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
@@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList>
) : (
<EmptyLog />
)}
</div>
),
@@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
{hpoNodeStatus ? (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
@@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList>
) : (
<EmptyLog />
)}
</div>
),


+ 5
- 54
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

@@ -1,9 +1,7 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import RunDuration from '@/components/RunDuration';
import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
schedulerAlgorithms,
searchAlgorithms,
@@ -18,7 +16,6 @@ import {
formatMirror,
formatModel,
} from '@/utils/format';
import { Flex } from 'antd';
import classNames from 'classnames';
import { useMemo } from 'react';
import ParameterInfo from '../ParameterInfo';
@@ -33,14 +30,14 @@ type HyperParameterBasicProps = {
info?: HyperParameterData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus;
};

function HyperParameterBasic({
info,
className,
runStatus,
workflowStatus,
instanceStatus,
isInstance = false,
}: HyperParameterBasicProps) {
@@ -83,7 +80,7 @@ function HyperParameterBasic({
}
return [
{
label: '代码',
label: '代码配置',
value: info.code_config,
format: formatCodeConfig,
},
@@ -145,56 +142,10 @@ function HyperParameterBasic({
];
}, [info, getResourceDescription]);

const instanceDatas = useMemo(() => {
if (!info || !runStatus) {
return [];
}

return [
{
label: '启动时间',
value: formatDate(info.create_time),
ellipsis: true,
},
{
label: '执行时长',
value: <RunDuration createTime={info.create_time} finishTime={runStatus.finishedAt} />,
ellipsis: true,
},
{
label: '状态',
value: (
<Flex align="center">
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[runStatus.phase]?.icon}
draggable={false}
alt=""
/>
<div
style={{
color: experimentStatusInfo[runStatus?.phase]?.color,
fontSize: '15px',
lineHeight: 1.6,
}}
>
{experimentStatusInfo[runStatus?.phase]?.label}
</div>
</Flex>
),
ellipsis: true,
},
];
}, [runStatus, info]);

return (
<div className={classNames(styles['hyper-parameter-basic'], className)}>
{isInstance && runStatus && (
<ExperimentRunBasic
create_time={info?.create_time}
runStatus={runStatus}
instanceStatus={instanceStatus}
/>
{isInstance && workflowStatus && (
<ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} />
)}
{!isInstance && (
<ConfigInfo


+ 8
- 0
react-ui/src/pages/Mirror/Create/index.less View File

@@ -9,6 +9,14 @@
background-color: white;
border-radius: 10px;

&__name-row {
:global {
.ant-form-item-row {
flex-wrap: nowrap;
}
}
}

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


+ 29
- 6
react-ui/src/pages/Mirror/Create/index.tsx View File

@@ -123,8 +123,6 @@ function MirrorCreate() {
return true;
};

const descTitle = isAddVersion ? '版本描述' : '镜像描述';

return (
<div className={styles['mirror-create']}>
<PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle>
@@ -161,6 +159,7 @@ function MirrorCreate() {
message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)',
},
]}
className={styles['mirror-create__content__name-row']}
>
<Input
placeholder="请输入镜像名称"
@@ -193,21 +192,45 @@ function MirrorCreate() {
</Form.Item>
</Col>
</Row>
{!isAddVersion && (
<Row gutter={10}>
<Col span={20}>
<Form.Item
label="镜像描述"
name="description"
rules={[
{
required: true,
message: '请输入镜像描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入镜像描述"
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
)}
<Row gutter={10}>
<Col span={20}>
<Form.Item
label={descTitle}
name="description"
label="版本描述"
name="version_description"
rules={[
{
required: true,
message: `请输入${descTitle}`,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder={`请输入${descTitle}`}
placeholder="请输入版本描述"
maxLength={128}
showCount
allowClear


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

@@ -53,6 +53,7 @@ export type MirrorVersionData = {
file_size: string;
create_time: string;
tag_name: string;
description: string;
};

function MirrorInfo() {
@@ -125,6 +126,7 @@ function MirrorInfo() {
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
getMirrorInfo();
}
};

@@ -164,20 +166,21 @@ function MirrorInfo() {
title: '镜像版本',
dataIndex: 'tag_name',
key: 'tag_name',
width: '25%',
width: '30%',
render: tableCellRender(),
},
{
title: '镜像地址',
dataIndex: 'url',
key: 'url',
width: '25%',
width: '40%',
render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }),
},
{
title: '版本描述',
dataIndex: 'description',
key: 'description',
width: '30%',
render: tableCellRender(true),
},
{
@@ -209,7 +212,7 @@ function MirrorInfo() {
hidden: isPublic,
render: (_: any, record: MirrorVersionData) => (
<div>
{!isPublic && (
{!isPublic && record.status && record.status !== MirrorVersionStatus.Building && (
<ConfigProvider
theme={{
token: {


+ 13
- 9
react-ui/src/pages/Model/components/MetricsChart/index.tsx View File

@@ -65,14 +65,18 @@ export type MetricsChartProps = {
function MetricsChart({ name, chartData }: MetricsChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const xAxisData = chartData[0]?.iters;
const seriesData = chartData.map((item) => {
return {
name: item.version,
type: 'line' as const,
smooth: true,
data: item.values,
};
});
const seriesData = useMemo(
() =>
chartData.map((item) => {
return {
name: item.version,
type: 'line' as const,
smooth: true,
data: item.values,
};
}),
[chartData],
);

const options: echarts.EChartsOption = useMemo(
() => ({
@@ -158,7 +162,7 @@ function MetricsChart({ name, chartData }: MetricsChartProps) {

// 组件卸载
return () => {
// myChart.dispose() 销毁实例
// 销毁实例
chart.dispose();
};
}, [options]);


+ 1
- 1
react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -238,7 +238,7 @@ function CreateServiceVersion() {
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '服务支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
]}
>


+ 29
- 26
react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx View File

@@ -3,16 +3,16 @@
* @Date: 2024-04-16 13:58:08
* @Description: 服务版本详情
*/
import FullScreenFrame from '@/components/FullScreenFrame';
import IframePage from '@/components/IFramePage';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ServiceRunStatus } from '@/enums';
import { getServiceVersionInfoReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
import { Tabs } from 'antd';
import { useEffect, useState } from 'react';
import ServerLog from '../components/ServerLog';
import UserGuide from '../components/UserGuide';
import VersionBasicInfo from '../components/VersionBasicInfo';
import { ServiceVersionData } from '../types';
import styles from './index.less';
@@ -50,32 +50,35 @@ function ServiceVersionInfo() {
icon: <KFIcon type="icon-jibenxinxi" />,
children: <VersionBasicInfo info={versionInfo} />,
},
{
key: ModelDeploymentTabKey.Predict,
label: '预测',
icon: <KFIcon type="icon-yuce" />,
children: (
<div style={{ height: '100%', width: '100%' }}>
{versionInfo?.page_path && (
<FullScreenFrame url={versionInfo?.page_path}></FullScreenFrame>
)}
</div>
),
},
{
key: ModelDeploymentTabKey.Guide,
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
children: <UserGuide info={versionInfo}></UserGuide>,
},
{
key: ModelDeploymentTabKey.Log,
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
children: <ServerLog info={versionInfo}></ServerLog>,
},
];

if (versionInfo?.run_state === ServiceRunStatus.Running) {
if (versionInfo?.page_path) {
tabItems.push({
key: ModelDeploymentTabKey.Predict,
label: '预测',
icon: <KFIcon type="icon-yuce" />,
children: <IframePage url={versionInfo?.page_path} showLoading={false}></IframePage>,
});
}

if (versionInfo?.doc_path) {
tabItems.push({
key: ModelDeploymentTabKey.Guide,
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
children: <IframePage url={versionInfo?.doc_path}></IframePage>,
});
}
}

tabItems.push({
key: ModelDeploymentTabKey.Log,
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
children: <ServerLog info={versionInfo}></ServerLog>,
});

return (
<div className={styles['service-version-info']}>
<PageTitle title="服务版本详情"></PageTitle>


+ 9
- 1
react-ui/src/pages/ModelDeployment/components/ServerLog/index.less View File

@@ -11,10 +11,18 @@
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
display: flex;
flex-direction: column;
align-items: center;
align-items: left;

&--empty {
align-items: center;
}

&__more {
padding: 20px 0;
}

&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.5);
}
}
}

+ 9
- 2
react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx View File

@@ -3,6 +3,7 @@ import { getServiceVersionLogReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { DoubleRightOutlined } from '@ant-design/icons';
import { Button, DatePicker, type TimeRangePickerProps } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import styles from './index.less';
@@ -106,6 +107,8 @@ function ServerLog({ info }: ServerLogProps) {
}
};

const logContent = logData.map((v) => v.log_content).join('');

return (
<div className={styles['server-log']}>
<div>
@@ -122,9 +125,9 @@ function ServerLog({ info }: ServerLogProps) {
查询
</Button>
</div>
{logData.length > 0 && (
{logContent ? (
<div className={styles['server-log__data']} id="server-log">
<div>{logData.map((v) => v.log_content).join('') || '暂无日志'}</div>
<div>{logContent}</div>
{hasMore && (
<Button
type="text"
@@ -137,6 +140,10 @@ function ServerLog({ info }: ServerLogProps) {
</Button>
)}
</div>
) : (
<div className={classNames(styles['server-log__data'], styles['server-log__data--empty'])}>
暂无日志
</div>
)}
</div>
);


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

@@ -110,7 +110,7 @@ const EditPipeline = () => {
// console.log(data);
const errorNode = data.nodes.find((item) => item.formError === true);
if (errorNode) {
message.error(`【${errorNode.label}】节点必填项必须配置`);
message.error(`【${errorNode.label}】节点配置验证失败`);
const graphNode = graph.findById(errorNode.id);
if (graphNode) {
openNodeDrawer(graphNode, true);


+ 2
- 2
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx View File

@@ -148,7 +148,7 @@ const GlobalParamsDrawer = forwardRef(
>
{getParamComponent(type)}
</Form.Item>
{type !== 3 && (
{/* {type !== 3 && (
<Form.Item
{...restField}
name={[name, 'is_sensitive']}
@@ -161,7 +161,7 @@ const GlobalParamsDrawer = forwardRef(
<Radio value={0}>否</Radio>
</Radio.Group>
</Form.Item>
)}
)} */}
</>
);
}}


+ 158
- 94
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -1,3 +1,4 @@
import { type ServerCodeData } from '@/components/CodeSelect';
import CodeSelectorModal from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
@@ -15,10 +16,12 @@ import {
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
import { parseJsonText } from '@/utils';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { INode } from '@antv/g6';
import { Button, Drawer, Form, Input, MenuProps } from 'antd';
import { RuleObject } from 'antd/es/form';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle, useState } from 'react';
import PropsLabel from '../PropsLabel';
@@ -34,6 +37,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize,
);
const nodeId = Form.useWatch('id', form) as string;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');
const [open, setOpen] = useState(false);
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);

@@ -45,10 +54,11 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
const fields = form.getFieldsValue();

// 保存字段顺序
const control_strategy = {
...stagingItem.control_strategy,
...fields.control_strategy,
};
// const control_strategy = {
// ...stagingItem.control_strategy,
// ...fields.control_strategy,
// };

const in_parameters = {
...stagingItem.in_parameters,
...fields.in_parameters,
@@ -63,7 +73,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
const res = {
...stagingItem,
...fields,
control_strategy: JSON.stringify(control_strategy),
// control_strategy: JSON.stringify(control_strategy),
in_parameters: JSON.stringify(in_parameters),
out_parameters: JSON.stringify(out_parameters),
formError: !!error,
@@ -90,7 +100,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
// control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', nodeData);
setStagingItem({
@@ -147,11 +157,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => {
const jsonValue = form.getFieldValue(formItemName)?.value;
const fieldValue = parseJsonText(jsonValue) as ServerCodeData;
const defaultSelected = fieldValue
? {
id: fieldValue.id,
code_repo_name: fieldValue.name,
git_url: fieldValue.code_path,
git_branch: fieldValue.branch,
git_user_name: fieldValue.username,
git_password: fieldValue.password,
ssh_key: fieldValue.ssh_private_key,
is_public: fieldValue.is_public,
}
: undefined;
const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected,
onOk: (res) => {
if (res) {
const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } =
res;
const {
id,
code_repo_name,
git_url,
git_branch,
git_user_name,
git_password,
ssh_key,
is_public,
} = res;
const value = JSON.stringify({
id,
name: code_repo_name,
@@ -160,6 +193,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
is_public,
});
form.setFieldValue(formItemName, {
...item,
@@ -309,22 +343,47 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
);
};

// 模型部署-服务版本验证
const serviceVersionValidator = (_rule: RuleObject, value: any) => {
// 输入参数值都是对象,如果是手动输入,要求 /^[a-zA-Z0-9._-]+$/
if (
typeof value === 'object' &&
value &&
!value.fromSelect &&
value.value &&
!/^[a-zA-Z0-9._-]+$/.test(value.value)
) {
return Promise.reject('服务版本只支持字母、数字、点(.)、下划线(_)、中横线(-)');
}

return Promise.resolve();
};

// 必填项校验规则
const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => {
return item.value.require
const id = form.getFieldValue('id') as string;
const rules = item.value.require
? [
{
validator: requiredValidator,
message: '必填项',
},
]
: [];

// 模型部署-服务版本验证
if (id.startsWith('model-deploy') && item.key === '--version') {
rules.push({
validator: serviceVersionValidator,
});
}

return rules;
};

// 控制策略
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
);
// const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
// ([key, value]) => ({ key, value }),
// );

// 输入参数
const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({
@@ -396,71 +455,73 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
>
<Input disabled />
</Form.Item>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
{hasTaskInfo && (
<>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
size="small"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['pipeline-drawer__ref-row__select-button']}
>
选择镜像
</Button>
</Form.Item>
</div>
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
size="small"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['pipeline-drawer__ref-row__select-button']}
>
选择镜像
</Button>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" />
</Form.Item>
<Form.Item
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" />
</Form.Item>
{/* <Form.Item
name="mount_path"
label={
<PropsLabel
@@ -473,23 +534,23 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
}
>
<Input placeholder="请输入挂载路径" allowClear />
</Form.Item>
<Form.Item
name="env_variables"
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
onClick={(value) => {
handleParameterClick('env_variables', value);
}}
/>
}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{/* 控制参数 */}
{controlStrategyList.map((item) => (
</Form.Item> */}
<Form.Item
name="env_variables"
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
onClick={(value) => {
handleParameterClick('env_variables', value);
}}
/>
}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{/* 控制参数 */}
{/* {controlStrategyList.map((item) => (
<Form.Item
key={item.key}
name={['control_strategy', item.key]}
@@ -499,7 +560,10 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
>
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
</Form.Item>
))}
))} */}
</>
)}

{/* 输入参数 */}
{inParametersList.length > 0 && (
<>


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

@@ -190,7 +190,7 @@ const Pipeline = () => {
}),
},
{
title: '流水线名称',
title: '流水线模板名称',
dataIndex: 'name',
key: 'name',
width: '50%',
@@ -199,7 +199,7 @@ const Pipeline = () => {
}),
},
{
title: '流水线描述',
title: '流水线模板描述',
dataIndex: 'description',
key: 'description',
width: '50%',


+ 1
- 1
react-ui/src/pages/System/Role/authUser.tsx View File

@@ -113,7 +113,7 @@ const AuthUserTableList: React.FC = () => {
dataIndex: 'createTime',
valueType: 'dateRange',
render: (_, record) => {
return <span>{record.createTime.toString()} </span>;
return <span>{record.createTime?.toString()} </span>;
},
hideInSearch: true,
},


+ 1
- 1
react-ui/src/pages/System/Role/components/UserSelectorModal.tsx View File

@@ -84,7 +84,7 @@ const UserSelectorModal: React.FC<DataScopeFormProps> = (props) => {
valueType: 'dateRange',
hideInSearch: true,
render: (_, record) => {
return <span>{record.createTime.toString()} </span>;
return <span>{record.createTime?.toString()} </span>;
},
},
];


+ 2
- 2
react-ui/src/pages/System/Role/index.tsx View File

@@ -177,7 +177,7 @@ const RoleTableList: React.FC = () => {
confirm({
title: `确认要${text}${record.roleName}角色吗?`,
onOk() {
changeRoleStatus(record.roleId, newStatus).then((resp) => {
changeRoleStatus(record.roleId, record.roleKey, newStatus).then((resp) => {
if (resp.code === 200) {
messageApi.open({
type: 'success',
@@ -240,7 +240,7 @@ const RoleTableList: React.FC = () => {
dataIndex: 'createTime',
valueType: 'dateRange',
render: (_, record) => {
return <span>{record.createTime.toString()} </span>;
return <span>{record.createTime?.toString()} </span>;
},
search: {
transform: (value) => {


+ 5
- 8
react-ui/src/pages/System/User/components/DeptTree.tsx View File

@@ -28,8 +28,11 @@ const DeptTree: React.FC<TreeProps> = (props) => {
const res = await getDeptTree({});
const treeData = res.map((item: any) => ({ ...item, key: item.id }));
setTreeData(treeData);
setExpandedKeys([treeData[0].key]);
setSelectedKeys([treeData[0].key]);
if (treeData.length > 0) {
onSelect(treeData[0]);
setExpandedKeys([treeData[0].key]);
setSelectedKeys([treeData[0].key]);
}
hide();
return true;
} catch (error) {
@@ -41,12 +44,6 @@ const DeptTree: React.FC<TreeProps> = (props) => {
fetchDeptList();
}, []);

useEffect(() => {
if (treeData.length > 0) {
onSelect(treeData[0]);
}
}, [treeData, onSelect]);

const handleSelect = (keys: React.Key[], info: any) => {
setSelectedKeys(keys);
onSelect(info.node);


+ 2
- 3
react-ui/src/pages/System/User/edit.tsx View File

@@ -203,9 +203,8 @@ const UserForm: React.FC<UserFormProps> = (props) => {
required: true,
},
{
pattern: /^[a-zA-Z](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/,
message:
'只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以字母开头,数字或字母结尾',
pattern: /^[a-zA-Z][a-zA-Z0-9]{3,14}$/,
message: '用户账号长度为4 ~ 15位,且必须以字母开头,只支持字母和数字',
},
]}
/>


+ 0
- 60
react-ui/src/pages/User/Center/Center.less View File

@@ -1,60 +0,0 @@
.avatarHolder {
position: relative;
display: inline-block;
height: 120px;
margin-bottom: 16px;
text-align: center;

& > img {
width: 120px;
height: 120px;
margin-bottom: 20px;
border-radius: 50%;
}
&:hover:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
color: #eee;
font-size: 24px;
font-style: normal;
line-height: 110px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
content: '+';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

.teamTitle {
margin-bottom: 12px;
color: @heading-color;
font-weight: 500;
}

.team {
:global {
.ant-avatar {
margin-right: 12px;
}
}

a {
display: block;
margin-bottom: 24px;
overflow: hidden;
color: @text-color;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
transition: color 0.3s;

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

+ 94
- 90
react-ui/src/pages/User/Center/components/BaseInfo/index.tsx View File

@@ -1,91 +1,107 @@
import KFModal from '@/components/KFModal';
import { updateUserProfile } from '@/services/system/user';
import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Form, message, Row } from 'antd';
import { to } from '@/utils/promise';
import { Form, Input, message, Radio } from 'antd';
import React from 'react';

export type BaseInfoProps = {
values: Partial<API.CurrentUser> | undefined;
values: Partial<API.CurrentUser>;
open: boolean;
onFinished?: (isSuccess: boolean) => void;
};

const BaseInfo: React.FC<BaseInfoProps> = (props) => {
const BaseInfo: React.FC<BaseInfoProps> = ({ open, onFinished, values: initialValues }) => {
const [form] = Form.useForm();
const intl = useIntl();

const handleFinish = async (values: Record<string, any>) => {
const data = { ...props.values, ...values } as API.CurrentUser;
const resp = await updateUserProfile(data);
if (resp.code === 200) {
const handleFinish = async (formData: Record<string, any>) => {
const data = { userId: initialValues.userId, ...formData } as API.CurrentUser;
const [res] = await to(updateUserProfile(data));
if (res) {
message.success('修改成功');
} else {
message.warning(resp.msg);
onFinished?.(true);
}
};

return (
<>
<ProForm form={form} onFinish={handleFinish} initialValues={props.values}>
<Row>
<ProFormText
name="nickName"
label={intl.formatMessage({
id: 'system.user.nick_name',
defaultMessage: '用户昵称',
})}
width="xl"
placeholder="请输入用户昵称"
rules={[
{
required: true,
message: (
<FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
),
},
]}
/>
</Row>
<Row>
<ProFormText
name="phonenumber"
label={intl.formatMessage({
id: 'system.user.phonenumber',
defaultMessage: '手机号码',
})}
width="xl"
placeholder="请输入手机号码"
rules={[
{
required: false,
message: (
<FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
),
},
]}
/>
</Row>
<Row>
<ProFormText
name="email"
label={intl.formatMessage({
id: 'system.user.email',
defaultMessage: '邮箱',
})}
width="xl"
placeholder="请输入邮箱"
rules={[
{
type: 'email',
message: '无效的邮箱地址!',
},
{
required: false,
message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
},
]}
/>
</Row>
<Row>
<ProFormRadio.Group
<KFModal
width={800}
title="修改基本信息"
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'basic-info-form',
}}
onCancel={() => onFinished?.(false)}
destroyOnClose
>
<Form
name="basic-info-form"
form={form}
layout="vertical"
size="large"
autoComplete="off"
scrollToFirstError
initialValues={initialValues}
onFinish={handleFinish}
>
<Form.Item
name="nickName"
label="用户昵称"
rules={[
{
required: true,
message: '请输入用户昵称',
},
]}
>
<Input placeholder="请输入用户昵称" allowClear></Input>
</Form.Item>

<Form.Item
name="phonenumber"
label="手机号码"
rules={[
{
required: true,
message: '请输入手机号码',
},
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
},
]}
>
<Input placeholder="请输入手机号码" allowClear></Input>
</Form.Item>

<Form.Item
name="email"
label="邮箱"
rules={[
{
required: true,
message: '请输入邮箱',
},
{
type: 'email',
message: '请输入正确的邮箱地址',
},
]}
>
<Input placeholder="请输入邮箱" allowClear></Input>
</Form.Item>

<Form.Item
name="sex"
label="性别"
rules={[
{
required: false,
message: '请选择性别',
},
]}
>
<Radio.Group
options={[
{
label: '男',
@@ -96,22 +112,10 @@ const BaseInfo: React.FC<BaseInfoProps> = (props) => {
value: '1',
},
]}
name="sex"
label={intl.formatMessage({
id: 'system.user.sex',
defaultMessage: 'sex',
})}
width="xl"
rules={[
{
required: false,
message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />,
},
]}
/>
</Row>
</ProForm>
</>
</Form.Item>
</Form>
</KFModal>
);
};



+ 60
- 51
react-ui/src/pages/User/Center/components/ResetPassword/index.tsx View File

@@ -1,81 +1,90 @@
import KFModal from '@/components/KFModal';
import { updateUserPwd } from '@/services/system/user';
import { ProForm, ProFormText } from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Form, message } from 'antd';
import React from 'react';
import { to } from '@/utils/promise';
import { Form, Input, message } from 'antd';

const ResetPassword: React.FC = () => {
export type ResetPasswordProps = {
open: boolean;
onFinished?: (isSuccess: boolean) => void;
};

const ResetPassword = ({ open, onFinished }: ResetPasswordProps) => {
const [form] = Form.useForm();
const intl = useIntl();

const handleFinish = async (values: Record<string, any>) => {
const resp = await updateUserPwd(values.oldPassword, values.newPassword);
if (resp.code === 200) {
message.success('密码重置成功。');
} else {
message.warning(resp.msg);
const [res] = await to(updateUserPwd(values.oldPassword, values.newPassword));
if (res) {
message.success('密码重置成功');
onFinished?.(true);
}
};

const checkPassword = (_rule: any, value: string) => {
const login_password = form.getFieldValue('newPassword');
if (value === login_password) {
return Promise.resolve();
if (!value) {
return Promise.reject(new Error('请输入确认密码'));
} else if (value !== login_password) {
return Promise.reject(new Error('两次密码输入不一致'));
}
return Promise.reject(new Error('两次密码输入不一致'));
return Promise.resolve();
};

return (
<>
<ProForm form={form} onFinish={handleFinish}>
<ProFormText.Password
<KFModal
width={800}
title="重置密码"
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'reset-pwd-form',
}}
onCancel={() => onFinished?.(false)}
destroyOnClose
>
<Form
form={form}
name="reset-pwd-form"
layout="vertical"
size="large"
autoComplete="off"
scrollToFirstError
onFinish={handleFinish}
>
<Form.Item
name="oldPassword"
label={intl.formatMessage({
id: 'system.user.old_password',
defaultMessage: '旧密码',
})}
width="xl"
placeholder="请输入旧密码"
label="旧密码"
rules={[
{
required: true,
message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />,
message: '请输入旧密码',
},
]}
/>
<ProFormText.Password
>
<Input.Password placeholder="请输入旧密码" allowClear></Input.Password>
</Form.Item>
<Form.Item
name="newPassword"
label={intl.formatMessage({
id: 'system.user.new_password',
defaultMessage: '新密码',
})}
width="xl"
placeholder="请输入新密码"
label="新密码"
rules={[
{
required: true,
message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />,
message: '请输入新密码',
},
]}
/>
<ProFormText.Password
>
<Input.Password placeholder="请输入新密码" allowClear></Input.Password>
</Form.Item>
<Form.Item
name="confirmPassword"
label={intl.formatMessage({
id: 'system.user.confirm_password',
defaultMessage: '确认密码',
})}
width="xl"
placeholder="请输入确认密码"
rules={[
{
required: true,
message: <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />,
},
{ validator: checkPassword },
]}
/>
</ProForm>
</>
label="确认密码"
dependencies={['newPassword']}
required
rules={[{ validator: checkPassword }]}
>
<Input.Password placeholder="请输入确认密码" allowClear></Input.Password>
</Form.Item>
</Form>
</KFModal>
);
};



+ 64
- 0
react-ui/src/pages/User/Center/index.less View File

@@ -0,0 +1,64 @@
@avaterSize: 180px;

.avatarHolder {
position: relative;
display: inline-block;
height: @avaterSize;
margin-bottom: 30px;
text-align: center;

& > img {
width: @avaterSize;
height: @avaterSize;
border-radius: 50%;
}
// &:hover:after {
// position: absolute;
// top: 0;
// right: 0;
// bottom: 0;
// left: 0;
// color: #eee;
// font-size: 24px;
// font-style: normal;
// line-height: @avaterSize;
// background: rgba(0, 0, 0, 0.5);
// border-radius: 50%;
// cursor: pointer;
// content: '+';
// -webkit-font-smoothing: antialiased;
// -moz-osx-font-smoothing: grayscale;
// }
}

.user-center {
height: calc(100% - 50px - 120px);
padding: 30px;
background: white;
border-radius: 8px;
width: 50%;
margin: 60px auto 0;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;

:global {
.ant-list {
width: 100%;

.ant-list-item {
height: 80px;
border-block-end: 1px solid rgba(5, 5, 5, 0.06);
font-size: 16px;
}
}
}

&__buttons {
display: flex;
align-items: center;
margin-top: 60px;
flex-direction: row;
}
}

+ 98
- 88
react-ui/src/pages/User/Center/index.tsx View File

@@ -1,6 +1,9 @@
import { getUserInfo } from '@/services/session';
import DefaultAvatar from '@/assets/img/avatar-default.png';
import PageTitle from '@/components/PageTitle';
import { to } from '@/utils/promise';
import {
ClusterOutlined,
HeartOutlined,
MailOutlined,
ManOutlined,
MobileOutlined,
@@ -8,45 +11,52 @@ import {
UserOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Card, Col, Divider, List, Row } from 'antd';
import React, { useState } from 'react';
import styles from './Center.less';
import { useModel } from '@umijs/max';
import { Button, List } from 'antd';
import { useCallback, useState } from 'react';
import { flushSync } from 'react-dom';
import AvatarCropper from './components/AvatarCropper';
import BaseInfo from './components/BaseInfo';
import ResetPassword from './components/ResetPassword';
import BaseInfoModal from './components/BaseInfo';
import ResetPasswordModal from './components/ResetPassword';
import styles from './index.less';

const operationTabList = [
{
key: 'base',
tab: <span>基本资料</span>,
},
{
key: 'password',
tab: <span>重置密码</span>,
},
];
const Center = () => {
const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
const [infoModalOpen, setInfoModalOpen] = useState<boolean>(false);
const [resetModalOpen, setRestModalOpen] = useState<boolean>(false);

export type tabKeyType = 'base' | 'password';
const { initialState, setInitialState } = useModel('@@initialState');
const { currentUser, fetchUserInfo } = initialState || {};

const Center: React.FC = () => {
const [tabKey, setTabKey] = useState<tabKeyType>('base');
const refreshUserInfo = useCallback(async () => {
if (fetchUserInfo) {
const [res] = await to(fetchUserInfo());
if (res) {
flushSync(() => {
setInitialState((s) => ({ ...s, currentUser: res }));
});
}
}
}, [setInitialState, fetchUserInfo]);

const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
const handleBaseInfoChange = (success: boolean) => {
setInfoModalOpen(false);

// 获取用户信息
const { data: userInfo, loading } = useRequest(async () => {
return { data: await getUserInfo() };
});
if (loading) {
return <div>loading...</div>;
}
if (success) {
refreshUserInfo();
}
};

const currentUser = userInfo?.user;
const handleResetPassword = (success: boolean) => {
setRestModalOpen(false);
if (success) {
}
};

// 渲染用户信息
const renderUserInfo = ({
userName,
nickName,
phonenumber,
email,
sex,
@@ -65,6 +75,17 @@ const Center: React.FC = () => {
</div>
<div>{userName}</div>
</List.Item>
<List.Item>
<div>
<HeartOutlined
style={{
marginRight: 8,
}}
/>
昵称
</div>
<div>{nickName}</div>
</List.Item>
<List.Item>
<div>
<ManOutlined
@@ -109,75 +130,53 @@ const Center: React.FC = () => {
</div>
<div>{dept?.deptName}</div>
</List.Item>
<List.Item>
<div>
<TeamOutlined
style={{
marginRight: 8,
}}
/>
角色
</div>
<div>{currentUser?.roles?.map((item: any) => item.roleName)?.join(',')}</div>
</List.Item>
</List>
);
};

// 渲染tab切换
const renderChildrenByTabKey = (tabValue: tabKeyType) => {
if (tabValue === 'base') {
return <BaseInfo values={currentUser} />;
}
if (tabValue === 'password') {
return <ResetPassword />;
}
return null;
};

if (!currentUser) {
return <PageLoading />;
}

return (
<div>
<Row gutter={[16, 24]}>
<Col lg={8} md={24}>
<Card title="个人信息" bordered={false} loading={loading}>
{!loading && (
<div style={{ textAlign: 'center' }}>
<div
className={styles.avatarHolder}
onClick={() => {
setCropperModalOpen(true);
}}
>
<img src={currentUser.avatar} draggable={false} alt="" />
</div>
{renderUserInfo(currentUser)}
<Divider dashed />
<div className={styles.team}>
<div className={styles.teamTitle}>角色</div>
<Row gutter={36}>
{currentUser.roles &&
currentUser.roles.map((item: any) => (
<Col key={item.roleId} lg={24} xl={12}>
<TeamOutlined
style={{
marginRight: 8,
}}
/>
{item.roleName}
</Col>
))}
</Row>
</div>
</div>
)}
</Card>
</Col>
<Col lg={16} md={24}>
<Card
bordered={false}
tabList={operationTabList}
activeTabKey={tabKey}
onTabChange={(_tabKey: string) => {
setTabKey(_tabKey as tabKeyType);
}}
<div style={{ height: '100%' }}>
<PageTitle title="个人中心"></PageTitle>
<div className={styles['user-center']}>
<div
className={styles.avatarHolder}
// onClick={() => {
// setCropperModalOpen(true);
// }}
>
<img src={currentUser.avatar || DefaultAvatar} draggable={false} alt="" />
</div>
{renderUserInfo(currentUser)}
<div className={styles['user-center__buttons']}>
<Button
type="primary"
size="large"
style={{ marginRight: 50 }}
onClick={() => setInfoModalOpen(true)}
>
{renderChildrenByTabKey(tabKey)}
</Card>
</Col>
</Row>
修改基本信息
</Button>
<Button type="primary" size="large" onClick={() => setRestModalOpen(true)}>
重置密码
</Button>
</div>
</div>

<AvatarCropper
onFinished={() => {
setCropperModalOpen(false);
@@ -185,6 +184,17 @@ const Center: React.FC = () => {
open={cropperModalOpen}
data={currentUser.avatar}
/>

<BaseInfoModal
open={infoModalOpen}
values={currentUser}
onFinished={handleBaseInfoChange}
></BaseInfoModal>

<ResetPasswordModal
open={resetModalOpen}
onFinished={handleResetPassword}
></ResetPasswordModal>
</div>
);
};


+ 1
- 1
react-ui/src/pages/User/Login/login.tsx View File

@@ -97,7 +97,7 @@ const Login = () => {

await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
history.replace(urlParams.get('redirect') || '/');
} else {
if (error?.data?.code === 500 && error?.data?.msg === '验证码错误') {
captchaInputRef.current?.focus({


+ 12
- 1
react-ui/src/pages/Workspace/components/AssetsManagement/index.less View File

@@ -37,7 +37,18 @@
&__summary {
display: flex;
flex-direction: column;
width: 33.33%;
width: 40%;
text-align: left;

&:nth-child(3n+2) {
text-align: center;
width: 30%;
}

&:nth-child(3n) {
text-align: right;
width: 30%;
}

&__title {
margin-bottom: 12px;


+ 16
- 8
react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx View File

@@ -16,7 +16,7 @@ function AssetsManagement() {
};
const [res] = await to(getWorkspaceAssetCountReq(params));
if (res && res.data) {
const { component, dataset, image, model, workflow } = res.data;
const { dataset, image, model, workflow, codeConfig, service } = res.data;
const items = [
{
title: '数据集',
@@ -30,10 +30,10 @@ function AssetsManagement() {
title: '镜像',
value: image,
},
{
title: '组件',
value: component,
},
// {
// title: '组件',
// value: component,
// },
// {
// title: '代码配置',
// value: 0,
@@ -42,6 +42,14 @@ function AssetsManagement() {
title: '流水线模版',
value: workflow,
},
{
title: '代码配置',
value: codeConfig,
},
{
title: '服务',
value: service,
},
];
setAssetCounts(items);
}
@@ -53,7 +61,7 @@ function AssetsManagement() {
return (
<div className={styles['assets-management']}>
<Flex justify="space-between">
<div className={styles['assets-management__title']}>AI资产</div>
<div className={styles['assets-management__title']}>多形态资源库</div>
<Select
size="small"
value={type}
@@ -67,11 +75,11 @@ function AssetsManagement() {
/>
</Flex>
{/* <div className={styles['assets-management__increase']}>今日新增数量:5</div> */}
<Flex gap="22px 0" wrap="wrap" style={{ marginTop: '40px' }}>
<Flex gap="22px 0" wrap="wrap" style={{ marginTop: '40px', padding: '0 8px' }}>
{assetCounts.map((item, index) => (
<div className={styles['assets-management__summary']} key={index}>
<div className={styles['assets-management__summary__title']}>{item.title}</div>
<div className={styles['assets-management__summary__value']}>{item.value}</div>
<div className={styles['assets-management__summary__value']}>{item.value ?? '-'}</div>
</div>
))}
</Flex>


+ 4
- 4
react-ui/src/pages/Workspace/components/ExperimentTable/index.less View File

@@ -1,6 +1,6 @@
.experiment-table {
flex: 1;
min-width: 378px;
min-width: 460px;
height: 140px;
padding: 12px;
background: @workspace-background;
@@ -32,7 +32,7 @@
}

&__status {
width: 15%;
width: 25%;
}

&__duration {
@@ -40,11 +40,11 @@
}

&__date {
width: calc(60% - 60px);
width: 40%;
}

&__operation {
width: 60px;
width: 10%;

:global {
.ant-btn-link {


+ 8
- 29
react-ui/src/pages/Workspace/components/ExperimentTable/index.tsx View File

@@ -1,10 +1,9 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus, experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { useNavigate } from '@umijs/max';
import { Button, Empty } from 'antd';
import { Empty } from 'antd';
import styles from './index.less';
import ExperimentInstanceComponent from './instance';

type ExperimentTableProps = {
tableData: ExperimentInstance[];
style?: React.CSSProperties;
@@ -26,31 +25,11 @@ function ExperimentTable({ tableData = [], style }: ExperimentTableProps) {
</div>
{Array.isArray(tableData) && tableData.length > 0 ? (
tableData.map((item) => (
<div className={styles['experiment-table__content']} key={item.id}>
<div className={styles['experiment-table__status']} style={{ paddingLeft: '6.5px' }}>
<img
src={experimentStatusInfo[item.status as ExperimentStatus]?.icon}
width={17}
height={17}
draggable={false}
alt=""
/>
</div>
<div className={styles['experiment-table__duration']}>
{elapsedTime(item.create_time, item.finish_time)}
</div>
<div className={styles['experiment-table__date']}>{formatDate(item.create_time)}</div>
<div className={styles['experiment-table__operation']}>
<Button
size="small"
type="link"
icon={<KFIcon type="icon-xiangqing2" font={14} />}
onClick={() => gotoExperiment(item)}
>
详情
</Button>
</div>
</div>
<ExperimentInstanceComponent
instance={item}
key={item.id}
onClick={() => gotoExperiment(item)}
/>
))
) : (
<Empty


+ 76
- 0
react-ui/src/pages/Workspace/components/ExperimentTable/instance.tsx View File

@@ -0,0 +1,76 @@
import KFIcon from '@/components/KFIcon';
import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums';
import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance, NodeStatus } from '@/types';
import { formatDate } from '@/utils/date';
import { getExperimentInstanceStatus, getWorkflowStatus } from '@/utils/experiment';
import { Button } from 'antd';
import { useCallback, useState } from 'react';
import styles from './index.less';

const NodePrefix = 'workflow';

type ExperimentInstanceComponentProps = {
instance: ExperimentInstance;
onClick: () => void;
};

function ExperimentInstanceComponent({ instance, onClick }: ExperimentInstanceComponentProps) {
const { experiment_id, id, argo_ins_name, argo_ins_ns, nodes_status } = instance;
const initialWorkflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined;
const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(
initialWorkflowStatus,
);
const status = getExperimentInstanceStatus(instance.status as ExperimentStatus, workflowStatus);
const createTime = workflowStatus?.startedAt;
const finishTime = workflowStatus?.finishedAt;
const statusInfo = experimentStatusInfo[status];

const handleSSEMessage: MessageHandler = useCallback(
(
_experimentId: number,
_experimentInsId: number,
_status: string,
_finishTime: string,
nodes,
) => {
if (nodes) {
// 设置总 workflow 状态
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;
if (workflowStatus) {
setWorkflowStatus(workflowStatus);
}
}
},
[],
);
useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage);

return (
<div className={styles['experiment-table__content']} key={id}>
<div className={styles['experiment-table__status']} style={{ paddingLeft: '6.5px' }}>
<img src={statusInfo?.icon} width={17} height={17} draggable={false} alt="" />
</div>
<div className={styles['experiment-table__duration']}>
<RunDuration createTime={createTime} finishTime={finishTime} />
</div>
<div className={styles['experiment-table__date']}>{formatDate(createTime)}</div>
<div className={styles['experiment-table__operation']}>
<Button
size="small"
type="link"
icon={<KFIcon type="icon-xiangqing2" font={14} />}
onClick={onClick}
>
详情
</Button>
</div>
</div>
);
}

export default ExperimentInstanceComponent;

+ 21
- 21
react-ui/src/pages/Workspace/components/QuickStart/index.tsx View File

@@ -16,13 +16,13 @@ function QuickStart() {
const changeScale = () => {
// body 的宽度 - 菜单的宽度 - 两个 padding - 右边用户管理的宽度 - 右边用户管理的 marginLeft - 滚动条的宽度,
const width = document.body.offsetWidth - 256 - 80 - 60 - 326 - 15 - 8;
if (width >= 1223) {
const spaceX = (width - 192 * 5 - 60) / 7;
if (width >= 1002) {
const spaceX = (width - 192 * 4 - 60) / 6;
setSpace(spaceX);
setCanvasWidth('100%');
setScale(1.0);
} else {
const ratio = width / 1223;
const ratio = width / 1002;
setCanvasWidth('1223px');
setSpace(29);
setScale(ratio);
@@ -54,56 +54,56 @@ function QuickStart() {
>
<WorkFlow
content="为开发者提供数据智能标注与数据回流服务"
buttonText="数据准备"
buttonTop={40}
buttonText="数据标注"
buttonTop={20}
x={left}
y={309}
onClick={() => navigate('/datasetPreparation/datasetAnnotation')}
/>
<WorkFlow
{/* <WorkFlow
content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用"
buttonText="开发环境"
buttonTop={20}
x={left + 192 + space}
y={301}
onClick={() => navigate('/developmentEnvironment')}
/>
/> */}
<WorkFlow
content="为开发者提供定制化编辑器,开发者可根据自己需求选择配置,保存编译器中的调试环境为镜像供训练使用"
tips="可视化建模Designer"
buttonText="流水线"
buttonText="流水线模板"
buttonTop={20}
x={left + 2 * (192 + space)}
x={left + 1 * (192 + space)}
y={276}
onClick={() => navigate('/pipeline/template')}
/>
<WorkFlow
content="开发者可以在这里运行流水线模板,产生实验实例,对比实验训练过程与产生的实验训练数据"
buttonText="实验"
buttonTop={40}
x={left + 3 * (192 + space)}
buttonTop={20}
x={left + 2 * (192 + space)}
y={295}
onClick={() => navigate('/pipeline/experiment')}
/>
<WorkFlow
content="支持异构硬件(CPU/GPU)的模型加载,高吞吐,低延迟;支持大规模复杂模型的一键部署,实时弹性扩缩容;提供完整的运维监控体系。"
tips="模型在线服务"
buttonText="模型在线部署"
buttonText="服务"
buttonTop={20}
x={left + 4 * (192 + space) + 60 + space}
x={left + 3 * (192 + space) + 60 + space}
y={263}
onClick={() => navigate('/dataset/modelDeployment')}
/>
<div
className={styles['quick-start__content__canvas__model']}
style={{ top: '358px', left: left + 4 * (192 + space) + 'px' }}
style={{ top: '358px', left: left + 3 * (192 + space) + 'px' }}
>
<KFIcon type="icon-moxingguanli" font={38} />
<span>模型管理</span>
</div>
<div
className={styles['quick-start__content__canvas__task']}
style={{ top: '110px', left: left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 }}
style={{ top: '110px', left: left + 1 * (192 + space) + 56 + taskLeftArrowWidth + 16 }}
>
<KFIcon type="icon-tiaoduguanli" font={13} style={{ marginRight: '5px' }} />
<span>任务自动调度</span>
@@ -117,7 +117,7 @@ function QuickStart() {
arrorwTop={-4}
borderBottom={1}
/>
<WorkArrow
{/* <WorkArrow
x={left + 2 * 192 + space + 1}
y={378}
width={arrowWidth}
@@ -125,9 +125,9 @@ function QuickStart() {
arrowLeft={arrowWidth}
arrorwTop={-4}
borderBottom={1}
/>
/> */}
<WorkArrow
x={left + 4 * 192 + 3 * space + 1}
x={left + 3 * 192 + 2 * space + 1}
y={378}
width={arrowWidth + 10}
height={1}
@@ -136,7 +136,7 @@ function QuickStart() {
borderBottom={1}
/>
<WorkArrow
x={left + 4 * 192 + 60 + 4 * space + 1 - 10}
x={left + 3 * 192 + 60 + 3 * space + 1 - 10}
y={378}
width={arrowWidth + 10}
height={1}
@@ -145,7 +145,7 @@ function QuickStart() {
borderBottom={1}
/>
<WorkArrow
x={left + 2 * (192 + space) + 56}
x={left + 1 * (192 + space) + 56}
y={139}
width={taskLeftArrowWidth}
height={120}
@@ -155,7 +155,7 @@ function QuickStart() {
borderTop={1}
/>
<WorkArrow
x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4}
x={left + 1 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4}
y={127}
width={taskRightArrowWidth}
height={156}


+ 7
- 7
react-ui/src/pages/Workspace/components/TotalStatistics/index.less View File

@@ -3,16 +3,16 @@
align-items: center;
justify-content: center;
height: 140px;
padding: 0 16px;
padding: 0 35px;

// 媒体查询
@media screen and (max-width: 1600px) {
flex: auto;
}
// // 媒体查询
// @media screen and (max-width: 1600px) {
// flex: auto;
// }

&__icon {
width: 63px;
margin-right: 16px;
width: 80px;
margin-right: 20px;
}

&__title {


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

Loading…
Cancel
Save