Compare commits

...

240 Commits

Author SHA1 Message Date
  zhaowei f1c7d0ad15 fix:注释掉修改头像 8 months ago
  zhaowei 431903b1de feat: 领域知识图谱菜单移动到多形态资源库 9 months ago
  zhaowei 4220ad1323 feat: 调整用户头像的大小 9 months ago
  zhaowei 6f9354ddc0 feat: 重新设计个人中心 9 months ago
  zhaowei 03136fe4c2 feat: 重新设计个人中心 9 months ago
  zhaowei 4fe48f99d1 feat: app 启动优化 9 months ago
  zhaowei 8fda69e19a feat: 开发环境添加代码配置 9 months ago
  zhaowei 3a3f98d0f1 feat: 代码选择分页数组 9 months ago
  zhaowei e24f5c4876 feat: 代码选择分页数量为18 9 months ago
  zhaowei 29cfff97ec feat: 代码选择分页数量为21 9 months ago
  zhaowei cff33a8e62 feat: 代码选择分页数量为20 9 months ago
  zhaowei 2fdbf62b2b feat: 代码配置选择回显选中的 9 months ago
  zhaowei 5fb95438bd feat: 修复token失效之后,重新登录,创建表单成功后返回到登录界面的问题 9 months ago
  zhaowei 3518879e12 feat: 选择模型添加版本描述 9 months ago
  zhaowei e91aade6af feat: 修改使用指南 9 months ago
  zhaowei b11b4d9f78 fix: 数据集、模型版本不能是origin 9 months ago
  zhaowei e8af394523 fix: 超参数寻优添加可视化对比iframe 9 months ago
  zhaowei eafca94b60 fix: 导出到数据集添加is_public参数 9 months ago
  zhaowei cd4071149b fix: 导出到数据集添加owner参数 9 months ago
  zhaowei f11582bc64 fix: 数据集和模型回退时分页没有设置 9 months ago
  zhaowei fddb63d293 fix: 流水线模板配置参数修改,历史实验实例配置参数变换 9 months ago
  zhaowei fd7f0008c8 Merge branch 'dev-check' into dev-zw 9 months ago
  zhaowei 531beedac6 chore: merge 9 months ago
  zhaowei 0e8efbb692 fix: 工作空间添加代码配置和服务数量 9 months ago
  chenzhihang 742be0bb03 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 9 months ago
  zhaowei 512aa61b05 fix: 调整工作空间快速开始按钮的偏移 9 months ago
  chenzhihang b6273098c6 新增代码,服务统计 9 months ago
  zhaowei 921bc1d49d fix: 调整工作空间样式 9 months ago
  zhaowei 78eb6ee12f fix: 修复控制参数转成object的问题 9 months ago
  cp3hnu 57a0ec1040 Merge pull request '合并' (#258) from dev-zw into dev-check 9 months ago
  cp3hnu 154c834223 Merge pull request '合并' (#257) from dev-zw into dev-check 9 months ago
  zhaowei 560ff3411c fix: 实验隐藏流水线控制参数 9 months ago
  zhaowei bd269cffbf fix: 流水线隐藏参数 9 months ago
  cp3hnu 6646aff405 Merge pull request '合并' (#256) from dev-zw into dev-check 9 months ago
  zhaowei 1b497e1743 fix: 自动机器学习添加算法描述 9 months ago
  cp3hnu ff69531b22 Merge pull request '合并' (#255) from dev-zw into dev-check 9 months ago
  chenzhihang 399d611607 优化用户 9 months ago
  chenzhihang 8e024c9813 优化用户 9 months ago
  chenzhihang 5c9f59887c 优化实验 9 months ago
  chenzhihang b3e5eb08e9 优化实验 9 months ago
  chenzhihang 6efe25a3f0 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 9 months ago
  chenzhihang 548016091e 优化实验 9 months ago
  cp3hnu 5d61ade863 Merge pull request '合并' (#254) from dev-zw into dev-check 9 months ago
  zhaowei 458621fe91 fix: 自主机器学习改为自动机器学习 9 months ago
  chenzhihang 55b49047c3 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 9 months ago
  chenzhihang 597e16f81f 优化实验 9 months ago
  zhaowei aef75f4a37 fix: 修改角色 9 months ago
  cp3hnu 153b7de3ec Merge pull request '合并' (#253) from dev-zw into dev-check 9 months ago
  chenzhihang 4f415719f8 优化主动学习 9 months ago
  chenzhihang aa410acdfe 优化 9 months ago
  chenzhihang 4252f1710d 优化项目分页查询 10 months ago
  chenzhihang 23af257179 优化项目分页查询 10 months ago
  chenzhihang f0b70feca7 优化项目分页查询 10 months ago
  chenzhihang b014c9ce92 优化项目分页查询 10 months ago
  chenzhihang 976fb1dce5 优化项目分页查询 10 months ago
  chenzhihang 3a5845b623 优化项目分页查询 10 months ago
  chenzhihang b811bb51cd 优化项目分页查询 10 months ago
  chenzhihang 90c958b974 优化项目分页查询 10 months ago
  chenzhihang 19a4d6aed3 优化项目分页查询 10 months ago
  chenzhihang 6e27e5da0d 优化项目分页查询 10 months ago
  chenzhihang 3d0ea6603f 优化项目分页查询 10 months ago
  chenzhihang dc7e8dc801 优化项目分页查询 10 months ago
  chenzhihang 8e1f5fc587 优化 10 months ago
  chenzhihang 746314e5d2 优化 10 months ago
  chenzhihang d169d3d9db 优化 10 months ago
  chenzhihang 7a939f3f29 优化 10 months ago
  chenzhihang dc6cf41651 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 17c1c86043 测试 10 months ago
  cp3hnu a46a948760 Merge pull request '合并' (#252) from dev-zw into dev-check 10 months ago
  zhaowei 40ca029363 fix:自动机器学习添加mlp算法 10 months ago
  zhaowei 767a208732 fix:工作空间实验运行时长动态变化 10 months ago
  cp3hnu b314476ac3 Merge pull request '合并' (#251) from dev-zw into dev-check 10 months ago
  chenzhihang 39c0c2c01a 优化 10 months ago
  chenzhihang 873dd0ed5e Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 2a101d7f47 优化 10 months ago
  cp3hnu bcff86269b Merge pull request '合并' (#250) from dev-zw into dev-check 10 months ago
  zhaowei b2b74686ca fix: 自动机器学习创建时间改为更新时间 10 months ago
  chenzhihang 316cca31a4 优化排序 10 months ago
  zhaowei d1c41934b0 fix: 预测有两个loading 10 months ago
  zhaowei 274b8612e9 fix: 流水线模型部署服务版本验证 10 months ago
  chenzhihang df7460a01b Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 48d46a0723 优化用户 10 months ago
  zhaowei 8f72953683 fix: 服务日志样式错误 10 months ago
  cp3hnu 62bd8049ba Merge pull request '合并' (#249) from dev-zw into dev-check 10 months ago
  zhaowei 1ec43a60cf fix: 添加预测加载状态 10 months ago
  cp3hnu c42cf77939 Merge pull request '合并' (#248) from dev-zw into dev-check 10 months ago
  chenzhihang 85c8a3e6dc Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 96bf3351db 优化积分扣除结束 10 months ago
  cp3hnu b2ac5877d0 Merge pull request '合并' (#247) from dev-zw into dev-check 10 months ago
  zhaowei 0bace90a23 fix: 全局参数删除脱敏的配置 10 months ago
  cp3hnu 4c73d4339d Merge branch 'dev-zw' of code.gitlink.org.cn:ci4s/ci4sManagement-cloud into dev-zw 10 months ago
  cp3hnu ef9a78b167 fix: 最近更新时间 10 months ago
  chenzhihang 68ad21fadf 优化 10 months ago
  chenzhihang 696f939295 优化 10 months ago
  chenzhihang cc98a699d4 优化 10 months ago
  chenzhihang c11c728c71 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 31334c3a11 优化 10 months ago
  cp3hnu 3066853aeb Merge pull request '合并' (#246) from dev-zw into dev-check 10 months ago
  zhaowei f3f9846dff fix: 模型指标对比图错误 10 months ago
  cp3hnu 7715ee272d Merge pull request '合并' (#245) from dev-zw into dev-check 10 months ago
  zhaowei 65c588ac8a fix: 集成模型数量>=1 10 months ago
  zhaowei 34e2b8bb05 fix: mlp 显示成tablenet 10 months ago
  chenzhihang ff07d4c4a8 优化用户 10 months ago
  chenzhihang 6ad1910cb0 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 879c2e5802 优化更新ray 10 months ago
  chenzhihang 50dce4003f 优化用户 10 months ago
  cp3hnu e4385544b1 Merge pull request '合并dev-zw' (#244) from dev-zw into dev-check 10 months ago
  cp3hnu 4a6f4d2120 fix: 退出登录获取label-studio地址 10 months ago
  cp3hnu 8ef236f5b4 feat: 修改服务调用指南 10 months ago
  chenzhihang a5914e67c8 优化 10 months ago
  chenzhihang cccb2ae8c2 测试登录 10 months ago
  chenzhihang e5978fde0c 测试登录 10 months ago
  chenzhihang e76a0d7544 测试登录 10 months ago
  chenzhihang d19f0cfa82 优化 10 months ago
  chenzhihang d3508e6eba Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang b42a63f209 优化 10 months ago
  cp3hnu 603b943699 Merge pull request '合并dev' (#243) from dev into dev-check 10 months ago
  cp3hnu c8fc258089 Merge pull request '合并dev-zw' (#242) from dev-zw into dev 10 months ago
  cp3hnu 53fe983462 fix: 用户管理界面无法退出登录 10 months ago
  chenzhihang 1b63a74ce0 优化 10 months ago
  chenzhihang c4a3275358 优化 10 months ago
  chenzhihang fc026e9d15 优化 10 months ago
  chenzhihang 9d3e0ad5f3 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang a2555f6ac3 优化 10 months ago
  cp3hnu fefc545dad Merge pull request '合并dev' (#241) from dev into dev-check 10 months ago
  cp3hnu b419ab9485 Merge pull request '合并dev-zw' (#240) from dev-zw into dev 10 months ago
  cp3hnu 16d4b476f2 fix: 分配用户创建时间为null 10 months ago
  cp3hnu b8049721df fix: 退出登录两次 10 months ago
  chenzhihang 272ec6ef97 优化 10 months ago
  chenzhihang 46eff25e01 优化 10 months ago
  chenzhihang dcc591cacd Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 4da7042f61 优化 10 months ago
  chenzhihang 236ac4da0f 优化 10 months ago
  cp3hnu 2b0c11525b test: 添加 giturl 的测试 10 months ago
  cp3hnu 67a2b41240 Merge pull request '合并dev' (#239) from dev into dev-check 10 months ago
  cp3hnu 7de2295191 Merge pull request '合并dev-zw' (#238) from dev-zw into dev 10 months ago
  cp3hnu 0ac624ed2a fix: 实验无法查看更多 10 months ago
  chenzhihang 277ed0a710 优化积分扣除结束 10 months ago
  chenzhihang 2aaa8a9fe7 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 19b797189d 优化积分扣除 10 months ago
  cp3hnu 2d5538251e Merge pull request '合并dev' (#237) from dev into dev-check 10 months ago
  cp3hnu b142d5985d Merge pull request '合并dev-zw' (#236) from dev-zw into dev 10 months ago
  cp3hnu 14a9629167 fix: 添加iframe 加载失败日志 10 months ago
  chenzhihang 48fdf61dff 优化积分扣除 10 months ago
  chenzhihang 48851a2d4b 优化积分查询 10 months ago
  cp3hnu 5fe010f52d fix: 服务只有运行中才显示预测 10 months ago
  cp3hnu e6f74dc513 Merge pull request '合并dev' (#235) from dev into dev-check 10 months ago
  cp3hnu e4ffcea914 Merge pull request '合并dev' (#234) from dev-zw into dev 10 months ago
  cp3hnu 44673f39ed fix: 实验状态不同步 10 months ago
  chenzhihang 88227a85cb 优化运行开发环境 10 months ago
  chenzhihang a0d17e6dd3 优化查询pod状态 10 months ago
  chenzhihang adf3b8d02a 优化查询pod状态 10 months ago
  chenzhihang 596aa80315 优化查询pod状态 10 months ago
  chenzhihang 7b146651c7 优化创建pod 10 months ago
  chenzhihang eb50e76a54 优化实验状态查询 10 months ago
  chenzhihang a2f1c0532b 优化dvc 10 months ago
  chenzhihang 694f142b3f 优化积分扣除,优化dvc 10 months ago
  chenzhihang cdceefcb24 优化实验状态查询 10 months ago
  chenzhihang 1cfbd5185c 优化积分扣除 10 months ago
  chenzhihang bee9f43762 优化 10 months ago
  chenzhihang aa3909a047 优化 10 months ago
  chenzhihang 111ade2f49 优化项目排序 10 months ago
  chenzhihang 6670aa0658 优化项目排序 10 months ago
  chenzhihang 6748c46d6f 优化项目排序 10 months ago
  chenzhihang d6ed84ac59 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 5a7a188478 优化积分更新 10 months ago
  cp3hnu 5d641cb61b Merge pull request '合并dev' (#233) from dev into dev-check 10 months ago
  chenzhihang 6809f62b9c 优化 10 months ago
  cp3hnu 8110739002 Merge pull request '合并dev-zw' (#232) from dev-zw into dev 10 months ago
  cp3hnu 1fd40f927c fix: 用户账号支持4-15位 10 months ago
  chenzhihang b02d15662f Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 20b1e78df7 优化 10 months ago
  cp3hnu d7ddcefb96 fix: 自动机器学习创建时间有误 10 months ago
  cp3hnu 1f08241ca9 Merge pull request '合并dev' (#231) from dev into dev-check 10 months ago
  cp3hnu b4873208c2 Merge pull request '合并dev-zw' (#230) from dev-zw into dev 10 months ago
  chenzhihang 25381c26ae 优化 10 months ago
  chenzhihang 567263b11a Merge remote-tracking branch 'origin/dev' into dev-check 10 months ago
  chenzhihang cdb3fafd14 Merge branch 'dev' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev 10 months ago
  chenzhihang 25dec31eaa 优化 10 months ago
  cp3hnu 5b8a10006d feat: 删除机器人 10 months ago
  zhaowei b0d8a19975 feat: 快速开始删除开发环境 10 months ago
  cp3hnu f5d4158532 Merge pull request '合并dev' (#229) from dev into dev-check 10 months ago
  cp3hnu dcb73eacf9 Merge pull request '合并dev-zw' (#228) from dev-zw into dev 10 months ago
  fanshuai 84dbb1e8fb 修改BUG 【开发环境】新创建开发环境处于列表底部,未处于列表前列 10 months ago
  cp3hnu 95cfbefda4 feat: 自动机器学习运行&获取tensorboard状态 10 months ago
  fanshuai ae86b2a835 Merge remote-tracking branch 'origin/dev-check' into dev-check 10 months ago
  fanshuai 68ee173591 修改BUG 【工作空间】AI资产卡片数据统计错误 10 months ago
  chenzhihang 71edeb6922 优化状态更新 10 months ago
  chenzhihang a4b82fb9f5 优化状态更新 10 months ago
  cp3hnu 9a89988e95 fix: 删除“构建中”状态镜像版本,构建成功/失败状态返回后重新显示在列表 10 months ago
  cp3hnu b9f4c48ea6 fix: 位于大于筛选结果的页码,点击左侧边栏筛选,页面提示暂无数据 10 months ago
  chenzhihang 8270406d3c 优化http请求 10 months ago
  chenzhihang 870fbce684 优化查询代码配置bug 10 months ago
  cp3hnu 7a4852908b fix: 添加镜像版本描述 10 months ago
  chenzhihang ce0d898af8 优化积分扣除 10 months ago
  chenzhihang b5fd8eb031 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang b9b0db8442 优化异常提示 10 months ago
  chenzhihang a2cf8fe75e 优化 10 months ago
  cp3hnu 6bb5a9e8e6 Merge pull request '合并dev' (#227) from dev into dev-check 10 months ago
  cp3hnu aa980985d5 Merge pull request '合并dev-zw' (#226) from dev-zw into dev 10 months ago
  cp3hnu b4d99f8e5c fix: 代码配置30202改为30203 10 months ago
  chenzhihang 5d49e2d1b5 优化镜像版本描述 10 months ago
  chenzhihang 20ce4e4758 优化镜像版本描述 10 months ago
  chenzhihang b994fb3f31 优化 10 months ago
  chenzhihang c0320a2a68 优化 10 months ago
  chenzhihang 41c2faf9cb 优化 10 months ago
  cp3hnu e92ac40694 fix: 列表运行时长和详情运行时长不一致 10 months ago
  chenzhihang 45990fa2b7 优化 10 months ago
  cp3hnu 9c62812424 fix: 列表运行时长和详情运行时长不一致 10 months ago
  chenzhihang d1928d702b 优化 10 months ago
  chenzhihang eea826de2c 优化构建镜像 10 months ago
  chenzhihang 55fd9d7271 优化代码配置 10 months ago
  chenzhihang 3905841db7 优化状态更新 10 months ago
  chenzhihang 0a7be4e261 优化结束时间 10 months ago
  chenzhihang 47f2c80a00 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 9def5a4d7a 优化状态更新 10 months ago
  cp3hnu 7fc016c3a0 Merge pull request '合并dev' (#225) from dev into dev-check 10 months ago
  cp3hnu 260812125a Merge pull request '合并dev-zw' (#224) from dev-zw into dev 10 months ago
  cp3hnu 22f85fb3ae fix: 实验取流水线节点 10 months ago
  chenzhihang 765f37ab7c 优化主动学习 10 months ago
  chenzhihang 57296d364e 优化主动学习 10 months ago
  chenzhihang 04a9f8f125 优化主动学习 10 months ago
  chenzhihang 640c49f507 优化 10 months ago
  chenzhihang 85c92e8fae 优化主动学习 10 months ago
  chenzhihang 7b51ed5bc6 优化主动学习 10 months ago
  chenzhihang b78b0075cd 优化主动学习 10 months ago
  chenzhihang e4dd26c87d 优化 10 months ago
  chenzhihang f5f3aca3e5 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang 5aa9cdc62f 优化 10 months ago
  cp3hnu 5d60751a66 feat: 内嵌tesorboard 10 months ago
  cp3hnu 0e54f6e95d Merge pull request '合并dev' (#223) from dev into dev-check 10 months ago
  cp3hnu bc18fb14c2 Merge pull request '合并dev-zw' (#222) from dev-zw into dev 10 months ago
  cp3hnu c103375c8c fix: 自动机器学习日志一片空白 10 months ago
  cp3hnu ebd1d8680a fix: 全选时可以选中运行的实验实例 10 months ago
  chenzhihang ac95c975a5 Merge branch 'dev-check' of https://gitlink.org.cn/ci4s/ci4sManagement-cloud into dev-check 10 months ago
  chenzhihang c16c9d75a3 Merge remote-tracking branch 'origin/dev' into dev-check 10 months ago
  cp3hnu b6f0ffbfe1 style: 调整样式 10 months ago
  cp3hnu 039ad81fc7 Merge pull request '合并dev' (#221) from dev into dev-check 10 months ago
  cp3hnu e553a21c2f Merge pull request '合并dev-zw' (#220) from dev-zw into dev 10 months ago
  zhaowei c719141676 feat: 验收 10 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