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

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


+ 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.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
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,


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

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


+ 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",
"deploy": "npm run build && npm run gh-pages",
"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-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
@@ -35,7 +35,7 @@
"serve": "umi-serve",
"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: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:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"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';
import './styles/menu.less';
import { needAuth } from './utils';
import { closeAllModals } from './utils/modal';
import { gotoLoginPage } from './utils/ui';
export { requestConfig as request } from './requestConfig';

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


}

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

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

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

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

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

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

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


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

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

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

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

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


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

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

&__empty {


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

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

export { type CodeConfigData };

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

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

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

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

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

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

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

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

// 分页切换
@@ -77,7 +136,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
title="选择代码配置"
image={require('@/assets/img/modal-code-config.png')}
width={920}
footer={null}
onOk={() => onOk?.(selected)}
destroyOnClose
>
<div className="kf-code-selector-modal">
@@ -93,23 +152,31 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
prefix={
<KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} />
}
// prefix={
// <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} />
// }
/>
{dataList?.length !== 0 ? (
<>
<div className="kf-code-selector-modal__content">
{dataList?.map((item) => (
<CodeConfigItem item={item} key={item.id} onClick={handleClick} />
<CodeConfigItem
item={item}
key={item.id}
checked={item.id === selected?.id}
onChange={handleChange}
/>
))}
</div>
<Pagination
align="center"
align="end"
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
defaultPageSize={DefaultPageSize}
pageSizeOptions={[
DefaultPageSize,
2 * DefaultPageSize,
3 * DefaultPageSize,
4 * DefaultPageSize,
5 * DefaultPageSize,
]}
showQuickJumper
onChange={handlePageChange}
{...pagination}


+ 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 { Typography, type SelectProps } from 'antd';
import classNames from 'classnames';
@@ -16,6 +17,8 @@ type FormInfoProps = {
options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames'];
/** 全局参数 */
globalParams?: PipelineGlobalParam[] | null;
/** 自定义类名 */
className?: string;
/** 自定义样式 */
@@ -32,12 +35,29 @@ function FormInfo({
select = false,
options,
fieldNames,
globalParams,
className,
style,
}: FormInfoProps) {
let showValue = value;
if (value && typeof value === 'object' && 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) {
let _options: SelectProps['options'] = options;
if (fieldNames) {


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

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

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

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

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

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

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

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

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

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

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


+ 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 { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import { ReactNode } from 'react';
import './index.less';

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

function ParameterInput({
@@ -75,7 +78,7 @@ function ParameterInput({
const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) {
valueObj.showValue = valueObj.value;
valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : '';
}
const isSelect = valueObj?.fromSelect;
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 { getDatasetList, getModelList } from '@/services/dataset/index.js';
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 { 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 = {
getOptions: () => Promise<any>; // 获取下拉数据
getOptions?: () => Promise<any>; // 获取下拉数据
fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
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: {
getOptions: async () => {
const res = await getDatasetList({
@@ -31,13 +36,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000,
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: {
getOptions: async () => {
@@ -46,13 +54,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000,
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: {
getOptions: async () => {
@@ -60,25 +71,58 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
page: 0,
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',
getValue: (value: ServiceData) => {
return value.id;
},
getLabel: (value: ServiceData) => {
return value.service_name;
},
isObjectValue: true,
},
resource: {
getOptions: async () => {
// 不需要这个函数
return [];
},
fieldNames: resourceFieldNames,
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: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/

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

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

export type ParameterSelectObject = {
value: 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 {
/** 类型 */
@@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps {
display?: boolean;
/** 值,支持对象,对象必须包含 value */
value?: string | ParameterSelectObject;
/** 用于流水线, 流水线资源规格要求 id 为字符串 */
isPipeline?: boolean;
/** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void;
}
@@ -36,69 +40,126 @@ function ParameterSelect({
dataType,
display = false,
value,
isPipeline = false,
onChange,
...rest
}: ParameterSelectProps) {
const [options, setOptions] = useState<SelectProps['options']>([]);
const [options, setOptions] = useState<SelectOptions>([]);
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(() => {
// 获取下拉数据
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();
}, [propsConfig]);
}, [getOptions, dataType, getResourceTypes]);

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

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 {
onChange?.(text);
const selectValue = text ? text : '';
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
}
};

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

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


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

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

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

// 获取选择数据集、模型、镜像后面按钮 icon
@@ -47,68 +49,51 @@ function ResourceSelect({
}: ResourceSelectProps) {
const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize;
let selectedResource: ResourceSelectorResponse | undefined = undefined;
if (
value &&
typeof value === 'object' &&
value.activeTab &&
value.id &&
value.name &&
value.version &&
value.path &&
(type === ResourceSelectorType.Mirror || (value.identifier && value.owner))
) {
selectedResource = pick(value, [
'activeTab',
'id',
'identifier',
'name',
'owner',
'version',
'path',
]) as ResourceSelectorResponse;
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 { close } = openAntdModal(ResourceSelectorModal, {
type,
defaultExpandedKeys: selectedResource ? [selectedResource.id] : [],
defaultCheckedKeys: selectedResource
? [`${selectedResource.id}-${selectedResource.version}`]
: [],
defaultActiveTab: selectedResource?.activeTab,
defaultExpandedKeys: defaultExpandedKeys,
defaultCheckedKeys: defaultCheckedKeys,
defaultActiveTab: defaultActiveTab,
onOk: (res) => {
if (res) {
const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) {
const { activeTab, ...rest } = res;
const { url } = rest;
onChange?.({
value: path,
showValue: path,
...rest,
value: url,
showValue: url,
fromSelect: true,
activeTab,
id,
name,
version,
path,
});
} 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}`;
onChange?.({
value: jsonObjStr,
...rest,
value: showValue,
showValue,
fromSelect: true,
activeTab,
...jsonObj,
});
}
} 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 mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png';
import { AvailableRange, CommonTabKeys } from '@/enums';
import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums';
import { DatasetData, ModelData, ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { MirrorVersionData } from '@/pages/Mirror/Info';
import { MirrorData } from '@/pages/Mirror/List';
import {
@@ -24,11 +24,11 @@ export enum ResourceSelectorType {
}

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

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

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


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

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

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

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

+ 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 { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config';
import { type MirrorVersionData } from '@/pages/Mirror/Info';
import { to } from '@/utils/promise';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd';
@@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig };
// 选择数据集、模型、镜像的返回类型
export type ResourceSelectorResponse = {
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'> {
/** 类型,数据集、模型、镜像 */
@@ -84,6 +85,7 @@ function ResourceSelectorModal({
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFileData[]>([]);
const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined);
const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState('');
const [firstLoadList, setFirstLoadList] = useState(false);
@@ -119,6 +121,7 @@ function ResourceSelectorModal({
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionDesc(undefined);
setVersionPath('');
setSearchText('');
getTreeData();
@@ -169,9 +172,11 @@ function ResourceSelectorModal({
if (res) {
setVersionPath(res.path);
setFiles(res.content);
setVersionDesc(res.versionDesc);
} else {
setVersionPath('');
setFiles([]);
setVersionDesc(undefined);
}
};

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

@@ -236,15 +242,22 @@ function ResourceSelectorModal({
const name = (treeNode?.title ?? '') as string;
const identifier = (treeNode?.identifier ?? '') 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);
} else {
onOk?.(undefined);
@@ -253,8 +266,9 @@ function ResourceSelectorModal({

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

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


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

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

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

const loading = (


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

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

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

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

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


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

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

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

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

// 自动化任务类型
@@ -163,3 +163,11 @@ export enum AutoMLTrailStatus {
CANCELLED = 'CANCELLED', // 取消
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
*/

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(
(id?: string | number) => {
if (!id) {
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 { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { useEffect } from 'react';

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

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

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

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

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

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

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

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

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

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


+ 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 { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import {
classifierAlgorithms,
FrameworkType,
@@ -28,18 +28,18 @@ type BasicInfoProps = {
info?: ActiveLearnData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
workflowStatus?: NodeStatus;
instanceStatus?: ExperimentStatus;
};

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

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


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

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

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


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

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

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

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


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

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



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

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

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

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

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

if (workflowStatus) {
setWorkflowStatus(workflowStatus);

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


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

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

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


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

@@ -6,11 +6,22 @@ import {
autoMLEnsembleClassOptions,
autoMLTaskTypeOptions,
} 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 { type NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format';
import {
formatBoolean,
formatDataset,
formatDate,
formatEnum,
type EnumOptions,
} from '@/utils/format';
import classNames from 'classnames';
import { useMemo } from 'react';
import ExperimentRunBasic from '../ExperimentRunBasic';
@@ -21,6 +32,7 @@ const formatOptimizeMode = (value: boolean) => {
return value ? '越大越好' : '越小越好';
};

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

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

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

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

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


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

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

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

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

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

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


+ 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;
align-items: center;
width: 200px;

.statusIcon {
visibility: visible;
}
}
}



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

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


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

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

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

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

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

export default ExperimentInstance;
export default ExperimentInstanceComponent;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 实验实例状态变化
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data;
if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;
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);
return () => {
window.removeEventListener('message', handleMessage);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}
};
}, [refreshExperimentList]);
}, [experimentInsList, editExperimentIns]);

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

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

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

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

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

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

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


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

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

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

export default EmptyLog;

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

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

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

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

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

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


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

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

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

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

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

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

return (
<ConfigInfo


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

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

&__empty {
height: 100%;
}
}

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

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

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

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

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

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

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

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

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

export default ExperimentVisualResult;

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

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

&__branch {


+ 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 { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addDataset } from '@/services/dataset/index.js';
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'> {
typeList: CategoryData[];
@@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {
}

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 [res] = await to(addDataset(params));
@@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr

// 提交
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 (
@@ -108,30 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
>
<Input placeholder="请输入数据名称" showCount allowClear maxLength={40} />
</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">
<Select
allowClear
@@ -170,24 +99,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
allowClear
/>
</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
label="可见性"
name="is_public"
@@ -198,29 +109,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
<Radio value={true}>公开</Radio>
</Radio.Group>
</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>
</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 { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { CategoryData, DataSource } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js';
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'> {
typeList: CategoryData[];
@@ -28,18 +11,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> {
}

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 [res] = await to(addModel(params));
@@ -51,22 +22,11 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)

// 提交
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 (
@@ -99,34 +59,10 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
>
<Input placeholder="请输入模型名称" showCount allowClear maxLength={40} />
</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">
<Select
allowClear
placeholder="请选择模型类型"
placeholder="请选择模型框架"
options={typeList}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
@@ -136,7 +72,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Form.Item label="模型能力" name="model_tag">
<Select
allowClear
placeholder="请选择模型标签"
placeholder="请选择模型能力"
options={tagList}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
@@ -161,24 +97,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
allowClear
/>
</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
label="可见性"
name="is_public"
@@ -189,28 +107,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
<Radio value={true}>公开</Radio>
</Radio.Group>
</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>
</KFModal>
);


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

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

interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> {
@@ -40,6 +40,23 @@ function AddVersionModal({
}: AddVersionModalProps) {
const [uuid] = useState(Date.now());
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 = {
@@ -109,6 +126,7 @@ function AddVersionModal({
}}
onFinish={onFinish}
autoComplete="off"
form={form}
>
<Form.Item
label={`${name}名称`}
@@ -132,19 +150,21 @@ function AddVersionModal({
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`,
},
{
validator: (_rule, value) => {
if (value === 'master') {
return Promise.reject(`版本不能为 master`);
return Promise.reject(`${name}版本不能为 master`);
} else if (value === 'origin') {
return Promise.reject(`${name}版本不能为 origin`);
}
return Promise.resolve();
},
},
]}
>
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear />
<Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled />
</Form.Item>
<Form.Item
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;
}

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

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

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

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


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

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

import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon';
import {
ResourceData,
@@ -19,10 +20,11 @@ import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { useParams, useSearchParams } from '@umijs/max';
import { App, Button, Flex, Select, Tabs } from 'antd';
import { App, Button, Flex, Select, Tabs, Typography } from 'antd';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import AddVersionModal from '../AddVersionModal';
import EditVersionModal from '../EditVersionModal';
import ResourceIntro from '../ResourceIntro';
import ResourceVersion from '../ResourceVersion';
import VersionCompareModal from '../VersionCompareModal';
@@ -61,21 +63,24 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
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(
@@ -100,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
}
} else {
setVersion(undefined);
getResourceDetail(undefined);
}
},
[config, owner, identifier, versionParam],
[config, owner, identifier, versionParam, getResourceDetail],
);

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

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

// 新建版本
const showModal = () => {
const showAddVersionModal = () => {
const { close } = openAntdModal(AddVersionModal, {
resourceType: resourceType,
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 { close } = openAntdModal(VersionSelectorModal, {
@@ -278,44 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
<span>{info.praises_count}</span>
</div>
</Flex>
<Flex align="center">
<span style={{ marginRight: '10px' }}>版本号:</span>
<Select
placeholder="请选择版本号"
style={{ width: '160px', marginRight: '20px' }}
value={version}
onChange={handleVersionChange}
fieldNames={{ label: 'name', value: 'name' }}
options={versionList}
/>
<Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}>
创建新版本
</Button>
<Button
type="default"
style={{ marginLeft: '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 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>
);


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

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

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


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

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

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

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


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

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

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

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

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


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

@@ -9,11 +9,15 @@ import {
deleteDatasetVersion,
deleteModel,
deleteModelVersion,
editDatasetVersion,
editModelVersion,
getDatasetInfo,
getDatasetList,
getDatasetNextVersionReq,
getDatasetVersionList,
getModelInfo,
getModelList,
getModelNextVersionReq,
getModelVersionList,
} from '@/services/dataset/index.js';
import { limitUploadFileType } from '@/utils/ui';
@@ -36,9 +40,11 @@ type ResourceTypeInfo = {
getVersions: (params: any) => Promise<any>; // 获取版本列表
deleteRecord: (params: any) => Promise<any>; // 删除
addVersion: (params: any) => Promise<any>; // 新增版本
editVersion: (params: any) => Promise<any>; // 编辑版本
deleteVersion: (params: any) => Promise<any>; // 删除版本
getInfo: (params: any) => Promise<any>; // 获取详情
compareVersion: (params: any) => Promise<any>; // 版本对比
getNextVersion: (params: any) => Promise<any>; // 获取下一个版本
name: string; // 名称
typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用
tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用
@@ -65,9 +71,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getDatasetVersionList,
deleteRecord: deleteDataset,
addVersion: addDatasetVersion,
editVersion: editDatasetVersion,
deleteVersion: deleteDatasetVersion,
getInfo: getDatasetInfo,
compareVersion: compareDatasetVersion,
getNextVersion: getDatasetNextVersionReq,
name: '数据集',
typeParamKey: 'data_type',
tagParamKey: 'data_tag',
@@ -103,9 +111,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
getVersions: getModelVersionList,
deleteRecord: deleteModel,
addVersion: addModelVersion,
editVersion: editModelVersion,
deleteVersion: deleteModelVersion,
getInfo: getModelInfo,
compareVersion: compareModelVersion,
getNextVersion: getModelNextVersionReq,
name: '模型',
typeParamKey: 'model_type',
tagParamKey: 'model_tag',
@@ -164,6 +174,7 @@ export interface ResourceData {
train_task?: TrainTask; // 训练任务
praises_count: number; // 点赞数
praised: boolean; // 是否点赞
full_last_update_time: string; // 完整的更新时间
}

// 数据集数据


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

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

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

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


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

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

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

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

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

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

setCacheState({
pagination,
});

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

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

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

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

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

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

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

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


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

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


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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 更新各个节点
function updateWorkflowNode(workflowNode, statusNode) {
if (!statusNode) {
return;
@@ -471,29 +518,30 @@ function ExperimentText() {
<div className={styles['pipeline-container']}>
<div className={styles['pipeline-container__top']}>
<div className={styles['pipeline-container__top__info']}>
启动时间:{formatDate(experimentIns?.create_time)}
启动时间:{formatDate(workflowStatus?.startedAt)}
</div>
<div className={styles['pipeline-container__top__info']}>
执行时长:
<RunDuration
createTime={experimentIns?.create_time}
finishTime={experimentIns?.finish_time}
createTime={workflowStatus?.startedAt}
finishTime={workflowStatus?.finishedAt}
/>
</div>
<div className={styles['pipeline-container__top__info']}>
状态:
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
marginRight: '6px',
backgroundColor: experimentStatusInfo[experimentIns?.status]?.color,
}}
></div>
<span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}>
{experimentStatusInfo[experimentIns?.status]?.label}
</span>
{statusInfo ? (
<>
<img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo.icon}
draggable={false}
alt=""
/>
<span style={{ color: statusInfo.color }}>{statusInfo.label}</span>
</>
) : (
'--'
)}
</div>
<Button
className={styles['pipeline-container__top__param-button']}
@@ -519,12 +567,13 @@ function ExperimentText() {
instanceNodeStatus={experimentNodeData.experimentStatus}
instanceNodeStartTime={experimentNodeData.experimentStartTime}
instanceNodeEndTime={experimentNodeData.experimentEndTime}
globalParams={experimentIns?.global_param}
></ExperimentDrawer>
) : null}
<ParamsModal
open={paramsModalOpen}
onCancel={closeParamsModal}
globalParam={experimentIns?.global_param}
globalParams={experimentIns?.global_param}
></ParamsModal>
</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-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 editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd';
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 类型
if (Number(paramType) === 3) {
if (Number(paramType) === PipelineGlobalParamType.Boolean) {
return (
<Radio.Group>
<Radio value={1}>是</Radio>
@@ -40,9 +40,9 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX.
</Radio.Group>
);
}
if (isSensitive && Number(isSensitive) === 1) {
return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />;
}
// if (isSensitive && Number(isSensitive) === 1) {
// return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />;
// }
return <Input placeholder="请输入值" allowClear />;
};

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

// 根据参数设置 label
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]})`;
return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>;
@@ -95,8 +95,8 @@ function AddExperimentModal({
};

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

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


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

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

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



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

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

.statusIcon {
visibility: visible;
}
}
}



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

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

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

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


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

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

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

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

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

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

export default ExperimentInstance;
export default ExperimentInstanceComponent;

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

@@ -15,4 +15,24 @@
font-size: @font-size;
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 ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
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';

type ExperimentParameterProps = {
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]) => ({
@@ -74,92 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
>
<FormInfo />
</Form.Item>
<div className={styles['experiment-parameter__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<FormInfo />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<FormInfo />
</Form.Item>

<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>
);
}


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

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

const layout = {
@@ -77,35 +74,24 @@ function ExportModelModal({
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) => {
if (id) {
getRecourceVersions(id);
getRecourceNextVersion(id);
} else {
setVersions([]);
form.setFieldValue('version', '');
}
};

// 获取数据集、模型版本列表
const getRecourceVersions = async (id: number) => {
// 获取数据集、模型下一个版本
const getRecourceNextVersion = async (id: number) => {
const resource = getSelectedResource(id);
if (!resource) {
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) {
setVersions(res.data);
form.setFieldValue('version', res.data);
}
};

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


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

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


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

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

:global {
.ant-form-item-row {
align-items: center;
}
}
}
.params-empty {
: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 = {
open: boolean;
onCancel: () => void;
globalParam?: PipelineGlobalParam[] | null;
globalParams?: PipelineGlobalParam[] | null;
};

function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) {
return (
<KFModal
title="执行参数"
@@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }}
width={825}
>
{Array.isArray(globalParam) && globalParam.length > 0 ? (
{Array.isArray(globalParams) && globalParams.length > 0 ? (
<div className={styles['params-container']}>
<Form
name="view_params_form"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
initialValues={{ global_param: globalParam }}
initialValues={{ global_param: globalParams }}
labelAlign="left"
disabled
>
@@ -45,12 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
{...restField}
key={key}
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>
))
}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 新增,删除版本时,重置分页,然后刷新版本列表
// 更新实验实例状态
useEffect(() => {
const handleMessage = (e) => {
const { type, payload } = e.data;
if (type === ExperimentCompleted) {
const { id, status, finish_time } = payload;
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);
return () => {
window.removeEventListener('message', handleMessage);
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = undefined;
}
};
}, [refreshExperimentList]);
}, [experimentInsList, editExperimentIns]);

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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


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

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



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

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


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

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

const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) {
@@ -83,7 +80,7 @@ function HyperParameterBasic({
}
return [
{
label: '代码',
label: '代码配置',
value: info.code_config,
format: formatCodeConfig,
},
@@ -145,56 +142,10 @@ function HyperParameterBasic({
];
}, [info, getResourceDescription]);

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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

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


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

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

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

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


+ 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 { useNavigate, useParams } from '@umijs/max';
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 { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less';
@@ -79,7 +79,7 @@ function CreateServiceVersion() {
if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
// model.value = model.showValue;
}
// 环境变量
if (res.env_variables && typeof res.env_variables === 'object') {
@@ -117,7 +117,6 @@ function CreateServiceVersion() {
// 创建版本
const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables'];
const model = formData['model'];
const envVariables = envList?.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
@@ -125,13 +124,9 @@ function CreateServiceVersion() {

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

@@ -238,7 +233,7 @@ function CreateServiceVersion() {
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '服务支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
]}
>
@@ -427,6 +422,7 @@ function CreateServiceVersion() {
{...restField}
name={[name, 'key']}
style={{ flex: 1 }}
dependencies={fields.map((_, i) => ['env_variables', i, 'key'])}
rules={[
{
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 { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { useSystemResource } from '@/hooks/useComputingResource';
import { ModelData } from '@/pages/Dataset/config';
import {
deleteServiceVersionReq,
getServiceInfoReq,
@@ -18,6 +19,7 @@ import {
} from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date';
import { formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
@@ -87,7 +89,7 @@ function ServiceInfo() {
format: formatDate,
},
];
const getResourceDescription = useComputingResource()[1];
const getResourceDescription = useSystemResource();

// 获取服务详情
const getServiceInfo = useCallback(async () => {
@@ -110,8 +112,8 @@ function ServiceInfo() {
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
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);
@@ -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'] = [
{
title: '序号',
@@ -278,10 +294,12 @@ function ServiceInfo() {
},
{
title: '模型版本',
dataIndex: ['model', 'show_value'],
dataIndex: ['model', 'showValue'],
key: 'model',
width: '20%',
render: tableCellRender(true),
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoModel,
}),
},
{
title: '镜像版本',


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

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

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

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

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

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


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

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

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

&__more {
padding: 20px 0;
}

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

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

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

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

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


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

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

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

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

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


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

@@ -34,7 +34,7 @@ export type ServiceVersionData = {
path: string;
identifier: string;
owner: string;
show_value: string;
showValue: string;
};
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 { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { fittingString, s8 } from '@/utils';
import { to } from '@/utils/promise';
import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max';
@@ -54,11 +54,20 @@ const EditPipeline = () => {
const onDragEnd = (val) => {
const { x, y } = val;
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 = {
...val,
x: point.x,
y: point.y,
label,
id: val.component_name + '-' + s8(),
isCluster: false,
formError: true,
@@ -90,38 +99,57 @@ const EditPipeline = () => {

// 保存
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(() => {
const data = graph.save();
// console.log(data);
// 验证节点必填参数
const errorNode = data.nodes.find((item) => item.formError === true);
if (errorNode) {
message.error(`【${errorNode.label}】节点必填项必须配置`);
message.error(`【${errorNode.label}】节点配置验证失败`);
const graphNode = graph.findById(errorNode.id);
if (graphNode) {
openNodeDrawer(graphNode, true);
}
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 = {
...locationParams,
name: workflowInfo?.name,
dag: JSON.stringify(data),
global_param: JSON.stringify(globalParamRes.global_param),
dag: data,
global_param: globalParam,
};
saveWorkflow(params).then((ret) => {
message.success('保存成功');
@@ -290,7 +318,7 @@ const EditPipeline = () => {
const { global_param, dag } = res.data;
setGlobalParam(global_param || []);
if (dag) {
getGraphData(parseJsonText(dag));
getGraphData(dag);
}
}
};
@@ -299,13 +327,19 @@ const EditPipeline = () => {
const openNodeDrawer = (node, validate = false) => {
// 获取所有的上游节点
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);
};

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

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


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

Loading…
Cancel
Save