Compare commits

...

261 Commits

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

+ 1
- 0
.gitignore View File

@@ -65,3 +65,4 @@ mvnw
/react-ui/types/tsconfig.tsbuildinfo /react-ui/types/tsconfig.tsbuildinfo
/react-ui/storybook-static /react-ui/storybook-static
/react-ui/.storybook/scripts /react-ui/.storybook/scripts
/react-ui/dist.zip

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

@@ -18,6 +18,11 @@ spec:
image: ${k8s-5auth-image} image: ${k8s-5auth-image}
ports: ports:
- containerPort: 9200 - 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 apiVersion: v1
@@ -28,9 +33,15 @@ metadata:
spec: spec:
type: NodePort type: NodePort
ports: ports:
- port: 9200
- name: http
port: 9200
nodePort: 31206 nodePort: 31206
protocol: TCP protocol: TCP
- name: debug
nodePort: 31221
port: 5005
protocol: TCP
targetPort: 5005
selector: selector:
app: ci4s-auth app: ci4s-auth



+ 2
- 2
react-ui/config/proxy.ts View File

@@ -22,8 +22,8 @@ export default {
// 要代理的地址 // 要代理的地址
// target: 'http://172.20.32.197:31213', // 开发环境 // target: 'http://172.20.32.197:31213', // 开发环境
// target: 'http://172.20.32.235:31213', // 测试环境 // target: 'http://172.20.32.235:31213', // 测试环境
target: 'http://172.20.32.44:8082',
// target: 'http://172.20.32.150:8082',
// target: 'http://172.20.32.44:8082',
target: 'http://172.20.32.164:8082',
// 配置了这个可以从 http 代理到 https // 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie // 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true, changeOrigin: true,


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

@@ -141,12 +141,23 @@ export default [
{ {
name: '实验对比', name: '实验对比',
path: 'compare', 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: '实验实例详情', name: '实验实例详情',
path: 'instance/:experimentId/:id', 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: '*', path: '*',
layout: false, layout: false,


+ 1294
- 0
react-ui/mock/components.ts
File diff suppressed because it is too large
View File


+ 2
- 2
react-ui/package.json View File

@@ -8,7 +8,7 @@
"build": "max build", "build": "max build",
"deploy": "npm run build && npm run gh-pages", "deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev", "dev": "npm run start:dev",
"dev-no-sso": "cross-env NO_SSO=true npm run start:dev",
"dev-no-sso": "cross-env NO_SSO=true npm run start:mock",
"docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
"docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
@@ -35,7 +35,7 @@
"serve": "umi-serve", "serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev", "start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev", "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev",
"start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev",
"start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",


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'; } from './services/session';
import './styles/menu.less'; import './styles/menu.less';
import { needAuth } from './utils'; import { needAuth } from './utils';
import { closeAllModals } from './utils/modal';
import { gotoLoginPage } from './utils/ui'; import { gotoLoginPage } from './utils/ui';
export { requestConfig as request } from './requestConfig'; export { requestConfig as request } from './requestConfig';


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


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

// console.log('getInitialState', needAuth(location.pathname));
if (needAuth(location.pathname)) {
const token = getAccessToken();
if (token) {
const currentUser = await fetchUserInfo(); const currentUser = await fetchUserInfo();
return { return {
fetchUserInfo, fetchUserInfo,
@@ -71,9 +68,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
return { return {
ErrorBoundary: ErrorBoundary, ErrorBoundary: ErrorBoundary,
rightContentRender: false, rightContentRender: false,
waterMarkProps: {
// content: initialState?.currentUser?.nickName,
},
menu: { menu: {
locale: false, locale: false,
// 每当 initialState?.currentUser?.userid 发生修改时重新执行 request // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
@@ -84,45 +78,9 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
if (!initialState?.currentUser?.userId) { if (!initialState?.currentUser?.userId) {
return []; return [];
} }
// console.log('get menus')
// initialState.currentUser 中包含了所有用户信息
// console.log('get routers')
// setInitialState((preInitialState) => ({
// ...preInitialState,
// menus,
// }));
return getRemoteMenu(); 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) => { childrenRender: (children) => {
// 增加一个 loading 的状态 // 增加一个 loading 的状态
// if (initialState?.loading) return <PageLoading />; // if (initialState?.loading) return <PageLoading />;
@@ -159,9 +117,26 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
}; };


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

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

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

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


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

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

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

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


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


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


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

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


&:hover &__name {
&--active &__name {
color: @primary-color; 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 { 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 classNames from 'classnames';
import { useState } from 'react'; import { useState } from 'react';
import styles from './index.less'; import styles from './index.less';


type CodeConfigItemProps = { type CodeConfigItemProps = {
item: CodeConfigData; 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 [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 ( 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' }}> <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 <div
className={classNames( className={classNames(
styles['code-config-item__tag'], styles['code-config-item__tag'],
@@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
className={styles['code-config-item__url']} className={styles['code-config-item__url']}
ellipsis={{ ellipsis={{
rows: 2, 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} {item.git_url}
</Typography.Paragraph> </Typography.Paragraph>


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

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


import CodeSelectorModal from '@/components/CodeSelectorModal';
import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd'; import { Button } from 'antd';
@@ -18,7 +18,9 @@ export {
type ParameterInputValue, type ParameterInputValue,
} from '../ParameterInput'; } from '../ParameterInput';


type CodeSelectProps = ParameterInputProps;
export interface CodeSelectProps extends ParameterInputProps {
value?: CodeConfigData;
}


/** 代码配置选择表单组件 */ /** 代码配置选择表单组件 */
function CodeSelect({ function CodeSelect({
@@ -32,26 +34,18 @@ function CodeSelect({
}: CodeSelectProps) { }: CodeSelectProps) {
// 选择代码配置 // 选择代码配置
const selectResource = () => { const selectResource = () => {
const defaultSelected: CodeConfigData | undefined =
value && typeof value === 'object' ? value : undefined;
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected: defaultSelected,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } =
res;
const jsonObj = {
id,
name: code_repo_name,
code_path: git_url,
branch: git_branch,
username: git_user_name,
password: git_password,
ssh_private_key: ssh_key,
};
const jsonObjStr = JSON.stringify(jsonObj);
const { code_repo_name } = res;
onChange?.({ onChange?.({
value: jsonObjStr,
...res,
value: code_repo_name,
showValue: code_repo_name, showValue: code_repo_name,
fromSelect: true, fromSelect: true,
...jsonObj,
}); });
} else { } else {
onChange?.(undefined); onChange?.(undefined);


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

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


&__empty { &__empty {


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

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


export { type CodeConfigData }; 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'> { 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 [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>({
current: 1,
pageSize: 20,
});
const [searchText, setSearchText] = useState<string | undefined>(undefined); const [searchText, setSearchText] = useState<string | undefined>(undefined);
const [inputText, setInputText] = 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(() => { useEffect(() => {
// 获取数据请求 // 获取数据请求
const getDataList = async () => { const getDataList = async () => {
// 为 0 时,不请求,等待接口返回选中的代码配置在第几页
if (pagination.current === 0) {
return;
}
const params = { const params = {
page: pagination.current! - 1, page: pagination.current! - 1,
size: pagination.pageSize, size: pagination.pageSize,
@@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
getDataList(); getDataList();
}, [pagination, searchText]); }, [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) => { const handleSearch = (value: string) => {
setSearchText(value); 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="选择代码配置" title="选择代码配置"
image={require('@/assets/img/modal-code-config.png')} image={require('@/assets/img/modal-code-config.png')}
width={920} width={920}
footer={null}
onOk={() => onOk?.(selected)}
destroyOnClose destroyOnClose
> >
<div className="kf-code-selector-modal"> <div className="kf-code-selector-modal">
@@ -93,23 +152,31 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
prefix={ prefix={
<KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} />
} }
// prefix={
// <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} />
// }
/> />
{dataList?.length !== 0 ? ( {dataList?.length !== 0 ? (
<> <>
<div className="kf-code-selector-modal__content"> <div className="kf-code-selector-modal__content">
{dataList?.map((item) => ( {dataList?.map((item) => (
<CodeConfigItem item={item} key={item.id} onClick={handleClick} />
<CodeConfigItem
item={item}
key={item.id}
checked={item.id === selected?.id}
onChange={handleChange}
/>
))} ))}
</div> </div>
<Pagination <Pagination
align="center"
align="end"
total={total} total={total}
showSizeChanger showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
defaultPageSize={DefaultPageSize}
pageSizeOptions={[
DefaultPageSize,
2 * DefaultPageSize,
3 * DefaultPageSize,
4 * DefaultPageSize,
5 * DefaultPageSize,
]}
showQuickJumper showQuickJumper
onChange={handlePageChange} onChange={handlePageChange}
{...pagination} {...pagination}


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

@@ -1,3 +1,4 @@
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { formatEnum } from '@/utils/format'; import { formatEnum } from '@/utils/format';
import { Typography, type SelectProps } from 'antd'; import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -16,6 +17,8 @@ type FormInfoProps = {
options?: SelectProps['options']; options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */ /** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames']; fieldNames?: SelectProps['fieldNames'];
/** 全局参数 */
globalParams?: PipelineGlobalParam[] | null;
/** 自定义类名 */ /** 自定义类名 */
className?: string; className?: string;
/** 自定义样式 */ /** 自定义样式 */
@@ -32,12 +35,29 @@ function FormInfo({
select = false, select = false,
options, options,
fieldNames, fieldNames,
globalParams,
className, className,
style, style,
}: FormInfoProps) { }: FormInfoProps) {
let showValue = value; let showValue = value;
if (value && typeof value === 'object' && valuePropName) { if (value && typeof value === 'object' && valuePropName) {
showValue = value[valuePropName]; showValue = value[valuePropName];
const reg = /^\$\{(.*)\}$/;
if (value.fromSelect && Array.isArray(globalParams) && globalParams.length > 0) {
const match = reg.exec(showValue);
if (match) {
const paramName = match[1];
const foundParam = globalParams.find((v) => v.param_name === paramName);
if (foundParam) {
showValue =
foundParam.param_type === PipelineGlobalParamType.Boolean // 布尔类型转换
? foundParam.param_value
? 'true'
: 'false'
: foundParam.param_value;
}
}
}
} else if (select === true && options) { } else if (select === true && options) {
let _options: SelectProps['options'] = options; let _options: SelectProps['options'] = options;
if (fieldNames) { if (fieldNames) {


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

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


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


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


/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ /** 系统内嵌 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 [iframeUrl, setIframeUrl] = useState('');
const [loading, setLoading] = useState(false);
// const [loading, setLoading] = useState(false);


useEffect(() => { useEffect(() => {
const requestIframeUrl = async () => {
setLoading(true);
const requestIframeUrl = async (type: IframePageType) => {
if (showLoading) {
Loading.show();
}
const [res] = await to(getRequestAPI(type)()); const [res] = await to(getRequestAPI(type)());
if (res && res.data) { if (res && res.data) {
setIframeUrl(res.data); setIframeUrl(res.data);
} else { } 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 ( return (
<div className={classNames('kf-iframe-page', className)} style={style}> <div className={classNames('kf-iframe-page', className)} style={style}>
{loading && createPortal(<KFSpin size="large" />, document.body)}
<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')} />} {openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />}
</div> </div>
); );


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

@@ -9,6 +9,7 @@ import { CloseOutlined } from '@ant-design/icons';
import { ConfigProvider, Form, Input, Typography } from 'antd'; import { ConfigProvider, Form, Input, Typography } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReactNode } from 'react';
import './index.less'; import './index.less';


// 如果值是对象时的类型 // 如果值是对象时的类型
@@ -55,6 +56,8 @@ export interface ParameterInputProps {
disabled?: boolean; disabled?: boolean;
/** 元素 id */ /** 元素 id */
id?: string; id?: string;
/** 带标签的 input,设置后置标签 */
addonAfter?: ReactNode;
} }


function ParameterInput({ function ParameterInput({
@@ -75,7 +78,7 @@ function ParameterInput({
const valueObj = const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) { if (valueObj && !valueObj.showValue) {
valueObj.showValue = valueObj.value;
valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : '';
} }
const isSelect = valueObj?.fromSelect; const isSelect = valueObj?.fromSelect;
const placeholder = valueObj?.placeholder || rest?.placeholder; const placeholder = valueObj?.placeholder || rest?.placeholder;


+ 83
- 39
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -1,29 +1,34 @@
import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config';
import { ServiceData } from '@/pages/ModelDeployment/types'; import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment'; import { getServiceListReq } from '@/services/modelDeployment';
import type { JCCResourceImage, JCCResourceStandard, JCCResourceType } from '@/state/jcdResource';
import { filterResourceStandard, resourceFieldNames } from '@/state/systemResource';
import { type SelectProps } from 'antd'; import { type SelectProps } from 'antd';
import { pick } from 'lodash';

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


export type SelectPropsConfig = { export type SelectPropsConfig = {
getOptions: () => Promise<any>; // 获取下拉数据
getOptions?: () => Promise<any>; // 获取下拉数据
fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
filterOption?: SelectProps['filterOption']; // 过滤函数 filterOption?: SelectProps['filterOption']; // 过滤函数
isObjectValue: boolean; // value 是对象
getValue?: (value: any) => string | number; // 对象类型时,获取其值
getLabel?: (value: any) => string; // 对象类型时,获取其 label
}; };


export const paramSelectConfig: Record<string, SelectPropsConfig> = {
export const ParameterSelectTypeList = [
'dataset',
'model',
'service',
'resource',
'remote-image',
'remote-resource-type',
'remote-resource',
] as const;

export type ParameterSelectDataType = (typeof ParameterSelectTypeList)[number];

export const paramSelectConfig: Record<ParameterSelectDataType, SelectPropsConfig> = {
dataset: { dataset: {
getOptions: async () => { getOptions: async () => {
const res = await getDatasetList({ const res = await getDatasetList({
@@ -31,13 +36,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
}, },
fieldNames: {
label: 'name',
value: 'id',
optionFilterProp: 'label',
getValue: (value: DatasetData) => {
return value.id;
},
getLabel: (value: DatasetData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
model: { model: {
getOptions: async () => { getOptions: async () => {
@@ -46,13 +54,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
},
optionFilterProp: 'label',
getValue: (value: ModelData) => {
return value.id;
}, },
fieldNames: {
label: 'name',
value: 'id',
getLabel: (value: ModelData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
service: { service: {
getOptions: async () => { getOptions: async () => {
@@ -60,25 +71,58 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
page: 0, page: 0,
size: 1000, size: 1000,
}); });
return (
res?.data?.content?.map((item: ServiceData) => ({
label: item.service_name,
value: JSON.stringify(pick(item, ['id', 'service_name'])),
})) ?? []
);
},
fieldNames: {
label: 'label',
value: 'value',
return res?.data?.content ?? [];
}, },
optionFilterProp: 'label', optionFilterProp: 'label',
getValue: (value: ServiceData) => {
return value.id;
},
getLabel: (value: ServiceData) => {
return value.service_name;
},
isObjectValue: true,
}, },
resource: { resource: {
getOptions: async () => {
// 不需要这个函数
return [];
},
fieldNames: resourceFieldNames, fieldNames: resourceFieldNames,
filterOption: filterResourceStandard as SelectProps['filterOption'], filterOption: filterResourceStandard as SelectProps['filterOption'],
isObjectValue: false,
},
'remote-resource-type': {
optionFilterProp: 'label',
isObjectValue: false,
getValue: (value: JCCResourceType) => {
return value.value;
},
getLabel: (value: JCCResourceType) => {
return value.label;
},
},
'remote-image': {
optionFilterProp: 'label',
getValue: (value: JCCResourceImage) => {
return value.imageID;
},
getLabel: (value: JCCResourceImage) => {
return value.name;
},
isObjectValue: true,
},
'remote-resource': {
optionFilterProp: 'label',
getValue: (value: JCCResourceStandard) => {
return value.id;
},
getLabel: (value: JCCResourceStandard) => {
const cpu = value.baseResourceSpecs.find((v) => v.type === 'CPU');
const ram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'RAM');
const vram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'VRAM');
const cpuText = cpu ? `CPU:${cpu.availableValue}, ` : '';
const ramText = ram ? `内存: ${ram.availableValue}${ram.availableUnit?.toUpperCase()}` : '';
const vramText = vram
? `(显存${vram.availableValue}${vram.availableUnit?.toUpperCase()})`
: '';
return `${value.type}: ${value.availableCount}*${value.name}${vramText}, ${cpuText}${ramText}`;
},
isObjectValue: true,
}, },
}; };

+ 102
- 41
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -4,19 +4,25 @@
* @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/ */


import { useComputingResource } from '@/hooks/useComputingResource';
import jccResourceState from '@/state/jcdResource';
import systemResourceState, { getSystemResources } from '@/state/systemResource';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { useSnapshot } from '@umijs/max';
import { Select, type SelectProps } from 'antd'; import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import FormInfo from '../FormInfo'; import FormInfo from '../FormInfo';
import { paramSelectConfig } from './config';
import { paramSelectConfig, type ParameterSelectDataType } from './config';

export { ParameterSelectTypeList, type ParameterSelectDataType } from './config';


export type ParameterSelectObject = { export type ParameterSelectObject = {
value: any; value: any;
[key: string]: any; [key: string]: any;
}; };


export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource';
type SelectOptions = SelectProps['options'];

const identityFunc = (value: any) => value;


export interface ParameterSelectProps extends SelectProps { export interface ParameterSelectProps extends SelectProps {
/** 类型 */ /** 类型 */
@@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps {
display?: boolean; display?: boolean;
/** 值,支持对象,对象必须包含 value */ /** 值,支持对象,对象必须包含 value */
value?: string | ParameterSelectObject; value?: string | ParameterSelectObject;
/** 用于流水线, 流水线资源规格要求 id 为字符串 */
isPipeline?: boolean;
/** 修改后回调 */ /** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void; onChange?: (value: string | ParameterSelectObject) => void;
} }
@@ -36,69 +40,126 @@ function ParameterSelect({
dataType, dataType,
display = false, display = false,
value, value,
isPipeline = false,
onChange, onChange,
...rest ...rest
}: ParameterSelectProps) { }: ParameterSelectProps) {
const [options, setOptions] = useState<SelectProps['options']>([]);
const [options, setOptions] = useState<SelectOptions>([]);
const propsConfig = paramSelectConfig[dataType]; const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;
const [resourceStandardList] = useComputingResource();
const computingResource = isPipeline
? resourceStandardList.map((v) => ({
...v,
id: String(v.id),
}))
: resourceStandardList;
const {
getLabel = identityFunc,
getValue = identityFunc,
getOptions,
filterOption,
fieldNames,
optionFilterProp,
isObjectValue,
} = propsConfig;
const selectValue = typeof value === 'object' && value !== null ? value.value : value;
// 数据集、模型、服务,对象转换成 json 字符串
const valueText =
typeof selectValue === 'object' && selectValue !== null ? getValue(selectValue) : selectValue;
const jccResourceSnap = useSnapshot(jccResourceState);
const { getResourceTypes } = jccResourceSnap;
const systemResourceSnap = useSnapshot(systemResourceState);

const objectOptions = useMemo(() => {
return dataType === 'remote-resource-type'
? jccResourceSnap.types
: dataType === 'remote-image'
? jccResourceSnap.images
: dataType === 'remote-resource'
? jccResourceSnap.resources
: options;
}, [dataType, options, jccResourceSnap.types, jccResourceSnap.images, jccResourceSnap.resources]);

// 将对象类型转换为 Select Options
const converObjectToOptions = useCallback(
(v: any) => {
return {
label: getLabel(v),
value: getValue(v),
};
},
[getLabel, getValue],
);

// 数据集、模型、服务获取数据后,进行转换
const objectSelectOptions = useMemo(() => {
return objectOptions?.map(converObjectToOptions);
}, [converObjectToOptions, objectOptions]);

// 快速得到选中的对象
const valueMap = useMemo(() => {
const map = new Map<string | number, any>();
objectOptions?.forEach((v) => {
map.set(getValue(v), v);
});

return map;
}, [objectOptions, getValue]);


useEffect(() => { useEffect(() => {
// 获取下拉数据 // 获取下拉数据
const getSelectOptions = async () => { const getSelectOptions = async () => {
if (!propsConfig) {
return;
}
const getOptions = propsConfig.getOptions;
const [res] = await to(getOptions());
if (res) {
setOptions(res);
if (getOptions) {
const [res] = await to(getOptions());
if (res) {
setOptions(res);
}
} else if (dataType === 'remote-resource-type') {
getResourceTypes();
} else if (dataType === 'resource') {
getSystemResources();
} }
}; };

getSelectOptions(); getSelectOptions();
}, [propsConfig]);
}, [getOptions, dataType, getResourceTypes]);


const selectOptions = dataType === 'resource' ? computingResource : options;
const selectOptions = (
dataType === 'resource' ? systemResourceSnap.resources : objectSelectOptions
) as SelectOptions;


const handleChange = (text: string) => { const handleChange = (text: string) => {
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: text,
});
// 数据集、模型、服务,转换成对象
if (isObjectValue) {
// 设置为 null 是因为 antv g6 的 bug
// 如果值为 undefined 时, graph.changeData(data) 会保留前面的值
const selectValue = text ? valueMap.get(text) : null;
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
} else { } else {
onChange?.(text);
const selectValue = text ? text : '';
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
} }
}; };


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


return ( return (
<Select <Select
{...rest} {...rest}
filterOption={propsConfig?.filterOption}
options={selectOptions} options={selectOptions}
fieldNames={propsConfig?.fieldNames}
optionFilterProp={propsConfig?.optionFilterProp}
fieldNames={fieldNames}
optionFilterProp={optionFilterProp}
filterOption={filterOption}
value={valueText} value={valueText}
onChange={handleChange} onChange={handleChange}
showSearch showSearch


+ 29
- 44
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -10,10 +10,10 @@ import ResourceSelectorModal, {
ResourceSelectorType, ResourceSelectorType,
selectorTypeConfig, selectorTypeConfig,
} from '@/components/ResourceSelectorModal'; } from '@/components/ResourceSelectorModal';
import { CommonTabKeys } from '@/enums';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button, ConfigProvider } from 'antd'; import { Button, ConfigProvider } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { pick } from 'lodash';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less'; import './index.less';


@@ -27,6 +27,8 @@ export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse
interface ResourceSelectProps extends ParameterInputProps { interface ResourceSelectProps extends ParameterInputProps {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
type: ResourceSelectorType; type: ResourceSelectorType;
/** 值 */
value?: ResourceSelectorResponse;
} }


// 获取选择数据集、模型、镜像后面按钮 icon // 获取选择数据集、模型、镜像后面按钮 icon
@@ -47,68 +49,51 @@ function ResourceSelect({
}: ResourceSelectProps) { }: ResourceSelectProps) {
const { componentSize } = ConfigProvider.useConfig(); const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize; const mySize = size || componentSize;
let selectedResource: ResourceSelectorResponse | undefined = undefined;
if (
value &&
typeof value === 'object' &&
value.activeTab &&
value.id &&
value.name &&
value.version &&
value.path &&
(type === ResourceSelectorType.Mirror || (value.identifier && value.owner))
) {
selectedResource = pick(value, [
'activeTab',
'id',
'identifier',
'name',
'owner',
'version',
'path',
]) as ResourceSelectorResponse;
let defaultActiveTab: CommonTabKeys | undefined,
defaultExpandedKeys: string[] = [],
defaultCheckedKeys: string[] = [];
if (value && typeof value === 'object') {
defaultActiveTab = value.activeTab;
if (type === ResourceSelectorType.Mirror) {
if (value.image_id && value.id) {
defaultExpandedKeys = [`${value.image_id}`];
defaultCheckedKeys = [`${value.image_id}-${value.id}`];
}
} else if (value.id && value.version) {
defaultExpandedKeys = [value.id];
defaultCheckedKeys = [`${value.id}-${value.version}`];
}
} }


// 选择数据集、模型、镜像 // 选择数据集、模型、镜像
const selectResource = () => { const selectResource = () => {
const { close } = openAntdModal(ResourceSelectorModal, { const { close } = openAntdModal(ResourceSelectorModal, {
type, type,
defaultExpandedKeys: selectedResource ? [selectedResource.id] : [],
defaultCheckedKeys: selectedResource
? [`${selectedResource.id}-${selectedResource.version}`]
: [],
defaultActiveTab: selectedResource?.activeTab,
defaultExpandedKeys: defaultExpandedKeys,
defaultCheckedKeys: defaultCheckedKeys,
defaultActiveTab: defaultActiveTab,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
const { activeTab, ...rest } = res;
const { url } = rest;
onChange?.({ onChange?.({
value: path,
showValue: path,
...rest,
value: url,
showValue: url,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
id,
name,
version,
path,
}); });
} else { } else {
const jsonObj = {
id,
name,
version,
path,
identifier,
owner,
};
const jsonObjStr = JSON.stringify(jsonObj);
const { activeTab, ...rest } = res;
const { name, version } = rest;
const showValue = `${name}:${version}`; const showValue = `${name}:${version}`;
onChange?.({ onChange?.({
value: jsonObjStr,
...rest,
value: showValue,
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
...jsonObj,
}); });
} }
} else { } else {


+ 28
- 17
react-ui/src/components/ResourceSelectorModal/config.tsx View File

@@ -1,8 +1,8 @@
import datasetImg from '@/assets/img/modal-select-dataset.png'; import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png'; import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.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 { MirrorVersionData } from '@/pages/Mirror/Info';
import { MirrorData } from '@/pages/Mirror/List'; import { MirrorData } from '@/pages/Mirror/List';
import { import {
@@ -24,11 +24,11 @@ export enum ResourceSelectorType {
} }


// 数据集、模型列表转为树形结构 // 数据集、模型列表转为树形结构
const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => {
const convertDatasetToTreeData = (list: ResourceData[], isPublic: boolean): TreeDataNode[] => {
return list.map((v) => ({ return list.map((v) => ({
...v, ...v,
key: `${v.id}`, key: `${v.id}`,
title: v.name,
title: isPublic ? `${v.name} (${v.owner})` : v.name,
isLeaf: false, isLeaf: false,
checkable: false, checkable: false,
})); }));
@@ -52,9 +52,9 @@ const convertResourceVersionToTreeData = (
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: ResourceVersionData) => ({ return list.map((item: ResourceVersionData) => ({
...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']), ...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']),
version: item.name,
title: item.name,
key: `${parentId}-${item.name}`, key: `${parentId}-${item.name}`,
title: item.name,
version: item.name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
})); }));
@@ -66,9 +66,9 @@ const convertMirrorVersionToTreeData = (
list: MirrorVersionData[], list: MirrorVersionData[],
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: MirrorVersionData) => ({ return list.map((item: MirrorVersionData) => ({
url: item.url,
title: item.tag_name,
...item,
key: `${parentId}-${item.id}`, key: `${parentId}-${item.id}`,
title: item.tag_name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
})); }));
@@ -106,7 +106,7 @@ export class DatasetSelector implements SelectorTypeInfo {
const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) { if (res && res.data) {
const list = res.data.content || []; const list = res.data.content || [];
return convertDatasetToTreeData(list);
return convertDatasetToTreeData(list, isPublic);
} else { } else {
return Promise.reject('获取数据集列表失败'); return Promise.reject('获取数据集列表失败');
} }
@@ -125,11 +125,16 @@ export class DatasetSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getDatasetInfo(params); const res = await getDatasetInfo(params);
if (res && res.data) { 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 { return {
path, path,
content: list, content: list,
versionDesc,
}; };
} else { } else {
return Promise.reject('获取数据集文件列表失败'); return Promise.reject('获取数据集文件列表失败');
@@ -158,7 +163,7 @@ export class ModelSelector implements SelectorTypeInfo {
const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) { if (res && res.data) {
const list = res.data.content || []; const list = res.data.content || [];
return convertDatasetToTreeData(list);
return convertDatasetToTreeData(list, isPublic);
} else { } else {
return Promise.reject('获取模型列表失败'); return Promise.reject('获取模型列表失败');
} }
@@ -177,11 +182,17 @@ export class ModelSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getModelInfo(params); const res = await getModelInfo(params);
if (res && res.data) { 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 { return {
path, path,
content: list, content: list,
versionDesc,
}; };
} else { } else {
return Promise.reject('获取模型文件列表失败'); return Promise.reject('获取模型文件列表失败');
@@ -224,8 +235,7 @@ export class MirrorSelector implements SelectorTypeInfo {
image_id: parentKey, image_id: parentKey,
page: 0, page: 0,
size: 2000, size: 2000,
status: 'available',
state: 1,
status: MirrorVersionStatus.Available,
}); });
if (res && res.data) { if (res && res.data) {
const list = res.data.content || []; const list = res.data.content || [];
@@ -236,7 +246,7 @@ export class MirrorSelector implements SelectorTypeInfo {
} }


async getFiles(_parentKey: string, parentNode: MirrorVersionData) { async getFiles(_parentKey: string, parentNode: MirrorVersionData) {
const { url } = parentNode;
const { url, description } = parentNode;
return { return {
path: url, path: url,
content: [ content: [
@@ -245,6 +255,7 @@ export class MirrorSelector implements SelectorTypeInfo {
file_name: `${url}`, 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); border-bottom: 1px solid rgba(22, 100, 255, 0.1);
} }
&__files { &__files {
height: calc(100% - 75px);
height: calc(100% - 61px);
overflow-y: auto; overflow-y: auto;


&__file { &__file {
@@ -76,7 +76,22 @@
word-break: break-all; word-break: break-all;
background: rgba(4, 3, 3, 0.06); background: rgba(4, 3, 3, 0.06);
border-radius: 4px; 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);
}
} }
} }

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

@@ -8,6 +8,7 @@ import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CommonTabKeys } from '@/enums'; import { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config'; import { ResourceFileData } from '@/pages/Dataset/config';
import { type MirrorVersionData } from '@/pages/Mirror/Info';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd'; import { Input, Tabs, Tree } from 'antd';
@@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig };
// 选择数据集、模型、镜像的返回类型 // 选择数据集、模型、镜像的返回类型
export type ResourceSelectorResponse = { export type ResourceSelectorResponse = {
activeTab: CommonTabKeys; // 是我的还是公开的 activeTab: CommonTabKeys; // 是我的还是公开的
id: string; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
version: string; // 数据集\模型\镜像版本
path: string; // 数据集\模型\镜像版本路径
identifier: string; // 数据集\模型 identifier,镜像这个字段为空
owner: string; // 数据集\模型 owner,镜像这个字段为空
};
id: string; // 数据集\模型 id
name: string; // 数据集\模型 name
identifier: string; // 数据集\模型 identifier
owner: string; // 数据集\模型 owner
version: string; // 数据集\模型 version
path: string; // 数据集\模型 版本路径
} & MirrorVersionData;


export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
@@ -84,6 +85,7 @@ function ResourceSelectorModal({
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]); const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFileData[]>([]); const [files, setFiles] = useState<ResourceFileData[]>([]);
const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined);
const [versionPath, setVersionPath] = useState(''); const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [firstLoadList, setFirstLoadList] = useState(false); const [firstLoadList, setFirstLoadList] = useState(false);
@@ -119,6 +121,7 @@ function ResourceSelectorModal({
setCheckedKeys([]); setCheckedKeys([]);
setLoadedKeys([]); setLoadedKeys([]);
setFiles([]); setFiles([]);
setVersionDesc(undefined);
setVersionPath(''); setVersionPath('');
setSearchText(''); setSearchText('');
getTreeData(); getTreeData();
@@ -169,9 +172,11 @@ function ResourceSelectorModal({
if (res) { if (res) {
setVersionPath(res.path); setVersionPath(res.path);
setFiles(res.content); setFiles(res.content);
setVersionDesc(res.versionDesc);
} else { } else {
setVersionPath(''); setVersionPath('');
setFiles([]); setFiles([]);
setVersionDesc(undefined);
} }
}; };


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


@@ -236,15 +242,22 @@ function ResourceSelectorModal({
const name = (treeNode?.title ?? '') as string; const name = (treeNode?.title ?? '') as string;
const identifier = (treeNode?.identifier ?? '') as string; const identifier = (treeNode?.identifier ?? '') as string;
const owner = (treeNode?.owner ?? '') as string; const owner = (treeNode?.owner ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
identifier,
owner,
activeTab: activeTab,
};
const childNode = treeNode.children.filter((v: TreeDataNode) => v.key === last)[0];
const res =
type === ResourceSelectorType.Mirror
? {
activeTab: activeTab,
...childNode,
}
: {
activeTab: activeTab,
id: Number(id),
name,
path: versionPath,
version,
identifier,
owner,
};
onOk?.(res); onOk?.(res);
} else { } else {
onOk?.(undefined); onOk?.(undefined);
@@ -253,8 +266,9 @@ function ResourceSelectorModal({


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


@@ -312,14 +326,24 @@ function ResourceSelectorModal({
/> />
</div> </div>
<div className={styles['model-selector__right']}> <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> </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> </div>
</div> </div>


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

@@ -1,14 +1,16 @@
import { clearSessionToken } from '@/access'; import { clearSessionToken } from '@/access';
import DefaultAvatar from '@/assets/img/avatar-default.png';
import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { setRemoteMenu } from '@/services/session'; import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth'; import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types'; import { ClientInfo } from '@/types';
import { sleep } from '@/utils/promise';
import { sleep, to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage, oauthLogout } from '@/utils/ui'; import { gotoLoginPage, oauthLogout } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components'; import { setAlpha } from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css'; 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 { Avatar, Spin } from 'antd';
import type { MenuInfo } from 'rc-menu/lib/interface'; import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react'; 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 AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
const navigate = useNavigate();
/** /**
* 退出登录,并且将当前的 url 保存 * 退出登录,并且将当前的 url 保存
*/ */
const loginOut = async () => { 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 秒后跳转,希望子系统能完成注销 // 至少 1 秒后跳转,希望子系统能完成注销
await Promise.all([logout(), sleep(1000)]); await Promise.all([logout(), sleep(1000)]);
clearSessionToken(); clearSessionToken();
setRemoteMenu(null); setRemoteMenu(null);
gotoLoginPage();
// 退出 oauth2
const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
if (clientInfo) { if (clientInfo) {
const { logoutUri } = clientInfo; const { logoutUri } = clientInfo;
location.replace(logoutUri); location.replace(logoutUri);
} else {
gotoLoginPage();
} }
}; };
const actionClassName = useEmotionCss(({ token }) => { const actionClassName = useEmotionCss(({ token }) => {
@@ -102,9 +117,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
loginOut(); loginOut();
return; return;
} }
history.push(`/account/${key}`);
navigate(`/account/${key}`);
}, },
[setInitialState],
[setInitialState, navigate],
); );


const loading = ( 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) { function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) {
const [now] = useServerTime(); 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(() => { useEffect(() => {
if (finishTime) { if (finishTime) {
setCurrentTime(new Date(finishTime)); setCurrentTime(new Date(finishTime));
} else { } else {
setCurrentTime(now());
const timer = setInterval(() => { const timer = setInterval(() => {
setCurrentTime(now()); setCurrentTime(now());
}, 1000); }, 1000);
@@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr
}; };
} }
}, [finishTime, now]); }, [finishTime, now]);

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


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

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


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


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


// 自动化任务类型 // 自动化任务类型
@@ -163,3 +163,11 @@ export enum AutoMLTrailStatus {
CANCELLED = 'CANCELLED', // 取消 CANCELLED = 'CANCELLED', // 取消
MEMOUT = 'MEMOUT', // 内存溢出 MEMOUT = 'MEMOUT', // 内存溢出
} }

// 流水线组件类型
export enum ComponentType {
Ref = 'ref',
Select = 'select',
Map = 'map',
Str = 'str',
}

+ 71
- 47
react-ui/src/hooks/useComputingResource.ts View File

@@ -4,66 +4,90 @@
* @Description: 资源规格 hook * @Description: 资源规格 hook
*/ */


import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { to } from '@/utils/promise';
import { type SelectProps } from 'antd';
import { useCallback, useEffect, useState } from 'react';
// import { getComputingResourceReq } from '@/services/pipeline';
// import { ComputingResource } from '@/types';
// import { to } from '@/utils/promise';
// import { type SelectProps } from 'antd';
// import { useCallback, useEffect, useState } from 'react';


const computingResource: ComputingResource[] = [];
// const computingResource: ComputingResource[] = [];


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


/** 资源规格字段 */
export const resourceFieldNames = {
label: 'description',
value: 'id',
};
// /** 资源规格字段 */
// export const resourceFieldNames = {
// label: 'description',
// value: 'id',
// };


/** 获取资源规格 */
export function useComputingResource() {
const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);
// /** 获取资源规格 */
// export function useComputingResource() {
// const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);


useEffect(() => {
// 获取资源规格列表数据
const getComputingResource = async () => {
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && Array.isArray(res.data.content)) {
setResourceStandardList(res.data.content);
computingResource.splice(0, computingResource.length, ...res.data.content);
}
};
// useEffect(() => {
// // 获取资源规格列表数据
// const getComputingResource = async () => {
// const params = {
// page: 0,
// size: 1000,
// resource_type: '',
// };
// const [res] = await to(getComputingResourceReq(params));
// if (res && res.data && Array.isArray(res.data.content)) {
// setResourceStandardList(res.data.content);
// computingResource.splice(0, computingResource.length, ...res.data.content);
// }
// };

// if (computingResource.length > 0) {
// setResourceStandardList(computingResource);
// } else {
// getComputingResource();
// }
// }, []);


if (computingResource.length > 0) {
setResourceStandardList(computingResource);
} else {
getComputingResource();
}
// // 根据 standard 获取 description
// const getDescription = useCallback(
// (id?: string | number) => {
// if (!id) {
// return undefined;
// }
// return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description;
// },
// [resourceStandardList],
// );

// return [resourceStandardList, getDescription] as const;
// }

import state, { getSystemResources } from '@/state/systemResource';
import { useSnapshot } from '@umijs/max';
import { useCallback, useEffect } from 'react';

export const useSystemResource = () => {
useEffect(() => {
getSystemResources();
}, []); }, []);


// 根据 standard 获取 description
const snap = useSnapshot(state);
/* 根据 standard 获取 description */
const getDescription = useCallback( const getDescription = useCallback(
(id?: string | number) => { (id?: string | number) => {
if (!id) { if (!id) {
return undefined; return undefined;
} }
return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description;
return snap.resources.find((item) => Number(item.id) === Number(id))?.description;
}, },
[resourceStandardList],
[snap.resources],
); );


return [resourceStandardList, getDescription] as const;
}
return getDescription;
};

+ 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 { ExperimentStatus } from '@/enums';
import { NodeStatus } from '@/types'; 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(() => { useEffect(() => {
if (isRunning) { if (isRunning) {
const { origin } = location; const { origin } = location;
@@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name:
const dataJson = parseJsonText(data); const dataJson = parseJsonText(data);
const statusData = dataJson?.result?.object?.status; const statusData = dataJson?.result?.object?.status;
if (statusData) { 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 () => { return () => {
evtSource.close(); 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)); const [res] = await to(getActiveLearnInsReq(instanceId));
if (res && res.data) { if (res && res.data) {
const info = res.data as ActiveLearnInstanceData; 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); const paramJson = parseJsonText(param);
if (paramJson) { if (paramJson) {
setExperimentInfo({ setExperimentInfo({
...paramJson.data, ...paramJson.data,
create_time,
}); });
} }


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


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


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

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


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


+ 7
- 11
react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx View File

@@ -1,6 +1,6 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { import {
classifierAlgorithms, classifierAlgorithms,
FrameworkType, FrameworkType,
@@ -28,18 +28,18 @@ type BasicInfoProps = {
info?: ActiveLearnData; info?: ActiveLearnData;
className?: string; className?: string;
isInstance?: boolean; isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus; instanceStatus?: ExperimentStatus;
}; };


function BasicInfo({ function BasicInfo({
info, info,
className, className,
runStatus,
workflowStatus,
instanceStatus, instanceStatus,
isInstance = false, isInstance = false,
}: BasicInfoProps) { }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();
const basicDatas: BasicInfoData[] = useMemo(() => { const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) { if (!info) {
return []; return [];
@@ -154,7 +154,7 @@ function BasicInfo({
value: info.dataset_py, value: info.dataset_py,
}, },
{ {
label: '数据集类名',
label: '数据集处理类名',
value: info.dataset_class_name, value: info.dataset_class_name,
}, },
{ {
@@ -212,12 +212,8 @@ function BasicInfo({


return ( return (
<div className={classNames(styles['active-learn-basic'], className)}> <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 && ( {!isInstance && (
<ConfigInfo <ConfigInfo


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

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


function ExecuteConfig() { function ExecuteConfig() {
const form = Form.useFormInstance(); 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 ( return (
<> <>
<SubAreaTitle <SubAreaTitle
@@ -101,16 +106,16 @@ function ExecuteConfig() {
<Row gutter={8}> <Row gutter={8}>
<Col span={10}> <Col span={10}>
<Form.Item <Form.Item
label="数据集类名"
label="数据集处理类名"
name="dataset_class_name" name="dataset_class_name"
rules={[ rules={[
{ {
required: true, required: true,
message: '请输入数据集类名',
message: '请输入数据集处理类名',
}, },
]} ]}
> >
<Input placeholder="请输入数据集类名" maxLength={64} showCount allowClear />
<Input placeholder="请输入数据集处理类名" maxLength={64} showCount allowClear />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
@@ -488,7 +493,12 @@ function ExecuteConfig() {
}, },
]} ]}
> >
<Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear />
<Select
placeholder="请选择查询策略"
options={queryStrategiesOptions}
showSearch
allowClear
/>
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </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', label: 'upper_confidence_bound',
value: '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 { ExperimentStatus } from '@/enums';
import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types'; import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types';
import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty';
import LogList from '@/pages/Experiment/components/LogList'; import LogList from '@/pages/Experiment/components/LogList';
import { NodeStatus } from '@/types'; import { NodeStatus } from '@/types';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
@@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />, // icon: <KFIcon type="icon-rizhi1" />,
children: ( children: (
<div className={styles['experiment-log__tabs__log']}> <div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
{trainCloneNodeStatus ? (
<LogList <LogList
instanceName={instanceInfo.argo_ins_name} instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns} instanceNamespace={instanceInfo.argo_ins_ns}
@@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={trainCloneNodeStatus.startedAt} instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList> ></LogList>
) : (
<EmptyLog />
)} )}
</div> </div>
), ),
@@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />, // icon: <KFIcon type="icon-rizhi1" />,
children: ( children: (
<div className={styles['experiment-log__tabs__log']}> <div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
{hpoNodeStatus ? (
<LogList <LogList
instanceName={instanceInfo.argo_ins_name} instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns} instanceNamespace={instanceInfo.argo_ins_ns}
@@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={hpoNodeStatus.startedAt} instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList> ></LogList>
) : (
<EmptyLog />
)} )}
</div> </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); setSessionToken(access_token, access_token, expires_in);
message.success('登录成功!'); message.success('登录成功!');
await fetchUserInfo(); await fetchUserInfo();
history.push(redirect || '/');
history.replace(redirect || '/');
} }
}, [fetchUserInfo, redirect, code]); }, [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)); const [res] = await to(getExperimentInsReq(instanceId));
if (res && res.data) { if (res && res.data) {
const info = res.data as AutoMLInstanceData; 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); setType(type);
// 解析配置参数 // 解析配置参数
@@ -59,7 +59,6 @@ function AutoMLInstance() {
if (paramJson) { if (paramJson) {
setAutoMLInfo({ setAutoMLInfo({
...paramJson.data, ...paramJson.data,
create_time,
type, type,
}); });
} }
@@ -95,7 +94,10 @@ function AutoMLInstance() {
}; };


const setupSSE = (name: string, namespace: string) => { 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 params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource( const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
@@ -110,17 +112,17 @@ function AutoMLInstance() {
if (dataJson) { if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes; const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) { if (nodes) {
// 节点
// 设置节点
setNodes(nodes); setNodes(nodes);


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

if (workflowStatus) { if (workflowStatus) {
setWorkflowStatus(workflowStatus); setWorkflowStatus(workflowStatus);


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


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

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


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


+ 46
- 11
react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx View File

@@ -6,11 +6,22 @@ import {
autoMLEnsembleClassOptions, autoMLEnsembleClassOptions,
autoMLTaskTypeOptions, autoMLTaskTypeOptions,
} from '@/enums'; } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import {
classificationAlgorithms,
featureAlgorithms,
regressorAlgorithms,
} from '@/pages/AutoML/components/CreateForm/utils';
import { AutoMLData } from '@/pages/AutoML/types'; import { AutoMLData } from '@/pages/AutoML/types';
import { type NodeStatus } from '@/types'; import { type NodeStatus } from '@/types';
import { parseJsonText } from '@/utils'; 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 classNames from 'classnames';
import { useMemo } from 'react'; import { useMemo } from 'react';
import ExperimentRunBasic from '../ExperimentRunBasic'; import ExperimentRunBasic from '../ExperimentRunBasic';
@@ -21,6 +32,7 @@ const formatOptimizeMode = (value: boolean) => {
return value ? '越大越好' : '越小越好'; return value ? '越大越好' : '越小越好';
}; };


// 格式化权重
const formatMetricsWeight = (value: string) => { const formatMetricsWeight = (value: string) => {
if (!value) { if (!value) {
return '--'; return '--';
@@ -34,22 +46,37 @@ const formatMetricsWeight = (value: string) => {
.join('\n'); .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 = { type AutoMLBasicProps = {
info?: AutoMLData; info?: AutoMLData;
className?: string; className?: string;
isInstance?: boolean; isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus; instanceStatus?: ExperimentStatus;
instanceCreateTime?: string;
}; };


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


return ( return (
<div className={classNames(styles['auto-ml-basic'], className)}> <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 && ( {!isInstance && (
<ConfigInfo <ConfigInfo


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

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


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

@@ -0,0 +1,85 @@
// 分类算法
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: 'miss_value_impute (缺失值填充)', value: 'miss_value_impute' },
{ 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; display: flex;
align-items: center; align-items: center;
width: 200px; 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) { }: ExperimentInstanceListProps) {
const { message } = App.useApp(); const { message } = App.useApp();
const allIntanceIds = useMemo(() => { 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]); }, [experimentInsList]);
const [ const [
selectedIns, selectedIns,
@@ -126,7 +133,12 @@ function ExperimentInstanceList({
<div> <div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}> <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>
<div className={styles.index}>序号</div> <div className={styles.index}>序号</div>
<div className={styles.description}>运行时长</div> <div className={styles.description}>运行时长</div>
@@ -171,12 +183,8 @@ function ExperimentInstanceList({
{index + 1} {index + 1}
</a> </a>
<ExperimentInstanceComponent <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> ></ExperimentInstanceComponent>
<div className={styles.operation}> <div className={styles.operation}>
<Button <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 { ExperimentStatus } from '@/enums';
import { useSSE, type MessageHandler } from '@/hooks/useSSE'; import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance, NodeStatus } from '@/types';
import { ExperimentCompleted } from '@/utils/constant'; import { ExperimentCompleted } from '@/utils/constant';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { getWorkflowStatus } from '@/utils/experiment';
import { Typography } from 'antd'; import { Typography } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import styles from './index.less'; 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( const handleSSEMessage: MessageHandler = useCallback(
(experimentInsId: number, status: string, finish_time: string) => {
(experimentId: number, experimentInsId: number, status: string, finishTime: string) => {
window.postMessage({ window.postMessage({
type: ExperimentCompleted, type: ExperimentCompleted,
payload: { payload: {
id: experimentInsId,
experimentId,
experimentInsId,
status, 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 ( return (
<React.Fragment> <React.Fragment>
<div className={styles.description}> <div className={styles.description}>
<RunDuration createTime={create_time} finishTime={finish_time} />
<RunDuration createTime={createTime} finishTime={finishTime} />
</div> </div>
<div className={styles.startTime}> <div className={styles.startTime}>
<Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}>
{formatDate(create_time)}
<Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}>
{formatDate(createTime)}
</Typography.Text> </Typography.Text>
</div> </div>
<div className={styles.statusBox}> <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> </div>
</React.Fragment> </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, batchDeleteActiveLearnInsReq,
deleteActiveLearnInsReq, deleteActiveLearnInsReq,
deleteActiveLearnReq, deleteActiveLearnReq,
editActiveLearnInsReq,
getActiveLearnInsListReq, getActiveLearnInsListReq,
getActiveLearnListReq, getActiveLearnListReq,
runActiveLearnReq, runActiveLearnReq,
@@ -17,6 +18,7 @@ import {
batchDeleteExperimentInsReq, batchDeleteExperimentInsReq,
deleteAutoMLReq, deleteAutoMLReq,
deleteExperimentInsReq, deleteExperimentInsReq,
editExperimentInsReq,
getAutoMLListReq, getAutoMLListReq,
getExperimentInsListReq, getExperimentInsListReq,
runAutoMLReq, runAutoMLReq,
@@ -26,6 +28,7 @@ import {
batchDeleteRayInsReq, batchDeleteRayInsReq,
deleteRayInsReq, deleteRayInsReq,
deleteRayReq, deleteRayReq,
editRayInsReq,
getRayInsListReq, getRayInsListReq,
getRayListReq, getRayListReq,
runRayReq, runRayReq,
@@ -39,18 +42,20 @@ export enum ExperimentListType {
} }


type ExperimentListInfo = { 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>; // 删除 deleteRecordReq: (params: any) => Promise<any>; // 删除
runRecordReq: (params: any) => Promise<any>; // 运行 runRecordReq: (params: any) => Promise<any>; // 运行
deleteInsReq: (params: any) => Promise<any>; // 删除实例 deleteInsReq: (params: any) => Promise<any>; // 删除实例
batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例
stopInsReq: (params: any) => Promise<any>; // 终止实例 stopInsReq: (params: any) => Promise<any>; // 终止实例
editInsReq: (params: any) => Promise<any>; // 编辑实例
title: string; // 标题 title: string; // 标题
pathPrefix: string; // 路由路径前缀 pathPrefix: string; // 路由路径前缀
idProperty: string; // ID属性 idProperty: string; // ID属性
nameProperty: string; // 名称属性 nameProperty: string; // 名称属性
descProperty: string; // 描述属性 descProperty: string; // 描述属性
idInsProperty: string; // 实例返回的ID属性
}; };


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

+ 127
- 62
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -30,7 +30,7 @@ import {
} from 'antd'; } from 'antd';
import { type SearchProps } from 'antd/es/input'; import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import ExperimentInstanceList from '../ExperimentInstanceList'; import ExperimentInstanceList from '../ExperimentInstanceList';
import { ExperimentListType, experimentListConfig } from './config'; import { ExperimentListType, experimentListConfig } from './config';
import styles from './index.less'; import styles from './index.less';
@@ -52,7 +52,6 @@ function ExperimentList({ type }: ExperimentListProps) {
const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]);
const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]);
const [experimentInsTotal, setExperimentInsTotal] = useState(0); const [experimentInsTotal, setExperimentInsTotal] = useState(0);
const [now] = useServerTime();
const [pagination, setPagination] = useState<TablePaginationConfig>( const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? { cacheState?.pagination ?? {
current: 1, current: 1,
@@ -60,38 +59,37 @@ function ExperimentList({ type }: ExperimentListProps) {
}, },
); );
const config = experimentListConfig[type]; 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( const getExperimentInsList = useCallback(
async (recordId: number, page: number, size: number) => {
async (recordId: number, page: number, size: number, skipLoading: boolean = false) => {
const params = { const params = {
[config.idProperty]: recordId, [config.idProperty]: recordId,
page: page, page: page,
size: size, size: size,
}; };
const request = config.getInsListReq; const request = config.getInsListReq;
const [res] = await to(request(params));
const [res] = await to(request(params, skipLoading));
if (res && res.data) { if (res && res.data) {
const { content = [], totalElements = 0 } = res.data; const { content = [], totalElements = 0 } = res.data;
try { try {
@@ -111,59 +109,115 @@ function ExperimentList({ type }: ExperimentListProps) {


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


// 刷新实验实例列表 // 刷新实验实例列表
const refreshExperimentIns = useCallback( const refreshExperimentIns = useCallback(
(experimentId: number) => {
(experimentId: number, skipLoading: boolean = false) => {
const length = experimentInsList.length; const length = experimentInsList.length;
getExperimentInsList(experimentId, 0, length);
getExperimentInsList(experimentId, 0, length, skipLoading);
}, },
[getExperimentInsList, experimentInsList], [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(() => { useEffect(() => {
const handleMessage = (e: MessageEvent) => { const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data; const { type, payload } = e.data;
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;
const { experimentId, experimentInsId, status /*finishTime*/ } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );

if (
!currentIns ||
currentIns.status === ExperimentStatus.Terminated ||
currentIns.status === status
) {
return;
}


// 修改实例的状态和结束时间
setExperimentInsList((prev) =>
prev.map((v) =>
v.id === id
? {
...v,
status: status,
finish_time: finish_time,
}
: v,
),
// refreshExperimentList(true);
// refreshExperimentIns(experimentId);
editExperimentIns(
experimentId,
experimentInsId,
status,
currentIns.argo_ins_name,
currentIns.argo_ins_ns,
); );


if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}

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


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


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


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


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


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


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


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


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


const NodePrefix = 'auto-ml'; const NodePrefix = 'auto-ml';
@@ -19,7 +20,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
return ( return (
<div className={styles['experiment-log']}> <div className={styles['experiment-log']}>
<div className={styles['experiment-log__log']}> <div className={styles['experiment-log__log']}>
{nodeStatus && (
{nodeStatus ? (
<LogList <LogList
instanceName={instanceInfo.argo_ins_name} instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns} instanceNamespace={instanceInfo.argo_ins_ns}
@@ -28,6 +29,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={nodeStatus.startedAt} instanceNodeStartTime={nodeStatus.startedAt}
instanceNodeStatus={nodeStatus.phase as ExperimentStatus} instanceNodeStatus={nodeStatus.phase as ExperimentStatus}
></LogList> ></LogList>
) : (
<EmptyLog />
)} )}
</div> </div>
</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 { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { type NodeStatus } from '@/types'; import { type NodeStatus } from '@/types';
import { getExperimentInstanceStatus } from '@/utils/experiment';
import { formatDate } from '@/utils/format'; import { formatDate } from '@/utils/format';
import { Flex } from 'antd'; import { Flex } from 'antd';
import { useMemo } from 'react'; import { useMemo } from 'react';


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


function ExperimentRunBasic({ create_time, runStatus, instanceStatus }: ExperimentRunBasicProps) {
function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) {
const instanceDatas = useMemo(() => { 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]; const statusInfo = experimentStatusInfo[status];


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


return ( return (
<ConfigInfo <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 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 { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; 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 = { type TensorBoardProps = {
namespace?: string; namespace?: string;
@@ -16,28 +21,75 @@ type TensorBoardProps = {
}; };


function ExperimentVisualResult({ namespace, path }: 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) { if (namespace && path) {
runTensorBoard(); 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; export default ExperimentVisualResult;

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

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


&__branch { &__branch {


+ 7
- 119
react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx View File

@@ -1,30 +1,8 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addDataset } from '@/services/dataset/index.js'; import { addDataset } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import {
getFileListFromEvent,
limitUploadFileType,
removeUploadedFile,
validateUploadFiles,
} from '@/utils/ui';
import {
Button,
Form,
Input,
Radio,
Select,
Upload,
UploadFile,
message,
type ModalProps,
type UploadProps,
} from 'antd';
import { omit } from 'lodash';
import { useState } from 'react';
import styles from './index.less';
import { Form, Input, Radio, Select, message, type ModalProps } from 'antd';


interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
typeList: CategoryData[]; typeList: CategoryData[];
@@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
} }


function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) {
const [uuid] = useState(Date.now());

// 上传组件参数
const uploadProps: UploadProps = {
action: resourceConfig[ResourceType.Dataset].uploadAction,
headers: {
Authorization: getAccessToken() || '',
},
defaultFileList: [],
accept: '.zip,.tgz',
beforeUpload: limitUploadFileType('zip,tgz'),
onRemove: removeUploadedFile,
};

// 上传请求 // 上传请求
const createDataset = async (params: any) => { const createDataset = async (params: any) => {
const [res] = await to(addDataset(params)); const [res] = await to(addDataset(params));
@@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr


// 提交 // 提交
const onFinish = (formData: any) => { const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const params = {
...omit(formData, ['fileList']),
dataset_source: DataSource.Create,
dataset_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {};
return {
file_name: data.fileName,
file_size: data.fileSize,
url: data.url,
};
}),
};
createDataset(params);
}
const params = {
...formData,
dataset_source: DataSource.Create,
};
createDataset(params);
}; };


return ( return (
@@ -108,30 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
> >
<Input placeholder="请输入数据名称" showCount allowClear maxLength={40} /> <Input placeholder="请输入数据名称" showCount allowClear maxLength={40} />
</Form.Item> </Form.Item>
<Form.Item
label="数据集版本"
name="version"
rules={[
{
required: true,
message: '请输入数据集版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item label="数据集分类" name="data_type"> <Form.Item label="数据集分类" name="data_type">
<Select <Select
allowClear allowClear
@@ -170,24 +99,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
allowClear allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item <Form.Item
label="可见性" label="可见性"
name="is_public" name="is_public"
@@ -198,29 +109,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
<Radio value={true}>公开</Radio> <Radio value={true}>公开</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item
label="数据集文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传数据集文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }}>
<Button
className={styles['upload-button']}
type="default"
icon={<KFIcon type="icon-shangchuan" />}
>
上传文件
</Button>
<div className={styles['upload-tip']}>只允许上传 .zip 和 .tgz 格式文件</div>
</Upload>
</Form.Item>
</Form> </Form>
</KFModal> </KFModal>
); );


+ 9
- 113
react-ui/src/pages/Dataset/components/AddModelModal/index.tsx View File

@@ -1,25 +1,8 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js'; import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui';
import {
Button,
Form,
Input,
Radio,
Select,
Upload,
UploadFile,
message,
type ModalProps,
type UploadProps,
} from 'antd';
import { omit } from 'lodash';
import { useState } from 'react';
import styles from '../AddDatasetModal/index.less';
import { Form, Input, Radio, Select, message, type ModalProps } from 'antd';


interface AddModelModalProps extends Omit<ModalProps, 'onOk'> { interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {
typeList: CategoryData[]; typeList: CategoryData[];
@@ -28,18 +11,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {
} }


function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) { function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) {
const [uuid] = useState(Date.now());

// 上传组件参数
const uploadProps: UploadProps = {
action: resourceConfig[ResourceType.Model].uploadAction,
headers: {
Authorization: getAccessToken() || '',
},
defaultFileList: [],
onRemove: removeUploadedFile,
};

// 上传请求 // 上传请求
const createModel = async (params: any) => { const createModel = async (params: any) => {
const [res] = await to(addModel(params)); const [res] = await to(addModel(params));
@@ -51,22 +22,11 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)


// 提交 // 提交
const onFinish = (formData: any) => { const onFinish = (formData: any) => {
const fileList: UploadFile[] = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const params = {
...omit(formData, ['fileList']),
model_source: DataSource.Create,
model_version_vos: fileList.map((item) => {
const data = item.response?.data?.[0] ?? {};
return {
file_name: data.fileName,
file_size: data.fileSize,
url: data.url,
};
}),
};
createModel(params);
}
const params = {
...formData,
model_source: DataSource.Create,
};
createModel(params);
}; };


return ( return (
@@ -99,34 +59,10 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
> >
<Input placeholder="请输入模型名称" showCount allowClear maxLength={40} /> <Input placeholder="请输入模型名称" showCount allowClear maxLength={40} />
</Form.Item> </Form.Item>
<Form.Item
label="模型版本"
name="version"
rules={[
{
required: true,
message: '请输入模型版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入模型版本" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item label="模型框架" name="model_type"> <Form.Item label="模型框架" name="model_type">
<Select <Select
allowClear allowClear
placeholder="请选择模型类型"
placeholder="请选择模型框架"
options={typeList} options={typeList}
fieldNames={{ label: 'name', value: 'name' }} fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
@@ -136,7 +72,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Form.Item label="模型能力" name="model_tag"> <Form.Item label="模型能力" name="model_tag">
<Select <Select
allowClear allowClear
placeholder="请选择模型标签"
placeholder="请选择模型能力"
options={tagList} options={tagList}
fieldNames={{ label: 'name', value: 'name' }} fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name" optionFilterProp="name"
@@ -161,24 +97,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
allowClear allowClear
/> />
</Form.Item> </Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item <Form.Item
label="可见性" label="可见性"
name="is_public" name="is_public"
@@ -189,28 +107,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Radio value={true}>公开</Radio> <Radio value={true}>公开</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item
label="模型文件"
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
rules={[
{
required: true,
message: '请上传模型文件',
},
]}
>
<Upload {...uploadProps} data={{ uuid: uuid }}>
<Button
className={styles['upload-button']}
type="default"
icon={<KFIcon type="icon-shangchuan" />}
>
上传文件
</Button>
</Upload>
</Form.Item>
</Form> </Form>
</KFModal> </KFModal>
); );


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

@@ -15,7 +15,7 @@ import {
type UploadProps, type UploadProps,
} from 'antd'; } from 'antd';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import styles from '../AddDatasetModal/index.less'; import styles from '../AddDatasetModal/index.less';


interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
@@ -40,6 +40,23 @@ function AddVersionModal({
}: AddVersionModalProps) { }: AddVersionModalProps) {
const [uuid] = useState(Date.now()); const [uuid] = useState(Date.now());
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];
const [form] = Form.useForm();

useEffect(() => {
const getNextVersion = async () => {
const request = config.getNextVersion;
const params = {
identifier,
owner,
};
const [res] = await to(request(params));
if (res && res.data) {
const nextVersion = res.data;
form.setFieldValue('version', nextVersion);
}
};
getNextVersion();
}, [identifier, owner, config, form]);


// 上传组件参数 // 上传组件参数
const uploadProps: UploadProps = { const uploadProps: UploadProps = {
@@ -109,6 +126,7 @@ function AddVersionModal({
}} }}
onFinish={onFinish} onFinish={onFinish}
autoComplete="off" autoComplete="off"
form={form}
> >
<Form.Item <Form.Item
label={`${name}名称`} label={`${name}名称`}
@@ -132,19 +150,21 @@ function AddVersionModal({
}, },
{ {
pattern: /^[a-zA-Z0-9._-]+$/, pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
}, },
{ {
validator: (_rule, value) => { validator: (_rule, value) => {
if (value === 'master') { if (value === 'master') {
return Promise.reject(`版本不能为 master`);
return Promise.reject(`${name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${name}版本不能为 origin`);
} }
return Promise.resolve(); return Promise.resolve();
}, },
}, },
]} ]}
> >
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear />
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"


+ 121
- 0
react-ui/src/pages/Dataset/components/EditVersionModal/index.tsx View File

@@ -0,0 +1,121 @@
import KFModal from '@/components/KFModal';
import { DataSource, ResourceData, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { to } from '@/utils/promise';
import { Form, Input, message, type ModalProps } from 'antd';

interface EditVersionModalProps extends Omit<ModalProps, 'onOk'> {
resourceType: ResourceType;
resourceVersion: ResourceData;
onOk: () => void;
}

function EditVersionModal({ resourceType, resourceVersion, onOk, ...rest }: EditVersionModalProps) {
const config = resourceConfig[resourceType];
const { name: resoureName, version, version_desc } = resourceVersion;

// 修改请求
const editDatasetVersion = async (params: any) => {
const request = config.editVersion;
const [res] = await to(request(params));
if (res) {
message.success('编辑成功');
onOk?.();
}
};

// 提交
const onFinish = (formData: any) => {
const params = {
...resourceVersion,
...formData,
[config.sourceParamKey]: DataSource.Create,
};
editDatasetVersion(params);
};

const name = config.name;

return (
<KFModal
{...rest}
title="编辑版本"
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
>
<Form
name="form"
layout="vertical"
initialValues={{
name: resoureName,
version: version,
version_desc: version_desc,
}}
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label={`${name}名称`}
name="name"
rules={[
{
required: true,
message: `请输入${name}名称`,
},
]}
>
<Input disabled placeholder={`请输入${name}名称`} />
</Form.Item>
<Form.Item
label={`${name}版本`}
name="version"
rules={[
{
required: true,
message: `请输入${name}版本`,
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`${name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${name}版本不能为 origin`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled />
</Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
</Form>
</KFModal>
);
}

export default EditVersionModal;

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

@@ -28,8 +28,15 @@
border-radius: 4px; border-radius: 4px;
} }


&__desc {
margin-bottom: 0 !important;
color: @text-color;
font-size: @font-size;
}

&__praise { &__praise {
display: flex; display: flex;
flex: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 70px; width: 70px;
@@ -42,6 +49,10 @@
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;


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

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


+ 98
- 54
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -4,6 +4,7 @@
* @Description: 数据集、模型详情 * @Description: 数据集、模型详情
*/ */


import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { import {
ResourceData, ResourceData,
@@ -19,10 +20,11 @@ import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max'; import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Flex, Select, Tabs } from 'antd';
import { App, Button, Flex, Select, Tabs, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import AddVersionModal from '../AddVersionModal'; import AddVersionModal from '../AddVersionModal';
import EditVersionModal from '../EditVersionModal';
import ResourceIntro from '../ResourceIntro'; import ResourceIntro from '../ResourceIntro';
import ResourceVersion from '../ResourceVersion'; import ResourceVersion from '../ResourceVersion';
import VersionCompareModal from '../VersionCompareModal'; import VersionCompareModal from '../VersionCompareModal';
@@ -61,21 +63,24 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
const { message } = App.useApp(); const { message } = App.useApp();


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


// 获取版本列表 // 获取版本列表
const getVersionList = useCallback( const getVersionList = useCallback(
@@ -100,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
} }
} else { } else {
setVersion(undefined); setVersion(undefined);
getResourceDetail(undefined);
} }
}, },
[config, owner, identifier, versionParam],
[config, owner, identifier, versionParam, getResourceDetail],
); );


useEffect(() => { useEffect(() => {
if (version) { if (version) {
getResourceDetail();
getResourceDetail(version);
} }
}, [version, getResourceDetail]); }, [version, getResourceDetail]);


@@ -116,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}, [getVersionList]); }, [getVersionList]);


// 新建版本 // 新建版本
const showModal = () => {
const showAddVersionModal = () => {
const { close } = openAntdModal(AddVersionModal, { const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType, resourceType: resourceType,
resourceId: resourceId, resourceId: resourceId,
@@ -132,6 +138,18 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}); });
}; };


// 版本编辑
const showEditVersionModal = () => {
const { close } = openAntdModal(EditVersionModal, {
resourceType: resourceType,
resourceVersion: info,
onOk: () => {
getResourceDetail();
close();
},
});
};

// 选择版本 // 选择版本
const showVersionSelector = () => { const showVersionSelector = () => {
const { close } = openAntdModal(VersionSelectorModal, { const { close } = openAntdModal(VersionSelectorModal, {
@@ -278,44 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
<span>{info.praises_count}</span> <span>{info.praises_count}</span>
</div> </div>
</Flex> </Flex>
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
icon={<KFIcon type="icon-banbenduibi" />}
onClick={showVersionSelector}
>
版本对比
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
onClick={handleDelete}
icon={<KFIcon type="icon-shanchu" />}
disabled={!version}
danger
{version ? (
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button
type="default"
onClick={showAddVersionModal}
icon={<KFIcon type="icon-xinjian2" />}
>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
icon={<KFIcon type="icon-banbenduibi" />}
onClick={showVersionSelector}
>
版本对比
</Button>
<Button
type="default"
style={{ marginLeft: '20px' }}
onClick={handleDelete}
icon={<KFIcon type="icon-shanchu" />}
danger
>
删除版本
</Button>
</Flex>
) : (
<Typography.Paragraph
className={styles['resource-info__top__desc']}
ellipsis={{ tooltip: info.description }}
> >
删除版本
</Button>
</Flex>
{info.description ?? '暂无描述'}
</Typography.Paragraph>
)}
</div> </div>
<div className={styles['resource-info__bottom']}> <div className={styles['resource-info__bottom']}>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['resource-info__bottom__legend']}>
{activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />}
</div>
{version ? (
<>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['resource-info__bottom__legend']}>
{activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />}
</div>
</>
) : (
<KFEmpty
style={{ height: '100%' }}
type={EmptyType.NoData}
title="暂无版本"
content={`请创建${config.name}版本`}
hasFooter={true}
buttonTitle="创建版本"
onButtonClick={showAddVersionModal}
/>
)}
</div> </div>
</div> </div>
); );


+ 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) { 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 ?? ''; const create_by = item.create_by ?? '';
return ( return (
<div className={styles['resource-item']} onClick={() => onClick(item)}> <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 = { export type ResourceListRef = {
reset: () => void; reset: () => void;
resetPage: () => void;
}; };


type ResourceListProps = { type ResourceListProps = {
@@ -97,6 +98,12 @@ function ResourceList(
setDataList(undefined); setDataList(undefined);
setTotal(0); 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) => { const chooseType = (record: CategoryData) => {
dataListRef.current?.resetPage();
setActiveType((prev) => (prev === record.name ? undefined : record.name)); setActiveType((prev) => (prev === record.name ? undefined : record.name));
}; };


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


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


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

@@ -9,11 +9,15 @@ import {
deleteDatasetVersion, deleteDatasetVersion,
deleteModel, deleteModel,
deleteModelVersion, deleteModelVersion,
editDatasetVersion,
editModelVersion,
getDatasetInfo, getDatasetInfo,
getDatasetList, getDatasetList,
getDatasetNextVersionReq,
getDatasetVersionList, getDatasetVersionList,
getModelInfo, getModelInfo,
getModelList, getModelList,
getModelNextVersionReq,
getModelVersionList, getModelVersionList,
} from '@/services/dataset/index.js'; } from '@/services/dataset/index.js';
import { limitUploadFileType } from '@/utils/ui'; import { limitUploadFileType } from '@/utils/ui';
@@ -36,9 +40,11 @@ type ResourceTypeInfo = {
getVersions: (params: any) => Promise<any>; // 获取版本列表 getVersions: (params: any) => Promise<any>; // 获取版本列表
deleteRecord: (params: any) => Promise<any>; // 删除 deleteRecord: (params: any) => Promise<any>; // 删除
addVersion: (params: any) => Promise<any>; // 新增版本 addVersion: (params: any) => Promise<any>; // 新增版本
editVersion: (params: any) => Promise<any>; // 编辑版本
deleteVersion: (params: any) => Promise<any>; // 删除版本 deleteVersion: (params: any) => Promise<any>; // 删除版本
getInfo: (params: any) => Promise<any>; // 获取详情 getInfo: (params: any) => Promise<any>; // 获取详情
compareVersion: (params: any) => Promise<any>; // 版本对比 compareVersion: (params: any) => Promise<any>; // 版本对比
getNextVersion: (params: any) => Promise<any>; // 获取下一个版本
name: string; // 名称 name: string; // 名称
typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用
tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用
@@ -65,9 +71,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getDatasetVersionList, getVersions: getDatasetVersionList,
deleteRecord: deleteDataset, deleteRecord: deleteDataset,
addVersion: addDatasetVersion, addVersion: addDatasetVersion,
editVersion: editDatasetVersion,
deleteVersion: deleteDatasetVersion, deleteVersion: deleteDatasetVersion,
getInfo: getDatasetInfo, getInfo: getDatasetInfo,
compareVersion: compareDatasetVersion, compareVersion: compareDatasetVersion,
getNextVersion: getDatasetNextVersionReq,
name: '数据集', name: '数据集',
typeParamKey: 'data_type', typeParamKey: 'data_type',
tagParamKey: 'data_tag', tagParamKey: 'data_tag',
@@ -103,9 +111,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getModelVersionList, getVersions: getModelVersionList,
deleteRecord: deleteModel, deleteRecord: deleteModel,
addVersion: addModelVersion, addVersion: addModelVersion,
editVersion: editModelVersion,
deleteVersion: deleteModelVersion, deleteVersion: deleteModelVersion,
getInfo: getModelInfo, getInfo: getModelInfo,
compareVersion: compareModelVersion, compareVersion: compareModelVersion,
getNextVersion: getModelNextVersionReq,
name: '模型', name: '模型',
typeParamKey: 'model_type', typeParamKey: 'model_type',
tagParamKey: 'model_tag', tagParamKey: 'model_tag',
@@ -164,6 +174,7 @@ export interface ResourceData {
train_task?: TrainTask; // 训练任务 train_task?: TrainTask; // 训练任务
praises_count: number; // 点赞数 praises_count: number; // 点赞数
praised: boolean; // 是否点赞 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 * @Date: 2024-04-16 13:58:08
* @Description: 创建开发环境 * @Description: 创建开发环境
*/ */
import CodeSelect from '@/components/CodeSelect';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import KFRadio, { type KFRadioItem } from '@/components/KFRadio';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
@@ -187,6 +188,14 @@ function EditorCreate() {
</Col> </Col>
</Row> </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 }}> <Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
确定 确定


+ 76
- 17
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -4,10 +4,11 @@
* @Description: 开发环境列表 * @Description: 开发环境列表
*/ */


import { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { DevEditorStatus } from '@/enums'; import { DevEditorStatus } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config'; import { DatasetData, ModelData } from '@/pages/Dataset/config';
import { import {
deleteEditorReq, deleteEditorReq,
@@ -17,6 +18,7 @@ import {
} from '@/services/developmentEnvironment'; } from '@/services/developmentEnvironment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { parseJsonText } from '@/utils'; import { parseJsonText } from '@/utils';
import { formatCodeConfig, formatDataset, formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -49,6 +51,7 @@ export type EditorData = {
dataset?: string | DatasetData; dataset?: string | DatasetData;
model?: string | ModelData; model?: string | ModelData;
image?: string; image?: string;
code_config?: string | CodeConfigData;
}; };


function EditorList() { function EditorList() {
@@ -63,7 +66,7 @@ function EditorList() {
pageSize: 10, pageSize: 10,
}, },
); );
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


// 获取编辑器列表 // 获取编辑器列表
const getEditorList = useCallback(async () => { const getEditorList = useCallback(async () => {
@@ -78,6 +81,8 @@ function EditorList() {
item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null;
item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null;
item.image = typeof item.image === 'string' ? parseJsonText(item.image) : 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); setTableData(content);
setTotal(totalElements); setTotal(totalElements);
@@ -159,13 +164,54 @@ function EditorList() {
}; };


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

setCacheState({ setCacheState({
pagination, 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 CodeConfigData;
const url = formatCodeConfig(codeConfig)?.url;
if (url) {
window.open(url, '_blank');
}
}; };


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


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

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


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

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




+ 104
- 55
react-ui/src/pages/Experiment/Info/index.jsx View File

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


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


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


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


return () => { return () => {
if (evtSourceRef.current) { 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 getExperimentInstance = async () => {
const [res] = await to(getExperimentIns(locationParams.id)); const [res] = await to(getExperimentIns(locationParams.id));
if (res && res.data && workflowRef.current) {
if (res && res.data) {
setExperimentIns(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 = dag;
const experimentStatusObjs = parseJsonText(nodes_status); 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.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) { if (status === ExperimentStatus.Pending) {
// 如果状态是 Pending, 打开第一个节点 // 如果状态是 Pending, 打开第一个节点
const node = workflowData.nodes[0];
const node = workflow.nodes[0];
if (node) { if (node) {
setExperimentNodeData(node); setExperimentNodeData(node);
openPropsDrawer(); openPropsDrawer();
} }
} else if (status === ExperimentStatus.Running) { } else if (status === ExperimentStatus.Running) {
// 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,则打开第一个节点
// 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点
const node = 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) { if (node) {
setExperimentNodeData(node); setExperimentNodeData(node);
openPropsDrawer(); openPropsDrawer();
@@ -135,23 +168,36 @@ function ExperimentText() {
return; return;
} }
try { try {
const dataJson = JSON.parse(data);
const dataJson = parseJsonText(data);
const statusData = dataJson?.result?.object?.status; const statusData = dataJson?.result?.object?.status;
if (!statusData) { if (!statusData) {
return; 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; const workflowData = workflowRef.current;
workflowData.nodes.forEach((item) => { workflowData.nodes.forEach((item) => {
const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id);
updateWorkflowNode(item, experimentNode); updateWorkflowNode(item, experimentNode);
}); });

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


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


// 更新各个节点
function updateWorkflowNode(workflowNode, statusNode) { function updateWorkflowNode(workflowNode, statusNode) {
if (!statusNode) { if (!statusNode) {
return; return;
@@ -471,29 +518,30 @@ function ExperimentText() {
<div className={styles['pipeline-container']}> <div className={styles['pipeline-container']}>
<div className={styles['pipeline-container__top']}> <div className={styles['pipeline-container__top']}>
<div className={styles['pipeline-container__top__info']}> <div className={styles['pipeline-container__top__info']}>
启动时间:{formatDate(experimentIns?.create_time)}
启动时间:{formatDate(workflowStatus?.startedAt)}
</div> </div>
<div className={styles['pipeline-container__top__info']}> <div className={styles['pipeline-container__top__info']}>
执行时长: 执行时长:
<RunDuration <RunDuration
createTime={experimentIns?.create_time}
finishTime={experimentIns?.finish_time}
createTime={workflowStatus?.startedAt}
finishTime={workflowStatus?.finishedAt}
/> />
</div> </div>
<div className={styles['pipeline-container__top__info']}> <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> </div>
<Button <Button
className={styles['pipeline-container__top__param-button']} className={styles['pipeline-container__top__param-button']}
@@ -519,12 +567,13 @@ function ExperimentText() {
instanceNodeStatus={experimentNodeData.experimentStatus} instanceNodeStatus={experimentNodeData.experimentStatus}
instanceNodeStartTime={experimentNodeData.experimentStartTime} instanceNodeStartTime={experimentNodeData.experimentStartTime}
instanceNodeEndTime={experimentNodeData.experimentEndTime} instanceNodeEndTime={experimentNodeData.experimentEndTime}
globalParams={experimentIns?.global_param}
></ExperimentDrawer> ></ExperimentDrawer>
) : null} ) : null}
<ParamsModal <ParamsModal
open={paramsModalOpen} open={paramsModalOpen}
onCancel={closeParamsModal} onCancel={closeParamsModal}
globalParam={experimentIns?.global_param}
globalParams={experimentIns?.global_param}
></ParamsModal> ></ParamsModal>
</div> </div>
); );


+ 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;

+ 6
- 0
react-ui/src/pages/Experiment/components/AddExperimentModal/index.less View File

@@ -6,4 +6,10 @@
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 6px; border-radius: 6px;
} }

:global {
.ant-form-item-row {
align-items: center;
}
}
} }

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

@@ -1,7 +1,7 @@
import createExperimentIcon from '@/assets/img/create-experiment.png'; import createExperimentIcon from '@/assets/img/create-experiment.png';
import editExperimentIcon from '@/assets/img/edit-experiment.png'; import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
@@ -30,9 +30,9 @@ interface Workflow {
} }


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


@@ -50,7 +50,7 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX.
export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => { export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => {
const rules = []; const rules = [];
// 防止后台返回不是 number 类型 // 防止后台返回不是 number 类型
if (Number(paramType) === 2) {
if (Number(paramType) === PipelineGlobalParamType.Number) {
rules.push({ rules.push({
pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/, pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/,
message: '整型必须是数字', message: '整型必须是数字',
@@ -64,10 +64,10 @@ export const getParamRules = (paramType: number, required: boolean = false): For


// 根据参数设置 label // 根据参数设置 label
export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => {
const paramTypes: Readonly<Record<number, string>> = {
1: '字符串',
2: '整型',
3: '布尔类型',
const paramTypes: Readonly<Record<PipelineGlobalParamType, string>> = {
[PipelineGlobalParamType.String]: '字符串',
[PipelineGlobalParamType.Number]: '整型',
[PipelineGlobalParamType.Boolean]: '布尔类型',
}; };
const label = param.param_name + `(${paramTypes[param.param_type]})`; const label = param.param_name + `(${paramTypes[param.param_type]})`;
return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>;
@@ -95,8 +95,8 @@ function AddExperimentModal({
}; };


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


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


+ 5
- 2
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -1,7 +1,7 @@
import RunDuration from '@/components/RunDuration'; import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums'; import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs, Typography } from 'antd'; import { Drawer, Tabs, Typography } from 'antd';
@@ -25,6 +25,7 @@ type ExperimentDrawerProps = {
instanceNodeStatus?: ExperimentStatus; // 实例节点状态 instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间 instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
globalParams?: PipelineGlobalParam[] | null; // 全局参数
}; };


const ExperimentDrawer = ({ const ExperimentDrawer = ({
@@ -41,6 +42,7 @@ const ExperimentDrawer = ({
instanceNodeStatus, instanceNodeStatus,
instanceNodeStartTime, instanceNodeStartTime,
instanceNodeEndTime, instanceNodeEndTime,
globalParams,
}: ExperimentDrawerProps) => { }: ExperimentDrawerProps) => {
// 如果性能有问题,可以进一步拆解 // 如果性能有问题,可以进一步拆解
const items = useMemo( const items = useMemo(
@@ -66,7 +68,7 @@ const ExperimentDrawer = ({
key: '2', key: '2',
label: '配置参数', label: '配置参数',
icon: <DatabaseOutlined />, icon: <DatabaseOutlined />,
children: <ExperimentParameter nodeData={instanceNodeData} />,
children: <ExperimentParameter nodeData={instanceNodeData} globalParams={globalParams} />,
}, },
{ {
key: '3', key: '3',
@@ -94,6 +96,7 @@ const ExperimentDrawer = ({
experimentName, experimentName,
experimentId, experimentId,
pipelineId, pipelineId,
globalParams,
], ],
); );




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

@@ -55,10 +55,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
width: 160px; 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) { }: ExperimentInstanceListProps) {
const { message } = App.useApp(); const { message } = App.useApp();
const allIntanceIds = useMemo(() => { 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]); }, [experimentInsList]);
const [ const [
selectedIns, selectedIns,
@@ -127,7 +134,12 @@ function ExperimentInstanceList({
<div> <div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}> <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>
<div className={styles.index}>序号</div> <div className={styles.index}>序号</div>
<div className={styles.tensorBoard}>可视化</div> <div className={styles.tensorBoard}>可视化</div>
@@ -185,14 +197,7 @@ function ExperimentInstanceList({
)} )}
</div> </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}> <div className={styles.operation}>
<Button <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 { ExperimentStatus } from '@/enums';
import { useSSE, type MessageHandler } from '@/hooks/useSSE'; import { useSSE, type MessageHandler } from '@/hooks/useSSE';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { ExperimentInstance, NodeStatus } from '@/types';
import { ExperimentCompleted } from '@/utils/constant'; import { ExperimentCompleted } from '@/utils/constant';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { getWorkflowStatus } from '@/utils/experiment';
import { Typography } from 'antd'; import { Typography } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import styles from './index.less'; 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( const handleSSEMessage: MessageHandler = useCallback(
(experimentInsId: number, status: string, finish_time: string) => {
(experimentId: number, experimentInsId: number, status: string, finishTime: string) => {
window.postMessage({ window.postMessage({
type: ExperimentCompleted, type: ExperimentCompleted,
payload: { payload: {
id: experimentInsId,
experimentId,
experimentInsId,
status, 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 ( return (
<React.Fragment> <React.Fragment>
<div className={styles.description}> <div className={styles.description}>
<div style={{ width: '50%' }}> <div style={{ width: '50%' }}>
<RunDuration createTime={create_time} finishTime={finish_time} />
<RunDuration createTime={createTime} finishTime={finishTime} />
</div> </div>
<div style={{ width: '50%' }} className={styles.startTime}> <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> </Typography.Text>
</div> </div>
</div> </div>
<div className={styles.statusBox}> <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> </div>
</React.Fragment> </React.Fragment>
); );
} }


export default ExperimentInstance;
export default ExperimentInstanceComponent;

+ 20
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.less View File

@@ -15,4 +15,24 @@
font-size: @font-size; font-size: @font-size;
background: #f8fbff; background: #f8fbff;
} }

&__form-list {
:global {
.ant-row {
padding: 0 !important;
}
}

&:last-child {
:global {
.ant-form-item {
margin-bottom: 0 !important;
}
}
}
}

&__list-empty {
color: @text-color-tertiary;
}
} }

+ 129
- 92
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -1,19 +1,92 @@
import FormInfo from '@/components/FormInfo'; import FormInfo from '@/components/FormInfo';
import ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { PipelineNodeModelSerialize } from '@/types';
import { Form } from 'antd';
import { ComponentType } from '@/enums';
import type {
PipelineGlobalParam,
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
import { Flex, Form } from 'antd';
import styles from './index.less'; import styles from './index.less';


type ExperimentParameterProps = { type ExperimentParameterProps = {
nodeData: PipelineNodeModelSerialize; nodeData: PipelineNodeModelSerialize;
globalParams?: PipelineGlobalParam[] | null; // 全局参数
}; };


function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
function ExperimentParameter({ nodeData, globalParams }: ExperimentParameterProps) {
// 表单组件
const getFormComponent = (
item: { key: string; value: PipelineNodeModelParameter },
parentName: string,
) => {
return (
<Form.Item
key={item.key}
name={[parentName, item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
>
{item.value.type === ComponentType.Map && (
<Form.List name={[parentName, item.key, 'value']}>
{(fields) => (
<>
{fields.length > 0 ? (
fields.map(({ key, name, ...restField }) => (
<Flex
key={key}
gap="0 8px"
style={{ width: '100%' }}
className={styles['experiment-parameter__form-list']}
>
<Form.Item
{...restField}
name={[name, 'name']}
style={{ flex: 1, minWidth: 0 }}
>
<FormInfo />
</Form.Item>
<span style={{ lineHeight: '32px' }}>=</span>
<Form.Item
{...restField}
name={[name, 'value']}
style={{ flex: 1, minWidth: 0 }}
>
<FormInfo valuePropName="showValue" globalParams={globalParams} />
</Form.Item>
</Flex>
))
) : (
<div className={styles['experiment-parameter__list-empty']}>无</div>
)}
</>
)}
</Form.List>
)}
{item.value.type === ComponentType.Select &&
(['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect dataType={item.value.item_type as ParameterSelectDataType} display />
) : null)}
{item.value.type !== ComponentType.Map && item.value.type !== ComponentType.Select && (
<FormInfo valuePropName="showValue" globalParams={globalParams} />
)}
</Form.Item>
);
};

// 基本参数
const basicParametersList = Object.entries(nodeData.task_info ?? {})
.map(([key, value]) => ({
key,
value,
}))
.filter((v) => v.value.visible === true);

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


// 输入参数 // 输入参数
const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({ const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
@@ -74,92 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
> >
<FormInfo /> <FormInfo />
</Form.Item> </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>


<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')}
title="输入参数"
></SubAreaTitle>
</div>
{inParametersList.map((item) => (
<Form.Item
key={item.key}
name={['in_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
>
{item.value.type === 'select' ? (
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect dataType={item.value.item_type as any} display />
) : null
) : (
<FormInfo valuePropName="showValue" />
)}
</Form.Item>
))}
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="输出参数"
></SubAreaTitle>
</div>
{outParametersList.map((item) => (
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
>
<FormInfo valuePropName="showValue" />
</Form.Item>
))}
{basicParametersList.length + controlStrategyList.length > 0 && (
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
)}

{/* 基本参数 */}
{basicParametersList.map((item) => getFormComponent(item, 'task_info'))}

{/* 控制参数 */}
{controlStrategyList.map((item) => getFormComponent(item, 'control_strategy'))}

{/* 输入参数 */}
{inParametersList.length > 0 && (
<>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="输入参数"
></SubAreaTitle>
</div>
{inParametersList.map((item) => getFormComponent(item, 'in_parameters'))}
</>
)}

{/* 输出参数 */}
{outParametersList.length > 0 && (
<>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="输出参数"
></SubAreaTitle>
</div>
{outParametersList.map((item) => (
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
>
<FormInfo valuePropName="showValue" />
</Form.Item>
))}
</>
)}
</Form> </Form>
); );
} }


+ 25
- 32
react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx View File

@@ -3,12 +3,10 @@ import KFModal from '@/components/KFModal';
import { import {
DataSource, DataSource,
ResourceType, ResourceType,
ResourceVersionData,
resourceConfig, resourceConfig,
type ResourceData, type ResourceData,
} from '@/pages/Dataset/config'; } from '@/pages/Dataset/config';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Form, Input, ModalProps, Select } from 'antd'; import { Form, Input, ModalProps, Select } from 'antd';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -44,7 +42,6 @@ function ExportModelModal({
}: ExportModelModalProps) { }: ExportModelModalProps) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [resources, setResources] = useState<ResourceData[]>([]); const [resources, setResources] = useState<ResourceData[]>([]);
const [versions, setVersions] = useState<ResourceVersionData[]>([]);
const config = resourceConfig[resourceType]; const config = resourceConfig[resourceType];


const layout = { const layout = {
@@ -77,35 +74,24 @@ function ExportModelModal({
return undefined; return undefined;
}; };


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

// 处理数据集、模型选择变化 // 处理数据集、模型选择变化
const handleResourceChange = (id: number | undefined) => { const handleResourceChange = (id: number | undefined) => {
if (id) { if (id) {
getRecourceVersions(id);
getRecourceNextVersion(id);
} else { } else {
setVersions([]);
form.setFieldValue('version', '');
} }
}; };


// 获取数据集、模型版本列表
const getRecourceVersions = async (id: number) => {
// 获取数据集、模型下一个版本
const getRecourceNextVersion = async (id: number) => {
const resource = getSelectedResource(id); const resource = getSelectedResource(id);
if (!resource) { if (!resource) {
return; return;
} }
const [res] = await to(config.getVersions(pick(resource, ['identifier', 'owner'])));
const [res] = await to(config.getNextVersion(pick(resource, ['identifier', 'owner'])));
if (res && res.data) { if (res && res.data) {
setVersions(res.data);
form.setFieldValue('version', res.data);
} }
}; };


@@ -121,6 +107,8 @@ function ExportModelModal({
const params = { const params = {
...formData, ...formData,
identifier: resource?.identifier, identifier: resource?.identifier,
owner: resource?.owner,
is_public: resource?.is_public,
name: resource?.name, name: resource?.name,
[config.sourceParamKey]: DataSource.HandExport, [config.sourceParamKey]: DataSource.HandExport,
train_task: { train_task: {
@@ -174,27 +162,26 @@ function ExportModelModal({
onChange={handleResourceChange} onChange={handleResourceChange}
options={resources} options={resources}
fieldNames={{ label: 'name', value: 'id' }} fieldNames={{ label: 'name', value: 'id' }}
optionFilterProp="name"
showSearch
allowClear allowClear
></Select> ></Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={`${config.name}版本`} label={`${config.name}版本`}
name="version" name="version"
tooltip={
getTooltip()
? {
overlayClassName: styles['export-model-modal__tooltip'],
title: getTooltip(),
icon: <InfoCircleOutlined />,
}
: undefined
}
rules={[ rules={[
{ required: true, message: `请输入${config.name}版本` }, { required: true, message: `请输入${config.name}版本` },
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: `${config.name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{ {
validator: (_, value) => { validator: (_, value) => {
if (value && versions.map((item) => item.name).includes(value)) {
return Promise.reject(`${config.name}版本已存在`);
if (value === 'master') {
return Promise.reject(`${config.name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${config.name}版本不能为 origin`);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@@ -202,7 +189,13 @@ function ExportModelModal({
}, },
]} ]}
> >
<Input placeholder={`请输入${config.name}版本`} maxLength={64} showCount allowClear />
<Input
placeholder={`请输入${config.name}版本`}
maxLength={64}
showCount
allowClear
disabled
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="版本描述" label="版本描述"


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

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


+ 6
- 0
react-ui/src/pages/Experiment/components/ViewParamsModal/index.less View File

@@ -4,6 +4,12 @@
overflow-y: auto; overflow-y: auto;
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 8px; border-radius: 8px;

:global {
.ant-form-item-row {
align-items: center;
}
}
} }
.params-empty { .params-empty {
:global { :global {


+ 6
- 9
react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx View File

@@ -14,10 +14,10 @@ import styles from './index.less';
type ParamsModalProps = { type ParamsModalProps = {
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
globalParam?: PipelineGlobalParam[] | null;
globalParams?: PipelineGlobalParam[] | null;
}; };


function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) {
return ( return (
<KFModal <KFModal
title="执行参数" title="执行参数"
@@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }} cancelButtonProps={{ style: { display: 'none' } }}
width={825} width={825}
> >
{Array.isArray(globalParam) && globalParam.length > 0 ? (
{Array.isArray(globalParams) && globalParams.length > 0 ? (
<div className={styles['params-container']}> <div className={styles['params-container']}>
<Form <Form
name="view_params_form" name="view_params_form"
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }} wrapperCol={{ span: 18 }}
initialValues={{ global_param: globalParam }}
initialValues={{ global_param: globalParams }}
labelAlign="left" labelAlign="left"
disabled disabled
> >
@@ -45,12 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
{...restField} {...restField}
key={key} key={key}
name={[name, 'param_value']} name={[name, 'param_value']}
label={getParamLabel(globalParam[name])}
label={getParamLabel(globalParams[name])}
> >
{getParamComponent(
globalParam[name]['param_type'],
globalParam[name]['is_sensitive'],
)}
{getParamComponent(globalParams[name]['param_type'])}
</Form.Item> </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 PageTitle from '@/components/PageTitle';
import { ExperimentStatus, TensorBoardStatus } from '@/enums'; import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useServerTime } from '@/hooks/useServerTime';
import { import {
deleteExperimentById, deleteExperimentById,
editExperimentInsReq,
getExperiment, getExperiment,
getExperimentById, getExperimentById,
getQueryByExperimentId, getQueryByExperimentId,
@@ -17,6 +19,7 @@ import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { ExperimentCompleted } from '@/utils/constant'; import { ExperimentCompleted } from '@/utils/constant';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table'; import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; 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 ExperimentInstanceList from './components/ExperimentInstanceList';
import styles from './index.less'; import styles from './index.less';
import { experimentStatusInfo } from './status'; import { experimentStatusInfo } from './status';
import { useServerTime } from '@/hooks/useServerTime';


// 定时器 // 定时器
const timerIds = new Map(); const timerIds = new Map();
@@ -39,7 +41,7 @@ function Experiment() {
const [workflowList, setWorkflowList] = useState([]); const [workflowList, setWorkflowList] = useState([]);
const [experimentId, setExperimentId] = useState(null); const [experimentId, setExperimentId] = useState(null);
const [experimentInsList, setExperimentInsList] = useState([]); const [experimentInsList, setExperimentInsList] = useState([]);
const [expandedRowKeys, setExpandedRowKeys] = useState(null);
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [isAdd, setIsAdd] = useState(true); const [isAdd, setIsAdd] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -59,29 +61,137 @@ function Experiment() {
const timerRef = useRef(); 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 = { 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(() => { useEffect(() => {
@@ -104,52 +214,66 @@ function Experiment() {
clearExperimentInTimers(); clearExperimentInTimers();
}; };
}, []); }, []);

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


// 新增,删除版本时,重置分页,然后刷新版本列表
// 更新实验实例状态
useEffect(() => { useEffect(() => {
const handleMessage = (e) => { const handleMessage = (e) => {
const { type, payload } = e.data; const { type, payload } = e.data;
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;
const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );

if (
!currentIns ||
currentIns.status === ExperimentStatus.Terminated ||
currentIns.status === status
) {
return;
}


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


if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}
// refreshExperimentList(true);
// refreshExperimentIns(experimentId);


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


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


// 搜索 // 搜索
const onSearch = (value) => { 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 // 运行 TensorBoard
const runTensorBoard = async (experimentIn) => { const runTensorBoard = async (experimentIn) => {
const params = { 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(); clearExperimentInTimers();
setExperimentInsList([]); setExperimentInsList([]);
if (record.id === expandedRowKeys) {
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id, 0, 5);
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0, 5);
refreshExperimentList(); refreshExperimentList();
} else {
setExpandedRowKeys([]);
} }
}; };


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


@@ -372,14 +426,16 @@ function Experiment() {
experimentIn.tensorBoardStatus === TensorBoardStatus.Running && experimentIn.tensorBoardStatus === TensorBoardStatus.Running &&
experimentIn.tensorboardUrl 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) => { const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
// 修改实例的状态和结束时间
setExperimentInsList((prevList) => { setExperimentInsList((prevList) => {
return prevList.map((item) => { return prevList.map((item) => {
if (item.id === experimentIn.id) { if (item.id === experimentIn.id) {
@@ -392,6 +448,9 @@ function Experiment() {
return item; 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 loadMoreExperimentIns = () => {
const page = Math.round(experimentInsList.length / 5); const page = Math.round(experimentInsList.length / 5);
getQueryByExperiment(expandedRowKeys, page, 5);
getExperimentInsList(expandedRowKeys[0], page, 5);
}; };


// 处理删除 // 处理删除
@@ -607,7 +660,7 @@ function Experiment() {
></ExperimentInstanceList> ></ExperimentInstanceList>
), ),
onExpand: expandChange, onExpand: expandChange,
expandedRowKeys: [expandedRowKeys],
expandedRowKeys: expandedRowKeys,
}} }}
/> />
</div> </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: 赵伟 * @Author: 赵伟
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 自主机器学习详情
* @Description: 自动机器学习详情
*/ */
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import { getRayInfoReq } from '@/services/hyperParameter'; 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)); const [res] = await to(getRayInsReq(instanceId));
if (res && res.data) { if (res && res.data) {
const info = res.data as HyperParameterInstanceData; 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; const paramJson = parseJsonText(param).data;
if (paramJson) { if (paramJson) {
@@ -72,7 +72,6 @@ function HyperParameterInstance() {
} }
setExperimentInfo({ setExperimentInfo({
...paramJson, ...paramJson,
create_time,
}); });
} }


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


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

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


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


+ 1
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx View File

@@ -323,6 +323,7 @@ function ExecuteConfig() {
className={styles['hyper-parameter__body__name']} className={styles['hyper-parameter__body__name']}
{...restField} {...restField}
name={[name, 'name']} name={[name, 'name']}
dependencies={fields.map((_, i) => ['parameters', i, 'name'])}
required required
rules={[ rules={[
{ {


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


const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ const trialColumns: TableProps<HyperParameterTrial>['columns'] = [
{ {
@@ -160,7 +163,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
const [res] = await to(getExpMetricsReq(selectedRowKeys)); const [res] = await to(getExpMetricsReq(selectedRowKeys));
if (res && res.data) { if (res && res.data) {
const url = 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 { ExperimentStatus } from '@/enums';
import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty';
import LogList from '@/pages/Experiment/components/LogList'; import LogList from '@/pages/Experiment/components/LogList';
import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
import { NodeStatus } from '@/types'; import { NodeStatus } from '@/types';
@@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />, // icon: <KFIcon type="icon-rizhi1" />,
children: ( children: (
<div className={styles['experiment-log__tabs__log']}> <div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
{trainCloneNodeStatus ? (
<LogList <LogList
instanceName={instanceInfo.argo_ins_name} instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns} instanceNamespace={instanceInfo.argo_ins_ns}
@@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={trainCloneNodeStatus.startedAt} instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList> ></LogList>
) : (
<EmptyLog />
)} )}
</div> </div>
), ),
@@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
// icon: <KFIcon type="icon-rizhi1" />, // icon: <KFIcon type="icon-rizhi1" />,
children: ( children: (
<div className={styles['experiment-log__tabs__log']}> <div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
{hpoNodeStatus ? (
<LogList <LogList
instanceName={instanceInfo.argo_ins_name} instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns} instanceNamespace={instanceInfo.argo_ins_ns}
@@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
instanceNodeStartTime={hpoNodeStatus.startedAt} instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList> ></LogList>
) : (
<EmptyLog />
)} )}
</div> </div>
), ),


+ 7
- 56
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

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


function HyperParameterBasic({ function HyperParameterBasic({
info, info,
className, className,
runStatus,
workflowStatus,
instanceStatus, instanceStatus,
isInstance = false, isInstance = false,
}: HyperParameterBasicProps) { }: HyperParameterBasicProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const basicDatas: BasicInfoData[] = useMemo(() => { const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) { if (!info) {
@@ -83,7 +80,7 @@ function HyperParameterBasic({
} }
return [ return [
{ {
label: '代码',
label: '代码配置',
value: info.code_config, value: info.code_config,
format: formatCodeConfig, format: formatCodeConfig,
}, },
@@ -145,56 +142,10 @@ function HyperParameterBasic({
]; ];
}, [info, getResourceDescription]); }, [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 ( return (
<div className={classNames(styles['hyper-parameter-basic'], className)}> <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 && ( {!isInstance && (
<ConfigInfo <ConfigInfo


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

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


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

&__type { &__type {
color: @text-color; color: @text-color;
font-size: @font-size-input-lg; 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; return true;
}; };


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

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


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

@@ -46,6 +46,7 @@ export type MirrorInfoData = {
}; };


export type MirrorVersionData = { export type MirrorVersionData = {
image_id: number;
id: number; id: number;
version: string; version: string;
url: string; url: string;
@@ -53,6 +54,7 @@ export type MirrorVersionData = {
file_size: string; file_size: string;
create_time: string; create_time: string;
tag_name: string; tag_name: string;
description: string;
}; };


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


@@ -164,20 +167,21 @@ function MirrorInfo() {
title: '镜像版本', title: '镜像版本',
dataIndex: 'tag_name', dataIndex: 'tag_name',
key: 'tag_name', key: 'tag_name',
width: '25%',
width: '30%',
render: tableCellRender(), render: tableCellRender(),
}, },
{ {
title: '镜像地址', title: '镜像地址',
dataIndex: 'url', dataIndex: 'url',
key: 'url', key: 'url',
width: '25%',
width: '40%',
render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }),
}, },
{ {
title: '版本描述', title: '版本描述',
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
width: '30%',
render: tableCellRender(true), render: tableCellRender(true),
}, },
{ {
@@ -209,7 +213,7 @@ function MirrorInfo() {
hidden: isPublic, hidden: isPublic,
render: (_: any, record: MirrorVersionData) => ( render: (_: any, record: MirrorVersionData) => (
<div> <div>
{!isPublic && (
{!isPublic && record.status && record.status !== MirrorVersionStatus.Building && (
<ConfigProvider <ConfigProvider
theme={{ theme={{
token: { 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) { function MetricsChart({ name, chartData }: MetricsChartProps) {
const chartRef = useRef<HTMLDivElement>(null); const chartRef = useRef<HTMLDivElement>(null);
const xAxisData = chartData[0]?.iters; 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( const options: echarts.EChartsOption = useMemo(
() => ({ () => ({
@@ -158,7 +162,7 @@ function MetricsChart({ name, chartData }: MetricsChartProps) {


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


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

@@ -23,7 +23,7 @@ import { removeFormListItem } from '@/utils/ui';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd'; import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd';
import { omit, pick } from 'lodash';
import { omit } from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';
@@ -79,7 +79,7 @@ function CreateServiceVersion() {
if (res.model && typeof res.model === 'object') { if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' }); model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value // 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
// model.value = model.showValue;
} }
// 环境变量 // 环境变量
if (res.env_variables && typeof res.env_variables === 'object') { if (res.env_variables && typeof res.env_variables === 'object') {
@@ -117,7 +117,6 @@ function CreateServiceVersion() {
// 创建版本 // 创建版本
const createServiceVersion = async (formData: FormData) => { const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables']; const envList = formData['env_variables'];
const model = formData['model'];
const envVariables = envList?.reduce((acc, cur) => { const envVariables = envList?.reduce((acc, cur) => {
acc[cur.key] = cur.value; acc[cur.key] = cur.value;
return acc; return acc;
@@ -125,13 +124,9 @@ function CreateServiceVersion() {


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


@@ -238,7 +233,7 @@ function CreateServiceVersion() {
}, },
{ {
pattern: /^[a-zA-Z0-9._-]+$/, pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '服务支持字母、数字、点(.)、下划线(_)、中横线(-)',
}, },
]} ]}
> >
@@ -427,6 +422,7 @@ function CreateServiceVersion() {
{...restField} {...restField}
name={[name, 'key']} name={[name, 'key']}
style={{ flex: 1 }} style={{ flex: 1 }}
dependencies={fields.map((_, i) => ['env_variables', i, 'key'])}
rules={[ rules={[
{ {
validator: (_, value) => { validator: (_, value) => {


+ 24
- 6
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -9,7 +9,8 @@ import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; import { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { ModelData } from '@/pages/Dataset/config';
import { import {
deleteServiceVersionReq, deleteServiceVersionReq,
getServiceInfoReq, getServiceInfoReq,
@@ -18,6 +19,7 @@ import {
} from '@/services/modelDeployment'; } from '@/services/modelDeployment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -87,7 +89,7 @@ function ServiceInfo() {
format: formatDate, format: formatDate,
}, },
]; ];
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


// 获取服务详情 // 获取服务详情
const getServiceInfo = useCallback(async () => { const getServiceInfo = useCallback(async () => {
@@ -110,8 +112,8 @@ function ServiceInfo() {
if (res && res.data) { if (res && res.data) {
const { content = [], totalElements = 0 } = res.data; const { content = [], totalElements = 0 } = res.data;
content.forEach((item: ServiceVersionData) => { content.forEach((item: ServiceVersionData) => {
if (item.model && !item.model.show_value) {
item.model.show_value = `${item.model.name}:${item.model.version}`;
if (item.model && !item.model.showValue) {
item.model.showValue = `${item.model.name}:${item.model.version}`;
} }
}); });
setTableData(content); setTableData(content);
@@ -258,6 +260,20 @@ function ServiceInfo() {
}, },
}; };


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

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

const columns: TableProps<ServiceVersionData>['columns'] = [ const columns: TableProps<ServiceVersionData>['columns'] = [
{ {
title: '序号', title: '序号',
@@ -278,10 +294,12 @@ function ServiceInfo() {
}, },
{ {
title: '模型版本', title: '模型版本',
dataIndex: ['model', 'show_value'],
dataIndex: ['model', 'showValue'],
key: 'model', key: 'model',
width: '20%', width: '20%',
render: tableCellRender(true),
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoModel,
}),
}, },
{ {
title: '镜像版本', title: '镜像版本',


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

@@ -3,16 +3,16 @@
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 服务版本详情 * @Description: 服务版本详情
*/ */
import FullScreenFrame from '@/components/FullScreenFrame';
import IframePage from '@/components/IFramePage';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import { ServiceRunStatus } from '@/enums';
import { getServiceVersionInfoReq } from '@/services/modelDeployment'; import { getServiceVersionInfoReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { useParams } from '@umijs/max'; import { useParams } from '@umijs/max';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ServerLog from '../components/ServerLog'; import ServerLog from '../components/ServerLog';
import UserGuide from '../components/UserGuide';
import VersionBasicInfo from '../components/VersionBasicInfo'; import VersionBasicInfo from '../components/VersionBasicInfo';
import { ServiceVersionData } from '../types'; import { ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';
@@ -50,32 +50,35 @@ function ServiceVersionInfo() {
icon: <KFIcon type="icon-jibenxinxi" />, icon: <KFIcon type="icon-jibenxinxi" />,
children: <VersionBasicInfo info={versionInfo} />, 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 ( return (
<div className={styles['service-version-info']}> <div className={styles['service-version-info']}>
<PageTitle title="服务版本详情"></PageTitle> <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; font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
align-items: left;

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


&__more { &__more {
padding: 20px 0; 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 { to } from '@/utils/promise';
import { DoubleRightOutlined } from '@ant-design/icons'; import { DoubleRightOutlined } from '@ant-design/icons';
import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; import { Button, DatePicker, type TimeRangePickerProps } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styles from './index.less'; import styles from './index.less';
@@ -106,6 +107,8 @@ function ServerLog({ info }: ServerLogProps) {
} }
}; };


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

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


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

@@ -1,6 +1,6 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import { ServiceRunStatus } from '@/enums'; import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { ServiceVersionData } from '@/pages/ModelDeployment/types'; import { ServiceVersionData } from '@/pages/ModelDeployment/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { formatMirror, formatModel } from '@/utils/format'; import { formatMirror, formatModel } from '@/utils/format';
@@ -36,7 +36,7 @@ const formatEnvText = (env?: Record<string, string>) => {
}; };


function VersionBasicInfo({ info }: BasicInfoProps) { function VersionBasicInfo({ info }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const datas: BasicInfoData[] = [ const datas: BasicInfoData[] = [
{ {


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

@@ -1,6 +1,6 @@
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { ServiceRunStatus } from '@/enums'; import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; import { type ServiceVersionData } from '@/pages/ModelDeployment/types';
import { getServiceVersionCompareReq } from '@/services/modelDeployment'; import { getServiceVersionCompareReq } from '@/services/modelDeployment';
import { isEmpty } from '@/utils'; import { isEmpty } from '@/utils';
@@ -42,7 +42,7 @@ const formatEnvText = (env: Record<string, string>) => {


function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) { function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) {
const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); const [compareData, setCompareData] = useState<CompareData | undefined>(undefined);
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();


const fields: FiledType[] = useMemo( const fields: FiledType[] = useMemo(
() => [ () => [


+ 1
- 1
react-ui/src/pages/ModelDeployment/types.ts View File

@@ -34,7 +34,7 @@ export type ServiceVersionData = {
path: string; path: string;
identifier: string; identifier: string;
owner: string; owner: string;
show_value: string;
showValue: string;
}; };
code_config: { code_config: {
// 代码配置 // 代码配置


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

@@ -3,7 +3,7 @@ import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible'; import { useVisible } from '@/hooks/useVisible';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { fittingString, s8 } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
@@ -54,11 +54,20 @@ const EditPipeline = () => {
const onDragEnd = (val) => { const onDragEnd = (val) => {
const { x, y } = val; const { x, y } = val;
const point = graph.getPointByClient(x, y); const point = graph.getPointByClient(x, y);

let label = val.label;
const data = graph.save();
const nodeLabels = data.nodes.map((v) => v.label);
if (nodeLabels.includes(label)) {
label += '-' + s8();
}

// 元模型 // 元模型
const model = { const model = {
...val, ...val,
x: point.x, x: point.x,
y: point.y, y: point.y,
label,
id: val.component_name + '-' + s8(), id: val.component_name + '-' + s8(),
isCluster: false, isCluster: false,
formError: true, formError: true,
@@ -90,38 +99,57 @@ const EditPipeline = () => {


// 保存 // 保存
const savePipeline = async (isBack) => { const savePipeline = async (isBack) => {
const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
if (globalParamError) {
message.error('全局参数配置有误');
openParamsDrawer();
return;
}
closeParamsDrawer();
// 验证全局参数
// 现在改为关闭的时候就验证了
// const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
// if (globalParamError) {
// message.error('全局参数配置有误');
// openParamsDrawer();
// return;
// }
// closeParamsDrawer();


const [propsRes, propsError] = await to(propsRef.current.validateFields());
if (propsError) {
message.error('节点必填项必须配置');
return;
}
propsRef.current.close();
// 以前没有遮挡【保存】按钮时有用
// 验证节点必填参数
// const [propsRes, propsError] = await to(propsRef.current.validateFields());
// if (propsError) {
// message.error('节点必填项必须配置');
// return;
// }
// propsRef.current.close();


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

// 验证节点名称是否有重命名
const nodeLabels = data.nodes.map((v) => v.label);
for (let i = 0; i < nodeLabels.length; i++) {
const current = nodeLabels[i];
for (let j = i + 1; j < nodeLabels.length; j++) {
const next = nodeLabels[j];
if (current === next) {
message.error(`存在重名的【${current}】节点`);
return;
}
}
}

const params = { const params = {
...locationParams, ...locationParams,
name: workflowInfo?.name, name: workflowInfo?.name,
dag: JSON.stringify(data),
global_param: JSON.stringify(globalParamRes.global_param),
dag: data,
global_param: globalParam,
}; };
saveWorkflow(params).then((ret) => { saveWorkflow(params).then((ret) => {
message.success('保存成功'); message.success('保存成功');
@@ -290,7 +318,7 @@ const EditPipeline = () => {
const { global_param, dag } = res.data; const { global_param, dag } = res.data;
setGlobalParam(global_param || []); setGlobalParam(global_param || []);
if (dag) { if (dag) {
getGraphData(parseJsonText(dag));
getGraphData(dag);
} }
} }
}; };
@@ -299,13 +327,19 @@ const EditPipeline = () => {
const openNodeDrawer = (node, validate = false) => { const openNodeDrawer = (node, validate = false) => {
// 获取所有的上游节点 // 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, node); const parentNodes = findAllParentNodes(graph, node);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// q全局参数
const globalParams = globalParamRef.current;
// 打开节点编辑抽屉 // 打开节点编辑抽屉
propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
}; };


// 关闭全局参数节点,获取全局参数
const closeGlobalParamsDrawer = () => {
const { global_param } = paramsDrawerRef.current.getFieldsValue();
setGlobalParam(global_param);
closeParamsDrawer();
};

// 初始化图 // 初始化图
const initGraph = () => { const initGraph = () => {
const contextMenu = initMenu(); const contextMenu = initMenu();
@@ -730,7 +764,7 @@ const EditPipeline = () => {
ref={paramsDrawerRef} ref={paramsDrawerRef}
open={paramsDrawerOpen} open={paramsDrawerOpen}
globalParam={globalParam} globalParam={globalParam}
onClose={closeParamsDrawer}
onClose={closeGlobalParamsDrawer}
></GlobalParamsDrawer> ></GlobalParamsDrawer>
</div> </div>
); );


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

Loading…
Cancel
Save