From 89ed8ac54467de9e19cbce9c3f29a45f0121c8eb Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 6 Jan 2025 14:47:00 +0800 Subject: [PATCH 001/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ray/RayController.java | 62 +++++++++ .../controller/ray/RayInsController.java | 60 ++++++++ .../java/com/ruoyi/platform/domain/Ray.java | 77 +++++++++++ .../com/ruoyi/platform/domain/RayIns.java | 45 ++++++ .../com/ruoyi/platform/mapper/RayDao.java | 21 +++ .../com/ruoyi/platform/mapper/RayInsDao.java | 21 +++ .../ruoyi/platform/service/RayInsService.java | 21 +++ .../ruoyi/platform/service/RayService.java | 22 +++ .../service/impl/RayInsServiceImpl.java | 130 ++++++++++++++++++ .../platform/service/impl/RayServiceImpl.java | 119 ++++++++++++++++ .../java/com/ruoyi/platform/vo/RayVo.java | 76 ++++++++++ .../managementPlatform/RayDaoMapper.xml | 109 +++++++++++++++ .../managementPlatform/RayInsDaoMapper.xml | 69 ++++++++++ 13 files changed, 832 insertions(+) create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayController.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayDao.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayService.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java create mode 100644 ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml create mode 100644 ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayController.java new file mode 100644 index 00000000..719f5747 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayController.java @@ -0,0 +1,62 @@ +package com.ruoyi.platform.controller.ray; + +import com.ruoyi.common.core.web.controller.BaseController; +import com.ruoyi.common.core.web.domain.GenericsAjaxResult; +import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.service.RayService; +import com.ruoyi.platform.vo.RayVo; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.io.IOException; + +@RestController +@RequestMapping("ray") +@Api("自动超参数寻优") +public class RayController extends BaseController { + @Resource + private RayService rayService; + + @GetMapping + @ApiOperation("分页查询") + public GenericsAjaxResult> queryByPage(@RequestParam("page") int page, + @RequestParam("size") int size, + @RequestParam(value = "name", required = false) String name) { + PageRequest pageRequest = PageRequest.of(page, size); + return genericsSuccess(this.rayService.queryByPage(name, pageRequest)); + } + + @PostMapping + @ApiOperation("新增自动超参数寻优") + public GenericsAjaxResult addRay(@RequestBody RayVo rayVo) throws Exception { + return genericsSuccess(this.rayService.save(rayVo)); + } + + @PutMapping + @ApiOperation("编辑自动超参数寻优") + public GenericsAjaxResult editRay(@RequestBody RayVo rayVo) throws Exception{ + return genericsSuccess(this.rayService.edit(rayVo)); + } + + @GetMapping("/getRayDetail") + @ApiOperation("获取自动超参数寻优详细信息") + public GenericsAjaxResult getRayDetail(@RequestParam("id") Long id) throws IOException { + return genericsSuccess(this.rayService.getRayDetail(id)); + } + + @DeleteMapping("{id}") + @ApiOperation("删除自动超参数寻优") + public GenericsAjaxResult deleteRay(@PathVariable("id") Long id) { + return genericsSuccess(this.rayService.delete(id)); + } + + @PostMapping("/run/{id}") + @ApiOperation("运行自动超参数寻优实验") + public GenericsAjaxResult runRay(@PathVariable("id") Long id) throws Exception { + return genericsSuccess(this.rayService.runRayIns(id)); + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java new file mode 100644 index 00000000..469ce497 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java @@ -0,0 +1,60 @@ +package com.ruoyi.platform.controller.ray; + +import com.ruoyi.common.core.web.controller.BaseController; +import com.ruoyi.common.core.web.domain.GenericsAjaxResult; +import com.ruoyi.platform.domain.RayIns; +import com.ruoyi.platform.service.RayInsService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; + +@RestController +@RequestMapping("rayIns") +@Api("自动超参数寻优实验实例") +public class RayInsController extends BaseController { + @Resource + private RayInsService rayInsService; + + @GetMapping + @ApiOperation("分页查询") + public GenericsAjaxResult> queryByPage(Long rayId, int page, int size) throws IOException { + PageRequest pageRequest = PageRequest.of(page, size); + return genericsSuccess(this.rayInsService.queryByPage(rayId, pageRequest)); + } + + @PostMapping + @ApiOperation("新增实验实例") + public GenericsAjaxResult add(@RequestBody RayIns rayIns) { + return genericsSuccess(this.rayInsService.insert(rayIns)); + } + + @DeleteMapping("{id}") + @ApiOperation("删除实验实例") + public GenericsAjaxResult deleteById(@PathVariable("id") Long id) { + return genericsSuccess(this.rayInsService.deleteById(id)); + } + + @DeleteMapping("batchDelete") + @ApiOperation("批量删除实验实例") + public GenericsAjaxResult batchDelete(@RequestBody List ids) { + return genericsSuccess(this.rayInsService.batchDelete(ids)); + } + + @PutMapping("{id}") + @ApiOperation("终止实验实例") + public GenericsAjaxResult terminateRayIns(@PathVariable("id") Long id) throws Exception { + return genericsSuccess(this.rayInsService.terminateRayIns(id)); + } + + @GetMapping("{id}") + @ApiOperation("查看实验实例详情") + public GenericsAjaxResult getDetailById(@PathVariable("id") Long id) { + return genericsSuccess(this.rayInsService.getDetailById(id)); + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java new file mode 100644 index 00000000..838a0311 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -0,0 +1,77 @@ +package com.ruoyi.platform.domain; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@Data +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@ApiModel(description = "自动超参数寻优") +public class Ray { + private Long id; + + @ApiModelProperty(value = "实验名称") + private String name; + + @ApiModelProperty(value = "数据集") + private String dataset; + + @ApiModelProperty(value = "代码") + private String code; + + @ApiModelProperty(value = "主函数代码文件") + private String mainPy; + + @ApiModelProperty(value = "总实验次数") + private Integer numSamples; + + @ApiModelProperty(value = "参数") + private String parameters; + + @ApiModelProperty(value = "手动指定需要运行的参数") + private String pointsToEvaluate; + + @ApiModelProperty(value = "保存路径") + private String storagePath; + + @ApiModelProperty(value = "搜索算法") + private String searchAlg; + + @ApiModelProperty(value = "调度算法") + private String scheduler; + + @ApiModelProperty(value = "指标") + private String metric; + + @ApiModelProperty(value = "指标最大化或最小化,min or max") + private String mode; + + @ApiModelProperty(value = "搜索算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") + private Integer maxT; + + @ApiModelProperty(value = "搜索算法为MedianStopping时传入,计算中位数的最小试验数。") + private Integer minSamplesRequired; + + @ApiModelProperty(value = "使用cpu数") + private Integer cpu; + + @ApiModelProperty(value = "使用gpu数") + private Integer gpu; + + private Integer state; + + private String createBy; + + private String updateBy; + + private Date createTime; + + private Date updateTime; + + @ApiModelProperty(value = "状态列表") + private String statusList; +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java new file mode 100644 index 00000000..161a5f98 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java @@ -0,0 +1,45 @@ +package com.ruoyi.platform.domain; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@Data +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@ApiModel(description = "自动超参数寻优实验实例") +public class RayIns { + private Long id; + + private Long rayId; + + private String resultPath; + + private Integer state; + + private String status; + + private String nodeStatus; + + private String nodeResult; + + private String param; + + private String source; + + @ApiModelProperty(value = "Argo实例名称") + private String argoInsName; + + @ApiModelProperty(value = "Argo命名空间") + private String argoInsNs; + + private Date createTime; + + private Date updateTime; + + private Date finishTime; +} + diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayDao.java new file mode 100644 index 00000000..dad71b0f --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayDao.java @@ -0,0 +1,21 @@ +package com.ruoyi.platform.mapper; + +import com.ruoyi.platform.domain.Ray; +import org.apache.ibatis.annotations.Param; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +public interface RayDao { + long count(@Param("name") String name); + + List queryByPage(@Param("name") String name, @Param("pageable") PageRequest pageRequest); + + Ray getRayByName(@Param("name") String name); + + Ray getRayById(@Param("id") Long id); + + int save(@Param("ray") Ray ray); + + int edit(@Param("ray") Ray ray); +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java new file mode 100644 index 00000000..ceeb581d --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java @@ -0,0 +1,21 @@ +package com.ruoyi.platform.mapper; + +import com.ruoyi.platform.domain.RayIns; +import org.apache.ibatis.annotations.Param; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface RayInsDao { + long count(@Param("rayId") Long rayId); + + List queryAllByLimit(@Param("rayId") Long rayId, @Param("pageable") Pageable pageable); + + RayIns queryById(@Param("id") Long id); + + List getByRayId(@Param("rayId") Long rayId); + + int insert(@Param("rayIns") RayIns rayIns); + + int update(@Param("rayIns") RayIns rayIns); +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java new file mode 100644 index 00000000..10ea6983 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java @@ -0,0 +1,21 @@ +package com.ruoyi.platform.service; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import com.ruoyi.platform.domain.RayIns; +import java.io.IOException; +import java.util.List; +public interface RayInsService { + Page queryByPage(Long rayId, PageRequest pageRequest) throws IOException; + + RayIns insert(RayIns rayIns); + + String deleteById(Long id); + + String batchDelete(List ids); + + boolean terminateRayIns(Long id) throws Exception; + + RayIns getDetailById(Long id); + + void updateRayStatus(Long rayId); +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayService.java new file mode 100644 index 00000000..b3659240 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayService.java @@ -0,0 +1,22 @@ +package com.ruoyi.platform.service; + +import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.vo.RayVo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.io.IOException; + +public interface RayService { + Page queryByPage(String name, PageRequest pageRequest); + + Ray save(RayVo rayVo) throws Exception; + + String edit(RayVo rayVo) throws Exception; + + RayVo getRayDetail(Long id) throws IOException; + + String delete(Long id); + + String runRayIns(Long id) throws Exception; +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java new file mode 100644 index 00000000..a9a00d30 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -0,0 +1,130 @@ +package com.ruoyi.platform.service.impl; + +import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.domain.RayIns; +import com.ruoyi.platform.mapper.RayDao; +import com.ruoyi.platform.mapper.RayInsDao; +import com.ruoyi.platform.service.RayInsService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Service("rayInsService") +public class RayInsServiceImpl implements RayInsService { + @Resource + private RayInsDao rayInsDao; + + @Resource + private RayDao rayDao; + + @Override + public Page queryByPage(Long rayId, PageRequest pageRequest) throws IOException { + long total = this.rayInsDao.count(rayId); + List rayInsList = this.rayInsDao.queryAllByLimit(rayId, pageRequest); + return new PageImpl<>(rayInsList, pageRequest, total); + } + + @Override + public RayIns insert(RayIns rayIns) { + this.rayInsDao.insert(rayIns); + return rayIns; + } + + @Override + public String deleteById(Long id) { + RayIns rayIns = rayInsDao.queryById(id); + if (rayIns == null) { + return "实验实例不存在"; + } + if (StringUtils.isEmpty(rayIns.getStatus())) { + //todo queryStatusFromArgo + } + if (StringUtils.equals(rayIns.getStatus(), Constant.Running)) { + return "实验实例正在运行,不可删除"; + } + + rayIns.setState(Constant.State_invalid); + int update = rayInsDao.update(rayIns); + if (update > 0) { + updateRayStatus(rayIns.getRayId()); + return "删除成功"; + } else { + return "删除失败"; + } + } + + @Override + public String batchDelete(List ids) { + for (Long id : ids) { + String result = deleteById(id); + if (!"删除成功".equals(result)) { + return result; + } + } + return "删除成功"; + } + + @Override + public boolean terminateRayIns(Long id) throws Exception { + RayIns rayIns = rayInsDao.queryById(id); + if (rayIns == null) { + throw new IllegalStateException("实验实例未查询到,id: " + id); + } + + String currentStatus = rayIns.getStatus(); + String name = rayIns.getArgoInsName(); + String namespace = rayIns.getArgoInsNs(); + + // 获取当前状态,如果为空,则从Argo查询 + if (StringUtils.isEmpty(currentStatus)) { + // todo queryStatusFromArgo + } + + // 只有状态是"Running"时才能终止实例 + if (!currentStatus.equalsIgnoreCase(Constant.Running)) { + throw new Exception("终止错误,只有运行状态的实例才能终止"); // 如果不是"Running"状态,则不执行终止操作 + } + + //todo terminateFromArgo + + rayIns.setStatus(Constant.Terminated); + rayIns.setFinishTime(new Date()); + this.rayInsDao.update(rayIns); + updateRayStatus(rayIns.getRayId()); + return true; + } + + @Override + public RayIns getDetailById(Long id) { + RayIns rayIns = rayInsDao.queryById(id); + if (Constant.Running.equals(rayIns.getStatus()) || Constant.Pending.equals(rayIns.getStatus())) { + //todo queryStatusFromArgo + } + return rayIns; + } + + @Override + public void updateRayStatus(Long rayId) { + List insList = rayInsDao.getByRayId(rayId); + List statusList = new ArrayList<>(); + // 更新实验状态列表 + for (int i = 0; i < insList.size(); i++) { + statusList.add(insList.get(i).getStatus()); + } + String subStatus = statusList.toString().substring(1, statusList.toString().length() - 1); + Ray ray = rayDao.getRayById(rayId); + if (!StringUtils.equals(ray.getStatusList(), subStatus)) { + ray.setStatusList(subStatus); + rayDao.edit(ray); + } + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java new file mode 100644 index 00000000..e3fdebaa --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -0,0 +1,119 @@ +package com.ruoyi.platform.service.impl; + +import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.mapper.RayDao; +import com.ruoyi.platform.service.RayService; +import com.ruoyi.platform.utils.JacksonUtil; +import com.ruoyi.platform.utils.JsonUtils; +import com.ruoyi.platform.vo.RayVo; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; + +@Service("rayService") +public class RayServiceImpl implements RayService { + @Resource + private RayDao rayDao; + + @Override + public Page queryByPage(String name, PageRequest pageRequest) { + long total = rayDao.count(name); + List rays = rayDao.queryByPage(name, pageRequest); + return new PageImpl<>(rays, pageRequest, total); + } + + @Override + public Ray save(RayVo rayVo) throws Exception { + Ray rayByName = rayDao.getRayByName(rayVo.getName()); + if (rayByName != null) { + throw new RuntimeException("实验名称已存在"); + } + Ray ray = new Ray(); + BeanUtils.copyProperties(rayVo, ray); + String username = SecurityUtils.getLoginUser().getUsername(); + ray.setCreateBy(username); + ray.setUpdateBy(username); + String datasetJson = JacksonUtil.toJSONString(rayVo.getDataset()); + ray.setDataset(datasetJson); + String codeJson = JacksonUtil.toJSONString(rayVo.getCode()); + ray.setCode(codeJson); + rayDao.save(ray); + return ray; + } + + @Override + public String edit(RayVo rayVo) throws Exception { + Ray oldRay = rayDao.getRayByName(rayVo.getName()); + if (oldRay != null && !oldRay.getId().equals(rayVo.getId())) { + throw new RuntimeException("实验名称已存在"); + } + Ray ray = new Ray(); + BeanUtils.copyProperties(rayVo, ray); + String username = SecurityUtils.getLoginUser().getUsername(); + ray.setUpdateBy(username); + String parameters = JacksonUtil.toJSONString(rayVo.getParameters()); + ray.setParameters(parameters); + String pointsToEvaluate = JacksonUtil.toJSONString(rayVo.getPointsToEvaluate()); + ray.setPointsToEvaluate(pointsToEvaluate); + String datasetJson = JacksonUtil.toJSONString(rayVo.getDataset()); + ray.setDataset(datasetJson); + String codeJson = JacksonUtil.toJSONString(rayVo.getCode()); + ray.setCode(codeJson); + rayDao.edit(ray); + return "修改成功"; + } + + @Override + public RayVo getRayDetail(Long id) throws IOException { + Ray ray = rayDao.getRayById(id); + RayVo rayVo = new RayVo(); + BeanUtils.copyProperties(ray, rayVo); + if (StringUtils.isNotEmpty(ray.getParameters())) { + rayVo.setParameters(JsonUtils.jsonToMap(ray.getParameters())); + } + if (StringUtils.isNotEmpty(ray.getPointsToEvaluate())) { + rayVo.setPointsToEvaluate(JsonUtils.jsonToMap(ray.getPointsToEvaluate())); + } + if (StringUtils.isNotEmpty(ray.getDataset())) { + rayVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); + } + if (StringUtils.isNotEmpty(ray.getCode())) { + rayVo.setCode(JsonUtils.jsonToMap(ray.getCode())); + } + return rayVo; + } + + @Override + public String delete(Long id) { + Ray ray = rayDao.getRayById(id); + if (ray == null) { + throw new RuntimeException("实验不存在"); + } + String username = SecurityUtils.getLoginUser().getUsername(); + String createBy = ray.getCreateBy(); + if (!(StringUtils.equals(username, "admin") || StringUtils.equals(username, createBy))) { + throw new RuntimeException("无权限删除该实验"); + } + ray.setState(Constant.State_invalid); + return rayDao.edit(ray) > 0 ? "删除成功" : "删除失败"; + } + + @Override + public String runRayIns(Long id) throws Exception { + Ray ray = rayDao.getRayById(id); + if (ray == null) { + throw new Exception("自动超参数寻优配置不存在"); + } + //todo argo + return null; + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java new file mode 100644 index 00000000..00f16b12 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -0,0 +1,76 @@ +package com.ruoyi.platform.vo; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; +import java.util.Map; + +@Data +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@ApiModel(description = "自动超参数寻优") +public class RayVo { + private Long id; + + @ApiModelProperty(value = "实验名称") + private String name; + + @ApiModelProperty(value = "主函数代码文件") + private String mainPy; + + @ApiModelProperty(value = "总实验次数") + private Integer numSamples; + + @ApiModelProperty(value = "参数") + private Map parameters; + + @ApiModelProperty(value = "手动指定需要运行的参数") + private Map pointsToEvaluate; + + @ApiModelProperty(value = "保存路径") + private String storagePath; + + @ApiModelProperty(value = "搜索算法") + private String searchAlg; + + @ApiModelProperty(value = "调度算法") + private String scheduler; + + @ApiModelProperty(value = "指标") + private String metric; + + @ApiModelProperty(value = "指标最大化或最小化,min or max") + private String mode; + + @ApiModelProperty(value = "搜索算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") + private Integer maxT; + + @ApiModelProperty(value = "搜索算法为MedianStopping时传入,计算中位数的最小试验数。") + private Integer minSamplesRequired; + + @ApiModelProperty(value = "使用cpu数") + private Integer cpu; + + @ApiModelProperty(value = "使用gpu数") + private Integer gpu; + + private String createBy; + + private Date createTime; + + private String updateBy; + + private Date updateTime; + + private Integer state; + + private String runState; + + @ApiModelProperty(value = "代码") + private Map code; + + private Map dataset; +} diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml new file mode 100644 index 00000000..7d6f8bd8 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -0,0 +1,109 @@ + + + + + insert into ray(name, dataset, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, + search_alg, scheduler, metric, mode, max_t, + min_samples_required, cpu, gpu, create_by, update_by) + values (#{ray.name}, #{ray.dataset}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + #{ray.pointsToEvaluate}, #{ray.storagePath}, + #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, + #{ray.cpu}, #{ray.gpu}, #{ray.createBy}, #{ray.updateBy}) + + + + update ray + + + name = #{ray.name}, + + + dataset = #{ray.dataset}, + + + code = #{ray.code}, + + + main_py = #{ray.mainPy}, + + + num_samples = #{ray.numSamples}, + + + parameters = #{ray.parameters}, + + + points_to_evaluate = #{ray.pointsToEvaluate}, + + + storage_path = #{ray.storagePath}, + + + search_alg = #{ray.searchAlg}, + + + scheduler = #{ray.scheduler}, + + + metric = #{ray.metric}, + + + mode = #{ray.mode}, + + + max_t = #{ray.maxT}, + + + min_samples_required = #{ray.minSamplesRequired}, + + + cpu = #{ray.cpu}, + + + gpu = #{ray.gpu}, + + + update_by = #{ray.updateBy}, + + + status_list = #{ray.statusList}, + + + state = #{ray.state}, + + + where id = #{ray.id} + + + + + + + + + + + + + state = 1 + + and name like concat('%', #{name}, '%') + + + + \ No newline at end of file diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml new file mode 100644 index 00000000..ca4b7231 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml @@ -0,0 +1,69 @@ + + + + + insert into ray_ins(ray_id, result_path, argo_ins_name, argo_ins_ns, node_status, node_result, param, source, + status) + values (#{rayIns.rayId}, #{rayIns.resultPath}, #{rayIns.argoInsName}, #{rayIns.argoInsNs}, + #{rayIns.nodeStatus}, #{rayIns.nodeResult}, #{rayIns.param}, #{rayIns.source}, #{rayIns.status}) + + + + update ray_ins + + + result_path = #{rayIns.resultPath}, + + + status = #{rayIns.status}, + + + node_status = #{rayIns.nodeStatus}, + + + node_result = #{rayIns.nodeResult}, + + + state = #{rayIns.state}, + + + finish_time = #{rayIns.finishTime}, + + + where id = #{rayIns.id} + + + + + + + + + + \ No newline at end of file From 42cdfe89ce736db2ab47dce196bb0bd10f70ead4 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 6 Jan 2025 14:48:14 +0800 Subject: [PATCH 002/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E5=AD=A6=E4=B9=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/controller/autoML/AutoMlInsController.java | 2 +- .../java/com/ruoyi/platform/service/AutoMlInsService.java | 2 +- .../ruoyi/platform/service/impl/AutoMlInsServiceImpl.java | 6 +++--- .../com/ruoyi/platform/service/impl/AutoMlServiceImpl.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/autoML/AutoMlInsController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/autoML/AutoMlInsController.java index 874ec59c..d57e7dd8 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/autoML/AutoMlInsController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/autoML/AutoMlInsController.java @@ -49,7 +49,7 @@ public class AutoMlInsController extends BaseController { @PutMapping("{id}") @ApiOperation("终止实验实例") - public GenericsAjaxResult terminateAutoMlIns(@PathVariable("id") Long id) { + public GenericsAjaxResult terminateAutoMlIns(@PathVariable("id") Long id) throws Exception { return genericsSuccess(this.autoMLInsService.terminateAutoMlIns(id)); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/AutoMlInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/AutoMlInsService.java index 2131fc44..9776fa14 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/AutoMlInsService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/AutoMlInsService.java @@ -21,7 +21,7 @@ public interface AutoMlInsService { AutoMlIns queryStatusFromArgo(AutoMlIns autoMlIns); - boolean terminateAutoMlIns(Long id); + boolean terminateAutoMlIns(Long id) throws Exception; AutoMlIns getDetailById(Long id); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlInsServiceImpl.java index dd4c2f07..89358f00 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlInsServiceImpl.java @@ -57,7 +57,7 @@ public class AutoMlInsServiceImpl implements AutoMlInsService { if (StringUtils.isEmpty(autoMlIns.getStatus())) { autoMlIns = queryStatusFromArgo(autoMlIns); } - if (StringUtils.equals(autoMlIns.getStatus(), "Running")) { + if (StringUtils.equals(autoMlIns.getStatus(), Constant.Running)) { return "实验实例正在运行,不可删除"; } @@ -156,7 +156,7 @@ public class AutoMlInsServiceImpl implements AutoMlInsService { } @Override - public boolean terminateAutoMlIns(Long id) { + public boolean terminateAutoMlIns(Long id) throws Exception { AutoMlIns autoMlIns = autoMlInsDao.queryById(id); if (autoMlIns == null) { throw new IllegalStateException("实验实例未查询到,id: " + id); @@ -172,7 +172,7 @@ public class AutoMlInsServiceImpl implements AutoMlInsService { } // 只有状态是"Running"时才能终止实例 if (!currentStatus.equalsIgnoreCase(Constant.Running)) { - return false; // 如果不是"Running"状态,则不执行终止操作 + throw new Exception("终止错误,只有运行状态的实例才能终止"); // 如果不是"Running"状态,则不执行终止操作 } // 创建请求数据map diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java index d5e436e8..dfddf71f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java @@ -102,13 +102,13 @@ public class AutoMlServiceImpl implements AutoMlService { public String delete(Long id) { AutoMl autoMl = autoMlDao.getAutoMlById(id); if (autoMl == null) { - throw new RuntimeException("服务不存在"); + throw new RuntimeException("实验不存在"); } String username = SecurityUtils.getLoginUser().getUsername(); String createBy = autoMl.getCreateBy(); if (!(StringUtils.equals(username, "admin") || StringUtils.equals(username, createBy))) { - throw new RuntimeException("无权限删除该服务"); + throw new RuntimeException("无权限删除该实验"); } autoMl.setState(Constant.State_invalid); From e61429f7aabcf7e8ab709cfff9131014f66f172f Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 7 Jan 2025 08:33:40 +0800 Subject: [PATCH 003/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/domain/Ray.java | 3 +++ .../src/main/java/com/ruoyi/platform/vo/RayVo.java | 3 +++ .../resources/mapper/managementPlatform/RayDaoMapper.xml | 5 ++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index 838a0311..1cd7dbc8 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -17,6 +17,9 @@ public class Ray { @ApiModelProperty(value = "实验名称") private String name; + @ApiModelProperty(value = "实验描述") + private String description; + @ApiModelProperty(value = "数据集") private String dataset; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 00f16b12..4f6997c2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -18,6 +18,9 @@ public class RayVo { @ApiModelProperty(value = "实验名称") private String name; + @ApiModelProperty(value = "实验描述") + private String description; + @ApiModelProperty(value = "主函数代码文件") private String mainPy; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index 7d6f8bd8..2c2e6196 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -2,7 +2,7 @@ - insert into ray(name, dataset, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, + insert into ray(name, description, dataset, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, min_samples_required, cpu, gpu, create_by, update_by) values (#{ray.name}, #{ray.dataset}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, @@ -17,6 +17,9 @@ name = #{ray.name}, + + description = #{ray.description}, + dataset = #{ray.dataset}, From 8fc2a4b5cfccc506f7718f4c56bee29b2cbf2d76 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 8 Jan 2025 16:46:04 +0800 Subject: [PATCH 004/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/domain/Ray.java | 3 +++ .../src/main/java/com/ruoyi/platform/vo/RayVo.java | 7 +++++-- .../resources/mapper/managementPlatform/RayDaoMapper.xml | 7 +++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index 1cd7dbc8..47a68380 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -23,6 +23,9 @@ public class Ray { @ApiModelProperty(value = "数据集") private String dataset; + @ApiModelProperty(value = "数据集挂载路径") + private String datasetPath; + @ApiModelProperty(value = "代码") private String code; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 4f6997c2..0f3a7eda 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -48,10 +48,10 @@ public class RayVo { @ApiModelProperty(value = "指标最大化或最小化,min or max") private String mode; - @ApiModelProperty(value = "搜索算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") + @ApiModelProperty(value = "单次试验最大时间:单位秒,搜索算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") private Integer maxT; - @ApiModelProperty(value = "搜索算法为MedianStopping时传入,计算中位数的最小试验数。") + @ApiModelProperty(value = "计算中位数的最小试验数:搜索算法为MedianStopping时传入,计算中位数的最小试验数。") private Integer minSamplesRequired; @ApiModelProperty(value = "使用cpu数") @@ -76,4 +76,7 @@ public class RayVo { private Map code; private Map dataset; + + @ApiModelProperty(value = "数据集挂载路径") + private String datasetPath; } diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index 2c2e6196..d592d444 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -2,10 +2,10 @@ - insert into ray(name, description, dataset, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, + insert into ray(name, description, dataset, dataset_path, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, min_samples_required, cpu, gpu, create_by, update_by) - values (#{ray.name}, #{ray.dataset}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + values (#{ray.name}, #{ray.dataset}, #{ray.datasetPath}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, #{ray.cpu}, #{ray.gpu}, #{ray.createBy}, #{ray.updateBy}) @@ -23,6 +23,9 @@ dataset = #{ray.dataset}, + + dataset_path = #{ray.datasetPath}, + code = #{ray.code}, From 4358c6b3e2b3cd022d344b18b95a7580be517622 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 8 Jan 2025 16:57:31 +0800 Subject: [PATCH 005/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/vo/RayVo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 0f3a7eda..82e66c2b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -48,10 +48,10 @@ public class RayVo { @ApiModelProperty(value = "指标最大化或最小化,min or max") private String mode; - @ApiModelProperty(value = "单次试验最大时间:单位秒,搜索算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") + @ApiModelProperty(value = "单次试验最大时间:单位秒,调度算法为ASHA,HyperBand时传入,每次试验的最大时间单位。测试将在max_t时间单位后停止。") private Integer maxT; - @ApiModelProperty(value = "计算中位数的最小试验数:搜索算法为MedianStopping时传入,计算中位数的最小试验数。") + @ApiModelProperty(value = "计算中位数的最小试验数:调度算法为MedianStopping时传入,计算中位数的最小试验数。") private Integer minSamplesRequired; @ApiModelProperty(value = "使用cpu数") From a40a7a3892c3838b6e6da68a31e1195d32f8476a Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 9 Jan 2025 17:19:19 +0800 Subject: [PATCH 006/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/service/impl/RayServiceImpl.java | 22 +++++++++++++------ .../java/com/ruoyi/platform/vo/RayVo.java | 5 +++-- .../managementPlatform/RayDaoMapper.xml | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index e3fdebaa..93add6d3 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -1,5 +1,7 @@ package com.ruoyi.platform.service.impl; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.Ray; @@ -17,7 +19,9 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map; @Service("rayService") public class RayServiceImpl implements RayService { @@ -39,13 +43,13 @@ public class RayServiceImpl implements RayService { } Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); - String username = SecurityUtils.getLoginUser().getUsername(); +// String username = SecurityUtils.getLoginUser().getUsername(); + String username = "admin"; ray.setCreateBy(username); ray.setUpdateBy(username); - String datasetJson = JacksonUtil.toJSONString(rayVo.getDataset()); - ray.setDataset(datasetJson); - String codeJson = JacksonUtil.toJSONString(rayVo.getCode()); - ray.setCode(codeJson); + ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); + ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); + ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); rayDao.save(ray); return ray; } @@ -77,11 +81,15 @@ public class RayServiceImpl implements RayService { Ray ray = rayDao.getRayById(id); RayVo rayVo = new RayVo(); BeanUtils.copyProperties(ray, rayVo); + + Gson gson = new Gson(); + Type listType = new TypeToken>>() { + }.getType(); if (StringUtils.isNotEmpty(ray.getParameters())) { - rayVo.setParameters(JsonUtils.jsonToMap(ray.getParameters())); + rayVo.setParameters(gson.fromJson(ray.getParameters(), listType)); } if (StringUtils.isNotEmpty(ray.getPointsToEvaluate())) { - rayVo.setPointsToEvaluate(JsonUtils.jsonToMap(ray.getPointsToEvaluate())); + rayVo.setPointsToEvaluate(gson.fromJson(ray.getPointsToEvaluate(), listType)); } if (StringUtils.isNotEmpty(ray.getDataset())) { rayVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 82e66c2b..af8b3e17 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -7,6 +7,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.Date; +import java.util.List; import java.util.Map; @Data @@ -28,10 +29,10 @@ public class RayVo { private Integer numSamples; @ApiModelProperty(value = "参数") - private Map parameters; + private List> parameters; @ApiModelProperty(value = "手动指定需要运行的参数") - private Map pointsToEvaluate; + private List> pointsToEvaluate; @ApiModelProperty(value = "保存路径") private String storagePath; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index d592d444..86d88f35 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -5,7 +5,7 @@ insert into ray(name, description, dataset, dataset_path, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, min_samples_required, cpu, gpu, create_by, update_by) - values (#{ray.name}, #{ray.dataset}, #{ray.datasetPath}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.datasetPath}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, #{ray.cpu}, #{ray.gpu}, #{ray.createBy}, #{ray.updateBy}) From 6470f4e3971cbaad2c2788cd0b090fd422882fbb Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 10 Jan 2025 14:46:35 +0800 Subject: [PATCH 007/127] =?UTF-8?q?fix:=20=E8=B5=84=E6=BA=90=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=BB=84=E4=BB=B6=E7=BC=96=E8=BE=91=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E6=98=BE=E7=A4=BA=E5=B7=B2=E9=80=89=E4=B8=AD=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParameterInput/index.tsx | 7 ++-- .../src/components/ParameterSelect/index.tsx | 2 +- .../src/components/ResourceSelect/index.tsx | 38 +++++++++++++++---- .../components/VersionCompareModal/index.less | 1 + .../components/VersionCompareModal/index.less | 1 + .../ResourceSelectorModal/index.tsx | 12 +++--- 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 94fdddde..aa0c7492 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -1,21 +1,22 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数输入组件 + * @Description: 参数输入组件,支持手动输入,选择全局参数,选择数据集/模型/镜像 */ +import { CommonTabKeys } from '@/enums'; import { CloseOutlined } from '@ant-design/icons'; import { Form, Input } from 'antd'; import { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; -// 对象 +// 如果值是对象时的类型 export type ParameterInputObject = { value?: any; // 值 showValue?: any; // 显示值 fromSelect?: boolean; // 是否来自选择 - activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab + activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys [key: string]: any; diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index ffaf8415..2c9f862f 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数选择组件 + * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ import { PipelineNodeModelParameter } from '@/types'; diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 5f2142d8..e818df85 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -12,7 +12,8 @@ import ResourceSelectorModal, { } from '@/pages/Pipeline/components/ResourceSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; -import { useState } from 'react'; +import { pick } from 'lodash'; +import { useEffect, useState } from 'react'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; @@ -33,6 +34,31 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe undefined, ); + useEffect(() => { + if ( + value && + typeof value === 'object' && + value.activeTab && + value.id && + value.name && + value.version && + value.path && + (type === ResourceSelectorType.Mirror || value.identifier) && + (type === ResourceSelectorType.Mirror || value.owner) + ) { + const originResource = pick(value, [ + 'activeTab', + 'id', + 'identifier', + 'name', + 'owner', + 'version', + 'path', + ]) as ResourceSelectorResponse; + setSelectedResource(originResource); + } + }, [value]); + const selectResource = () => { const resource = selectedResource; const { close } = openAntdModal(ResourceSelectorModal, { @@ -50,8 +76,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue: path, fromSelect: true, activeTab, - expandedKeys: [`${id}`], - checkedKeys: [`${id}-${version}`], + id, + name, + version, + path, }); } else { const jsonObj = { @@ -69,8 +97,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue, fromSelect: true, activeTab, - expandedKeys: [`${id}`], - checkedKeys: [`${id}-${version}`], ...jsonObj, }); } @@ -80,8 +106,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue: undefined, fromSelect: false, activeTab: undefined, - expandedKeys: [], - checkedKeys: [], }); } close(); diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less index 70de3494..f1935eb2 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less @@ -3,6 +3,7 @@ .title(@color, @background) { width: 100%; margin-bottom: 20px; + padding: 0 15px; color: @color; font-weight: 500; font-size: @font-size; diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less index 70de3494..f1935eb2 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less @@ -3,6 +3,7 @@ .title(@color, @background) { width: 100%; margin-bottom: 20px; + padding: 0 15px; color: @color; font-weight: 500; font-size: @font-size; diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx index ed6ed80e..06fdc242 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx @@ -18,13 +18,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 - activeTab: CommonTabKeys; // 是我的还是公开的 + identifier: string; // 数据集\模型 identifier,镜像没有这个字段 + owner: string; // 数据集\模型 owner,镜像没有这个字段 }; export interface ResourceSelectorModalProps extends Omit { @@ -69,7 +69,7 @@ function ResourceSelectorModal({ onOk, ...rest }: ResourceSelectorModalProps) { - const [activeTab, setActiveTab] = useState(defaultActiveTab); + const [activeTab, setActiveTab] = useState(defaultActiveTab); const [expandedKeys, setExpandedKeys] = useState([]); const [checkedKeys, setCheckedKeys] = useState([]); const [loadedKeys, setLoadedKeys] = useState([]); @@ -234,7 +234,7 @@ function ResourceSelectorModal({ version, identifier, owner, - activeTab: activeTab as CommonTabKeys, + activeTab: activeTab, }; onOk?.(res); } else { @@ -255,7 +255,7 @@ function ResourceSelectorModal({ setActiveTab(e as CommonTabKeys)} className={styles['model-tabs']} />
From 62c7f056e41004a2a677efd08813c8a4ca12104e Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 10 Jan 2025 14:47:26 +0800 Subject: [PATCH 008/127] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B6=85?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=87=AA=E5=8A=A8=E5=AF=BB=E4=BC=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/config/routes.ts | 36 ++ react-ui/src/pages/AutoML/Create/index.tsx | 5 +- react-ui/src/pages/AutoML/List/index.tsx | 414 +------------- .../components/CreateForm/TrialConfig.tsx | 5 +- .../AutoML/components/CreateForm/index.less | 11 +- .../components/ExperimentInstance/index.tsx | 18 +- .../components/ExperimentList/config.ts | 75 +++ .../ExperimentList}/index.less | 2 +- .../components/ExperimentList/index.tsx | 428 +++++++++++++++ .../pages/HyperParameter/Create/index.less | 55 ++ .../src/pages/HyperParameter/Create/index.tsx | 165 ++++++ .../src/pages/HyperParameter/Info/index.less | 40 ++ .../src/pages/HyperParameter/Info/index.tsx | 61 +++ .../pages/HyperParameter/Instance/index.less | 42 ++ .../pages/HyperParameter/Instance/index.tsx | 215 ++++++++ .../src/pages/HyperParameter/List/index.tsx | 13 + .../components/AutoMLBasic/index.less | 13 + .../components/AutoMLBasic/index.tsx | 308 +++++++++++ .../components/ConfigInfo/index.less | 20 + .../components/ConfigInfo/index.tsx | 26 + .../components/CreateForm/BasicConfig.tsx | 54 ++ .../components/CreateForm/ExecuteConfig.tsx | 504 ++++++++++++++++++ .../CreateForm/ParameterRange/index.less | 13 + .../CreateForm/ParameterRange/index.tsx | 141 +++++ .../CreateForm/PopParameterRange/index.less | 47 ++ .../CreateForm/PopParameterRange/index.tsx | 97 ++++ .../components/CreateForm/index.less | 108 ++++ .../components/CreateForm/utils.ts | 155 ++++++ .../components/ExperimentHistory/index.less | 14 + .../components/ExperimentHistory/index.tsx | 132 +++++ .../components/ExperimentLog/index.less | 0 .../components/ExperimentLog/index.tsx | 0 .../components/ExperimentResult/index.less | 52 ++ .../components/ExperimentResult/index.tsx | 83 +++ react-ui/src/pages/HyperParameter/types.ts | 64 +++ react-ui/src/services/hyperParameter/index.js | 93 ++++ react-ui/src/utils/constant.ts | 3 + 37 files changed, 3084 insertions(+), 428 deletions(-) create mode 100644 react-ui/src/pages/AutoML/components/ExperimentList/config.ts rename react-ui/src/pages/AutoML/{List => components/ExperimentList}/index.less (94%) create mode 100644 react-ui/src/pages/AutoML/components/ExperimentList/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Create/index.less create mode 100644 react-ui/src/pages/HyperParameter/Create/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Info/index.less create mode 100644 react-ui/src/pages/HyperParameter/Info/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/Instance/index.less create mode 100644 react-ui/src/pages/HyperParameter/Instance/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/List/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/types.ts create mode 100644 react-ui/src/services/hyperParameter/index.js create mode 100644 react-ui/src/utils/constant.ts diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index eaddb001..45c65760 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -181,6 +181,42 @@ export default [ }, ], }, + { + name: '超参数自动寻优', + path: 'hyperparameter', + routes: [ + { + name: '超参数寻优', + path: '', + component: './HyperParameter/List/index', + }, + { + name: '实验详情', + path: 'info/:id', + component: './HyperParameter/Info/index', + }, + { + name: '创建实验', + path: 'create', + component: './HyperParameter/Create/index', + }, + { + name: '编辑实验', + path: 'edit/:id', + component: './HyperParameter/Create/index', + }, + { + name: '复制实验', + path: 'copy/:id', + component: './HyperParameter/Create/index', + }, + { + name: '实验实例详情', + path: 'instance/:autoMLId/:id', + component: './HyperParameter/Instance/index', + }, + ], + }, ], }, { diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index 19fedd19..30699063 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 13:58:08 - * @Description: 创建服务版本 + * @Description: 创建实验 */ import PageTitle from '@/components/PageTitle'; import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; @@ -11,7 +11,6 @@ import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useLocation, useNavigate, useParams } from '@umijs/max'; import { App, Button, Form } from 'antd'; -import { omit } from 'lodash'; import { useEffect } from 'react'; import BasicConfig from '../components/CreateForm/BasicConfig'; import DatasetConfig from '../components/CreateForm/DatasetConfig'; @@ -106,7 +105,7 @@ function CreateAutoML() { // 根据后台要求,修改表单数据 const object = { - ...omit(formData), + ...formData, include_classifier: convertEmptyStringToUndefined(include_classifier), include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), include_regressor: convertEmptyStringToUndefined(include_regressor), diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx index 13e3dcbe..a4488e4d 100644 --- a/react-ui/src/pages/AutoML/List/index.tsx +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -3,419 +3,11 @@ * @Date: 2024-04-16 13:58:08 * @Description: 自主机器学习列表 */ -import KFIcon from '@/components/KFIcon'; -import PageTitle from '@/components/PageTitle'; -import { ExperimentStatus } from '@/enums'; -import { useCacheState } from '@/hooks/pageCacheState'; -import { experimentStatusInfo } from '@/pages/Experiment/status'; -import { - deleteAutoMLReq, - getAutoMLListReq, - getExperimentInsListReq, - runAutoMLReq, -} from '@/services/autoML'; -import themes from '@/styles/theme.less'; -import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; -import { to } from '@/utils/promise'; -import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { modalConfirm } from '@/utils/ui'; -import { useNavigate } from '@umijs/max'; -import { - App, - Button, - ConfigProvider, - Input, - Table, - Tooltip, - type TablePaginationConfig, - type TableProps, -} from 'antd'; -import { type SearchProps } from 'antd/es/input'; -import classNames from 'classnames'; -import { useEffect, useState } from 'react'; -import ExperimentInstance from '../components/ExperimentInstance'; -import { AutoMLData } from '../types'; -import styles from './index.less'; -function AutoMLList() { - const navigate = useNavigate(); - const { message } = App.useApp(); - const [cacheState, setCacheState] = useCacheState(); - const [searchText, setSearchText] = useState(cacheState?.searchText); - const [inputText, setInputText] = useState(cacheState?.searchText); - const [tableData, setTableData] = useState([]); - const [total, setTotal] = useState(0); - const [experimentInsList, setExperimentInsList] = useState([]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [experimentInsTotal, setExperimentInsTotal] = useState(0); - const [pagination, setPagination] = useState( - cacheState?.pagination ?? { - current: 1, - pageSize: 10, - }, - ); - - useEffect(() => { - getAutoMLList(); - }, [pagination, searchText]); - - // 获取自主机器学习列表 - const getAutoMLList = async () => { - const params: Record = { - page: pagination.current! - 1, - size: pagination.pageSize, - ml_name: searchText || undefined, - }; - const [res] = await to(getAutoMLListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; - - // 搜索 - const onSearch: SearchProps['onSearch'] = (value) => { - setSearchText(value); - setPagination((prev) => ({ - ...prev, - current: 1, - })); - }; - - // 删除模型部署 - const deleteAutoML = async (record: AutoMLData) => { - const [res] = await to(deleteAutoMLReq(record.id)); - if (res) { - message.success('删除成功'); - // 如果是一页的唯一数据,删除时,请求第一页的数据 - // 否则直接刷新这一页的数据 - // 避免回到第一页 - if (tableData.length > 1) { - setPagination((prev) => ({ - ...prev, - current: 1, - })); - } else { - getAutoMLList(); - } - } - }; - - // 处理删除 - const handleAutoMLDelete = (record: AutoMLData) => { - modalConfirm({ - title: '删除后,该实验将不可恢复', - content: '是否确认删除?', - onOk: () => { - deleteAutoML(record); - }, - }); - }; - - // 创建、编辑、复制自动机器学习 - const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { - setCacheState({ - pagination, - searchText, - }); - - if (record) { - if (isCopy) { - navigate(`/pipeline/autoML/copy/${record.id}`); - } else { - navigate(`/pipeline/autoML/edit/${record.id}`); - } - } else { - navigate(`/pipeline/autoML/create`); - } - }; - - // 查看自动机器学习详情 - const gotoDetail = (record: AutoMLData) => { - setCacheState({ - pagination, - searchText, - }); - - navigate(`/pipeline/autoML/info/${record.id}`); - }; - - // 启动自动机器学习 - const startAutoML = async (record: AutoMLData) => { - const [res] = await to(runAutoMLReq(record.id)); - if (res) { - message.success('运行成功'); - setExpandedRowKeys([record.id]); - refreshExperimentList(); - refreshExperimentIns(record.id); - } - }; +import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; - // --------------------------- 实验实例 --------------------------- - // 获取实验实例列表 - const getExperimentInsList = async (autoMLId: number, page: number) => { - const params = { - autoMlId: autoMLId, - page: page, - size: 5, - }; - const [res] = await to(getExperimentInsListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - try { - if (page === 0) { - setExperimentInsList(content); - } else { - setExperimentInsList((prev) => [...prev, ...content]); - } - setExperimentInsTotal(totalElements); - } catch (error) { - console.error('JSON parse error: ', error); - } - } - }; - // 展开实例 - const handleExpandChange = (expanded: boolean, record: AutoMLData) => { - setExperimentInsList([]); - if (expanded) { - setExpandedRowKeys([record.id]); - getExperimentInsList(record.id, 0); - } else { - setExpandedRowKeys([]); - } - }; - - // 跳转到实验实例详情 - const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { - navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); - }; - - // 刷新实验实例列表 - const refreshExperimentIns = (experimentId: number) => { - getExperimentInsList(experimentId, 0); - }; - - // 加载更多实验实例 - const loadMoreExperimentIns = () => { - const page = Math.round(experimentInsList.length / 5); - const autoMLId = expandedRowKeys[0]; - getExperimentInsList(autoMLId, page); - }; - - // 实验实例终止 - const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { - // 刷新实验列表 - refreshExperimentList(); - setExperimentInsList((prevList) => { - return prevList.map((item) => { - if (item.id === experimentIns.id) { - return { - ...item, - status: ExperimentStatus.Terminated, - }; - } - return item; - }); - }); - }; - - // 刷新实验列表状态, - // 目前是直接刷新实验列表,后续需要优化,只刷新状态 - const refreshExperimentList = () => { - getAutoMLList(); - }; - - // --------------------------- Table --------------------------- - // 分页切换 - const handleTableChange: TableProps['onChange'] = ( - pagination, - _filters, - _sorter, - { action }, - ) => { - if (action === 'paginate') { - setPagination(pagination); - } - }; - - const columns: TableProps['columns'] = [ - { - title: '实验名称', - dataIndex: 'ml_name', - key: 'ml_name', - width: '16%', - render: tableCellRender(false, TableCellValueType.Link, { - onClick: gotoDetail, - }), - }, - { - title: '实验描述', - dataIndex: 'ml_description', - key: 'ml_description', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, - - { - title: '创建时间', - dataIndex: 'update_time', - key: 'update_time', - width: '20%', - render: tableCellRender(true, TableCellValueType.Date), - ellipsis: { showTitle: false }, - }, - { - title: '最近五次运行状态', - dataIndex: 'status_list', - key: 'status_list', - width: 200, - render: (text) => { - const newText: string[] = text && text.replace(/\s+/g, '').split(','); - return ( - <> - {newText && newText.length > 0 - ? newText.map((item, index) => { - return ( - - - - ); - }) - : null} - - ); - }, - }, - { - title: '操作', - dataIndex: 'operation', - width: 360, - key: 'operation', - render: (_: any, record: AutoMLData) => ( -
- - - - - - - -
- ), - }, - ]; - - return ( -
- -
-
- setInputText(e.target.value)} - style={{ width: 300 }} - value={inputText} - allowClear - /> - -
-
- `共${total}条`, - }} - onChange={handleTableChange} - expandable={{ - expandedRowRender: (record) => ( - gotoInstanceInfo(record, item)} - onRemove={() => { - refreshExperimentIns(record.id); - refreshExperimentList(); - }} - onTerminate={handleInstanceTerminate} - onLoadMore={() => loadMoreExperimentIns()} - > - ), - onExpand: (e, a) => { - handleExpandChange(e, a); - }, - expandedRowKeys: expandedRowKeys, - rowExpandable: () => true, - }} - rowKey="id" - /> - - - - ); +function AutoMLList() { + return ; } export default AutoMLList; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx index f0fdfa5d..0d8008fb 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx @@ -62,10 +62,7 @@ function TrialConfig() { > - + + + + + + + + + ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + allowClear + /> + +
+
+
`共${total}条`, + }} + onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(record, item)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: (e, a) => { + handleExpandChange(e, a); + }, + expandedRowKeys: expandedRowKeys, + rowExpandable: () => true, + }} + rowKey="id" + /> + + + + ); +} + +export default ExperimentList; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less new file mode 100644 index 00000000..0325570e --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -0,0 +1,55 @@ +.create-hyperparameter { + height: 100%; + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + padding: 30px 30px 10px; + overflow: auto; + color: @text-color; + font-size: @font-size-content; + background-color: white; + border-radius: 10px; + + &__type { + color: @text-color; + font-size: @font-size-input-lg; + } + + :global { + .ant-input-number { + width: 100%; + } + + .ant-form-item { + margin-bottom: 20px; + } + + .image-url { + margin-top: -15px; + .ant-form-item-label > label::after { + content: ''; + } + } + + .ant-btn-variant-text:disabled { + color: rgba(0, 0, 0, 0.25); + } + + .ant-btn-variant-text { + color: #565658; + } + + .ant-btn.ant-btn-icon-only .anticon { + font-size: 20px; + } + + .anticon-question-circle { + margin-top: -12px; + margin-left: 1px !important; + color: @text-color-tertiary !important; + font-size: 12px !important; + } + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx new file mode 100644 index 00000000..dfe367ba --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -0,0 +1,165 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 创建实验 + */ +import PageTitle from '@/components/PageTitle'; +import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useLocation, useNavigate, useParams } from '@umijs/max'; +import { App, Button, Form } from 'antd'; +import { useEffect } from 'react'; +import BasicConfig from '../components/CreateForm/BasicConfig'; +import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; +import { getReqParamName } from '../components/CreateForm/utils'; +import { FormData, HyperparameterData } from '../types'; +import styles from './index.less'; + +function CreateHyperparameter() { + const navigate = useNavigate(); + const [form] = Form.useForm(); + const { message } = App.useApp(); + const params = useParams(); + const id = safeInvoke(Number)(params.id); + const { pathname } = useLocation(); + const isCopy = pathname.includes('copy'); + + useEffect(() => { + // 编辑,复制 + if (id && !Number.isNaN(id)) { + getHyperparameterInfo(id); + } + }, [id]); + + // 获取服务详情 + const getHyperparameterInfo = async (id: number) => { + const [res] = await to(getRayInfoReq({ id })); + if (res && res.data) { + const info: HyperparameterData = res.data; + const { name: name_str, parameters, points_to_evaluate, ...rest } = info; + const name = isCopy ? `${name_str}-copy` : name_str; + if (parameters && Array.isArray(parameters)) { + parameters.forEach((item) => { + item.range = item.bounds || item.values || item.value; + delete item.bounds; + delete item.values; + delete item.value; + }); + } + + const formData = { + ...rest, + name, + parameters, + points_to_evaluate: points_to_evaluate ?? [undefined], + }; + + form.setFieldsValue(formData); + } + }; + + // 创建、更新、复制实验 + const createExperiment = async (formData: FormData) => { + // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value" + const parameters = formData['parameters']; + // const points_to_evaluate = formData['points_to_evaluate']; + // const runParameters = formData['parameters']; + parameters.forEach((item) => { + const paramName = getReqParamName(item.type); + item[paramName] = item.range; + delete item.range; + }); + + // 根据后台要求,修改表单数据 + const object = { + ...formData, + parameters: parameters, + }; + + const params = + id && !isCopy + ? { + id: id, + ...object, + } + : object; + + const request = id && !isCopy ? updateRayReq : addRayReq; + const [res] = await to(request(params)); + if (res) { + message.success('操作成功'); + navigate(-1); + } + }; + + // 提交 + const handleSubmit = (values: FormData) => { + createExperiment(values); + }; + + // 取消 + const cancel = () => { + navigate(-1); + }; + + let buttonText = '新建'; + let title = '新建实验'; + if (id) { + if (isCopy) { + title = '复制实验'; + buttonText = '确定'; + } else { + title = '编辑实验'; + buttonText = '更新'; + } + } + + return ( +
+ +
+
+
+ + + + + + + + +
+
+
+ ); +} + +export default CreateHyperparameter; diff --git a/react-ui/src/pages/HyperParameter/Info/index.less b/react-ui/src/pages/HyperParameter/Info/index.less new file mode 100644 index 00000000..e27756ef --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.less @@ -0,0 +1,40 @@ +.auto-ml-info { + position: relative; + height: 100%; + &__tabs { + height: 50px; + padding-left: 25px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + &__content { + height: calc(100% - 60px); + margin-top: 10px; + } + + &__tips { + position: absolute; + top: 11px; + left: 256px; + padding: 3px 12px; + color: #565658; + font-size: @font-size-content; + background: .addAlpha(@primary-color, 0.09) []; + border-radius: 4px; + + &::before { + position: absolute; + top: 10px; + left: -6px; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-right: 6px solid .addAlpha(@primary-color, 0.09) []; + border-bottom: 4px solid transparent; + content: ''; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx new file mode 100644 index 00000000..8b2fded3 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -0,0 +1,61 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 自主机器学习详情 + */ +import KFIcon from '@/components/KFIcon'; +import PageTitle from '@/components/PageTitle'; +import { CommonTabKeys } from '@/enums'; +import { getAutoMLInfoReq } from '@/services/autoML'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { useEffect, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import { HyperparameterData } from '../types'; +import styles from './index.less'; + +function AutoMLInfo() { + const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); + const params = useParams(); + const autoMLId = safeInvoke(Number)(params.id); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + + const tabItems = [ + { + key: CommonTabKeys.Public, + label: '基本信息', + icon: , + }, + { + key: CommonTabKeys.Private, + label: 'Trial列表', + icon: , + }, + ]; + + useEffect(() => { + if (autoMLId) { + getAutoMLInfo(); + } + }, []); + + // 获取自动机器学习详情 + const getAutoMLInfo = async () => { + const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + if (res && res.data) { + setAutoMLInfo(res.data); + } + }; + + return ( +
+ +
+ +
+
+ ); +} + +export default AutoMLInfo; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less new file mode 100644 index 00000000..889faeb5 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -0,0 +1,42 @@ +.auto-ml-instance { + height: 100%; + + &__tabs { + height: 100%; + :global { + .ant-tabs-nav-list { + width: 100%; + height: 50px; + padding-left: 15px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + .ant-tabs-content-holder { + height: calc(100% - 50px); + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } + } + } + } + + &__basic { + height: calc(100% - 10px); + margin-top: 10px; + } + + &__log { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px calc(@content-padding - 8px); + overflow-y: visible; + background-color: white; + border-radius: 10px; + } +} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx new file mode 100644 index 00000000..355ced01 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -0,0 +1,215 @@ +import KFIcon from '@/components/KFIcon'; +import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { getExperimentInsReq } from '@/services/autoML'; +import { NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { Tabs } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import AutoMLBasic from '../components/AutoMLBasic'; +import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentResult from '../components/ExperimentResult'; +import { AutoMLInstanceData, HyperparameterData } from '../types'; +import styles from './index.less'; + +enum TabKeys { + Params = 'params', + Log = 'log', + Result = 'result', + History = 'history', +} + +function AutoMLInstance() { + const [activeTab, setActiveTab] = useState(TabKeys.Params); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState(undefined); + const params = useParams(); + // const autoMLId = safeInvoke(Number)(params.autoMLId); + const instanceId = safeInvoke(Number)(params.id); + const evtSourceRef = useRef(null); + + useEffect(() => { + if (instanceId) { + getExperimentInsInfo(false); + } + return () => { + closeSSE(); + }; + }, []); + + // 获取实验实例详情 + const getExperimentInsInfo = async (isStatusDetermined: boolean) => { + 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 } = info; + // 解析配置参数 + const paramJson = parseJsonText(param); + if (paramJson) { + setAutoMLInfo(paramJson); + } + + // 这个接口返回的状态有延时,SSE 返回的状态是最新的 + // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE + if (isStatusDetermined) { + setInstanceInfo((prev) => ({ + ...info, + nodeStatus: prev!.nodeStatus, + })); + return; + } + + // 进行节点状态 + const nodeStatusJson = parseJsonText(node_status); + if (nodeStatusJson) { + Object.keys(nodeStatusJson).forEach((key) => { + if (key.startsWith('auto-ml')) { + const value = nodeStatusJson[key]; + info.nodeStatus = value; + } + }); + } + setInstanceInfo(info); + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } + } + }; + + const setupSSE = (name: string, namespace: string) => { + let { origin } = location; + if (process.env.NODE_ENV === 'development') { + origin = 'http://172.20.32.181:31213'; + } + const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); + const evtSource = new EventSource( + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, + { withCredentials: false }, + ); + evtSource.onmessage = (event) => { + const data = event?.data; + if (!data) { + return; + } + const dataJson = parseJsonText(data); + if (dataJson) { + const nodes = dataJson?.result?.object?.status?.nodes; + if (nodes) { + const statusData = Object.values(nodes).find((node: any) => + node.displayName.startsWith('auto-ml'), + ) as NodeStatus; + if (statusData) { + setInstanceInfo((prev) => ({ + ...prev!, + nodeStatus: statusData, + })); + + // 实验结束,关闭 SSE + if ( + statusData.phase !== ExperimentStatus.Pending && + statusData.phase !== ExperimentStatus.Running + ) { + closeSSE(); + getExperimentInsInfo(true); + } + } + } + } + }; + evtSource.onerror = (error) => { + console.error('SSE error: ', error); + }; + + evtSourceRef.current = evtSource; + }; + + const closeSSE = () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + + const basicTabItems = [ + { + key: TabKeys.Params, + label: '基本信息', + icon: , + children: ( + + ), + }, + { + key: TabKeys.Log, + label: '日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + const resultTabItems = [ + { + key: TabKeys.Result, + label: '实验结果', + icon: , + children: ( + + ), + }, + { + key: TabKeys.History, + label: 'Trial 列表', + icon: , + children: ( + + ), + }, + ]; + + const tabItems = + instanceInfo?.status === ExperimentStatus.Succeeded + ? [...basicTabItems, ...resultTabItems] + : basicTabItems; + + return ( +
+ +
+ ); +} + +export default AutoMLInstance; diff --git a/react-ui/src/pages/HyperParameter/List/index.tsx b/react-ui/src/pages/HyperParameter/List/index.tsx new file mode 100644 index 00000000..5ebfcde9 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/List/index.tsx @@ -0,0 +1,13 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 超参数自动寻优 + */ + +import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList'; + +function HyperParameter() { + return ; +} + +export default HyperParameter; diff --git a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less new file mode 100644 index 00000000..cbd05bcc --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less @@ -0,0 +1,13 @@ +.auto-ml-basic { + height: 100%; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + :global { + .kf-basic-info__item__value__text { + white-space: pre; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx new file mode 100644 index 00000000..854c6035 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx @@ -0,0 +1,308 @@ +import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; +import { AutoMLData } from '@/pages/AutoML/types'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { type NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { elapsedTime } from '@/utils/date'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { useMemo } from 'react'; +import ConfigInfo, { + formatBoolean, + formatDate, + formatEnum, + type BasicInfoData, +} from '../ConfigInfo'; +import styles from './index.less'; + +// 格式化数据集 +const formatDataset = (dataset: { name: string; version: string }) => { + if (!dataset || !dataset.name || !dataset.version) { + return '--'; + } + return `${dataset.name}:${dataset.version}`; +}; + +// 格式化优化方向 +const formatOptimizeMode = (value: boolean) => { + return value ? '越大越好' : '越小越好'; +}; + +const formatMetricsWeight = (value: string) => { + if (!value) { + return '--'; + } + const json = parseJsonText(value); + if (!json) { + return '--'; + } + return Object.entries(json) + .map(([key, value]) => `${key}:${value}`) + .join('\n'); +}; + +type AutoMLBasicProps = { + info?: AutoMLData; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; +}; + +function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { + const basicDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + + return [ + { + label: '实验名称', + value: info.ml_name, + ellipsis: true, + }, + { + label: '实验描述', + value: info.ml_description, + ellipsis: true, + }, + { + label: '创建人', + value: info.create_by, + ellipsis: true, + }, + { + label: '创建时间', + value: info.create_time, + ellipsis: true, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + ellipsis: true, + format: formatDate, + }, + ]; + }, [info]); + + const configDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '任务类型', + value: info.task_type, + ellipsis: true, + format: formatEnum(autoMLTaskTypeOptions), + }, + { + label: '特征预处理算法', + value: info.include_feature_preprocessor, + ellipsis: true, + }, + { + label: '排除的特征预处理算法', + value: info.exclude_feature_preprocessor, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.include_regressor + : info.include_classifier, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.exclude_regressor + : info.exclude_classifier, + ellipsis: true, + }, + { + label: '集成方式', + value: info.ensemble_class, + ellipsis: true, + format: formatEnum(autoMLEnsembleClassOptions), + }, + { + label: '集成模型数量', + value: info.ensemble_size, + ellipsis: true, + }, + { + label: '集成最佳模型数量', + value: info.ensemble_nbest, + ellipsis: true, + }, + { + label: '最大数量', + value: info.max_models_on_disc, + ellipsis: true, + }, + { + label: '内存限制(MB)', + value: info.memory_limit, + ellipsis: true, + }, + { + label: '单次时间限制(秒)', + value: info.per_run_time_limit, + ellipsis: true, + }, + { + label: '搜索时间限制(秒)', + value: info.time_left_for_this_task, + ellipsis: true, + }, + { + label: '重采样策略', + value: info.resampling_strategy, + ellipsis: true, + }, + { + label: '交叉验证折数', + value: info.folds, + ellipsis: true, + }, + { + label: '是否打乱', + value: info.shuffle, + ellipsis: true, + format: formatBoolean, + }, + { + label: '训练集比率', + value: info.train_size, + ellipsis: true, + }, + { + label: '测试集比率', + value: info.test_size, + ellipsis: true, + }, + { + label: '计算指标', + value: info.scoring_functions, + ellipsis: true, + }, + { + label: '随机种子', + value: info.seed, + ellipsis: true, + }, + + { + label: '数据集', + value: info.dataset, + ellipsis: true, + format: formatDataset, + }, + { + label: '预测目标列', + value: info.target_columns, + ellipsis: true, + }, + ]; + }, [info]); + + const metricsData = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '指标名称', + value: info.metric_name, + ellipsis: true, + }, + { + label: '优化方向', + value: info.greater_is_better, + ellipsis: true, + format: formatOptimizeMode, + }, + { + label: '指标权重', + value: info.metrics, + ellipsis: true, + format: formatMetricsWeight, + }, + ]; + }, [info]); + + const instanceDatas = useMemo(() => { + if (!runStatus) { + return []; + } + + return [ + { + label: '启动时间', + value: formatDate(runStatus.startedAt), + ellipsis: true, + }, + { + label: '执行时长', + value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), + ellipsis: true, + }, + { + label: '状态', + value: ( + + +
+ {experimentStatusInfo[runStatus?.phase]?.label} +
+
+ ), + ellipsis: true, + }, + ]; + }, [runStatus]); + + return ( +
+ {isInstance && runStatus && ( + + )} + {!isInstance && ( + + )} + + +
+ ); +} + +export default AutoMLBasic; diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less new file mode 100644 index 00000000..33fb3314 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less @@ -0,0 +1,20 @@ +.config-info { + :global { + .kf-basic-info { + width: 100%; + + &__item { + width: calc((100% - 80px) / 3); + &__label { + font-size: @font-size; + text-align: left; + text-align-last: left; + } + &__value { + min-width: 0; + font-size: @font-size; + } + } + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx new file mode 100644 index 00000000..10e042e4 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx @@ -0,0 +1,26 @@ +import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; +import InfoGroup from '@/components/InfoGroup'; +import classNames from 'classnames'; +import styles from './index.less'; +export * from '@/components/BasicInfo/format'; +export type { BasicInfoData }; + +type ConfigInfoProps = { + title: string; + data: BasicInfoData[]; + labelWidth: number; + className?: string; + style?: React.CSSProperties; +}; + +function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { + return ( + +
+ +
+
+ ); +} + +export default ConfigInfo; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx new file mode 100644 index 00000000..8829f12a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx @@ -0,0 +1,54 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; + +function BasicConfig() { + return ( + <> + + +
+ + + + + + + + + + + + + + ); +} + +export default BasicConfig; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..92e7f961 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,504 @@ +import CodeSelect from '@/components/CodeSelect'; +import KFIcon from '@/components/KFIcon'; +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { modalConfirm } from '@/utils/ui'; +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; +import { isEqual } from 'lodash'; +import PopParameterRange from './PopParameterRange'; +import styles from './index.less'; +import { axParameterOptions, parameterOptions, type FormParameter } from './utils'; + +// 搜索算法 +const searchAlgorithms = ['HyperOpt', 'HEBO', 'BayesOpt', 'Optuna', 'ZOOpt', 'Ax'].map((name) => ({ + label: name, + value: name, +})); + +// 调度算法 +const schedulerAlgorithms = ['ASHA', 'HyperBand', 'MedianStopping', 'PopulationBased', 'PB2'].map( + (name) => ({ label: name, value: name }), +); + +function ExecuteConfig() { + const form = Form.useFormInstance(); + const searchAlgorithm = Form.useWatch('search_alg', form); + const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; + // const parameters = Form.useWatch('parameters', form); + // console.log('parameters', parameters); + + const handleSearchAlgorithmChange = (value: string) => { + if ( + (value === 'Ax' && searchAlgorithm !== 'Ax') || + (value !== 'Ax' && searchAlgorithm === 'Ax') + ) { + form.setFieldValue('parameters', [{ name: '' }]); + } + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + const schedulerAlgorithm = getFieldValue('scheduler'); + if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') { + return ( + + + + + + + + ); + } else if (schedulerAlgorithm === 'MedianStopping') { + return ( + + + + + + + + ); + } + return null; + }} + + + + {(fields, { add, remove }) => ( + <> + + + + + +
+ +
参数名称
+
参数类型
+
取值范围
+
操作
+
+ + {fields.map(({ key, name, ...restField }, index) => ( + + { + if (!value) { + return Promise.reject(new Error('请输入参数名称')); + } + // 判断不能重名 + const list = form + .getFieldValue('parameters') + .filter( + (item: FormParameter | undefined) => + item !== undefined && item !== null, + ); + + const names = list.map((item: FormParameter) => item.name); + if (new Set(names).size !== names.length) { + return Promise.reject(new Error('名称不能重复')); + } + return Promise.resolve(); + }, + }, + ]} + > + + + + + + ))} +
+
+ + {index === fields.length - 1 && ( + + )} +
+ + ))} + + + )} + + ); + }} + + + + + + + + + + + + + + + 越大越好 + 越小越好 + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default ExecuteConfig; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less new file mode 100644 index 00000000..d49089f5 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less @@ -0,0 +1,13 @@ +.parameter-range { + width: 300px; + &__list { + width: 100%; + max-height: 300px; + overflow-x: visible; + overflow-y: auto; + } + &__button { + margin-bottom: 0; + text-align: center; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx new file mode 100644 index 00000000..240e90e6 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx @@ -0,0 +1,141 @@ +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import { Button, Flex, Form, Input, InputNumber } from 'antd'; +import { ParameterType, getFormOptions } from '../utils'; +import styles from './index.less'; + +type ParameterRangeProps = { + type?: ParameterType; + value?: any[]; + onCancel?: () => void; + onConfirm?: (value: any[]) => void; +}; + +function ParameterRange({ type, value, onCancel, onConfirm }: ParameterRangeProps) { + const [form] = Form.useForm(); + const isList = type === ParameterType.Choice || type === ParameterType.Grid; + const formOptions = getFormOptions(type, value); + + const initialValues = isList + ? { list: value && value.length > 0 ? value.map((item) => ({ value: item })) : [{ value: '' }] } + : formOptions.reduce((prev, item) => { + prev[item.name] = item.value; + return prev; + }, {} as Record); + + const handleFinish = (values: any) => { + if (type === ParameterType.Choice || type === ParameterType.Grid) { + const array = values.list.map((item: any) => item.value); + onConfirm?.(array); + } else { + const numbers = Object.values(values).map((item: any) => Number(item)); + onConfirm?.(numbers); + } + }; + + return ( +
+ {isList ? ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + + + + + {index === fields.length - 1 && ( + + )} + + + ))} + {fields.length === 0 && ( + + + + )} + + )} + +
+ ) : ( + formOptions.map((item) => { + return ( + + + + ); + }) + )} + + + + + + ); +} + +export default ParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less new file mode 100644 index 00000000..01faf3d0 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -0,0 +1,47 @@ +.parameter-range { + :global { + .ant-popconfirm-description { + padding-top: 20px; + } + + .ant-popconfirm-buttons { + display: none; + } + } + + &__input { + display: flex; + align-items: center; + width: 100%; + min-height: 46px; + padding: 10px 11px; + font-size: @font-size-input-lg; + line-height: 1.5; + background-color: white; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + + &:hover { + border-color: #4086ff; + } + + &--disabled { + background-color: rgba(0, 0, 0, 0.04); + cursor: not-allowed; + } + + &__text { + flex: 1; + margin-right: 10px; + } + + &__icon { + flex: none; + } + + &--disabled &__icon { + color: @text-color-tertiary; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx new file mode 100644 index 00000000..ca97b252 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx @@ -0,0 +1,97 @@ +import KFIcon from '@/components/KFIcon'; +import { Popconfirm, Typography } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useRef, useState } from 'react'; +import ParameterRange from '../ParameterRange'; +import { ParameterType } from '../utils'; +import styles from './index.less'; + +type ParameterRangeProps = { + type?: ParameterType; + value?: any[]; + onChange?: (value: any[]) => void; +}; + +function PopParameterRange({ type, value, onChange }: ParameterRangeProps) { + const [open, setOpen] = useState(false); + const popconfirmRef = useRef(null); + const disabled = !type; + const jsonText = JSON.stringify(value); + + const handleClickOutside = (event: MouseEvent) => { + // 判断点击是否在 Popconfirm 内 + const popconfirmNode = document.getElementById('pop-parameter'); + if (popconfirmNode && !popconfirmNode.contains(event.target as Node)) { + setOpen(false); + } + }; + + useEffect(() => { + if (open) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + // 清理事件监听器 + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [open]); + + const handleClick = () => { + if (!disabled) { + setOpen(true); + } + }; + + const handleCancel = () => { + setOpen(false); + }; + + const handleConfirm = (value: number[]) => { + onChange?.(value); + setOpen(false); + }; + + return ( +
+ + } + okText="确定" + cancelText="取消" + overlayClassName={styles['parameter-range']} + icon={null} + open={open} + destroyTooltipOnHide + > +
+ + {jsonText} + + +
+
+
+ ); +} + +export default PopParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less new file mode 100644 index 00000000..fcb77fdd --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -0,0 +1,108 @@ +.metrics-weight { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } +} + +.add-weight { + margin-bottom: 0 !important; + + // 增加样式权重 + & &__button { + border-color: .addAlpha(@primary-color, 0.5) []; + box-shadow: none !important; + &:hover { + border-style: solid; + } + } +} + +.hyper-parameter { + width: 83.33%; + margin-bottom: 20px; + border: 1px solid rgba(234, 234, 234, 0.8); + border-radius: 4px; + &__header { + height: 50px; + padding-left: 8px; + color: @text-color; + font-size: @font-size; + background: #f8f8f9; + border-radius: 4px 4px 0px 0px; + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + + &::before { + display: inline-block; + color: #c73131; + font-size: 14px; + font-family: SimSun, sans-serif; + line-height: 1; + content: '*'; + margin-inline-end: 4px; + } + } + + &__operation { + flex: none; + width: 100px; + } + } + &__body { + padding: 8px; + border-bottom: 1px solid rgba(234, 234, 234, 0.8); + + &:last-child { + border-bottom: none; + } + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + margin-bottom: 0 !important; + } + + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + height: 46px; + } + } + + &__add { + display: flex; + align-items: center; + justify-content: center; + padding: 15px 0; + } +} + +.run-parameter { + width: calc(41.66% + 126px); + margin-bottom: 20px; + border-radius: 8px; + &__body { + flex: 1; + margin-right: 10px; + padding: 20px 20px 0; + border: 1px dashed @border-color-base; + } + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts new file mode 100644 index 00000000..95aa3651 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts @@ -0,0 +1,155 @@ +export enum ParameterType { + Uniform = 'uniform', + QUniform = 'quniform', + LogUniform = 'loguniform', + QLogUniform = 'qloguniform', + Randn = 'randn', + QRandn = 'qrandn', + RandInt = 'randint', + QRandInt = 'qrandint', + LogRandInt = 'lograndint', + QLogRandInt = 'qlograndint', + Choice = 'choice', + Grid = 'grid', + Range = 'range', + Fixed = 'fixed', +} + +export const parameterOptions = [ + 'uniform', + 'quniform', + 'loguniform', + 'qloguniform', + 'randn', + 'qrandn', + 'randint', + 'qrandint', + 'lograndint', + 'qlograndint', + 'choice', + 'grid', +].map((name) => ({ + label: name, + value: name, +})); + +export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({ + label: name, + value: name, +})); + +export type ParameterData = { + label: string; + name: string; + value?: number; +}; + +// 参数表单数据 +export type FormParameter = { + name: string; // 参数名称 + type: ParameterType; // 参数类型 + range: any; // 参数值 + [key: string]: any; +}; + +export const getFormOptions = (type?: ParameterType, value?: number[]): ParameterData[] => { + const numbers = + value?.map((item) => { + const num = Number(item); + if (isNaN(num)) { + return undefined; + } + return num; + }) ?? []; + switch (type) { + case ParameterType.Uniform: + case ParameterType.LogUniform: + case ParameterType.RandInt: + case ParameterType.LogRandInt: + case ParameterType.Range: + return [ + { + name: 'min', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'max', + label: '最大值', + value: numbers?.[1], + }, + ]; + case ParameterType.QUniform: + case ParameterType.QLogUniform: + case ParameterType.QRandInt: + case ParameterType.QLogRandInt: + return [ + { + name: 'min', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'max', + label: '最大值', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Randn: + return [ + { + name: 'mean', + label: '均值', + value: numbers?.[0], + }, + { + name: 'std', + label: '方差', + value: numbers?.[1], + }, + ]; + case ParameterType.QRandn: + return [ + { + name: 'mean', + label: '均值', + value: numbers?.[0], + }, + { + name: 'std', + label: '方差', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Fixed: + return [ + { + name: 'value', + label: '值', + value: numbers?.[0], + }, + ]; + default: + return []; + } +}; + +export const getReqParamName = (type: ParameterType) => { + if (type === ParameterType.Fixed) { + return 'value'; + } else if (type === ParameterType.Choice || type === ParameterType.Grid) { + return 'values'; + } else { + return 'bounds'; + } +}; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less new file mode 100644 index 00000000..beac2a8a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -0,0 +1,14 @@ +.experiment-history { + height: calc(100% - 10px); + margin-top: 10px; + &__content { + height: 100%; + padding: 20px @content-padding; + background-color: white; + border-radius: 10px; + + &__table { + height: 100%; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx new file mode 100644 index 00000000..e95ccd42 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -0,0 +1,132 @@ +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import tableCellRender from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import styles from './index.less'; + +type ExperimentHistoryProps = { + fileUrl?: string; + isClassification: boolean; +}; + +type TableData = { + id?: string; + accuracy?: number; + duration?: number; + train_loss?: number; + status?: string; + feature?: string; + althorithm?: string; +}; + +function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { + const [tableData, setTableData] = useState([]); + useEffect(() => { + if (fileUrl) { + getHistoryFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: item[1]?.[5]?.accuracy, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + + const columns: TableProps['columns'] = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + render: tableCellRender(false), + }, + { + title: '准确率', + dataIndex: 'accuracy', + key: 'accuracy', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '耗时', + dataIndex: 'duration', + key: 'duration', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '训练损失', + dataIndex: 'train_loss', + key: 'train_loss', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '特征处理', + dataIndex: 'feature', + key: 'feature', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '算法', + dataIndex: 'althorithm', + key: 'althorithm', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 120, + render: tableCellRender(false), + }, + ]; + + return ( +
+
+
+
+ + + + ); +} + +export default ExperimentHistory; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less new file mode 100644 index 00000000..342817c3 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -0,0 +1,52 @@ +.experiment-result { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + &__download { + padding-top: 16px; + padding-bottom: 16px; + + padding-left: @content-padding; + color: @text-color; + font-size: 13px; + background-color: #f8f8f9; + border-radius: 4px; + + &__btn { + display: block; + height: 36px; + margin-top: 15px; + font-size: 14px; + } + } + + &__text { + white-space: pre-wrap; + } + + &__images { + display: flex; + align-items: flex-start; + width: 100%; + overflow-x: auto; + + :global { + .ant-image { + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } + } + + &__item { + height: 248px; + border: 1px solid rgba(96, 107, 122, 0.3); + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx new file mode 100644 index 00000000..a826155d --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -0,0 +1,83 @@ +import InfoGroup from '@/components/InfoGroup'; +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import { Button, Image } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import styles from './index.less'; + +type ExperimentResultProps = { + fileUrl?: string; + imageUrl?: string; + modelPath?: string; +}; + +function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { + const [result, setResult] = useState(''); + + const images = useMemo(() => { + if (imageUrl) { + return imageUrl.split(',').map((item) => item.trim()); + } + return []; + }, [imageUrl]); + + useEffect(() => { + if (fileUrl) { + getResultFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + + return ( +
+ +
{result}
+
+ +
+ + console.log(`current index: ${current}, prev index: ${prev}`), + }} + > + {images.map((item) => ( + + ))} + +
+
+ {modelPath && ( +
+ 文件名 + save_model.joblib + +
+ )} +
+ ); +} + +export default ExperimentResult; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts new file mode 100644 index 00000000..3eec723c --- /dev/null +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -0,0 +1,64 @@ +import { type ParameterInputObject } from '@/components/ResourceSelect'; +import { type NodeStatus } from '@/types'; +import { type FormParameter } from './components/CreateForm/utils'; + +// 操作类型 +export enum OperationType { + Create = 'Create', // 创建 + Update = 'Update', // 更新 +} + +// 表单数据 +export type FormData = { + name: string; // 实验名称 + description: string; // 实验描述 + code: ParameterInputObject; // 代码 + dataset: ParameterInputObject; // 数据集 + dataset_path: string; // 数据集路径 + main_py: string; // 主函数代码文件 + metrics: string; // 指标 + mode: string; // 优化方向 + search_alg?: string; // 搜索算法 + scheduler?: string; // 调度算法 + num_samples: number; // 总实验次数 + max_t: number; // 单次试验最大时间 + min_samples_required: number; // 计算中位数的最小试验数 + cpu: number; // cpu 数 + gpu: number; // gpu 数 + parameters: FormParameter[]; + points_to_evaluate: { [key: string]: any }[]; +}; + +export type HyperparameterData = { + id: number; + progress: number; + run_state: string; + state: number; + create_by?: string; + create_time?: string; + update_by?: string; + update_time?: string; + status_list: string; // 最近五次运行状态 +} & FormData; + +// 自动机器学习实验实例 +export type AutoMLInstanceData = { + id: number; + auto_ml_id: number; + result_path: string; + model_path: string; + img_path: string; + run_history_path: string; + state: number; + status: string; + node_status: string; + node_result: string; + param: string; + source: string | null; + argo_ins_name: string; + argo_ins_ns: string; + create_time: string; + update_time: string; + finish_time: string; + nodeStatus?: NodeStatus; +}; diff --git a/react-ui/src/services/hyperParameter/index.js b/react-ui/src/services/hyperParameter/index.js new file mode 100644 index 00000000..c97e617d --- /dev/null +++ b/react-ui/src/services/hyperParameter/index.js @@ -0,0 +1,93 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-18 10:18:27 + * @Description: 超参数自动寻优请求 + */ + +import { request } from '@umijs/max'; + + +// 分页查询超参数自动寻优 +export function getRayListReq(params) { + return request(`/api/mmp/ray`, { + method: 'GET', + params, + }); +} + +// 查询超参数自动寻优详情 +export function getRayInfoReq(params) { + return request(`/api/mmp/ray/getRayDetail`, { + method: 'GET', + params, + }); +} + +// 新增超参数自动寻优 +export function addRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'POST', + data, + }); +} + +// 编辑超参数自动寻优 +export function updateRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'PUT', + data, + }); +} + +// 删除超参数自动寻优 +export function deleteRayReq(id) { + return request(`/api/mmp/ray/${id}`, { + method: 'DELETE', + }); +} + +// 运行超参数自动寻优 +export function runRayReq(id) { + return request(`/api/mmp/ray/run/${id}`, { + method: 'POST', + }); +} + +// ----------------------- 实验实例 ----------------------- +// 获取实验实例列表 +export function getRayInsListReq(params) { + return request(`/api/mmp/rayIns`, { + method: 'GET', + params, + }); +} + +// 查询实验实例详情 +export function getRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'GET', + }); +} + +// 停止实验实例 +export function stopRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'PUT', + }); +} + +// 删除实验实例 +export function deleteRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'DELETE', + }); +} + +// 批量删除实验实例 +export function batchDeleteRayInsReq(data) { + return request(`/api/mmp/rayIns/batchDelete`, { + method: 'DELETE', + data + }); +} + diff --git a/react-ui/src/utils/constant.ts b/react-ui/src/utils/constant.ts new file mode 100644 index 00000000..4fe1ea9b --- /dev/null +++ b/react-ui/src/utils/constant.ts @@ -0,0 +1,3 @@ +export const xlCols = { span: 12 }; +export const xllCols = { span: 10 }; +export const formCols = { xl: xlCols, xxl: xllCols }; From 53731a2fc415ba0fb5984dc08cca2bec7587ab78 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 13 Jan 2025 14:01:21 +0800 Subject: [PATCH 009/127] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=B6=85?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E8=87=AA=E5=8A=A8=E5=AF=BB=E4=BC=98=E8=AF=A6?= =?UTF-8?q?=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/enums/index.ts | 11 + react-ui/src/pages/AutoML/Create/index.tsx | 2 +- react-ui/src/pages/AutoML/Info/index.tsx | 16 - .../AutoML/components/AutoMLBasic/index.tsx | 9 +- .../AutoML/components/ConfigInfo/index.tsx | 4 +- .../components/ExperimentList/index.tsx | 1 + .../components/ResourceIntro/index.tsx | 70 +--- .../components/VersionCompareModal/index.tsx | 6 +- .../src/pages/HyperParameter/Create/index.tsx | 11 +- .../src/pages/HyperParameter/Info/index.tsx | 42 +-- .../pages/HyperParameter/Instance/index.tsx | 4 +- .../components/AutoMLBasic/index.tsx | 308 ------------------ .../components/ConfigInfo/index.less | 20 -- .../components/ConfigInfo/index.tsx | 26 -- .../components/CreateForm/ExecuteConfig.tsx | 18 +- .../CreateForm/PopParameterRange/index.less | 30 +- .../components/CreateForm/index.less | 5 +- .../index.less | 2 +- .../components/HyperParameterBasic/index.tsx | 207 ++++++++++++ .../components/ParameterInfo/index.less | 7 + .../components/ParameterInfo/index.tsx | 108 ++++++ react-ui/src/pages/HyperParameter/types.ts | 2 +- react-ui/src/utils/format.ts | 87 +++++ 23 files changed, 487 insertions(+), 509 deletions(-) delete mode 100644 react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.tsx delete mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less delete mode 100644 react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx rename react-ui/src/pages/HyperParameter/components/{AutoMLBasic => HyperParameterBasic}/index.less (89%) create mode 100644 react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx create mode 100644 react-ui/src/utils/format.ts diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index 34d5b51b..bdfe9e00 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -118,3 +118,14 @@ export const autoMLResamplingStrategyOptions = [ { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, ]; + +// 超参数自动寻优优化方向 +export enum hyperParameterOptimizedMode { + Min = 'min', + Max = 'max', +} + +export const hyperParameterOptimizedModeOptions = [ + { label: '越大越好', value: hyperParameterOptimizedMode.Max }, + { label: '越小越好', value: hyperParameterOptimizedMode.Min }, +]; diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index 30699063..ec016c3c 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -190,7 +190,7 @@ function CreateAutoML() { - + diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx index cc5247e2..0d0ec460 100644 --- a/react-ui/src/pages/AutoML/Info/index.tsx +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -3,9 +3,7 @@ * @Date: 2024-04-16 13:58:08 * @Description: 自主机器学习详情 */ -import KFIcon from '@/components/KFIcon'; import PageTitle from '@/components/PageTitle'; -import { CommonTabKeys } from '@/enums'; import { getAutoMLInfoReq } from '@/services/autoML'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; @@ -16,24 +14,10 @@ import { AutoMLData } from '../types'; import styles from './index.less'; function AutoMLInfo() { - const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); const params = useParams(); const autoMLId = safeInvoke(Number)(params.id); const [autoMLInfo, setAutoMLInfo] = useState(undefined); - const tabItems = [ - { - key: CommonTabKeys.Public, - label: '基本信息', - icon: , - }, - { - key: CommonTabKeys.Private, - label: 'Trial列表', - icon: , - }, - ]; - useEffect(() => { if (autoMLId) { getAutoMLInfo(); diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 854c6035..76d26a75 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -4,6 +4,7 @@ import { experimentStatusInfo } from '@/pages/Experiment/status'; import { type NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; import { elapsedTime } from '@/utils/date'; +import { formatDataset } from '@/utils/format'; import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; @@ -15,14 +16,6 @@ import ConfigInfo, { } from '../ConfigInfo'; import styles from './index.less'; -// 格式化数据集 -const formatDataset = (dataset: { name: string; version: string }) => { - if (!dataset || !dataset.name || !dataset.version) { - return '--'; - } - return `${dataset.name}:${dataset.version}`; -}; - // 格式化优化方向 const formatOptimizeMode = (value: boolean) => { return value ? '越大越好' : '越小越好'; diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx index 10e042e4..256f7b16 100644 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx @@ -11,13 +11,15 @@ type ConfigInfoProps = { labelWidth: number; className?: string; style?: React.CSSProperties; + children?: React.ReactNode; }; -function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { +function ConfigInfo({ title, data, labelWidth, className, style, children }: ConfigInfoProps) { return (
+ {children}
); diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index c0740eee..0da5ca9c 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -400,6 +400,7 @@ function ExperimentList({ type }: ExperimentListProps) { expandable={{ expandedRowRender: (record) => ( gotoInstanceInfo(record, item)} diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index b0bad9de..807270a8 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -1,17 +1,8 @@ import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; import SubAreaTitle from '@/components/SubAreaTitle'; -import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; -import { - DataSource, - DatasetData, - ModelData, - ProjectDependency, - ResourceType, - TrainTask, - resourceConfig, -} from '@/pages/Dataset/config'; +import { DatasetData, ModelData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; import ModelMetrics from '@/pages/Model/components/ModelMetrics'; -import { getGitUrl } from '@/utils'; +import { formatCodeConfig, formatDatasets, formatSource, formatTrainTask } from '@/utils/format'; import classNames from 'classnames'; import styles from './index.less'; @@ -24,55 +15,6 @@ type ResourceIntroProps = { version?: string; }; -export const formatDataset = (datasets?: DatasetData[]) => { - if (!datasets || datasets.length === 0) { - return undefined; - } - return datasets.map((item) => ({ - value: item.name, - url: `${origin}/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Version}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`, - })); -}; - -export const getProjectUrl = (project?: ProjectDependency) => { - if (!project || !project.url || !project.branch) { - return undefined; - } - const { url, branch } = project; - return getGitUrl(url, branch); -}; - -export const formatProject = (project?: ProjectDependency) => { - if (!project) { - return undefined; - } - return { - value: project.name, - url: getProjectUrl(project), - }; -}; - -export const formatTrainTask = (task?: TrainTask) => { - if (!task) { - return undefined; - } - return { - value: task.name, - url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`, - }; -}; - -export const formatSource = (source?: string) => { - if (source === DataSource.Create) { - return '用户上传'; - } else if (source === DataSource.HandExport) { - return '手动导入'; - } else if (source === DataSource.AtuoExport) { - return '实验自动导入'; - } - return source; -}; - const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ { label: '数据集名称', @@ -109,7 +51,7 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ { label: '处理代码', value: data.processing_code, - format: formatProject, + format: formatCodeConfig, ellipsis: true, }, { @@ -153,19 +95,19 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ { label: '训练代码', value: data.project_depency, - format: formatProject, + format: formatCodeConfig, ellipsis: true, }, { label: '训练数据集', value: data.train_datasets, - format: formatDataset, + format: formatDatasets, ellipsis: true, }, { label: '测试数据集', value: data.test_datasets, - format: formatDataset, + format: formatDatasets, ellipsis: true, }, { diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx index 2ee76e78..7bafdf95 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx @@ -8,11 +8,11 @@ import { resourceConfig, } from '@/pages/Dataset/config'; import { isEmpty } from '@/utils'; +import { formatSource } from '@/utils/format'; import { to } from '@/utils/promise'; import { Typography, type ModalProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; -import { formatSource } from '../ResourceIntro'; import styles from './index.less'; type CompareData = { @@ -47,10 +47,10 @@ const formatProject = (project?: ProjectDependency) => { if (!project) { return undefined; } - return project.name; + return `${project.name}:${project.branch}`; }; -export const formatTrainTask = (task?: TrainTask) => { +const formatTrainTask = (task?: TrainTask) => { if (!task) { return undefined; } diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index dfe367ba..fa5fc8b8 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -41,10 +41,9 @@ function CreateHyperparameter() { const name = isCopy ? `${name_str}-copy` : name_str; if (parameters && Array.isArray(parameters)) { parameters.forEach((item) => { - item.range = item.bounds || item.values || item.value; - delete item.bounds; - delete item.values; - delete item.value; + const paramName = getReqParamName(item.type); + item.range = item[paramName]; + item[paramName] = undefined; }); } @@ -63,8 +62,6 @@ function CreateHyperparameter() { const createExperiment = async (formData: FormData) => { // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value" const parameters = formData['parameters']; - // const points_to_evaluate = formData['points_to_evaluate']; - // const runParameters = formData['parameters']; parameters.forEach((item) => { const paramName = getReqParamName(item.type); item[paramName] = item.range; @@ -142,7 +139,7 @@ function CreateHyperparameter() { - + diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx index 8b2fded3..9a37a68f 100644 --- a/react-ui/src/pages/HyperParameter/Info/index.tsx +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -3,48 +3,34 @@ * @Date: 2024-04-16 13:58:08 * @Description: 自主机器学习详情 */ -import KFIcon from '@/components/KFIcon'; import PageTitle from '@/components/PageTitle'; -import { CommonTabKeys } from '@/enums'; -import { getAutoMLInfoReq } from '@/services/autoML'; +import { getRayInfoReq } from '@/services/hyperParameter'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; import { useEffect, useState } from 'react'; -import AutoMLBasic from '../components/AutoMLBasic'; +import HyperParameterBasic from '../components/HyperParameterBasic'; import { HyperparameterData } from '../types'; import styles from './index.less'; -function AutoMLInfo() { - const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); +function HyperparameterInfo() { const params = useParams(); - const autoMLId = safeInvoke(Number)(params.id); - const [autoMLInfo, setAutoMLInfo] = useState(undefined); - - const tabItems = [ - { - key: CommonTabKeys.Public, - label: '基本信息', - icon: , - }, - { - key: CommonTabKeys.Private, - label: 'Trial列表', - icon: , - }, - ]; + const hyperparameterId = safeInvoke(Number)(params.id); + const [hyperparameterInfo, setHyperparameterInfo] = useState( + undefined, + ); useEffect(() => { - if (autoMLId) { - getAutoMLInfo(); + if (hyperparameterId) { + getHyperparameterInfo(); } }, []); // 获取自动机器学习详情 - const getAutoMLInfo = async () => { - const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + const getHyperparameterInfo = async () => { + const [res] = await to(getRayInfoReq({ id: hyperparameterId })); if (res && res.data) { - setAutoMLInfo(res.data); + setHyperparameterInfo(res.data); } }; @@ -52,10 +38,10 @@ function AutoMLInfo() {
- +
); } -export default AutoMLInfo; +export default HyperparameterInfo; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 355ced01..8f7faa33 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -9,9 +9,9 @@ import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; import { Tabs } from 'antd'; import { useEffect, useRef, useState } from 'react'; -import AutoMLBasic from '../components/AutoMLBasic'; import ExperimentHistory from '../components/ExperimentHistory'; import ExperimentResult from '../components/ExperimentResult'; +import HyperParameterBasic from '../components/HyperParameterBasic'; import { AutoMLInstanceData, HyperparameterData } from '../types'; import styles from './index.less'; @@ -140,7 +140,7 @@ function AutoMLInstance() { label: '基本信息', icon: , children: ( - { - if (!dataset || !dataset.name || !dataset.version) { - return '--'; - } - return `${dataset.name}:${dataset.version}`; -}; - -// 格式化优化方向 -const formatOptimizeMode = (value: boolean) => { - return value ? '越大越好' : '越小越好'; -}; - -const formatMetricsWeight = (value: string) => { - if (!value) { - return '--'; - } - const json = parseJsonText(value); - if (!json) { - return '--'; - } - return Object.entries(json) - .map(([key, value]) => `${key}:${value}`) - .join('\n'); -}; - -type AutoMLBasicProps = { - info?: AutoMLData; - className?: string; - isInstance?: boolean; - runStatus?: NodeStatus; -}; - -function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { - const basicDatas: BasicInfoData[] = useMemo(() => { - if (!info) { - return []; - } - - return [ - { - label: '实验名称', - value: info.ml_name, - ellipsis: true, - }, - { - label: '实验描述', - value: info.ml_description, - ellipsis: true, - }, - { - label: '创建人', - value: info.create_by, - ellipsis: true, - }, - { - label: '创建时间', - value: info.create_time, - ellipsis: true, - format: formatDate, - }, - { - label: '更新时间', - value: info.update_time, - ellipsis: true, - format: formatDate, - }, - ]; - }, [info]); - - const configDatas: BasicInfoData[] = useMemo(() => { - if (!info) { - return []; - } - return [ - { - label: '任务类型', - value: info.task_type, - ellipsis: true, - format: formatEnum(autoMLTaskTypeOptions), - }, - { - label: '特征预处理算法', - value: info.include_feature_preprocessor, - ellipsis: true, - }, - { - label: '排除的特征预处理算法', - value: info.exclude_feature_preprocessor, - ellipsis: true, - }, - { - label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', - value: - info.task_type === AutoMLTaskType.Regression - ? info.include_regressor - : info.include_classifier, - ellipsis: true, - }, - { - label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', - value: - info.task_type === AutoMLTaskType.Regression - ? info.exclude_regressor - : info.exclude_classifier, - ellipsis: true, - }, - { - label: '集成方式', - value: info.ensemble_class, - ellipsis: true, - format: formatEnum(autoMLEnsembleClassOptions), - }, - { - label: '集成模型数量', - value: info.ensemble_size, - ellipsis: true, - }, - { - label: '集成最佳模型数量', - value: info.ensemble_nbest, - ellipsis: true, - }, - { - label: '最大数量', - value: info.max_models_on_disc, - ellipsis: true, - }, - { - label: '内存限制(MB)', - value: info.memory_limit, - ellipsis: true, - }, - { - label: '单次时间限制(秒)', - value: info.per_run_time_limit, - ellipsis: true, - }, - { - label: '搜索时间限制(秒)', - value: info.time_left_for_this_task, - ellipsis: true, - }, - { - label: '重采样策略', - value: info.resampling_strategy, - ellipsis: true, - }, - { - label: '交叉验证折数', - value: info.folds, - ellipsis: true, - }, - { - label: '是否打乱', - value: info.shuffle, - ellipsis: true, - format: formatBoolean, - }, - { - label: '训练集比率', - value: info.train_size, - ellipsis: true, - }, - { - label: '测试集比率', - value: info.test_size, - ellipsis: true, - }, - { - label: '计算指标', - value: info.scoring_functions, - ellipsis: true, - }, - { - label: '随机种子', - value: info.seed, - ellipsis: true, - }, - - { - label: '数据集', - value: info.dataset, - ellipsis: true, - format: formatDataset, - }, - { - label: '预测目标列', - value: info.target_columns, - ellipsis: true, - }, - ]; - }, [info]); - - const metricsData = useMemo(() => { - if (!info) { - return []; - } - return [ - { - label: '指标名称', - value: info.metric_name, - ellipsis: true, - }, - { - label: '优化方向', - value: info.greater_is_better, - ellipsis: true, - format: formatOptimizeMode, - }, - { - label: '指标权重', - value: info.metrics, - ellipsis: true, - format: formatMetricsWeight, - }, - ]; - }, [info]); - - const instanceDatas = useMemo(() => { - if (!runStatus) { - return []; - } - - return [ - { - label: '启动时间', - value: formatDate(runStatus.startedAt), - ellipsis: true, - }, - { - label: '执行时长', - value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), - ellipsis: true, - }, - { - label: '状态', - value: ( - - -
- {experimentStatusInfo[runStatus?.phase]?.label} -
-
- ), - ellipsis: true, - }, - ]; - }, [runStatus]); - - return ( -
- {isInstance && runStatus && ( - - )} - {!isInstance && ( - - )} - - -
- ); -} - -export default AutoMLBasic; diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less deleted file mode 100644 index 33fb3314..00000000 --- a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.less +++ /dev/null @@ -1,20 +0,0 @@ -.config-info { - :global { - .kf-basic-info { - width: 100%; - - &__item { - width: calc((100% - 80px) / 3); - &__label { - font-size: @font-size; - text-align: left; - text-align-last: left; - } - &__value { - min-width: 0; - font-size: @font-size; - } - } - } - } -} diff --git a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx deleted file mode 100644 index 10e042e4..00000000 --- a/react-ui/src/pages/HyperParameter/components/ConfigInfo/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; -import InfoGroup from '@/components/InfoGroup'; -import classNames from 'classnames'; -import styles from './index.less'; -export * from '@/components/BasicInfo/format'; -export type { BasicInfoData }; - -type ConfigInfoProps = { - title: string; - data: BasicInfoData[]; - labelWidth: number; - className?: string; - style?: React.CSSProperties; -}; - -function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { - return ( - -
- -
-
- ); -} - -export default ConfigInfo; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 92e7f961..86760130 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -5,6 +5,7 @@ import ResourceSelect, { requiredValidator, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; +import { hyperParameterOptimizedModeOptions } from '@/enums'; import { modalConfirm } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; @@ -456,10 +457,7 @@ function ExecuteConfig() { name="mode" rules={[{ required: true, message: '请选择优化方向' }]} > - - 越大越好 - 越小越好 - +
@@ -467,16 +465,16 @@ function ExecuteConfig() {
- + @@ -484,16 +482,16 @@ function ExecuteConfig() { - + diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less index 01faf3d0..8f0a0f97 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -1,5 +1,8 @@ .parameter-range { :global { + .ant-popover-inner { + padding: 20px 20px 12px; + } .ant-popconfirm-description { padding-top: 20px; } @@ -22,15 +25,6 @@ border-radius: 8px; cursor: pointer; - &:hover { - border-color: #4086ff; - } - - &--disabled { - background-color: rgba(0, 0, 0, 0.04); - cursor: not-allowed; - } - &__text { flex: 1; margin-right: 10px; @@ -40,8 +34,22 @@ flex: none; } - &--disabled &__icon { - color: @text-color-tertiary; + &:hover { + border-color: #4086ff; + } + + &:hover &__icon { + color: #4086ff; + } + + &&--disabled { + background-color: rgba(0, 0, 0, 0.04); + border-color: rgba(0, 0, 0, 0.04) !important; + cursor: not-allowed; + } + + &&--disabled &__icon { + color: #aaaaaa !important; } } } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less index fcb77fdd..8f8bae7b 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -92,12 +92,13 @@ .run-parameter { width: calc(41.66% + 126px); margin-bottom: 20px; - border-radius: 8px; + &__body { flex: 1; margin-right: 10px; padding: 20px 20px 0; - border: 1px dashed @border-color-base; + border: 1px dashed #dddddd; + border-radius: 8px; } &__operation { display: flex; diff --git a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less similarity index 89% rename from react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less rename to react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less index cbd05bcc..f365aa66 100644 --- a/react-ui/src/pages/HyperParameter/components/AutoMLBasic/index.less +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less @@ -1,4 +1,4 @@ -.auto-ml-basic { +.hyper-parameter-basic { height: 100%; padding: 20px @content-padding; overflow-y: auto; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx new file mode 100644 index 00000000..167cbbe4 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -0,0 +1,207 @@ +import { hyperParameterOptimizedMode } from '@/enums'; +import ConfigInfo, { formatDate, type BasicInfoData } from '@/pages/AutoML/components/ConfigInfo'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { HyperparameterData } from '@/pages/HyperParameter/types'; +import { type NodeStatus } from '@/types'; +import { elapsedTime } from '@/utils/date'; +import { formatDataset, formatSelectCodeConfig } from '@/utils/format'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { useMemo } from 'react'; +import ParameterInfo from '../ParameterInfo'; +import styles from './index.less'; + +// 格式化优化方向 +const formatOptimizeMode = (value: string) => { + return value === hyperParameterOptimizedMode.Max ? '越大越好' : '越小越好'; +}; + +type HyperParameterBasicProps = { + info?: HyperparameterData; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; +}; + +function HyperParameterBasic({ + info, + className, + runStatus, + isInstance = false, +}: HyperParameterBasicProps) { + const basicDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + + return [ + { + label: '实验名称', + value: info.name, + ellipsis: true, + }, + { + label: '实验描述', + value: info.description, + ellipsis: true, + }, + { + label: '创建人', + value: info.create_by, + ellipsis: true, + }, + { + label: '创建时间', + value: info.create_time, + ellipsis: true, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + ellipsis: true, + format: formatDate, + }, + ]; + }, [info]); + + const configDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '代码', + value: info.code, + ellipsis: true, + format: formatSelectCodeConfig, + }, + { + label: '主函数代码文件', + value: info.main_py, + ellipsis: true, + }, + { + label: '数据集', + value: info.dataset, + ellipsis: true, + format: formatDataset, + }, + + { + label: '数据集挂载路径', + value: info.dataset_path, + ellipsis: true, + }, + { + label: '总实验次数', + value: info.num_samples, + ellipsis: true, + }, + { + label: '搜索算法', + value: info.search_alg, + ellipsis: true, + }, + { + label: '调度算法', + value: info.scheduler, + ellipsis: true, + }, + { + label: '优化方向', + value: info.mode, + ellipsis: true, + format: formatOptimizeMode, + }, + { + label: '指标', + value: info.metric, + ellipsis: true, + }, + { + label: 'CPU 数量', + value: info.cpu, + ellipsis: true, + }, + { + label: 'GPU 数量', + value: info.gpu, + ellipsis: true, + }, + ]; + }, [info]); + + const instanceDatas = useMemo(() => { + if (!runStatus) { + return []; + } + + return [ + { + label: '启动时间', + value: formatDate(runStatus.startedAt), + ellipsis: true, + }, + { + label: '执行时长', + value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), + ellipsis: true, + }, + { + label: '状态', + value: ( + + +
+ {experimentStatusInfo[runStatus?.phase]?.label} +
+
+ ), + ellipsis: true, + }, + ]; + }, [runStatus]); + + return ( +
+ {isInstance && runStatus && ( + + )} + {!isInstance && ( + + )} + + {info && } + +
+ ); +} + +export default HyperParameterBasic; diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less new file mode 100644 index 00000000..81d6fd56 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less @@ -0,0 +1,7 @@ +.parameter-info { + &__title { + margin: 20px 0; + color: @text-color-secondary; + font-size: @font-size; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx new file mode 100644 index 00000000..9b415d85 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -0,0 +1,108 @@ +import { + getReqParamName, + type FormParameter, +} from '@/pages/HyperParameter/components/CreateForm/utils'; +import { HyperparameterData } from '@/pages/HyperParameter/types'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, Tooltip, type TableProps } from 'antd'; +import { useMemo } from 'react'; +import styles from './index.less'; + +type ParameterInfoProps = { + info: HyperparameterData; +}; + +function ParameterInfo({ info }: ParameterInfoProps) { + const parameters = useMemo(() => { + if (!info.parameters) { + return []; + } + return info.parameters.map((item) => { + const paramName = getReqParamName(item.type); + const range = item[paramName]; + return { + ...item, + range, + }; + }); + }, [info]); + + const runParameters = useMemo(() => { + if (!info.points_to_evaluate) { + return []; + } + return info.points_to_evaluate.map((item, index) => ({ + ...item, + id: index, + })); + }, [info]); + + const columns: TableProps['columns'] = [ + { + title: '参数名称', + dataIndex: 'name', + key: 'type', + width: '40%', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '参数类型', + dataIndex: 'type', + key: 'type', + width: '20%', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '取值范围', + dataIndex: 'range', + key: 'range', + width: '40%', + render: tableCellRender(true, TableCellValueType.Custom, { + format: (value) => { + return JSON.stringify(value); + }, + }), + ellipsis: { showTitle: false }, + }, + ]; + + const runColumns: TableProps>['columns'] = + runParameters.length > 0 + ? Object.keys(runParameters[0]) + .filter((key) => key !== 'id') + .map((key) => { + return { + title: ( + + {key} + + ), + dataIndex: key, + key: key, + width: 150, + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }; + }) + : []; + + return ( +
+
参数
+
+
手动运行参数
+
+ + ); +} + +export default ParameterInfo; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 3eec723c..bb412c09 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -16,7 +16,7 @@ export type FormData = { dataset: ParameterInputObject; // 数据集 dataset_path: string; // 数据集路径 main_py: string; // 主函数代码文件 - metrics: string; // 指标 + metric: string; // 指标 mode: string; // 优化方向 search_alg?: string; // 搜索算法 scheduler?: string; // 调度算法 diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts new file mode 100644 index 00000000..579ae103 --- /dev/null +++ b/react-ui/src/utils/format.ts @@ -0,0 +1,87 @@ +import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; +import { DataSource, DatasetData, ProjectDependency, TrainTask } from '@/pages/Dataset/config'; +import { getGitUrl } from '@/utils'; + +// 格式化数据集数组 +export const formatDatasets = (datasets?: DatasetData[]) => { + if (!datasets || datasets.length === 0) { + return undefined; + } + return datasets.map((item) => ({ + value: item.name, + url: `${origin}/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`, + })); +}; + +// 格式化数据集 +export const formatDataset = (dataset?: DatasetData) => { + if (!dataset) { + return undefined; + } + return { + value: dataset.name, + url: `${origin}/dataset/dataset/info/${dataset.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${dataset.version}&name=${dataset.name}&owner=${dataset.owner}&identifier=${dataset.identifier}`, + }; +}; + +// 获取代码配置的仓库的 url +export const getRepoUrl = (project?: ProjectDependency) => { + if (!project) { + return undefined; + } + const { url, branch } = project; + return getGitUrl(url, branch); +}; + +// 格式化代码配置 +export const formatCodeConfig = (project?: ProjectDependency) => { + if (!project) { + return undefined; + } + return { + value: project.name, + url: getRepoUrl(project), + }; +}; + +// 格式化选中的代码配置 +export const formatSelectCodeConfig = (value?: { + code_path: string; + branch: string; + showValue: string; +}) => { + if (!value) { + return undefined; + } + const { showValue, code_path, branch } = value; + return { + value: showValue, + url: getRepoUrl({ + url: code_path, + branch, + } as ProjectDependency), + }; +}; + +// 格式化训练任务(实验实例) +export const formatTrainTask = (task?: TrainTask) => { + if (!task) { + return undefined; + } + return { + value: task.name, + url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`, + }; +}; + +// 格式化数据来源 +export const formatSource = (source?: string) => { + if (source === DataSource.Create) { + return '用户上传'; + } else if (source === DataSource.HandExport) { + return '手动导入'; + } else if (source === DataSource.AtuoExport) { + return '实验自动导入'; + } + return source; +}; From b1a7a3c12e50324a06d3526b172cac5398295abb Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 13 Jan 2025 14:28:47 +0800 Subject: [PATCH 010/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/service/impl/RayServiceImpl.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 93add6d3..82c983ae 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -43,13 +43,13 @@ public class RayServiceImpl implements RayService { } Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); -// String username = SecurityUtils.getLoginUser().getUsername(); - String username = "admin"; + String username = SecurityUtils.getLoginUser().getUsername(); ray.setCreateBy(username); ray.setUpdateBy(username); ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); + ray.setPointsToEvaluate(JacksonUtil.toJSONString(rayVo.getPointsToEvaluate())); rayDao.save(ray); return ray; } @@ -62,16 +62,11 @@ public class RayServiceImpl implements RayService { } Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); - String username = SecurityUtils.getLoginUser().getUsername(); - ray.setUpdateBy(username); - String parameters = JacksonUtil.toJSONString(rayVo.getParameters()); - ray.setParameters(parameters); - String pointsToEvaluate = JacksonUtil.toJSONString(rayVo.getPointsToEvaluate()); - ray.setPointsToEvaluate(pointsToEvaluate); - String datasetJson = JacksonUtil.toJSONString(rayVo.getDataset()); - ray.setDataset(datasetJson); - String codeJson = JacksonUtil.toJSONString(rayVo.getCode()); - ray.setCode(codeJson); + ray.setUpdateBy(SecurityUtils.getLoginUser().getUsername()); + ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); + ray.setPointsToEvaluate(JacksonUtil.toJSONString(rayVo.getPointsToEvaluate())); + ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); + ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); rayDao.edit(ray); return "修改成功"; } From d895f392bbabd1360f8e4265ca2094f3f789382e Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 15 Jan 2025 08:33:29 +0800 Subject: [PATCH 011/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/domain/Ray.java | 6 +----- .../src/main/java/com/ruoyi/platform/vo/RayVo.java | 6 +----- .../mapper/managementPlatform/RayDaoMapper.xml | 11 ++++------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index 47a68380..00ffb645 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -62,11 +62,7 @@ public class Ray { @ApiModelProperty(value = "搜索算法为MedianStopping时传入,计算中位数的最小试验数。") private Integer minSamplesRequired; - @ApiModelProperty(value = "使用cpu数") - private Integer cpu; - - @ApiModelProperty(value = "使用gpu数") - private Integer gpu; + private String resource; private Integer state; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index af8b3e17..8f001f38 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -55,11 +55,7 @@ public class RayVo { @ApiModelProperty(value = "计算中位数的最小试验数:调度算法为MedianStopping时传入,计算中位数的最小试验数。") private Integer minSamplesRequired; - @ApiModelProperty(value = "使用cpu数") - private Integer cpu; - - @ApiModelProperty(value = "使用gpu数") - private Integer gpu; + private String resource; private String createBy; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index 86d88f35..b8a7818c 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -4,11 +4,11 @@ insert into ray(name, description, dataset, dataset_path, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, - min_samples_required, cpu, gpu, create_by, update_by) + min_samples_required, resource, create_by, update_by) values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.datasetPath}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, - #{ray.cpu}, #{ray.gpu}, #{ray.createBy}, #{ray.updateBy}) + #{ray.resource}, #{ray.createBy}, #{ray.updateBy}) @@ -62,11 +62,8 @@ min_samples_required = #{ray.minSamplesRequired}, - - cpu = #{ray.cpu}, - - - gpu = #{ray.gpu}, + + resource = #{ray.resource}, update_by = #{ray.updateBy}, From 45f440b3481f5d853240bfb07eb9ae24ee563633 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 15 Jan 2025 16:15:58 +0800 Subject: [PATCH 012/127] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E6=95=B0=E6=8D=AE=E9=9B=86=E3=80=81=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E3=80=81=E4=BB=A3=E7=A0=81=E9=85=8D=E7=BD=AE=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{components.tsx => BasicInfoItem.tsx} | 61 +------- .../BasicInfo/BasicInfoItemValue.tsx | 54 +++++++ react-ui/src/components/BasicInfo/format.ts | 48 ------- react-ui/src/components/BasicInfo/index.tsx | 3 +- react-ui/src/components/BasicInfo/types.ts | 4 +- .../src/components/BasicTableInfo/index.tsx | 3 +- .../AutoML/components/AutoMLBasic/index.tsx | 9 +- .../AutoML/components/ConfigInfo/index.tsx | 1 - .../components/CreateForm/ExecuteConfig.tsx | 91 ++++++++---- .../components/CreateForm/index.less | 22 +++ .../components/HyperParameterBasic/index.tsx | 26 ++-- react-ui/src/pages/HyperParameter/types.ts | 3 +- .../ModelDeployment/CreateVersion/index.tsx | 14 +- .../ModelDeployment/VersionInfo/index.tsx | 4 +- .../components/BasicInfo/index.tsx | 120 ---------------- .../index.less | 0 .../components/VersionBasicInfo/index.tsx | 134 ++++++++++++++++++ react-ui/src/utils/format.ts | 63 +++++++- 18 files changed, 374 insertions(+), 286 deletions(-) rename react-ui/src/components/BasicInfo/{components.tsx => BasicInfoItem.tsx} (50%) create mode 100644 react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx delete mode 100644 react-ui/src/components/BasicInfo/format.ts delete mode 100644 react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx rename react-ui/src/pages/ModelDeployment/components/{BasicInfo => VersionBasicInfo}/index.less (100%) create mode 100644 react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx diff --git a/react-ui/src/components/BasicInfo/components.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx similarity index 50% rename from react-ui/src/components/BasicInfo/components.tsx rename to react-ui/src/components/BasicInfo/BasicInfoItem.tsx index b8932a25..776a7f94 100644 --- a/react-ui/src/components/BasicInfo/components.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -4,9 +4,8 @@ * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 */ -import { Link } from '@umijs/max'; -import { Typography } from 'antd'; import React from 'react'; +import BasicInfoItemValue from './BasicInfoItemValue'; import { type BasicInfoData, type BasicInfoLink } from './types'; type BasicInfoItemProps = { @@ -15,12 +14,14 @@ type BasicInfoItemProps = { classPrefix: string; }; -export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { +function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { const { label, value, format, ellipsis } = data; const formatValue = format ? format(value) : value; const myClassName = `${classPrefix}__item`; let valueComponent = undefined; - if (Array.isArray(formatValue)) { + if (React.isValidElement(formatValue)) { + valueComponent = formatValue; + } else if (Array.isArray(formatValue)) { valueComponent = (
{formatValue.map((item: BasicInfoLink) => ( @@ -35,11 +36,6 @@ export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemPr ))}
); - } else if (React.isValidElement(formatValue)) { - // 这个判断必须在下面的判断之前 - valueComponent = ( - - ); } else if (typeof formatValue === 'object' && formatValue) { valueComponent = ( - {value} - - ); - } else if (link && value) { - component = ( - - {value} - - ); - } else if (React.isValidElement(value)) { - return value; - } else { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} +export default BasicInfoItem; diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx new file mode 100644 index 00000000..eb2dab24 --- /dev/null +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -0,0 +1,54 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-29 09:27:19 + * @Description: 用于 BasicInfoItem 的组件 + */ + +import { Link } from '@umijs/max'; +import { Typography } from 'antd'; +import React from 'react'; + +type BasicInfoItemValueProps = { + ellipsis?: boolean; + classPrefix: string; + value: string | React.ReactNode; + link?: string; + url?: string; +}; + +function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicInfoItemValueProps) { + if (React.isValidElement(value)) { + return value; + } + + const myClassName = `${classPrefix}__item__value`; + let component = undefined; + if (url && value) { + component = ( + + {value} + + ); + } else if (link && value) { + component = ( + + {value} + + ); + } else { + component = {value ?? '--'}; + } + + return ( +
+ + {component} + +
+ ); +} + +export default BasicInfoItemValue; diff --git a/react-ui/src/components/BasicInfo/format.ts b/react-ui/src/components/BasicInfo/format.ts deleted file mode 100644 index 0dae2422..00000000 --- a/react-ui/src/components/BasicInfo/format.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * @Author: 赵伟 - * @Date: 2024-11-29 09:27:19 - * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式 - */ - -// 格式化日期 -export { formatDate } from '@/utils/date'; - -/** - * 格式化字符串数组 - * @param value - 字符串数组 - * @returns 逗号分隔的字符串 - */ -export const formatList = (value: string[] | null | undefined): string => { - if ( - value === undefined || - value === null || - Array.isArray(value) === false || - value.length === 0 - ) { - return '--'; - } - return value.join(','); -}; - -/** - * 格式化布尔值 - * @param value - 布尔值 - * @returns "是" 或 "否" - */ -export const formatBoolean = (value: boolean): string => { - return value ? '是' : '否'; -}; - -type FormatEnum = (value: string | number) => string; - -/** - * 格式化枚举 - * @param options - 枚举选项 - * @returns 格式化枚举函数 - */ -export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { - return (value: string | number) => { - const option = options.find((item) => item.value === value); - return option ? option.label : '--'; - }; -}; diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 1336d0b6..5f60b79a 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -1,9 +1,8 @@ import classNames from 'classnames'; import React from 'react'; -import { BasicInfoItem } from './components'; +import BasicInfoItem from './BasicInfoItem'; import './index.less'; import type { BasicInfoData, BasicInfoLink } from './types'; -export * from './format'; export type { BasicInfoData, BasicInfoLink }; type BasicInfoProps = { diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts index a7c10ba0..be2ac774 100644 --- a/react-ui/src/components/BasicInfo/types.ts +++ b/react-ui/src/components/BasicInfo/types.ts @@ -3,12 +3,12 @@ export type BasicInfoData = { label: string; value?: any; ellipsis?: boolean; - format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; + format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined; }; // 值为链接的类型 export type BasicInfoLink = { - value: string; + value?: string; link?: string; url?: string; }; diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index 571c4b5b..f24f3dc9 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,8 +1,7 @@ import classNames from 'classnames'; -import { BasicInfoItem } from '../BasicInfo/components'; +import BasicInfoItem from '../BasicInfo/BasicInfoItem'; import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; -export * from '../BasicInfo/format'; export type { BasicInfoData, BasicInfoLink }; type BasicTableInfoProps = { diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 76d26a75..bb30e064 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -4,16 +4,11 @@ import { experimentStatusInfo } from '@/pages/Experiment/status'; import { type NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; import { elapsedTime } from '@/utils/date'; -import { formatDataset } from '@/utils/format'; +import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; -import ConfigInfo, { - formatBoolean, - formatDate, - formatEnum, - type BasicInfoData, -} from '../ConfigInfo'; +import ConfigInfo, { type BasicInfoData } from '../ConfigInfo'; import styles from './index.less'; // 格式化优化方向 diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx index 256f7b16..72596581 100644 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx @@ -2,7 +2,6 @@ import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; import InfoGroup from '@/components/InfoGroup'; import classNames from 'classnames'; import styles from './index.less'; -export * from '@/components/BasicInfo/format'; export type { BasicInfoData }; type ConfigInfoProps = { diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 86760130..b900c2e5 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -6,9 +6,10 @@ import ResourceSelect, { } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { hyperParameterOptimizedModeOptions } from '@/enums'; +import { useComputingResource } from '@/hooks/resource'; import { modalConfirm } from '@/utils/ui'; -import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; -import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; +import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select, Tooltip } from 'antd'; import { isEqual } from 'lodash'; import PopParameterRange from './PopParameterRange'; import styles from './index.less'; @@ -25,12 +26,48 @@ const schedulerAlgorithms = ['ASHA', 'HyperBand', 'MedianStopping', 'PopulationB (name) => ({ label: name, value: name }), ); +const parameterTooltip = `uniform(-5, -1) + 在 -5.0 和 -1.0 之间均匀采样浮点数 + + quniform(3.2, 5.4, 0.2) + 在 3.2 和 5.4 之间均匀采样浮点数,四舍五入到 0.2 的倍数 + + loguniform(1e-4, 1e-2) + 在 0.0001 和 0.01 之间均匀采样浮点数,对数空间采样 + + qloguniform(1e-4, 1e-1, 5e-5) + 在 0.0001 和 0.01 之间均匀采样浮点数,对数空间采样并四舍五入到 0.00005 的倍数 + + randn(10, 2) + 在均值为 10,方差为 2 的正态分布中进行随机浮点数抽样 + + qrandn(10, 2, 0.2) + 在均值为 10,方差为 2 的正态分布中进行随机浮点数抽样,四舍五入到 0.2 的倍数 + + randint(-9, 15) + 在 -9(包括)到 15(不包括)之间均匀采样整数 + + qrandint(-21, 12, 3) + 在 -21(包括)到 12(不包括)之间均匀采样整数,四舍五入到 3 的倍数 + + lograndint(1, 10) + 在 1(包括)到 10(不包括)之间均匀采样整数,对数空间采样 + + qlograndint(1, 10, 2) + 在 1(包括)到 10(不包括)之间均匀采样整数,对数空间采样并四舍五入到 2 的倍数 + + choice(["a", "b", "c"]) + 从指定的选项中采样一个选项 + + grid([32, 64, 128]) + 对这些值进行网格搜索,每个值都将被采样 +`; + function ExecuteConfig() { const form = Form.useFormInstance(); const searchAlgorithm = Form.useWatch('search_alg', form); const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; - // const parameters = Form.useWatch('parameters', form); - // console.log('parameters', parameters); + const [resourceStandardList, filterResourceStandard] = useComputingResource(); const handleSearchAlgorithmChange = (value: string) => { if ( @@ -231,7 +268,17 @@ function ExecuteConfig() {
参数名称
-
参数类型
+
+ 参数类型 + + + +
取值范围
操作
@@ -465,33 +512,25 @@ function ExecuteConfig() {
- - - - - - - - - + + + @@ -405,15 +397,36 @@ function ExecuteConfig() { } return ( - - {(fields, { add, remove }) => ( + { + const parameters = form.getFieldValue('parameters'); + for (const item of parameters) { + const name = item.name; + const arr = runParameters.filter((item?: Record) => + isEmpty(item?.[name]), + ); + if (arr.length > 0 && arr.length < runParameters.length) { + return Promise.reject( + new Error(`手动运行参数 ${name} 必须全部填写或者都不填写`), + ); + } + } + + return Promise.resolve(); + }, + }, + ]} + > + {(fields, { add, remove }, { errors }) => ( <> @@ -429,15 +442,14 @@ function ExecuteConfig() { labelCol={{ flex: '140px' }} name={[name, item.name]} preserve={false} - required - rules={[ - { - required: true, - message: '请输入', - }, - ]} > - + form.validateFields(['points_to_evaluate'])} + /> ))} @@ -472,6 +484,7 @@ function ExecuteConfig() { ))} + )} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less index 7d945b21..7c218f63 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -119,13 +119,19 @@ flex: 1; margin-right: 10px; padding: 20px 20px 0; - border: 1px dashed #dddddd; + border: 1px dashed #e0e0e0; border-radius: 8px; } + &__operation { display: flex; flex: none; align-items: center; width: 100px; } + + &__error { + margin-top: -20px; + color: @error-color; + } } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts index 95aa3651..f3f92555 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts @@ -153,3 +153,55 @@ export const getReqParamName = (type: ParameterType) => { return 'bounds'; } }; + +// 搜索算法 +export const searchAlgorithms = [ + { + label: 'HyperOpt(分布式异步超参数优化)', + value: 'HyperOpt', + }, + { + label: 'HEBO(异方差进化贝叶斯优化)', + value: 'HEBO', + }, + { + label: 'BayesOpt(贝叶斯优化)', + value: 'BayesOpt', + }, + { + label: 'Optuna', + value: 'Optuna', + }, + { + label: 'ZOOpt', + value: 'ZOOpt', + }, + { + label: 'Ax', + value: 'Ax', + }, +]; + +// 调度算法 +export const schedulerAlgorithms = [ + { + label: 'ASHA(异步连续减半)', + value: 'ASHA', + }, + { + label: 'HyperBand(HyperBand 早停算法)', + value: 'HyperBand', + }, + { + label: 'MedianStopping(中值停止规则)', + value: 'MedianStopping', + }, + { + label: 'PopulationBased(基于种群训练)', + value: 'PopulationBased', + }, + { + label: 'PB2(Population Based Bandits)', + value: 'PB2', + }, +]; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 4daa2298..88a88800 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -2,10 +2,20 @@ import { hyperParameterOptimizedMode } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; import ConfigInfo, { type BasicInfoData } from '@/pages/AutoML/components/ConfigInfo'; import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { + schedulerAlgorithms, + searchAlgorithms, +} from '@/pages/HyperParameter/components/CreateForm/utils'; import { HyperparameterData } from '@/pages/HyperParameter/types'; import { type NodeStatus } from '@/types'; import { elapsedTime } from '@/utils/date'; -import { formatDataset, formatDate, formatSelectCodeConfig } from '@/utils/format'; +import { + formatCodeConfig, + formatDataset, + formatDate, + formatEnum, + formatModel, +} from '@/utils/format'; import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; @@ -86,7 +96,7 @@ function HyperParameterBasic({ label: '代码', value: info.code, ellipsis: true, - format: formatSelectCodeConfig, + format: formatCodeConfig, }, { label: '主函数代码文件', @@ -99,11 +109,11 @@ function HyperParameterBasic({ ellipsis: true, format: formatDataset, }, - { - label: '数据集挂载路径', - value: info.dataset_path, + label: '模型', + value: info.model, ellipsis: true, + format: formatModel, }, { label: '总实验次数', @@ -113,11 +123,23 @@ function HyperParameterBasic({ { label: '搜索算法', value: info.search_alg, + format: formatEnum(searchAlgorithms), ellipsis: true, }, { label: '调度算法', value: info.scheduler, + format: formatEnum(schedulerAlgorithms), + ellipsis: true, + }, + { + label: '单次试验最大时间', + value: info.max_t, + ellipsis: true, + }, + { + label: '最小试验数', + value: info.min_samples_required, ellipsis: true, }, { @@ -203,7 +225,7 @@ function HyperParameterBasic({ {info && } diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index 9b415d85..95a24e1f 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -70,22 +70,20 @@ function ParameterInfo({ info }: ParameterInfoProps) { const runColumns: TableProps>['columns'] = runParameters.length > 0 - ? Object.keys(runParameters[0]) - .filter((key) => key !== 'id') - .map((key) => { - return { - title: ( - - {key} - - ), - dataIndex: key, - key: key, - width: 150, - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }; - }) + ? parameters.map(({ name }) => { + return { + title: ( + + {name} + + ), + dataIndex: name, + key: name, + width: 150, + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }; + }) : []; return ( diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 68f77fb2..568dbd4a 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -14,7 +14,7 @@ export type FormData = { description: string; // 实验描述 code: ParameterInputObject; // 代码 dataset: ParameterInputObject; // 数据集 - dataset_path: string; // 数据集路径 + model: ParameterInputObject; // 模型 main_py: string; // 主函数代码文件 metric: string; // 指标 mode: string; // 优化方向 diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index 3ddfb0e2..28da0127 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -3,7 +3,7 @@ import { ServiceRunStatus } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; import { ServiceVersionData } from '@/pages/ModelDeployment/types'; import { formatDate } from '@/utils/date'; -import { formatModel, formatSelectCodeConfig } from '@/utils/format'; +import { formatCodeConfig, formatModel } from '@/utils/format'; import { Flex } from 'antd'; import ModelDeployStatusCell from '../ModelDeployStatusCell'; @@ -61,7 +61,7 @@ function VersionBasicInfo({ info }: BasicInfoProps) { { label: '代码配置', value: info?.code_config, - format: formatSelectCodeConfig, + format: formatCodeConfig, ellipsis: true, }, { diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index c7d34e7d..8e7bbbf4 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -10,6 +10,13 @@ import { getGitUrl } from '@/utils'; // 格式化日期 export { formatDate } from '@/utils/date'; +type SelectedCodeConfig = { + code_path: string; + branch: string; + showValue?: string; // 前端使用的 + show_value?: string; // 后端使用的 +}; + // 格式化数据集数组 export const formatDatasets = (datasets?: DatasetData[]) => { if (!datasets || datasets.length === 0) { @@ -37,51 +44,32 @@ export const formatModel = (model: ModelData) => { if (!model) { return undefined; } - return { value: model.name, link: `/dataset/model/info/${model.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${model.version}&name=${model.name}&owner=${model.owner}&identifier=${model.identifier}`, }; }; -// 获取代码配置的仓库的 url -export const getRepoUrl = (project?: ProjectDependency) => { - if (!project) { - return undefined; - } - const { url, branch } = project; - return getGitUrl(url, branch); -}; - // 格式化代码配置 -export const formatCodeConfig = (project?: ProjectDependency) => { +export const formatCodeConfig = (project?: ProjectDependency | SelectedCodeConfig) => { if (!project) { return undefined; } - return { - value: project.name, - url: getRepoUrl(project), - }; -}; - -// 格式化选中的代码配置 -export const formatSelectCodeConfig = (value?: { - code_path: string; - branch: string; - showValue?: string; - show_value?: string; -}) => { - if (!value) { - return undefined; + // 创建表单,CodeSelect 组件返回,目前有流水线、模型部署、超参数自动寻优创建时选择了代码配置 + if ('code_path' in project) { + const { showValue, show_value, code_path, branch } = project; + return { + value: showValue || show_value, + url: getGitUrl(code_path, branch), + }; + } else { + // 数据集和模型的代码配置 + const { url, branch, name } = project; + return { + value: name, + url: getGitUrl(url, branch), + }; } - const { showValue, show_value, code_path, branch } = value; - return { - value: showValue || show_value, - url: getRepoUrl({ - url: code_path, - branch, - } as ProjectDependency), - }; }; // 格式化训练任务(实验实例) @@ -107,7 +95,7 @@ export const formatSource = (source?: string) => { return source; }; -// 格式化字符串数组 +// 格式化字符串数组,以逗号分隔 export const formatList = (value: string[] | null | undefined): string => { if ( value === undefined || diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index 0d4b1927..d3ec10d6 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -4,6 +4,7 @@ * @Description: Table cell 自定义 render */ +import { isEmpty } from '@/utils'; import { formatDate } from '@/utils/date'; import { Tooltip } from 'antd'; import dayjs from 'dayjs'; @@ -113,7 +114,7 @@ function renderCell( } function renderText(text: any | undefined | null) { - return {text ?? '--'}; + return {!isEmpty(text) ? text : '--'}; } function renderLink( From 4fbd39d0e62bc4178bfad4548f8e91558c7c5fee Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Sat, 18 Jan 2025 11:01:27 +0800 Subject: [PATCH 014/127] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/app.tsx | 1 + .../BasicInfo/BasicInfoItemValue.tsx | 5 +- .../src/components/DisabledInput/index.less | 2 +- .../src/components/DisabledInput/index.tsx | 4 +- .../src/components/ParameterInput/index.less | 2 +- .../src/components/ResourceSelect/index.tsx | 5 +- .../ResourceSelectorModal/config.tsx | 0 .../ResourceSelectorModal/index.less | 0 .../ResourceSelectorModal/index.tsx | 4 +- react-ui/src/overrides.less | 5 + react-ui/src/pages/AutoML/Create/index.less | 2 +- .../AutoML/components/CopyingText/index.tsx | 6 +- .../Dataset/components/CategoryItem/index.tsx | 5 +- .../components/VersionCompareModal/index.tsx | 4 +- .../components/VersionSelectorModal/index.tsx | 4 +- .../pages/HyperParameter/Create/index.less | 2 +- .../src/pages/HyperParameter/Create/index.tsx | 10 -- .../components/CreateForm/ExecuteConfig.tsx | 112 +++++++++++++----- .../CreateForm/PopParameterRange/index.less | 5 + .../CreateForm/PopParameterRange/index.tsx | 5 +- .../components/CreateForm/index.less | 8 ++ react-ui/src/pages/HyperParameter/types.ts | 1 + .../ModelDeployment/CreateVersion/index.less | 2 +- .../components/VersionCompareModal/index.tsx | 4 +- .../components/PipelineNodeDrawer/index.tsx | 8 +- react-ui/src/styles/theme.less | 2 + 26 files changed, 130 insertions(+), 78 deletions(-) rename react-ui/src/{pages/Pipeline => }/components/ResourceSelectorModal/config.tsx (100%) rename react-ui/src/{pages/Pipeline => }/components/ResourceSelectorModal/index.less (100%) rename react-ui/src/{pages/Pipeline => }/components/ResourceSelectorModal/index.tsx (98%) diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index d08ca129..275e5e17 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -228,6 +228,7 @@ export const antd: RuntimeAntdConfig = (memo) => { }; memo.theme.components.Select = { singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], }; memo.theme.components.Table = { headerBg: 'rgba(242, 244, 247, 0.36)', diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx index eb2dab24..eca6c80c 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -41,10 +41,7 @@ function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicIn return (
- + {component}
diff --git a/react-ui/src/components/DisabledInput/index.less b/react-ui/src/components/DisabledInput/index.less index 2eb28917..06808c5a 100644 --- a/react-ui/src/components/DisabledInput/index.less +++ b/react-ui/src/components/DisabledInput/index.less @@ -1,6 +1,6 @@ .disabled-input { padding: 4px 11px; - color: rgba(0, 0, 0, 0.25); + color: @text-disabled-color; font-size: @font-size-input; background-color: rgba(0, 0, 0, 0.04); border: 1px solid #d9d9d9; diff --git a/react-ui/src/components/DisabledInput/index.tsx b/react-ui/src/components/DisabledInput/index.tsx index a3c67b7d..c951324e 100644 --- a/react-ui/src/components/DisabledInput/index.tsx +++ b/react-ui/src/components/DisabledInput/index.tsx @@ -10,9 +10,7 @@ function DisabledInput({ value, valuePropName }: DisabledInputProps) { const data = valuePropName ? value[valuePropName] : value; return (
- - {data} - + {data}
); } diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 4b22d208..cf249e0f 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -39,7 +39,7 @@ &__placeholder { min-height: 22px; - color: rgba(0, 0, 0, 0.25); + color: @text-placeholder-color; font-size: @font-size-input; line-height: 1.5714285714285714; } diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index e818df85..6e0179d4 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -9,7 +9,7 @@ import ResourceSelectorModal, { ResourceSelectorResponse, ResourceSelectorType, selectorTypeConfig, -} from '@/pages/Pipeline/components/ResourceSelectorModal'; +} from '@/components/ResourceSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; import { pick } from 'lodash'; @@ -43,8 +43,7 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe value.name && value.version && value.path && - (type === ResourceSelectorType.Mirror || value.identifier) && - (type === ResourceSelectorType.Mirror || value.owner) + (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) ) { const originResource = pick(value, [ 'activeTab', diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx b/react-ui/src/components/ResourceSelectorModal/config.tsx similarity index 100% rename from react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx rename to react-ui/src/components/ResourceSelectorModal/config.tsx diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less b/react-ui/src/components/ResourceSelectorModal/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less rename to react-ui/src/components/ResourceSelectorModal/index.less diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx similarity index 98% rename from react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx rename to react-ui/src/components/ResourceSelectorModal/index.tsx index 06fdc242..28beedd0 100644 --- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -23,8 +23,8 @@ export type ResourceSelectorResponse = { name: string; // 数据集\模型\镜像 name version: string; // 数据集\模型\镜像版本 path: string; // 数据集\模型\镜像版本路径 - identifier: string; // 数据集\模型 identifier,镜像没有这个字段 - owner: string; // 数据集\模型 owner,镜像没有这个字段 + identifier: string; // 数据集\模型 identifier,镜像这个字段为空 + owner: string; // 数据集\模型 owner,镜像这个字段为空 }; export interface ResourceSelectorModalProps extends Omit { diff --git a/react-ui/src/overrides.less b/react-ui/src/overrides.less index e129b4a7..4709a97c 100644 --- a/react-ui/src/overrides.less +++ b/react-ui/src/overrides.less @@ -261,3 +261,8 @@ } } } + +.ant-typography { + color: inherit; + font-size: inherit; +} diff --git a/react-ui/src/pages/AutoML/Create/index.less b/react-ui/src/pages/AutoML/Create/index.less index f8d15d2e..46c1f603 100644 --- a/react-ui/src/pages/AutoML/Create/index.less +++ b/react-ui/src/pages/AutoML/Create/index.less @@ -33,7 +33,7 @@ } .ant-btn-variant-text:disabled { - color: rgba(0, 0, 0, 0.25); + color: @text-disabled-color; } .ant-btn-variant-text { diff --git a/react-ui/src/pages/AutoML/components/CopyingText/index.tsx b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx index b4c56f4e..586de40b 100644 --- a/react-ui/src/pages/AutoML/components/CopyingText/index.tsx +++ b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx @@ -9,11 +9,7 @@ export type CopyingTextProps = { function CopyingText({ text }: CopyingTextProps) { return (
- + {text} - + {item.name}
diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx index 7bafdf95..b3b6b2f1 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx @@ -203,7 +203,7 @@ function VersionCompareModal({ [styles['version-compare__left__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} @@ -221,7 +221,7 @@ function VersionCompareModal({ [styles['version-compare__right__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} diff --git a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx index c58bf87e..b63d02fc 100644 --- a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx @@ -49,9 +49,7 @@ function VersionSelectorModal({ versions, onOk, ...rest }: VersionSelectorModalP onClick={() => handleClick(item.name)} > - - {item.name} - + {item.name} ); })} diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less index 0325570e..145be0d1 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.less +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -33,7 +33,7 @@ } .ant-btn-variant-text:disabled { - color: rgba(0, 0, 0, 0.25); + color: @text-disabled-color; } .ant-btn-variant-text { diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index fa94809b..bd83783a 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -73,16 +73,6 @@ function CreateHyperparameter() { }; }); - // const runParameters = formData['points_to_evaluate']; - // for (const item of parameters) { - // const name = item.name; - // const arr = runParameters.filter((item) => isEmpty(item[name])); - // if (arr.length > 0 && arr.length < runParameters.length) { - // message.error(`手动运行参数 ${name} 必须全部填写或者都不填写`); - // return; - // } - // } - // 根据后台要求,修改表单数据 const object = { ...formData, diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 056ae7e4..1609328c 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -10,7 +10,19 @@ import { useComputingResource } from '@/hooks/resource'; import { isEmpty } from '@/utils'; import { modalConfirm } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; -import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select, Tooltip } from 'antd'; +import { + Button, + Col, + Flex, + Form, + Input, + InputNumber, + Radio, + Row, + Select, + Tooltip, + Typography, +} from 'antd'; import { isEqual } from 'lodash'; import PopParameterRange from './PopParameterRange'; import styles from './index.less'; @@ -22,47 +34,58 @@ import { type FormParameter, } from './utils'; -const parameterTooltip = `uniform(-5, -1) - 在 -5.0 和 -1.0 之间均匀采样浮点数 - - quniform(3.2, 5.4, 0.2) - 在 3.2 和 5.4 之间均匀采样浮点数,四舍五入到 0.2 的倍数 +const parameterTooltip = `uniform(low, high) + 在 low 和 high 之间均匀采样浮点数 + + quniform(low, high, q) + 在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数 - loguniform(1e-4, 1e-2) - 在 0.0001 和 0.01 之间均匀采样浮点数,对数空间采样 + loguniform(low, high) + 在 low 和 high 之间均匀采样浮点数,对数空间采样 - qloguniform(1e-4, 1e-1, 5e-5) - 在 0.0001 和 0.01 之间均匀采样浮点数,对数空间采样并四舍五入到 0.00005 的倍数 + qloguniform(low, high, q) + 在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数 - randn(10, 2) - 在均值为 10,方差为 2 的正态分布中进行随机浮点数抽样 + randn(m, s) + 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样 - qrandn(10, 2, 0.2) - 在均值为 10,方差为 2 的正态分布中进行随机浮点数抽样,四舍五入到 0.2 的倍数 + qrandn(m, s, q) + 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数 - randint(-9, 15) - 在 -9(包括)到 15(不包括)之间均匀采样整数 + randint(low, high) + 在 low(包括)到 high(不包括)之间均匀采样整数 - qrandint(-21, 12, 3) - 在 -21(包括)到 12(不包括)之间均匀采样整数,四舍五入到 3 的倍数 + qrandint(low, high, q) + 在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high) - lograndint(1, 10) - 在 1(包括)到 10(不包括)之间均匀采样整数,对数空间采样 + lograndint(low, high) + 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数 - qlograndint(1, 10, 2) - 在 1(包括)到 10(不包括)之间均匀采样整数,对数空间采样并四舍五入到 2 的倍数 + qlograndint(low, high, q) + 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数 - choice(["a", "b", "c"]) + choice 从指定的选项中采样一个选项 - grid([32, 64, 128]) - 对这些值进行网格搜索,每个值都将被采样 + grid + 对选项进行网格搜索,每个值都将被采样 `; +const axParameterTooltip = `fixed + 固定取值 + + range(low, high) + 在 low 和 high 范围内采样取值 + + choice + 从指定的选项中采样一个选项 + `; + function ExecuteConfig() { const form = Form.useFormInstance(); const searchAlgorithm = Form.useWatch('search_alg', form); const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; + const paramsTypeTooltip = searchAlgorithm === 'Ax' ? axParameterTooltip : parameterTooltip; const [resourceStandardList, filterResourceStandard] = useComputingResource(); const handleSearchAlgorithmChange = (value: string) => { @@ -117,6 +140,29 @@ function ExecuteConfig() { + +
+ + + + + + 参数类型 { - const parameters = form.getFieldValue('parameters'); + const parameters = form + .getFieldValue('parameters') + .filter( + (item: FormParameter | undefined) => item !== undefined && item !== null, + ); for (const item of parameters) { - const name = item.name; + const name = item?.name; const arr = runParameters.filter((item?: Record) => isEmpty(item?.[name]), ); @@ -437,7 +487,11 @@ function ExecuteConfig() { {parameters.map((item: FormParameter) => ( + {item.name} + + } {...restField} labelCol={{ flex: '140px' }} name={[name, item.name]} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less index 8f0a0f97..1d8609d3 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -18,6 +18,7 @@ width: 100%; min-height: 46px; padding: 10px 11px; + color: @text-color; font-size: @font-size-input-lg; line-height: 1.5; background-color: white; @@ -48,6 +49,10 @@ cursor: not-allowed; } + &&--empty { + color: @text-placeholder-color; + } + &&--disabled &__icon { color: #aaaaaa !important; } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx index ca97b252..eb647f3a 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx @@ -1,4 +1,5 @@ import KFIcon from '@/components/KFIcon'; +import { isEmpty } from '@/utils'; import { Popconfirm, Typography } from 'antd'; import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; @@ -77,15 +78,15 @@ function PopParameterRange({ type, value, onChange }: ParameterRangeProps) {
- {jsonText} + {jsonText ?? '请选择'}
diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less index 7c218f63..5c91d2fc 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -121,6 +121,14 @@ padding: 20px 20px 0; border: 1px dashed #e0e0e0; border-radius: 8px; + + :global { + .ant-form-item-label { + label { + width: calc(100% - 10px); + } + } + } } &__operation { diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 568dbd4a..8c6a35d0 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -15,6 +15,7 @@ export type FormData = { code: ParameterInputObject; // 代码 dataset: ParameterInputObject; // 数据集 model: ParameterInputObject; // 模型 + image: ParameterInputObject; // 镜像 main_py: string; // 主函数代码文件 metric: string; // 指标 mode: string; // 优化方向 diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.less b/react-ui/src/pages/ModelDeployment/CreateVersion/index.less index 0460788f..bf7f7f9d 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.less +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.less @@ -30,7 +30,7 @@ } .ant-btn-variant-text:disabled { - color: rgba(0, 0, 0, 0.25); + color: @text-disabled-color; } .ant-btn-variant-text { diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx index da88b2de..b6562237 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -165,7 +165,7 @@ function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModa [styles['version-compare__left__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} @@ -183,7 +183,7 @@ function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModa [styles['version-compare__right__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index a7740e7b..f2624a58 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,6 +1,10 @@ import KFIcon from '@/components/KFIcon'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterSelect from '@/components/ParameterSelect'; +import ResourceSelectorModal, { + ResourceSelectorType, + selectorTypeConfig, +} from '@/components/ResourceSelectorModal'; import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; @@ -19,10 +23,6 @@ import { NamePath } from 'antd/es/form/interface'; import { forwardRef, useImperativeHandle, useState } from 'react'; import CodeSelectorModal from '../CodeSelectorModal'; import PropsLabel from '../PropsLabel'; -import ResourceSelectorModal, { - ResourceSelectorType, - selectorTypeConfig, -} from '../ResourceSelectorModal'; import styles from './index.less'; const { TextArea } = Input; diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index 758eec31..cf1daced 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -12,6 +12,8 @@ @text-color: #1d1d20; @text-color-secondary: #575757; @text-color-tertiary: #8a8a8a; +@text-placeholder-color: rgba(0, 0, 0, 0.25); +@text-disabled-color: rgba(0, 0, 0, 0.25); @success-color: #6ac21d; @error-color: #c73131; @warning-color: #f98e1b; From f08114ee03c8d2e5ea87840e7ca723dca75a0bf4 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 20 Jan 2025 10:00:43 +0800 Subject: [PATCH 015/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/ruoyi/platform/domain/Ray.java | 5 ++++- .../platform/service/impl/RayServiceImpl.java | 10 ++++++++++ .../main/java/com/ruoyi/platform/vo/RayVo.java | 7 +++++-- .../mapper/managementPlatform/RayDaoMapper.xml | 15 +++++++++------ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index 00ffb645..ce8257ab 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -24,11 +24,14 @@ public class Ray { private String dataset; @ApiModelProperty(value = "数据集挂载路径") - private String datasetPath; + private String model; @ApiModelProperty(value = "代码") private String code; + @ApiModelProperty(value = "镜像") + private String image; + @ApiModelProperty(value = "主函数代码文件") private String mainPy; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 82c983ae..fc7ef4be 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -48,6 +48,8 @@ public class RayServiceImpl implements RayService { ray.setUpdateBy(username); ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); + ray.setModel(JacksonUtil.toJSONString(rayVo.getModel())); + ray.setImage(JacksonUtil.toJSONString(rayVo.getImage())); ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); ray.setPointsToEvaluate(JacksonUtil.toJSONString(rayVo.getPointsToEvaluate())); rayDao.save(ray); @@ -67,6 +69,8 @@ public class RayServiceImpl implements RayService { ray.setPointsToEvaluate(JacksonUtil.toJSONString(rayVo.getPointsToEvaluate())); ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); + ray.setModel(JacksonUtil.toJSONString(rayVo.getModel())); + ray.setImage(JacksonUtil.toJSONString(rayVo.getImage())); rayDao.edit(ray); return "修改成功"; } @@ -92,6 +96,12 @@ public class RayServiceImpl implements RayService { if (StringUtils.isNotEmpty(ray.getCode())) { rayVo.setCode(JsonUtils.jsonToMap(ray.getCode())); } + if (StringUtils.isNotEmpty(ray.getModel())) { + rayVo.setModel(JsonUtils.jsonToMap(ray.getModel())); + } + if (StringUtils.isNotEmpty(ray.getImage())) { + rayVo.setImage(JsonUtils.jsonToMap(ray.getImage())); + } return rayVo; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 8f001f38..6e8963ee 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -74,6 +74,9 @@ public class RayVo { private Map dataset; - @ApiModelProperty(value = "数据集挂载路径") - private String datasetPath; + @ApiModelProperty(value = "模型") + private Map model; + + @ApiModelProperty(value = "镜像") + private Map image; } diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index b8a7818c..a23ba911 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -2,13 +2,13 @@ - insert into ray(name, description, dataset, dataset_path, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, + insert into ray(name, description, dataset, model, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, - min_samples_required, resource, create_by, update_by) - values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.datasetPath}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + min_samples_required, resource, image, create_by, update_by) + values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.model}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, - #{ray.resource}, #{ray.createBy}, #{ray.updateBy}) + #{ray.resource}, #{ray.image}, #{ray.createBy}, #{ray.updateBy}) @@ -23,12 +23,15 @@ dataset = #{ray.dataset}, - - dataset_path = #{ray.datasetPath}, + + model = #{ray.model}, code = #{ray.code}, + + image = #{ray.image}, + main_py = #{ray.mainPy}, From 7e09ff12110801eb99b385c203df776d2cad321d Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 20 Jan 2025 16:04:40 +0800 Subject: [PATCH 016/127] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=95=9C=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/HyperParameterBasic/index.tsx | 7 +++++++ react-ui/src/utils/format.ts | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 88a88800..62f33d6d 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -14,6 +14,7 @@ import { formatDataset, formatDate, formatEnum, + formatMirror, formatModel, } from '@/utils/format'; import { Flex } from 'antd'; @@ -103,6 +104,12 @@ function HyperParameterBasic({ value: info.main_py, ellipsis: true, }, + { + label: '镜像', + value: info.image, + format: formatMirror, + ellipsis: true, + }, { label: '数据集', value: info.dataset, diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index 8e7bbbf4..c540e441 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -1,3 +1,4 @@ +import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { DataSource, @@ -50,6 +51,14 @@ export const formatModel = (model: ModelData) => { }; }; +// 格式化镜像 +export const formatMirror = (mirror: ResourceSelectorResponse) => { + if (!mirror) { + return undefined; + } + return mirror.path; +}; + // 格式化代码配置 export const formatCodeConfig = (project?: ProjectDependency | SelectedCodeConfig) => { if (!project) { From 5b3daa7019638ac4eac3a804142f5902dd8d42e8 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 11 Feb 2025 09:19:28 +0800 Subject: [PATCH 017/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ray/RayInsController.java | 2 +- .../com/ruoyi/platform/domain/RayIns.java | 6 +++ .../ruoyi/platform/service/RayInsService.java | 2 +- .../service/impl/RayInsServiceImpl.java | 43 +++++++++++++++++-- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java index 469ce497..d2c34074 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java @@ -54,7 +54,7 @@ public class RayInsController extends BaseController { @GetMapping("{id}") @ApiOperation("查看实验实例详情") - public GenericsAjaxResult getDetailById(@PathVariable("id") Long id) { + public GenericsAjaxResult getDetailById(@PathVariable("id") Long id) throws IOException { return genericsSuccess(this.rayInsService.getDetailById(id)); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java index 161a5f98..3de2e1a9 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java @@ -1,12 +1,15 @@ package com.ruoyi.platform.domain; +import com.baomidou.mybatisplus.annotation.TableField; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.util.ArrayList; import java.util.Date; +import java.util.Map; @Data @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) @@ -41,5 +44,8 @@ public class RayIns { private Date updateTime; private Date finishTime; + + @TableField(exist = false) + private ArrayList> trialList; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java index 10ea6983..867dd0a9 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java @@ -15,7 +15,7 @@ public interface RayInsService { boolean terminateRayIns(Long id) throws Exception; - RayIns getDetailById(Long id); + RayIns getDetailById(Long id) throws IOException; void updateRayStatus(Long rayId); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index a9a00d30..976a43b5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; +import com.ruoyi.platform.utils.JsonUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -14,9 +15,11 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; @Service("rayInsService") public class RayInsServiceImpl implements RayInsService { @@ -104,11 +107,12 @@ public class RayInsServiceImpl implements RayInsService { } @Override - public RayIns getDetailById(Long id) { + public RayIns getDetailById(Long id) throws IOException { RayIns rayIns = rayInsDao.queryById(id); if (Constant.Running.equals(rayIns.getStatus()) || Constant.Pending.equals(rayIns.getStatus())) { //todo queryStatusFromArgo } + rayIns.setTrialList(getTrialList(rayIns.getResultPath())); return rayIns; } @@ -127,4 +131,35 @@ public class RayInsServiceImpl implements RayInsService { rayDao.edit(ray); } } + + public ArrayList> getTrialList(String directoryPath) throws IOException { + // 获取指定路径下的所有文件 + Path dirPath = Paths.get(directoryPath); + Path experimentState = Files.list(dirPath).filter(path -> Files.isRegularFile(path) && path.getFileName().toString().startsWith("experiment_state")).collect(Collectors.toList()).get(0); + String content = new String(Files.readAllBytes(experimentState)); + Map result = JsonUtils.jsonToMap(content); + ArrayList trial_data_list = (ArrayList) result.get("trial_data"); + + ArrayList> trialList = new ArrayList<>(); + + for (ArrayList trial_data : trial_data_list) { + Map trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); + Map trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); + + Map trial = new HashMap<>(); + trial.put("trial_id", trial_data_0.get("trial_id")); + trial.put("config", trial_data_0.get("config")); + trial.put("status", trial_data_0.get("status")); + + Map last_result = (Map) trial_data_1.get("last_result"); + Map metric_analysis = (Map) trial_data_1.get("metric_analysis"); + Map time_total_s = (Map) metric_analysis.get("time_total_s"); + + trial.put("training_iteration", last_result.get("training_iteration")); + trial.put("time", time_total_s.get("avg")); + + trialList.add(trial); + } + return trialList; + } } From c61b9bcf0233a208965d4bdc32277e94aeb6c6e3 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 12 Feb 2025 14:49:42 +0800 Subject: [PATCH 018/127] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=20BasicInfo?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ react-ui/.gitignore | 2 ++ react-ui/.nvmrc | 1 + .../components/BasicInfo/BasicInfoItem.tsx | 12 +++++-- .../BasicInfo/BasicInfoItemValue.tsx | 19 ++++++----- react-ui/src/components/BasicInfo/index.less | 6 ++++ react-ui/src/components/BasicInfo/index.tsx | 22 +++++++++++-- .../src/components/BasicTableInfo/index.tsx | 15 ++++----- .../AutoML/components/AutoMLBasic/index.tsx | 33 ------------------- .../components/ResourceIntro/index.tsx | 9 ----- .../components/HyperParameterBasic/index.tsx | 18 ---------- .../components/VersionBasicInfo/index.tsx | 15 --------- 12 files changed, 57 insertions(+), 97 deletions(-) create mode 100644 react-ui/.nvmrc diff --git a/.gitignore b/.gitignore index 21a12484..5510490a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ mvnw # web **/node_modules + +*storybook.log diff --git a/react-ui/.gitignore b/react-ui/.gitignore index 889039c2..7d3ed414 100644 --- a/react-ui/.gitignore +++ b/react-ui/.gitignore @@ -48,3 +48,5 @@ pnpm-lock.yaml /src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx /src/pages/CodeConfig/components/CodeConfigItem/index.tsx /src/pages/Dataset/components/ResourceItem/index.tsx + +*storybook.log diff --git a/react-ui/.nvmrc b/react-ui/.nvmrc new file mode 100644 index 00000000..8ddbc0c6 --- /dev/null +++ b/react-ui/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx index 776a7f94..871b8fb5 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -4,17 +4,23 @@ * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 */ +import { Typography } from 'antd'; import React from 'react'; import BasicInfoItemValue from './BasicInfoItemValue'; import { type BasicInfoData, type BasicInfoLink } from './types'; type BasicInfoItemProps = { + /** 基础信息 */ data: BasicInfoData; + /** 标题宽度 */ labelWidth: number; + /** 自定义类名前缀 */ classPrefix: string; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; }; -function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { +function BasicInfoItem({ data, labelWidth, classPrefix, labelEllipsis }: BasicInfoItemProps) { const { label, value, format, ellipsis } = data; const formatValue = format ? format(value) : value; const myClassName = `${classPrefix}__item`; @@ -54,7 +60,9 @@ function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { return (
- {label} + + {label} +
{valueComponent}
diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx index eca6c80c..7f36a13f 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -4,23 +4,24 @@ * @Description: 用于 BasicInfoItem 的组件 */ -import { Link } from '@umijs/max'; +import { isEmpty } from '@/utils'; import { Typography } from 'antd'; -import React from 'react'; +import { Link } from 'react-router-dom'; type BasicInfoItemValueProps = { + /** 值是否显示省略号 */ ellipsis?: boolean; + /** 自定义类名前缀 */ classPrefix: string; - value: string | React.ReactNode; + /** 值 */ + value?: string; + /** 内部链接 */ link?: string; + /** 外部链接 */ url?: string; }; function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicInfoItemValueProps) { - if (React.isValidElement(value)) { - return value; - } - const myClassName = `${classPrefix}__item__value`; let component = undefined; if (url && value) { @@ -36,12 +37,12 @@ function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicIn ); } else { - component = {value ?? '--'}; + component = {!isEmpty(value) ? value : '--'}; } return (
- + {component}
diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index e4570868..661938fb 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -20,6 +20,10 @@ text-align: justify; text-align-last: justify; + .ant-typography { + width: 100% !important; + } + &::after { position: absolute; content: ':'; @@ -31,10 +35,12 @@ flex: 1; flex-direction: column; gap: 5px 0; + min-width: 0; } &__value { flex: 1; + min-width: 0; margin-left: 16px; font-size: @font-size-content; line-height: 1.6; diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 5f60b79a..78dc2cfc 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -5,14 +5,29 @@ import './index.less'; import type { BasicInfoData, BasicInfoLink } from './types'; export type { BasicInfoData, BasicInfoLink }; -type BasicInfoProps = { +export type BasicInfoProps = { + /** 基础信息 */ datas: BasicInfoData[]; + /** 标题宽度 */ + labelWidth: number; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; - labelWidth: number; }; -export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { +/** + * 基础信息展示组件,用于展示基础信息,一行两列,支持格式化数据 + */ +export default function BasicInfo({ + datas, + className, + labelEllipsis, + style, + labelWidth, +}: BasicInfoProps) { return (
{datas.map((item) => ( @@ -21,6 +36,7 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic data={item} labelWidth={labelWidth} classPrefix="kf-basic-info" + labelEllipsis={labelEllipsis} /> ))}
diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index f24f3dc9..fab761e2 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,22 +1,20 @@ import classNames from 'classnames'; +import { BasicInfoProps } from '../BasicInfo'; import BasicInfoItem from '../BasicInfo/BasicInfoItem'; import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; export type { BasicInfoData, BasicInfoLink }; -type BasicTableInfoProps = { - datas: BasicInfoData[]; - className?: string; - style?: React.CSSProperties; - labelWidth: number; -}; - +/** + * 表格基础信息展示组件,用于展示基础信息,一行四列,支持格式化数据 + */ export default function BasicTableInfo({ datas, className, style, labelWidth, -}: BasicTableInfoProps) { + labelEllipsis, +}: BasicInfoProps) { const remainder = datas.length % 4; const array = []; if (remainder > 0) { @@ -36,6 +34,7 @@ export default function BasicTableInfo({ key={`${item.label}-${index}`} data={item} labelWidth={labelWidth} + labelEllipsis={labelEllipsis} classPrefix="kf-basic-table-info" /> ))} diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index bb30e064..6a4ceaa7 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -46,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '实验名称', value: info.ml_name, - ellipsis: true, }, { label: '实验描述', value: info.ml_description, - ellipsis: true, }, { label: '创建人', value: info.create_by, - ellipsis: true, }, { label: '创建时间', value: info.create_time, - ellipsis: true, format: formatDate, }, { label: '更新时间', value: info.update_time, - ellipsis: true, format: formatDate, }, ]; @@ -81,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '任务类型', value: info.task_type, - ellipsis: true, format: formatEnum(autoMLTaskTypeOptions), }, { label: '特征预处理算法', value: info.include_feature_preprocessor, - ellipsis: true, }, { label: '排除的特征预处理算法', value: info.exclude_feature_preprocessor, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', @@ -100,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.include_regressor : info.include_classifier, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', @@ -108,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.exclude_regressor : info.exclude_classifier, - ellipsis: true, }, { label: '集成方式', value: info.ensemble_class, - ellipsis: true, format: formatEnum(autoMLEnsembleClassOptions), }, { label: '集成模型数量', value: info.ensemble_size, - ellipsis: true, }, { label: '集成最佳模型数量', value: info.ensemble_nbest, - ellipsis: true, }, { label: '最大数量', value: info.max_models_on_disc, - ellipsis: true, }, { label: '内存限制(MB)', value: info.memory_limit, - ellipsis: true, }, { label: '单次时间限制(秒)', value: info.per_run_time_limit, - ellipsis: true, }, { label: '搜索时间限制(秒)', value: info.time_left_for_this_task, - ellipsis: true, }, { label: '重采样策略', value: info.resampling_strategy, - ellipsis: true, }, { label: '交叉验证折数', value: info.folds, - ellipsis: true, }, { label: '是否打乱', value: info.shuffle, - ellipsis: true, format: formatBoolean, }, { label: '训练集比率', value: info.train_size, - ellipsis: true, }, { label: '测试集比率', value: info.test_size, - ellipsis: true, }, { label: '计算指标', value: info.scoring_functions, - ellipsis: true, }, { label: '随机种子', value: info.seed, - ellipsis: true, }, - { label: '数据集', value: info.dataset, - ellipsis: true, format: formatDataset, }, { label: '预测目标列', value: info.target_columns, - ellipsis: true, }, ]; }, [info]); @@ -205,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '指标名称', value: info.metric_name, - ellipsis: true, }, { label: '优化方向', value: info.greater_is_better, - ellipsis: true, format: formatOptimizeMode, }, { label: '指标权重', value: info.metrics, - ellipsis: true, format: formatMetricsWeight, }, ]; @@ -231,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '启动时间', value: formatDate(runStatus.startedAt), - ellipsis: true, }, { label: '执行时长', value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), - ellipsis: true, }, { label: '状态', @@ -259,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB ), - ellipsis: true, }, ]; }, [runStatus]); diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index 807270a8..10d7d9d2 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -19,50 +19,41 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ { label: '数据集名称', value: data.name, - ellipsis: true, }, { label: '版本', value: data.version, - ellipsis: true, }, { label: '创建人', value: data.create_by, - ellipsis: true, }, { label: '更新时间', value: data.update_time, - ellipsis: true, }, { label: '数据来源', value: data.dataset_source, format: formatSource, - ellipsis: true, }, { label: '训练任务', value: data.train_task, format: formatTrainTask, - ellipsis: true, }, { label: '处理代码', value: data.processing_code, format: formatCodeConfig, - ellipsis: true, }, { label: '数据集分类', value: data.data_type, - ellipsis: true, }, { label: '研究方向', value: data.data_tag, - ellipsis: true, }, ]; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 62f33d6d..dc767ee8 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -61,28 +61,23 @@ function HyperParameterBasic({ { label: '实验名称', value: info.name, - ellipsis: true, }, { label: '实验描述', value: info.description, - ellipsis: true, }, { label: '创建人', value: info.create_by, - ellipsis: true, }, { label: '创建时间', value: info.create_time, - ellipsis: true, format: formatDate, }, { label: '更新时间', value: info.update_time, - ellipsis: true, format: formatDate, }, ]; @@ -96,75 +91,62 @@ function HyperParameterBasic({ { label: '代码', value: info.code, - ellipsis: true, format: formatCodeConfig, }, { label: '主函数代码文件', value: info.main_py, - ellipsis: true, }, { label: '镜像', value: info.image, format: formatMirror, - ellipsis: true, }, { label: '数据集', value: info.dataset, - ellipsis: true, format: formatDataset, }, { label: '模型', value: info.model, - ellipsis: true, format: formatModel, }, { label: '总实验次数', value: info.num_samples, - ellipsis: true, }, { label: '搜索算法', value: info.search_alg, format: formatEnum(searchAlgorithms), - ellipsis: true, }, { label: '调度算法', value: info.scheduler, format: formatEnum(schedulerAlgorithms), - ellipsis: true, }, { label: '单次试验最大时间', value: info.max_t, - ellipsis: true, }, { label: '最小试验数', value: info.min_samples_required, - ellipsis: true, }, { label: '优化方向', value: info.mode, - ellipsis: true, format: formatOptimizeMode, }, { label: '指标', value: info.metric, - ellipsis: true, }, { label: '资源规格', value: info.resource, format: formatResource, - ellipsis: true, }, ]; }, [info]); diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index 28da0127..7b0ec868 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -51,80 +51,65 @@ function VersionBasicInfo({ info }: BasicInfoProps) { { label: '服务名称', value: info?.service_name, - ellipsis: true, }, { label: '版本名称', value: info?.version, - ellipsis: true, }, { label: '代码配置', value: info?.code_config, format: formatCodeConfig, - ellipsis: true, }, { label: '镜像', value: info?.image, - ellipsis: true, }, { label: '状态', value: info?.run_state, format: formatStatus, - ellipsis: true, }, { label: '模型', value: info?.model, format: formatModel, - ellipsis: true, }, { label: '资源规格', value: info?.resource, format: formatResource, - ellipsis: true, }, { label: '挂载路径', value: info?.mount_path, - ellipsis: true, }, { label: 'API URL', value: info?.url, - ellipsis: true, }, - { label: '副本数量', value: info?.replicas, - ellipsis: true, }, { label: '创建时间', value: info?.create_time, format: formatDate, - ellipsis: true, }, { label: '更新时间', value: info?.update_time, format: formatDate, - ellipsis: true, }, { label: '环境变量', value: info?.env_variables, format: formatEnvText, - ellipsis: true, }, { label: '描述', value: info?.description, - ellipsis: true, }, ]; From 47aa7acfaa4c556ee910294903540bd5423e99b5 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 12 Feb 2025 16:12:15 +0800 Subject: [PATCH 019/127] docs: add storybook --- react-ui/.storybook/main.ts | 61 +++ react-ui/.storybook/preview.tsx | 32 ++ react-ui/.storybook/storybook.css | 12 + react-ui/package.json | 18 +- .../src/components/DisabledInput/index.tsx | 3 + react-ui/src/stories/BasicInfo.stories.tsx | 86 +++++ .../src/stories/BasicTableInfo.stories.tsx | 85 ++++ .../src/stories/example/Button.stories.ts | 53 +++ react-ui/src/stories/example/Button.tsx | 37 ++ react-ui/src/stories/example/Configure.mdx | 364 ++++++++++++++++++ .../src/stories/example/Header.stories.ts | 33 ++ react-ui/src/stories/example/Header.tsx | 56 +++ react-ui/src/stories/example/Page.stories.ts | 32 ++ react-ui/src/stories/example/Page.tsx | 73 ++++ .../stories/example/assets/accessibility.png | Bin 0 -> 42336 bytes .../stories/example/assets/accessibility.svg | 1 + .../stories/example/assets/addon-library.png | Bin 0 -> 467366 bytes .../src/stories/example/assets/assets.png | Bin 0 -> 3899 bytes .../example/assets/avif-test-image.avif | Bin 0 -> 829 bytes .../src/stories/example/assets/context.png | Bin 0 -> 6119 bytes .../src/stories/example/assets/discord.svg | 1 + react-ui/src/stories/example/assets/docs.png | Bin 0 -> 27875 bytes .../stories/example/assets/figma-plugin.png | Bin 0 -> 44246 bytes .../src/stories/example/assets/github.svg | 1 + react-ui/src/stories/example/assets/share.png | Bin 0 -> 40767 bytes .../src/stories/example/assets/styling.png | Bin 0 -> 7237 bytes .../src/stories/example/assets/testing.png | Bin 0 -> 49313 bytes .../src/stories/example/assets/theming.png | Bin 0 -> 44374 bytes .../src/stories/example/assets/tutorials.svg | 1 + .../src/stories/example/assets/youtube.svg | 1 + react-ui/src/stories/example/button.css | 30 ++ react-ui/src/stories/example/header.css | 32 ++ react-ui/src/stories/example/page.css | 68 ++++ 33 files changed, 1079 insertions(+), 1 deletion(-) create mode 100644 react-ui/.storybook/main.ts create mode 100644 react-ui/.storybook/preview.tsx create mode 100644 react-ui/.storybook/storybook.css create mode 100644 react-ui/src/stories/BasicInfo.stories.tsx create mode 100644 react-ui/src/stories/BasicTableInfo.stories.tsx create mode 100644 react-ui/src/stories/example/Button.stories.ts create mode 100644 react-ui/src/stories/example/Button.tsx create mode 100644 react-ui/src/stories/example/Configure.mdx create mode 100644 react-ui/src/stories/example/Header.stories.ts create mode 100644 react-ui/src/stories/example/Header.tsx create mode 100644 react-ui/src/stories/example/Page.stories.ts create mode 100644 react-ui/src/stories/example/Page.tsx create mode 100644 react-ui/src/stories/example/assets/accessibility.png create mode 100644 react-ui/src/stories/example/assets/accessibility.svg create mode 100644 react-ui/src/stories/example/assets/addon-library.png create mode 100644 react-ui/src/stories/example/assets/assets.png create mode 100644 react-ui/src/stories/example/assets/avif-test-image.avif create mode 100644 react-ui/src/stories/example/assets/context.png create mode 100644 react-ui/src/stories/example/assets/discord.svg create mode 100644 react-ui/src/stories/example/assets/docs.png create mode 100644 react-ui/src/stories/example/assets/figma-plugin.png create mode 100644 react-ui/src/stories/example/assets/github.svg create mode 100644 react-ui/src/stories/example/assets/share.png create mode 100644 react-ui/src/stories/example/assets/styling.png create mode 100644 react-ui/src/stories/example/assets/testing.png create mode 100644 react-ui/src/stories/example/assets/theming.png create mode 100644 react-ui/src/stories/example/assets/tutorials.svg create mode 100644 react-ui/src/stories/example/assets/youtube.svg create mode 100644 react-ui/src/stories/example/button.css create mode 100644 react-ui/src/stories/example/header.css create mode 100644 react-ui/src/stories/example/page.css diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts new file mode 100644 index 00000000..551aa73b --- /dev/null +++ b/react-ui/.storybook/main.ts @@ -0,0 +1,61 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import path from 'path'; +import webpack from 'webpack'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-onboarding', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + '@storybook/addon-styling-webpack', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.alias = { + ...config.resolve.alias, + '@': path.resolve(__dirname, '../src'), + // '@@': path.resolve(__dirname, '../src/.umi'), + // '@umijs/max': + // '/Users/cp3hnu/Documents/company/ci4sManagement-cloud/react-ui/node_modules/umi', + }; + } + if (config.module && config.module.rules) { + config.module.rules.push({ + test: /\.less$/, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + }); + } + if (config.plugins) { + config.plugins.push( + new webpack.ProvidePlugin({ + React: 'react', // 全局注入 React + }), + ); + } + + return config; + }, +}; +export default config; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx new file mode 100644 index 00000000..9e8c4b60 --- /dev/null +++ b/react-ui/.storybook/preview.tsx @@ -0,0 +1,32 @@ +import '@/global.less'; +import '@/overrides.less'; +import type { Preview } from '@storybook/react'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import './storybook.css'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + decorators: [ + (Story) => ( + + + + + + + + ), + ], +}; + +export default preview; diff --git a/react-ui/.storybook/storybook.css b/react-ui/.storybook/storybook.css new file mode 100644 index 00000000..0084b0f8 --- /dev/null +++ b/react-ui/.storybook/storybook.css @@ -0,0 +1,12 @@ +html, +body, +#root { + min-width: unset; + height: 100%; + margin: 0; + padding: 0; + overflow-y: visible; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; +} diff --git a/react-ui/package.json b/react-ui/package.json index dc5be1c5..9e28858d 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -39,7 +39,9 @@ "test": "jest", "test:coverage": "npm run jest -- --coverage", "test:update": "npm run jest -- -u", - "tsc": "tsc --noEmit" + "tsc": "tsc --noEmit", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", @@ -83,6 +85,16 @@ }, "devDependencies": { "@ant-design/pro-cli": "^3.1.0", + "@chromatic-com/storybook": "~3.2.4", + "@storybook/addon-essentials": "~8.5.3", + "@storybook/addon-interactions": "~8.5.3", + "@storybook/addon-onboarding": "~8.5.3", + "@storybook/addon-styling-webpack": "~1.0.1", + "@storybook/addon-webpack5-compiler-swc": "~2.0.0", + "@storybook/blocks": "~8.5.3", + "@storybook/react": "~8.5.3", + "@storybook/react-webpack5": "~8.5.3", + "@storybook/test": "~8.5.3", "@testing-library/react": "^14.0.0", "@types/antd": "^1.0.0", "@types/express": "^4.17.14", @@ -96,14 +108,18 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "less": "~4.2.2", + "less-loader": "~12.2.0", "lint-staged": "^13.2.0", "mockjs": "^1.1.0", "prettier": "^2.8.1", + "storybook": "~8.5.3", "swagger-ui-dist": "^4.18.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", diff --git a/react-ui/src/components/DisabledInput/index.tsx b/react-ui/src/components/DisabledInput/index.tsx index c951324e..3a31def8 100644 --- a/react-ui/src/components/DisabledInput/index.tsx +++ b/react-ui/src/components/DisabledInput/index.tsx @@ -6,6 +6,9 @@ type DisabledInputProps = { valuePropName?: string; }; +/** + * 模拟禁用的输入框,但是完全显示内容 + */ function DisabledInput({ value, valuePropName }: DisabledInputProps) { const data = valuePropName ? value[valuePropName] : value; return ( diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx new file mode 100644 index 00000000..f015096c --- /dev/null +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -0,0 +1,86 @@ +import BasicInfo from '@/components/BasicInfo'; +import { formatDate } from '@/utils/date'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; + +const formatList = (value: string[] | null | undefined): string => { + if ( + value === undefined || + value === null || + Array.isArray(value) === false || + value.length === 0 + ) { + return '--'; + } + return value.join(','); +}; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicInfo', + component: BasicInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + datas: [ + { label: '服务名称', value: '手写体识别' }, + { + label: '无数据', + value: '', + }, + { + label: '外部链接', + value: 'https://www.baidu.com/', + format: (value: string) => { + return { + value: '百度', + url: value, + }; + }, + }, + { + label: '内部链接', + value: 'https://www.baidu.com/', + format: () => { + return { + value: '实验', + link: '/pipeline/experiment/instance/1/1', + }; + }, + }, + { label: '日期', value: new Date(), format: formatDate }, + { label: '数组', value: ['a', 'b'], format: formatList }, + { + label: '带省略号', + value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', + }, + + { + label: '自定义组件', + value: ( + + ), + }, + ], + labelWidth: 100, + }, +}; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx new file mode 100644 index 00000000..36eb13ae --- /dev/null +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -0,0 +1,85 @@ +import BasicTableInfo from '@/components/BasicTableInfo'; +import { formatDate } from '@/utils/date'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; + +const formatList = (value: string[] | null | undefined): string => { + if ( + value === undefined || + value === null || + Array.isArray(value) === false || + value.length === 0 + ) { + return '--'; + } + return value.join(','); +}; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicTableInfo', + component: BasicTableInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + datas: [ + { label: '服务名称', value: '手写体识别' }, + { + label: '无数据', + value: '', + }, + { + label: '外部链接', + value: 'https://www.baidu.com/', + format: (value: string) => { + return { + value: '百度', + url: value, + }; + }, + }, + { + label: '内部链接', + value: 'https://www.baidu.com/', + format: () => { + return { + value: '实验', + link: '/pipeline/experiment/instance/1/1', + }; + }, + }, + { label: '日期', value: new Date(), format: formatDate }, + { label: '数组', value: ['a', 'b'], format: formatList }, + { + label: '带省略号', + value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', + }, + { + label: '自定义组件', + value: ( +
+ +
+ ), + }, + ], + labelWidth: 70, + }, +}; diff --git a/react-ui/src/stories/example/Button.stories.ts b/react-ui/src/stories/example/Button.stories.ts new file mode 100644 index 00000000..2a05e01b --- /dev/null +++ b/react-ui/src/stories/example/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/react-ui/src/stories/example/Button.tsx b/react-ui/src/stories/example/Button.tsx new file mode 100644 index 00000000..f35dafdc --- /dev/null +++ b/react-ui/src/stories/example/Button.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import './button.css'; + +export interface ButtonProps { + /** Is this the principal call to action on the page? */ + primary?: boolean; + /** What background color to use */ + backgroundColor?: string; + /** How large should the button be? */ + size?: 'small' | 'medium' | 'large'; + /** Button contents */ + label: string; + /** Optional click handler */ + onClick?: () => void; +} + +/** Primary UI component for user interaction */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/react-ui/src/stories/example/Configure.mdx b/react-ui/src/stories/example/Configure.mdx new file mode 100644 index 00000000..6a537304 --- /dev/null +++ b/react-ui/src/stories/example/Configure.mdx @@ -0,0 +1,364 @@ +import { Meta } from "@storybook/blocks"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/react-ui/src/stories/example/Header.stories.ts b/react-ui/src/stories/example/Header.stories.ts new file mode 100644 index 00000000..80c71d0f --- /dev/null +++ b/react-ui/src/stories/example/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/react-ui/src/stories/example/Header.tsx b/react-ui/src/stories/example/Header.tsx new file mode 100644 index 00000000..1bf981a4 --- /dev/null +++ b/react-ui/src/stories/example/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +export interface HeaderProps { + user?: User; + onLogin?: () => void; + onLogout?: () => void; + onCreateAccount?: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/react-ui/src/stories/example/Page.stories.ts b/react-ui/src/stories/example/Page.stories.ts new file mode 100644 index 00000000..5d2c688a --- /dev/null +++ b/react-ui/src/stories/example/Page.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +const meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on component testing: https://storybook.js.org/docs/writing-tests/component-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/react-ui/src/stories/example/Page.tsx b/react-ui/src/stories/example/Page.tsx new file mode 100644 index 00000000..e1174830 --- /dev/null +++ b/react-ui/src/stories/example/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/react-ui/src/stories/example/assets/accessibility.png b/react-ui/src/stories/example/assets/accessibility.png new file mode 100644 index 0000000000000000000000000000000000000000..6ffe6feabdc17f715771b69fd7f33ec2c57e7c30 GIT binary patch literal 42336 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

V}AC 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 literal 0 HcmV?d00001 diff --git a/react-ui/src/stories/example/assets/accessibility.svg b/react-ui/src/stories/example/assets/accessibility.svg new file mode 100644 index 00000000..107e93f8 --- /dev/null +++ b/react-ui/src/stories/example/assets/accessibility.svg @@ -0,0 +1 @@ +Accessibility \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/addon-library.png b/react-ui/src/stories/example/assets/addon-library.png new file mode 100644 index 0000000000000000000000000000000000000000..95deb38a88de416671e20ebc28dcd397d6910331 GIT binary patch literal 467366 zcmY(pWmp?s)HMtgDJ}^PEgqy$oZ#*dBv8CiifeEvP~4?JC>q?oSkYp^OL2<3Lvbip z;NyOt_p|vk*U33ES$mze_MYralqN(GABPGD1qB6PSxHVC1%(8Lf`UEw^2PHf)^0V7 zC@3#bG}UzEpNErsw;Q#}zkk#YE&j33oH^|^Lk{&lH4%PQ3E|}vx9E_dW8&V(%VcAh zni!trNf;+20xph?pJd2@zAXQq+knq5-F5T56V8ko&mNSJG5*<3P`N#3S3X_%^n5(u zAH<=%bo97)^FTpCfnIh%&%iKW^K{v+{8)`bM$O3_`t;N(#`(&ZgM;Hue48Al^7Wj~ zPa$yNTs}Zh@=4HP%p&7y3^iRwLsV3BIN?b*T~Td&Z}V7EY3XGkDGpM+j`s{dV0K@;FN>bu025@kbl~*-ww1qejk0yBOozi z`gcU@K^pUEdgCdrwS4aDG6whK1UC}ptwZ1A=½zy!j8k^D_+Xmby+9Nl|wWphp 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

    wyQuQbqeDC?b4FVon9>0iW}N&#hIJCuHi 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

    dk-)duz~D_SQhJOz!_4iA-bxFdQMD2V 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%0D8UB5C69Q~b=MLW-x{a!p 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


    61#osMoUDN;g&I5i2>jx!~^YnweY`Mk-ISn1mks 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 \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/docs.png b/react-ui/src/stories/example/assets/docs.png new file mode 100644 index 0000000000000000000000000000000000000000..a749629df9ef3b4e20b86ab1a2482040df3ce8dd GIT binary patch literal 27875 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

    hxL-UTbgFOs%C>cNb?R-MvpJYh!+yR< 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 TCS{<%00000NkvXXu0mjfa^c^P literal 0 HcmV?d00001 diff --git a/react-ui/src/stories/example/assets/figma-plugin.png b/react-ui/src/stories/example/assets/figma-plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..8f79b08cdf28f568a9bfc9d2fee39bd85d4831e3 GIT binary patch literal 44246 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 isSD^m{OQ5>%G^J3Sw0|FW7|9c0000 \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/share.png b/react-ui/src/stories/example/assets/share.png new file mode 100644 index 0000000000000000000000000000000000000000..8097a370777a782bbe52082a338584d04e62e796 GIT binary patch literal 40767 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 MsOkXq$~KYz50#aYqW}N^ literal 0 HcmV?d00001 diff --git a/react-ui/src/stories/example/assets/testing.png b/react-ui/src/stories/example/assets/testing.png new file mode 100644 index 0000000000000000000000000000000000000000..d4ac39a0ced924068b7625735c9bccac7b17f45d GIT binary patch literal 49313 zcmbqYWm6nXvn5Ed0Kt863mQm*F0R2LI4r?~yDjeS3GVLhun^qcZE;vE*e((R+~@rb z_e)px>8dl`GiRixqtsO7aIh$_kdTmY6y&8fkdRQoNJz*S7%2ZCc<=2#{pZlDDSeaq z&)q#f-#t3pJ3jl*?i`)&9G-1&U;YQ~A6@*1Y#*HL9-X|tzW%v=xVX80ettf`zB{`@ zSoVtSAD_QUBb{BspPrsh{@lVYZVnF*udlC< JACFEikIt@6Pfrg{{#;&Oa(xoW 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�B6};G&Xp9}-Hnlio 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

    ; } else if (Array.isArray(formatValue)) { valueComponent = (
    @@ -59,8 +67,14 @@ function BasicInfoItem({ data, labelWidth, classPrefix, labelEllipsis }: BasicIn } return (
    -
    - +
    + {label}
    diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx index 7f36a13f..8f81e92c 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -21,7 +21,13 @@ type BasicInfoItemValueProps = { url?: string; }; -function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicInfoItemValueProps) { +function BasicInfoItemValue({ + value, + link, + url, + classPrefix, + ellipsis = true, +}: BasicInfoItemValueProps) { const myClassName = `${classPrefix}__item__value`; let component = undefined; if (url && value) { diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index 661938fb..ced2d2ba 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -17,12 +17,6 @@ color: @text-color-secondary; font-size: @font-size-content; line-height: 1.6; - text-align: justify; - text-align-last: justify; - - .ant-typography { - width: 100% !important; - } &::after { position: absolute; @@ -55,5 +49,29 @@ text-underline-offset: 3px; } } + + &__node { + flex: 1; + min-width: 0; + margin-left: 16px; + font-size: @font-size-content; + line-height: 1.6; + word-break: break-all; + } + } +} + +.kf-basic-info--three-columns { + width: 100%; + + .kf-basic-info__item { + width: calc((100% - 80px) / 3); + + &__label { + font-size: @font-size; + } + &__value { + font-size: @font-size; + } } } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 78dc2cfc..11eacfde 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -12,6 +12,10 @@ export type BasicInfoProps = { labelWidth: number; /** 标题是否显示省略号 */ labelEllipsis?: boolean; + /** 是否一行三列 */ + threeColumns?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; /** 自定义类名 */ className?: string; /** 自定义样式 */ @@ -19,17 +23,26 @@ export type BasicInfoProps = { }; /** - * 基础信息展示组件,用于展示基础信息,一行两列,支持格式化数据 + * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 */ export default function BasicInfo({ datas, className, - labelEllipsis, style, labelWidth, + labelEllipsis = true, + threeColumns = false, + labelAlign = 'start', }: BasicInfoProps) { return ( -
    +
    {datas.map((item) => ( ))}
    diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index 850af79c..479fe332 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -34,7 +34,6 @@ &__value { flex: 1; min-width: 0; - margin: 0 !important; padding: 12px 20px 4px; font-size: @font-size; word-break: break-all; @@ -56,5 +55,13 @@ text-underline-offset: 3px; } } + + &__node { + flex: 1; + min-width: 0; + padding: 12px 20px; + font-size: @font-size; + word-break: break-all; + } } } diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index fab761e2..68c350bd 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -6,7 +6,7 @@ import './index.less'; export type { BasicInfoData, BasicInfoLink }; /** - * 表格基础信息展示组件,用于展示基础信息,一行四列,支持格式化数据 + * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 */ export default function BasicTableInfo({ datas, @@ -21,7 +21,7 @@ export default function BasicTableInfo({ for (let i = 0; i < 4 - remainder; i++) { array.push({ label: '', - value: '', + value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined }); } } diff --git a/react-ui/src/components/FullScreenFrame/index.tsx b/react-ui/src/components/FullScreenFrame/index.tsx index a5c3413a..41fae94e 100644 --- a/react-ui/src/components/FullScreenFrame/index.tsx +++ b/react-ui/src/components/FullScreenFrame/index.tsx @@ -2,22 +2,27 @@ import classNames from 'classnames'; import './index.less'; type FullScreenFrameProps = { + /** URL */ url: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; - onload?: (e?: React.SyntheticEvent) => void; - onerror?: (e?: React.SyntheticEvent) => void; + /** 加载完成回调 */ + onLoad?: (e?: React.SyntheticEvent) => void; + /** 加载失败回调 */ + onError?: (e?: React.SyntheticEvent) => void; }; -function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) { +function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { return (
    {url && ( )}
    diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index 9cce1a3b..c85afffb 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -66,7 +66,7 @@ function IframePage({ type, className, style }: IframePageProps) { return (
    {loading && createPortal(, document.body)} - +
    ); } diff --git a/react-ui/src/components/InfoGroup/index.tsx b/react-ui/src/components/InfoGroup/index.tsx index 8cf4b4e8..01e8c01b 100644 --- a/react-ui/src/components/InfoGroup/index.tsx +++ b/react-ui/src/components/InfoGroup/index.tsx @@ -3,14 +3,23 @@ import InfoGroupTitle from '../InfoGroupTitle'; import './index.less'; type InfoGroupProps = { + /** 标题 */ title: string; - height?: string | number; // 如果要纵向滚动,需要设置高度 - width?: string | number; // 如果要横向滚动,需要设置宽度 + /** 高度, 如果要纵向滚动,需要设置高度 */ + height?: string | number; + /** 宽度, 如果要横向滚动,需要设置宽度 */ + width?: string | number; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 子元素 */ children?: React.ReactNode; }; +/** + * 信息组,用于展示基本信息,支持横向、纵向滚动。自动机器学习、超参数寻优都是使用这个组件 + */ function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { const contentStyle: React.CSSProperties = {}; if (height) { diff --git a/react-ui/src/components/InfoGroupTitle/index.less b/react-ui/src/components/InfoGroupTitle/index.less index ee3c9011..26ed375a 100644 --- a/react-ui/src/components/InfoGroupTitle/index.less +++ b/react-ui/src/components/InfoGroupTitle/index.less @@ -1,7 +1,8 @@ .kf-info-group-title { + box-sizing: border-box; width: 100%; height: 56px; - padding-left: @content-padding; + padding: 0 @content-padding; background: linear-gradient( 179.03deg, rgba(199, 223, 255, 0.12) 0%, @@ -21,6 +22,7 @@ color: @text-color; font-weight: 500; font-size: @font-size-title; + .singleLine(); &::after { position: absolute; diff --git a/react-ui/src/components/InfoGroupTitle/index.tsx b/react-ui/src/components/InfoGroupTitle/index.tsx index 7eec2ab8..7524a9f0 100644 --- a/react-ui/src/components/InfoGroupTitle/index.tsx +++ b/react-ui/src/components/InfoGroupTitle/index.tsx @@ -3,11 +3,17 @@ import classNames from 'classnames'; import './index.less'; type InfoGroupTitleProps = { + /** 标题 */ title: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; +/** + * 信息组标题 + */ function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { return ( diff --git a/react-ui/src/components/KFEmpty/index.less b/react-ui/src/components/KFEmpty/index.less index 39f70281..3e5a85b4 100644 --- a/react-ui/src/components/KFEmpty/index.less +++ b/react-ui/src/components/KFEmpty/index.less @@ -33,7 +33,7 @@ margin-top: 20px; margin-bottom: 30px; - &__back-btn { + &__button { height: 32px; } } diff --git a/react-ui/src/components/KFEmpty/index.tsx b/react-ui/src/components/KFEmpty/index.tsx index e59b6f6b..b2a680f6 100644 --- a/react-ui/src/components/KFEmpty/index.tsx +++ b/react-ui/src/components/KFEmpty/index.tsx @@ -9,15 +9,24 @@ export enum EmptyType { } type EmptyProps = { - className?: string; - style?: React.CSSProperties; + /** 类型 */ type: EmptyType; + /** 标题 */ title?: string; + /** 内容 */ content?: string; + /** 是否有页脚,如果有默认是一个按钮 */ hasFooter?: boolean; - footer?: () => React.ReactNode; + /** 按钮标题,默认是"刷新" */ buttonTitle?: string; - onRefresh?: () => void; + /** 按钮点击回调 */ + onButtonClick?: () => void; + /** 自定义页脚内容 */ + footer?: () => React.ReactNode; + /** 自定义类名 */ + className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; }; function getEmptyImage(type: EmptyType) { @@ -31,6 +40,7 @@ function getEmptyImage(type: EmptyType) { } } +/** 空状态 */ function KFEmpty({ className, style, @@ -40,7 +50,7 @@ function KFEmpty({ hasFooter = true, footer, buttonTitle = '刷新', - onRefresh, + onButtonClick, }: EmptyProps) { const image = getEmptyImage(type); @@ -54,7 +64,7 @@ function KFEmpty({ {footer ? ( footer() ) : ( - )} diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index d84257a7..38d3644c 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ type IconFontProps = Parameters[0]; interface KFIconProps extends IconFontProps { + /** 图标 */ type: string; + /** 字体大小 */ font?: number; + /** 字体颜色 */ color?: string; - style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; } -function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { +/** 封装 iconfont 图标 */ +function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) { const iconStyle = { ...style, fontSize: font, diff --git a/react-ui/src/components/ModalTitle/index.less b/react-ui/src/components/KFModal/KFModalTitle.less similarity index 100% rename from react-ui/src/components/ModalTitle/index.less rename to react-ui/src/components/KFModal/KFModalTitle.less diff --git a/react-ui/src/components/ModalTitle/index.tsx b/react-ui/src/components/KFModal/KFModalTitle.tsx similarity index 84% rename from react-ui/src/components/ModalTitle/index.tsx rename to react-ui/src/components/KFModal/KFModalTitle.tsx index 4c0179eb..d2ec3265 100644 --- a/react-ui/src/components/ModalTitle/index.tsx +++ b/react-ui/src/components/KFModal/KFModalTitle.tsx @@ -6,12 +6,16 @@ import classNames from 'classnames'; import React from 'react'; -import './index.less'; +import './KFModalTitle.less'; type ModalTitleProps = { + /** 标题 */ title: React.ReactNode; + /** 图片 */ image?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; }; diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index c073ab27..4cb6a9c3 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -4,19 +4,21 @@ * @Description: 自定义 Modal */ -import ModalTitle from '@/components/ModalTitle'; import { Modal, type ModalProps } from 'antd'; import classNames from 'classnames'; +import KFModalTitle from './KFModalTitle'; import './index.less'; export interface KFModalProps extends ModalProps { image?: string; } + +/** 自定义 Modal */ function KFModal({ title, image, children, - className = '', + className, centered, maskClosable, ...rest @@ -27,7 +29,7 @@ function KFModal({ {...rest} centered={centered ?? true} maskClosable={maskClosable ?? false} - title={} + title={} > {children} diff --git a/react-ui/src/components/KFRadio/index.tsx b/react-ui/src/components/KFRadio/index.tsx index 4bb4ccce..7191dae6 100644 --- a/react-ui/src/components/KFRadio/index.tsx +++ b/react-ui/src/components/KFRadio/index.tsx @@ -8,32 +8,40 @@ import classNames from 'classnames'; import './index.less'; export type KFRadioItem = { - key: string; title: string; + value: string; icon?: React.ReactNode; }; type KFRadioProps = { + /** 选项 */ items: KFRadioItem[]; + /** 当前选中项 */ value?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 选中回调 */ onChange?: (value: string) => void; }; +/** + * 自定义 Radio + */ function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { return ( {items.map((item) => { return ( onChange?.(item.key)} + onClick={() => onChange?.(item.value)} > {item.icon} {item.title} diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 519ab6ef..161775b9 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -5,13 +5,14 @@ */ import { Spin, SpinProps } from 'antd'; -import styles from './index.less'; +import './index.less'; +/** 自定义 Spin */ function KFSpin(props: SpinProps) { return ( -
    +
    -
    加载中
    +
    加载中
    ); } diff --git a/react-ui/src/components/LabelValue/index.less b/react-ui/src/components/LabelValue/index.less deleted file mode 100644 index 5f1b9b0c..00000000 --- a/react-ui/src/components/LabelValue/index.less +++ /dev/null @@ -1,19 +0,0 @@ -.kf-label-value { - display: flex; - align-items: flex-start; - font-size: 16px; - line-height: 1.6; - - &__label { - flex: none; - width: 80px; - color: @text-color-secondary; - } - - &__value { - flex: 1; - color: @text-color; - white-space: pre-line; - word-break: break-all; - } -} diff --git a/react-ui/src/components/LabelValue/index.tsx b/react-ui/src/components/LabelValue/index.tsx deleted file mode 100644 index 22b9b3eb..00000000 --- a/react-ui/src/components/LabelValue/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import classNames from 'classnames'; -import './index.less'; - -type labelValueProps = { - label: string; - value?: any; - className?: string; - style?: React.CSSProperties; -}; - -function LabelValue({ label, value, className, style }: labelValueProps) { - return ( -
    -
    {label}
    -
    {value ?? '--'}
    -
    - ); -} - -export default LabelValue; diff --git a/react-ui/src/components/MenuIconSelector/index.less b/react-ui/src/components/MenuIconSelector/index.less index 77529762..5a64a8d3 100644 --- a/react-ui/src/components/MenuIconSelector/index.less +++ b/react-ui/src/components/MenuIconSelector/index.less @@ -1,5 +1,4 @@ .menu-icon-selector { - // grid 布局,每行显示 8 个图标 display: grid; grid-template-columns: repeat(4, 80px); gap: 20px; @@ -10,7 +9,7 @@ display: flex; align-items: center; justify-content: center; - width: 80x; + width: 80px; height: 80px; border: 1px solid transparent; border-radius: 4px; diff --git a/react-ui/src/components/MenuIconSelector/index.tsx b/react-ui/src/components/MenuIconSelector/index.tsx index dd57320e..fa38910b 100644 --- a/react-ui/src/components/MenuIconSelector/index.tsx +++ b/react-ui/src/components/MenuIconSelector/index.tsx @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; import styles from './index.less'; interface MenuIconSelectorProps extends Omit { + /** 选中的图标 */ selectedIcon?: string; + /** 选择回调 */ onOk: (param: string) => void; } @@ -21,6 +23,7 @@ type IconObject = { font_class: string; }; +/** 菜单图标选择器 */ function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { const [icons, setIcons] = useState([]); useEffect(() => { diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx index ca192454..ea8a65de 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -8,10 +8,17 @@ import React from 'react'; import './index.less'; type PageTitleProps = { + /** 标题 */ title: string; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; + +/** + * 页面标题 + */ function PageTitle({ title, style, className = '' }: PageTitleProps) { return (
    diff --git a/react-ui/src/components/RightContent/index.tsx b/react-ui/src/components/RightContent/index.tsx index 3ecb5777..9b84950a 100644 --- a/react-ui/src/components/RightContent/index.tsx +++ b/react-ui/src/components/RightContent/index.tsx @@ -1,9 +1,8 @@ -import { useModel } from '@umijs/max'; -import React from 'react'; -// import KFBreadcrumb from '../KFBreadcrumb'; import KFIcon from '@/components/KFIcon'; import { ProBreadcrumb } from '@ant-design/pro-components'; +import { useModel } from '@umijs/max'; import { Button } from 'antd'; +import React from 'react'; import Avatar from './AvatarDropdown'; import styles from './index.less'; // import { SelectLang } from '@umijs/max'; @@ -44,8 +43,6 @@ const GlobalHeaderRight: React.FC = () => { - {/* */} - {/* */}
    diff --git a/react-ui/src/components/SubAreaTitle/index.tsx b/react-ui/src/components/SubAreaTitle/index.tsx index cd07b206..4c94deee 100644 --- a/react-ui/src/components/SubAreaTitle/index.tsx +++ b/react-ui/src/components/SubAreaTitle/index.tsx @@ -8,13 +8,20 @@ import classNames from 'classnames'; import './index.less'; type SubAreaTitleProps = { + /** 标题 */ title: string; + /** 图片 */ image?: string; - style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; }; -function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { +/** + * 表单或者详情页的分区标题 + */ +function SubAreaTitle({ title, image, className, style }: SubAreaTitleProps) { return (
    {image && ( diff --git a/react-ui/src/pages/404.tsx b/react-ui/src/pages/404.tsx index dbacba53..d6f45451 100644 --- a/react-ui/src/pages/404.tsx +++ b/react-ui/src/pages/404.tsx @@ -12,7 +12,7 @@ const NoFoundPage = () => { content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} hasFooter={true} buttonTitle="返回首页" - onRefresh={() => navigate('/')} + onButtonClick={() => navigate('/')} > ); }; diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 2efef04c..0c484e54 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -197,7 +197,7 @@ function CodeConfigList() { title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )}
    diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 6f2c7523..9577ad41 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -226,7 +226,7 @@ function ResourceList( title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )}
    diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 90ad4d15..1a0b9a18 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -36,13 +36,13 @@ enum ComputingResourceType { const EditorRadioItems: KFRadioItem[] = [ { - key: ComputingResourceType.GPU, title: '英伟达GPU', + value: ComputingResourceType.GPU, icon: , }, { - key: ComputingResourceType.NPU, title: '昇腾NPU', + value: ComputingResourceType.NPU, icon: , }, ]; diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx index c4e89ce2..01a22031 100644 --- a/react-ui/src/pages/Mirror/Create/index.tsx +++ b/react-ui/src/pages/Mirror/Create/index.tsx @@ -30,13 +30,13 @@ type FormData = { const mirrorRadioItems: KFRadioItem[] = [ { - key: CommonTabKeys.Public, title: '基于公网镜像', + value: CommonTabKeys.Public, icon: , }, { - key: CommonTabKeys.Private, title: '本地上传', + value: CommonTabKeys.Private, icon: , }, ]; diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 9cd4ebfb..b4cfb84a 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -401,7 +401,12 @@ function ServiceInfo() { image={require('@/assets/img/mirror-basic.png')} style={{ marginBottom: '26px', flex: 'none' }} > - + ; + return ; } export default VersionBasicInfo; diff --git a/react-ui/src/components/RobotFrame/index.less b/react-ui/src/pages/Workspace/components/RobotFrame/index.less similarity index 100% rename from react-ui/src/components/RobotFrame/index.less rename to react-ui/src/pages/Workspace/components/RobotFrame/index.less diff --git a/react-ui/src/components/RobotFrame/index.tsx b/react-ui/src/pages/Workspace/components/RobotFrame/index.tsx similarity index 100% rename from react-ui/src/components/RobotFrame/index.tsx rename to react-ui/src/pages/Workspace/components/RobotFrame/index.tsx diff --git a/react-ui/src/pages/Workspace/index.tsx b/react-ui/src/pages/Workspace/index.tsx index bdef4f83..691ca550 100644 --- a/react-ui/src/pages/Workspace/index.tsx +++ b/react-ui/src/pages/Workspace/index.tsx @@ -1,4 +1,3 @@ -import RobotFrame from '@/components/RobotFrame'; import { useDraggable } from '@/hooks/draggable'; import { getWorkspaceOverviewReq } from '@/services/workspace'; import { ExperimentInstance } from '@/types'; @@ -9,6 +8,7 @@ import AssetsManagement from './components/AssetsManagement'; import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; import ExperitableTable from './components/ExperimentTable'; import QuickStart from './components/QuickStart'; +import RobotFrame from './components/RobotFrame'; import TotalStatistics from './components/TotalStatistics'; import UserSpace from './components/UserSpace'; import WorkspaceIntro from './components/WorkspaceIntro'; diff --git a/react-ui/src/pages/missingPage.jsx b/react-ui/src/pages/missingPage.jsx index e8e034a4..9b3e0323 100644 --- a/react-ui/src/pages/missingPage.jsx +++ b/react-ui/src/pages/missingPage.jsx @@ -12,7 +12,7 @@ const MissingPage = () => { content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} hasFooter={true} buttonTitle="返回首页" - onRefresh={() => navigate('/')} + onButtonClick={() => navigate('/')} > ); }; diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index f015096c..e6e71255 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -66,16 +66,32 @@ export const Primary: Story = { }, }, { label: '日期', value: new Date(), format: formatDate }, - { label: '数组', value: ['a', 'b'], format: formatList }, + { label: '数组', value: ['a', 'b', 'c'], format: formatList }, { label: '带省略号', value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', }, - + { + label: '多行', + value: [ + { + label: '服务名称', + value: '手写体识别', + }, + { + label: '服务名称', + value: '人脸识别', + }, + ], + format: (value: any) => + value.map((item: any) => ({ + value: item.label + ':' + item.value, + })), + }, { label: '自定义组件', value: ( - ), diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index 36eb13ae..3ed5f332 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -1,19 +1,6 @@ import BasicTableInfo from '@/components/BasicTableInfo'; -import { formatDate } from '@/utils/date'; import type { Meta, StoryObj } from '@storybook/react'; -import { Button } from 'antd'; - -const formatList = (value: string[] | null | undefined): string => { - if ( - value === undefined || - value === null || - Array.isArray(value) === false || - value.length === 0 - ) { - return '--'; - } - return value.join(','); -}; +import * as BasicInfoStories from './BasicInfo.stories'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -39,47 +26,7 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Primary: Story = { args: { - datas: [ - { label: '服务名称', value: '手写体识别' }, - { - label: '无数据', - value: '', - }, - { - label: '外部链接', - value: 'https://www.baidu.com/', - format: (value: string) => { - return { - value: '百度', - url: value, - }; - }, - }, - { - label: '内部链接', - value: 'https://www.baidu.com/', - format: () => { - return { - value: '实验', - link: '/pipeline/experiment/instance/1/1', - }; - }, - }, - { label: '日期', value: new Date(), format: formatDate }, - { label: '数组', value: ['a', 'b'], format: formatList }, - { - label: '带省略号', - value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', - }, - { - label: '自定义组件', - value: ( -
    - -
    - ), - }, - ], + ...BasicInfoStories.Primary.args, labelWidth: 70, }, }; diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx new file mode 100644 index 00000000..6a2748c2 --- /dev/null +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -0,0 +1,31 @@ +import FullScreenFrame from '@/components/FullScreenFrame'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/FullScreenFrame', + component: FullScreenFrame, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + url: 'https://www.hao123.com/', + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/InfoGroup.stories.tsx b/react-ui/src/stories/InfoGroup.stories.tsx new file mode 100644 index 00000000..f3a6def8 --- /dev/null +++ b/react-ui/src/stories/InfoGroup.stories.tsx @@ -0,0 +1,31 @@ +import InfoGroup from '@/components/InfoGroup'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/InfoGroup', + component: InfoGroup, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + children:
    I am a child element
    , + }, +}; diff --git a/react-ui/src/stories/InfoGroupTitle.stories.tsx b/react-ui/src/stories/InfoGroupTitle.stories.tsx new file mode 100644 index 00000000..c37ff07e --- /dev/null +++ b/react-ui/src/stories/InfoGroupTitle.stories.tsx @@ -0,0 +1,30 @@ +import InfoGroupTitle from '@/components/InfoGroupTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/InfoGroupTitle', + component: InfoGroupTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + }, +}; diff --git a/react-ui/src/stories/KFEmpty.stories.tsx b/react-ui/src/stories/KFEmpty.stories.tsx new file mode 100644 index 00000000..f26de4ee --- /dev/null +++ b/react-ui/src/stories/KFEmpty.stories.tsx @@ -0,0 +1,53 @@ +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFEmpty', + component: KFEmpty, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'select', options: Object.values(EmptyType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onButtonClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Developing: Story = { + args: { + type: EmptyType.Developing, + title: '敬请期待~', + content: '很抱歉,您访问的正在开发中,\n请耐心等待。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; + +export const NoData: Story = { + args: { + type: EmptyType.NoData, + title: '暂无数据', + content: '很抱歉,没有搜索到您想要的内容\n建议刷新试试', + hasFooter: true, + }, +}; + +export const NotFound: Story = { + args: { + type: EmptyType.NotFound, + title: '404', + content: '很抱歉,您访问的页面地址有误,\n或者该页面不存在。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; diff --git a/react-ui/src/stories/KFIcon.stories.tsx b/react-ui/src/stories/KFIcon.stories.tsx new file mode 100644 index 00000000..44cb274e --- /dev/null +++ b/react-ui/src/stories/KFIcon.stories.tsx @@ -0,0 +1,41 @@ +import KFIcon from '@/components/KFIcon'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFIcon', + component: KFIcon, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: {}, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: 'icon-xiazai', + }, +}; + +export const InButton: Story = { + args: { + type: 'icon-xiazai', + }, + render: function Render(args) { + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx new file mode 100644 index 00000000..ebebe0b3 --- /dev/null +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -0,0 +1,59 @@ +import CreateExperiment from '@/assets/img/create-experiment.png'; +import KFModal from '@/components/KFModal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFModal', + component: KFModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '创建实验', + image: CreateExperiment, + open: false, + children: '这是一个模态框', + }, + render: function Render(args) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onOk() { + updateArgs({ open: false }); + } + + function onCancel() { + updateArgs({ open: false }); + } + + return ( + <> + + + + ); + }, +}; diff --git a/react-ui/src/stories/KFRadio.stories.tsx b/react-ui/src/stories/KFRadio.stories.tsx new file mode 100644 index 00000000..c48c16e9 --- /dev/null +++ b/react-ui/src/stories/KFRadio.stories.tsx @@ -0,0 +1,52 @@ +import KFIcon from '@/components/KFIcon'; +import KFRadio from '@/components/KFRadio'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFRadio', + component: KFRadio, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + items: [ + { + title: '英伟达GPU', + value: 'GPU', + icon: , + }, + { + title: '昇腾NPU', + value: 'NPU', + icon: , + }, + ], + value: 'GPU', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function onChange(value: string) { + updateArgs({ value: value }); + } + + return ; + }, +}; diff --git a/react-ui/src/stories/KFSpin.stories.tsx b/react-ui/src/stories/KFSpin.stories.tsx new file mode 100644 index 00000000..7f3185d2 --- /dev/null +++ b/react-ui/src/stories/KFSpin.stories.tsx @@ -0,0 +1,35 @@ +import KFSpin from '@/components/KFSpin'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFSpin', + component: KFSpin, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + decorators: [ + (Story) => ( +
    + +
    + ), + ], + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: {}, +}; diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx new file mode 100644 index 00000000..45088835 --- /dev/null +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -0,0 +1,65 @@ +import MenuIconSelector from '@/components/MenuIconSelector'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/MenuIconSelector', + component: MenuIconSelector, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + open: { + control: 'boolean', + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + selectedIcon: 'manual-icon', + open: false, + }, + render: function Render(args) { + const [{ open, selectedIcon }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onOk(value: string) { + updateArgs({ selectedIcon: value, open: false }); + } + + function onCancel() { + updateArgs({ open: false }); + } + + return ( + <> + + + + ); + }, +}; diff --git a/react-ui/src/stories/PageTitle.stories.tsx b/react-ui/src/stories/PageTitle.stories.tsx new file mode 100644 index 00000000..216591b1 --- /dev/null +++ b/react-ui/src/stories/PageTitle.stories.tsx @@ -0,0 +1,30 @@ +import PageTitle from '@/components/PageTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/PageTitle', + component: PageTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '数据集列表', + }, +}; diff --git a/react-ui/src/stories/SubAreaTitle.stories.tsx b/react-ui/src/stories/SubAreaTitle.stories.tsx new file mode 100644 index 00000000..9fad7d71 --- /dev/null +++ b/react-ui/src/stories/SubAreaTitle.stories.tsx @@ -0,0 +1,32 @@ +import MirrorBasic from '@/assets/img/mirror-basic.png'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/SubAreaTitle', + component: SubAreaTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + image: MirrorBasic, + }, +}; From 1c9041707859c6ec98a2f37f7d6355730c4b8475 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 17:02:31 +0800 Subject: [PATCH 031/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index c2cafd9d..c1051396 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -31,10 +31,10 @@ spec: - name: resource-volume hostPath: path: /platform-data - initContainers: - - name: init-fs-check - image: 172.20.32.187/ci4s/busybox:1.31 - command: [ 'sh', '-c', "mount | grep /platform-data"] +# initContainers: +# - name: init-fs-check +# image: 172.20.32.187/ci4s/busybox:1.31 +# command: [ 'sh', '-c', "mount | grep /platform-data"] restartPolicy: Always --- apiVersion: v1 From 23ae5b9de9e84bb9e37d2012a8bb445b18668aba Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 17:21:53 +0800 Subject: [PATCH 032/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 51 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index c1051396..7bc82ab1 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -14,27 +14,38 @@ spec: app: ci4s-management-platform spec: containers: - - name: ci4s-management-platform - image: ${k8s-7management-image} - env: - - name: TZ - value: Asia/Shanghai - - name: JAVA_TOOL_OPTIONS - value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" - ports: - - containerPort: 9213 - volumeMounts: - - name: resource-volume - mountPath: /home/resource/ - subPath: mini-model-platform-data + - name: ci4s-management-platform + image: ${k8s-7management-image} + env: + - name: TZ + value: Asia/Shanghai + - name: JAVA_TOOL_OPTIONS + value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" + ports: + - containerPort: 9213 + volumeMounts: + - name: resource-volume + mountPath: /home/resource/ + subPath: mini-model-platform-data volumes: - - name: resource-volume - hostPath: - path: /platform-data -# initContainers: -# - name: init-fs-check -# image: 172.20.32.187/ci4s/busybox:1.31 -# command: [ 'sh', '-c', "mount | grep /platform-data"] + - name: resource-volume + hostPath: + path: /platform-data + initContainers: + - name: init-fs-check + image: ${k8s-7management-image} + volumeMounts: + - name: resource-volume + mountPath: /home/resource/ + subPath: mini-model-platform-data + command: [ "/bin/sh", "-c" ] + args: + - | + mounted=$(findmnt /platform-data/ | grep 'fuse.juicefs') + if [ -z "$mounted" ]; then + echo "/platform-data not mounted"; + exit 1 + fi restartPolicy: Always --- apiVersion: v1 From 9c45343c37ffa7f7526371290c7b99ff2a2c8b78 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 17 Feb 2025 09:28:18 +0800 Subject: [PATCH 033/127] feat: storybook mock @umijx/max --- react-ui/.gitignore | 7 -- .../babel-plugin-auto-css-modules.js | 16 +++ react-ui/.storybook/main.ts | 109 ++++++++++------ react-ui/.storybook/mock/umijs.tsx | 12 ++ react-ui/.storybook/preview.tsx | 117 ++++++++---------- react-ui/.storybook/tsconfig.json | 26 ++++ react-ui/.storybook/typings.d.ts | 20 +++ react-ui/package.json | 2 + .../BasicInfo/BasicInfoItemValue.tsx | 2 +- .../components/CodeConfigItem/index.less | 0 .../components/CodeConfigItem/index.tsx | 0 react-ui/src/components/CodeSelect/index.tsx | 2 +- .../components/CodeSelectorModal/index.less | 0 .../components/CodeSelectorModal/index.tsx | 0 .../src/components/FullScreenFrame/index.tsx | 3 + react-ui/src/components/IFramePage/index.tsx | 14 ++- .../InfoGroupTitle.less} | 0 .../InfoGroupTitle.tsx} | 2 +- react-ui/src/components/InfoGroup/index.tsx | 2 +- react-ui/src/components/KFModal/index.tsx | 1 + .../components/VersionBasicInfo/index.tsx | 2 +- .../components/PipelineNodeDrawer/index.tsx | 2 +- react-ui/src/stories/IFramePage.stories.tsx | 31 +++++ react-ui/src/stories/KFModal.stories.tsx | 6 + ...tories.tsx => ParameterSelect.stories.tsx} | 10 +- react-ui/src/styles/theme.less | 4 +- react-ui/tsconfig.json | 2 +- 27 files changed, 268 insertions(+), 124 deletions(-) create mode 100644 react-ui/.storybook/babel-plugin-auto-css-modules.js create mode 100644 react-ui/.storybook/mock/umijs.tsx create mode 100644 react-ui/.storybook/tsconfig.json create mode 100644 react-ui/.storybook/typings.d.ts rename react-ui/src/{pages/Pipeline => }/components/CodeConfigItem/index.less (100%) rename react-ui/src/{pages/Pipeline => }/components/CodeConfigItem/index.tsx (100%) rename react-ui/src/{pages/Pipeline => }/components/CodeSelectorModal/index.less (100%) rename react-ui/src/{pages/Pipeline => }/components/CodeSelectorModal/index.tsx (100%) rename react-ui/src/components/{InfoGroupTitle/index.less => InfoGroup/InfoGroupTitle.less} (100%) rename react-ui/src/components/{InfoGroupTitle/index.tsx => InfoGroup/InfoGroupTitle.tsx} (95%) create mode 100644 react-ui/src/stories/IFramePage.stories.tsx rename react-ui/src/stories/{InfoGroupTitle.stories.tsx => ParameterSelect.stories.tsx} (80%) diff --git a/react-ui/.gitignore b/react-ui/.gitignore index 7d3ed414..9d2581cd 100644 --- a/react-ui/.gitignore +++ b/react-ui/.gitignore @@ -41,12 +41,5 @@ screenshot build pnpm-lock.yaml -/src/services/codeConfig/index.js -/src/pages/CodeConfig/components/AddCodeConfigModal/index.less -/src/pages/CodeConfig/List/index.less -/src/pages/Dataset/components/ResourceItem/index.less -/src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx -/src/pages/CodeConfig/components/CodeConfigItem/index.tsx -/src/pages/Dataset/components/ResourceItem/index.tsx *storybook.log diff --git a/react-ui/.storybook/babel-plugin-auto-css-modules.js b/react-ui/.storybook/babel-plugin-auto-css-modules.js new file mode 100644 index 00000000..9c7709ff --- /dev/null +++ b/react-ui/.storybook/babel-plugin-auto-css-modules.js @@ -0,0 +1,16 @@ +export default function(babel) { + const { types: t } = babel; + return { + visitor: { + ImportDeclaration(path) { + const source = path.node.source.value; + // console.log("zzzz", source); + if (source.endsWith('.less')) { + if (path.node.specifiers.length > 0) { + path.node.source.value += "?modules"; + } + } + }, + }, + }; +}; \ No newline at end of file diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index ec302bf7..7db1da1d 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -5,7 +5,8 @@ import webpack from 'webpack'; const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - '@storybook/addon-webpack5-compiler-swc', + // '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-webpack5-compiler-babel', '@storybook/addon-onboarding', '@storybook/addon-essentials', '@chromatic-com/storybook', @@ -20,51 +21,75 @@ const config: StorybookConfig = { config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, '../src'), - // '@@': path.resolve(__dirname, '../src/.umi'), - // '@umijs/max': - // '/Users/cp3hnu/Documents/company/ci4sManagement-cloud/react-ui/node_modules/umi', + '@umijs/max$': path.resolve(__dirname, './mock/umijs.tsx'), }; } if (config.module && config.module.rules) { - config.module.rules.push({ - test: /\.less$/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 1, - import: true, - esModule: true, - modules: { - auto: (resourcePath: string) => { - if ( - resourcePath.endsWith('MenuIconSelector/index.less') || - resourcePath.endsWith('theme.less') - ) { - return true; - } else { - return false; - } + config.module.rules.push( + { + test: /\.less$/, + oneOf: [ + { + resourceQuery: /modules/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + import: true, + esModule: true, + modules: { + localIdentName: '[local]___[hash:base64:5]', + }, + }, }, - localIdentName: '[local]___[hash:base64:5]', - }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, + }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules }, - }, - { - loader: 'less-loader', - options: { - lessOptions: { - javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 - modifyVars: { - hack: 'true; @import "@/styles/theme.less";', + { + use: [ + 'style-loader', + 'css-loader', + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 + modifyVars: { + hack: 'true; @import "@/styles/theme.less";', + }, + }, + }, }, - }, + ], + include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules }, + ], + }, + { + test: /\.(tsx?|jsx?)$/, + loader: 'ts-loader', + options: { + transpileOnly: true, }, - ], - include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules - }); + include: [ + path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules + path.resolve(__dirname, './'), + ], + }, + ); } if (config.plugins) { config.plugins.push( @@ -76,5 +101,13 @@ const config: StorybookConfig = { return config; }, + babel: async (config: any) => { + if (!config.plugins) { + config.plugins = []; + } + + config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js')); + return config; + }, }; export default config; diff --git a/react-ui/.storybook/mock/umijs.tsx b/react-ui/.storybook/mock/umijs.tsx new file mode 100644 index 00000000..a6b04409 --- /dev/null +++ b/react-ui/.storybook/mock/umijs.tsx @@ -0,0 +1,12 @@ +export const Link = ({ to, children, ...props }: any) => ( +
    + {children} + +); + +export const request = () => { + return Promise.resolve({ + success: true, + data: { message: 'Mocked response' }, + }); +}; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index fc1de258..5fc43614 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -4,8 +4,6 @@ import themes from '@/styles/theme.less'; import type { Preview } from '@storybook/react'; import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; -import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; import './storybook.css'; const preview: Preview = { @@ -16,71 +14,66 @@ const preview: Preview = { date: /Date$/i, }, }, - actions: { argTypesRegex: '^on.*' }, }, decorators: [ (Story) => ( - - - - - - - - - + Select: { + singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], + }, + Table: { + headerBg: 'rgba(242, 244, 247, 0.36)', + headerBorderRadius: 4, + rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + }, + Tabs: { + titleFontSize: 16, + }, + Form: { + labelColor: 'rgba(29, 29, 32, 0.8);', + }, + Breadcrumb: { + iconFontSize: parseInt(themes['fontSize']), + linkColor: 'rgba(29, 29, 32, 0.7)', + separatorColor: 'rgba(29, 29, 32, 0.7)', + }, + }, + }} + > + + + + ), ], }; diff --git a/react-ui/.storybook/tsconfig.json b/react-ui/.storybook/tsconfig.json new file mode 100644 index 00000000..601d7708 --- /dev/null +++ b/react-ui/.storybook/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "esnext", // 指定ECMAScript目标版本 + "lib": ["dom", "dom.iterable", "esnext"], // 要包含在编译中的库文件列表 + "allowJs": true, // 允许编译JavaScript文件 + "skipLibCheck": true, // 跳过所有声明文件的类型检查 + "esModuleInterop": true, // 禁用命名空间导入(import * as fs from "fs"),并启用CJS/AMD/UMD样式的导入(import fs from "fs") + "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入 + "strict": true, // 启用所有严格类型检查选项 + "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 + "module": "esnext", // 指定模块代码生成 + "moduleResolution": "bundler", // 使用bundlers样式解析模块 + "isolatedModules": true, // 无条件地为未解析的文件发出导入 + "resolveJsonModule": true, // 包含.json扩展名的模块 + "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) + "jsx": "react-jsx", // 在.tsx文件中支持JSX + "sourceMap": true, // 生成相应的.map文件 + "declaration": true, // 生成相应的.d.ts文件 + "noUnusedLocals": true, // 报告未使用的局部变量错误 + "noUnusedParameters": true, // 报告未使用的参数错误 + "incremental": true, // 通过读写磁盘上的文件来启用增量编译 + "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 + "strictNullChecks": true, // 启用严格的null检查 + "baseUrl": "./" + } +} diff --git a/react-ui/.storybook/typings.d.ts b/react-ui/.storybook/typings.d.ts new file mode 100644 index 00000000..742f70c6 --- /dev/null +++ b/react-ui/.storybook/typings.d.ts @@ -0,0 +1,20 @@ +declare module 'slash2'; +declare module '*.css'; +declare module '*.less'; +declare module '*.scss'; +declare module '*.sass'; +declare module '*.svg'; +declare module '*.png'; +declare module '*.jpg'; +declare module '*.jpeg'; +declare module '*.gif'; +declare module '*.bmp'; +declare module '*.tiff'; +declare module 'omit.js'; +declare module 'numeral'; +declare module '@antv/data-set'; +declare module 'mockjs'; +declare module 'react-fittext'; +declare module 'bizcharts-plugin-slider'; + +declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; diff --git a/react-ui/package.json b/react-ui/package.json index 9e28858d..e69739ad 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -90,6 +90,7 @@ "@storybook/addon-interactions": "~8.5.3", "@storybook/addon-onboarding": "~8.5.3", "@storybook/addon-styling-webpack": "~1.0.1", + "@storybook/addon-webpack5-compiler-babel": "~3.0.5", "@storybook/addon-webpack5-compiler-swc": "~2.0.0", "@storybook/blocks": "~8.5.3", "@storybook/react": "~8.5.3", @@ -121,6 +122,7 @@ "prettier": "^2.8.1", "storybook": "~8.5.3", "swagger-ui-dist": "^4.18.2", + "ts-loader": "~9.5.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", "umi-presets-pro": "^2.0.0" diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx index 8f81e92c..c5a993e4 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -5,8 +5,8 @@ */ import { isEmpty } from '@/utils'; +import { Link } from '@umijs/max'; import { Typography } from 'antd'; -import { Link } from 'react-router-dom'; type BasicInfoItemValueProps = { /** 值是否显示省略号 */ diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less b/react-ui/src/components/CodeConfigItem/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less rename to react-ui/src/components/CodeConfigItem/index.less diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx b/react-ui/src/components/CodeConfigItem/index.tsx similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx rename to react-ui/src/components/CodeConfigItem/index.tsx diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 79401b25..284ae5e7 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -4,8 +4,8 @@ * @Description: 流水线选择代码配置表单 */ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; -import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; diff --git a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less b/react-ui/src/components/CodeSelectorModal/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less rename to react-ui/src/components/CodeSelectorModal/index.less diff --git a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx rename to react-ui/src/components/CodeSelectorModal/index.tsx diff --git a/react-ui/src/components/FullScreenFrame/index.tsx b/react-ui/src/components/FullScreenFrame/index.tsx index 41fae94e..800a727e 100644 --- a/react-ui/src/components/FullScreenFrame/index.tsx +++ b/react-ui/src/components/FullScreenFrame/index.tsx @@ -14,6 +14,9 @@ type FullScreenFrameProps = { onError?: (e?: React.SyntheticEvent) => void; }; +/** + * 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage + */ function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { return (
    diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index c85afffb..060f3bc1 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -12,32 +12,36 @@ export enum IframePageType { DatasetAnnotation = 'DatasetAnnotation', // 数据标注 AppDevelopment = 'AppDevelopment', // 应用开发 DevEnv = 'DevEnv', // 开发环境 - GitLink = 'GitLink', + GitLink = 'GitLink', // git link } const getRequestAPI = (type: IframePageType): (() => Promise) => { switch (type) { - case IframePageType.DatasetAnnotation: + case IframePageType.DatasetAnnotation: // 数据标注 return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; - case IframePageType.AppDevelopment: + case IframePageType.AppDevelopment: // 应用开发 return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); - case IframePageType.DevEnv: + case IframePageType.DevEnv: // 开发环境 return () => Promise.resolve({ code: 200, data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', }); - case IframePageType.GitLink: + case IframePageType.GitLink: // git link return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); } }; type IframePageProps = { + /** 子系统 */ type: IframePageType; + /** 自定义样式类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; +/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ function IframePage({ type, className, style }: IframePageProps) { const [iframeUrl, setIframeUrl] = useState(''); const [loading, setLoading] = useState(false); diff --git a/react-ui/src/components/InfoGroupTitle/index.less b/react-ui/src/components/InfoGroup/InfoGroupTitle.less similarity index 100% rename from react-ui/src/components/InfoGroupTitle/index.less rename to react-ui/src/components/InfoGroup/InfoGroupTitle.less diff --git a/react-ui/src/components/InfoGroupTitle/index.tsx b/react-ui/src/components/InfoGroup/InfoGroupTitle.tsx similarity index 95% rename from react-ui/src/components/InfoGroupTitle/index.tsx rename to react-ui/src/components/InfoGroup/InfoGroupTitle.tsx index 7524a9f0..bde57b3f 100644 --- a/react-ui/src/components/InfoGroupTitle/index.tsx +++ b/react-ui/src/components/InfoGroup/InfoGroupTitle.tsx @@ -1,6 +1,6 @@ import { Flex } from 'antd'; import classNames from 'classnames'; -import './index.less'; +import './InfoGroupTitle.less'; type InfoGroupTitleProps = { /** 标题 */ diff --git a/react-ui/src/components/InfoGroup/index.tsx b/react-ui/src/components/InfoGroup/index.tsx index 01e8c01b..0f2a3b42 100644 --- a/react-ui/src/components/InfoGroup/index.tsx +++ b/react-ui/src/components/InfoGroup/index.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import InfoGroupTitle from '../InfoGroupTitle'; +import InfoGroupTitle from './InfoGroupTitle'; import './index.less'; type InfoGroupProps = { diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index 4cb6a9c3..ec1b7717 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -10,6 +10,7 @@ import KFModalTitle from './KFModalTitle'; import './index.less'; export interface KFModalProps extends ModalProps { + /** 标题图片 */ image?: string; } diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index fde42ea7..4eacca04 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -18,7 +18,7 @@ const formatStatus = (status?: ServiceRunStatus) => { } return ( - + {ModelDeployStatusCell(status)} ); diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index f2624a58..de041c72 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,3 +1,4 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterSelect from '@/components/ParameterSelect'; @@ -21,7 +22,6 @@ import { INode } from '@antv/g6'; import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; import { NamePath } from 'antd/es/form/interface'; import { forwardRef, useImperativeHandle, useState } from 'react'; -import CodeSelectorModal from '../CodeSelectorModal'; import PropsLabel from '../PropsLabel'; import styles from './index.less'; const { TextArea } = Input; diff --git a/react-ui/src/stories/IFramePage.stories.tsx b/react-ui/src/stories/IFramePage.stories.tsx new file mode 100644 index 00000000..3d1714e8 --- /dev/null +++ b/react-ui/src/stories/IFramePage.stories.tsx @@ -0,0 +1,31 @@ +import IFramePage, { IframePageType } from '@/components/IFramePage'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/IFramePage', + component: IFramePage, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'select', options: Object.values(IframePageType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: IframePageType.GitLink, + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index ebebe0b3..455108c7 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -18,6 +18,12 @@ const meta = { // More on argTypes: https://storybook.js.org/docs/api/argtypes argTypes: { // backgroundColor: { control: 'color' }, + title: { + description: '标题', + }, + open: { + description: '对话框是否可见', + }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args args: { onCancel: fn(), onOk: fn() }, diff --git a/react-ui/src/stories/InfoGroupTitle.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx similarity index 80% rename from react-ui/src/stories/InfoGroupTitle.stories.tsx rename to react-ui/src/stories/ParameterSelect.stories.tsx index c37ff07e..4a1907ee 100644 --- a/react-ui/src/stories/InfoGroupTitle.stories.tsx +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -1,10 +1,11 @@ -import InfoGroupTitle from '@/components/InfoGroupTitle'; +import MirrorBasic from '@/assets/img/mirror-basic.png'; +import ParameterSelect from '@/components/ParameterSelect'; import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/InfoGroupTitle', - component: InfoGroupTitle, + title: 'Components/ParameterSelect', + component: ParameterSelect, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout // layout: 'centered', @@ -17,7 +18,7 @@ const meta = { }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args // args: { onClick: fn() }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -26,5 +27,6 @@ type Story = StoryObj; export const Primary: Story = { args: { title: '基本信息', + image: MirrorBasic, }, }; diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index cf1daced..ff7813f9 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -49,7 +49,7 @@ // padding @content-padding: 25px; -// 函数 +// 函数,hex 添加 alpha 值 .addAlpha(@color, @alpha) { @red: red(@color); @green: green(@color); @@ -58,6 +58,7 @@ } // 混合 +// 单行 .singleLine() { overflow: hidden; white-space: nowrap; @@ -65,6 +66,7 @@ word-break: break-all; } +// 多行 .multiLine(@line) { display: -webkit-box; overflow: hidden; diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index 0afa8788..55ce7f74 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -9,7 +9,7 @@ "strict": true, // 启用所有严格类型检查选项 "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 "module": "esnext", // 指定模块代码生成 - "moduleResolution": "node", // 使用Node.js样式解析模块 + "moduleResolution": "bundler", // 使用bundlers样式解析模块 "isolatedModules": true, // 无条件地为未解析的文件发出导入 "resolveJsonModule": true, // 包含.json扩展名的模块 "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) From ab0040191e07b2371aa7c61d6da53bad9adaf40e Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 17 Feb 2025 09:42:47 +0800 Subject: [PATCH 034/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 44 +++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 7bc82ab1..57088d5d 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -14,36 +14,42 @@ spec: app: ci4s-management-platform spec: containers: - - name: ci4s-management-platform - image: ${k8s-7management-image} - env: - - name: TZ - value: Asia/Shanghai - - name: JAVA_TOOL_OPTIONS - value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" - ports: - - containerPort: 9213 - volumeMounts: - - name: resource-volume - mountPath: /home/resource/ - subPath: mini-model-platform-data - volumes: + - name: ci4s-management-platform + image: ${k8s-7management-image} + securityContext: + privileged: true + env: + - name: TZ + value: Asia/Shanghai + - name: JAVA_TOOL_OPTIONS + value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" + ports: + - containerPort: 9213 + volumeMounts: - name: resource-volume - hostPath: - path: /platform-data + mountPath: /home/resource/ + subPath: mini-model-platform-data + mountPropagation: Bidirectional + volumes: + - name: resource-volume + hostPath: + path: /platform-data initContainers: - name: init-fs-check - image: ${k8s-7management-image} + image: 172.20.32.187/ci4s/ci4s-managent:202502141722 + securityContext: + privileged: true volumeMounts: - name: resource-volume mountPath: /home/resource/ subPath: mini-model-platform-data + mountPropagation: Bidirectional command: [ "/bin/sh", "-c" ] args: - | - mounted=$(findmnt /platform-data/ | grep 'fuse.juicefs') + mounted=$(findmnt /home/resource/ | grep 'fuse.juicefs') if [ -z "$mounted" ]; then - echo "/platform-data not mounted"; + echo "/platform-data not mounted"; exit 1 fi restartPolicy: Always From 13560fc65d5921f5dd7894c81c1315077f5069ce Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 17 Feb 2025 10:25:58 +0800 Subject: [PATCH 035/127] =?UTF-8?q?feat:=20=E4=BB=A5=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E5=BC=8F=E6=89=93=E5=BC=80=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/main.ts | 2 +- .../mock/{umijs.tsx => umijs.mock.tsx} | 7 +- react-ui/.storybook/preview.tsx | 1 + react-ui/src/components/CodeSelect/index.tsx | 5 +- .../components/CodeSelectorModal/index.tsx | 8 +- .../src/components/ParameterInput/index.tsx | 2 +- .../ResourceSelectorModal/index.tsx | 11 ++- react-ui/src/stories/CodeSelect.stories.tsx | 28 +++++++ .../src/stories/CodeSelectorModal.stories.tsx | 81 +++++++++++++++++++ react-ui/src/stories/KFModal.stories.tsx | 23 ++++++ 10 files changed, 154 insertions(+), 14 deletions(-) rename react-ui/.storybook/mock/{umijs.tsx => umijs.mock.tsx} (53%) create mode 100644 react-ui/src/stories/CodeSelect.stories.tsx create mode 100644 react-ui/src/stories/CodeSelectorModal.stories.tsx diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index 7db1da1d..2eff8e2c 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -21,7 +21,7 @@ const config: StorybookConfig = { config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, '../src'), - '@umijs/max$': path.resolve(__dirname, './mock/umijs.tsx'), + '@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'), }; } if (config.module && config.module.rules) { diff --git a/react-ui/.storybook/mock/umijs.tsx b/react-ui/.storybook/mock/umijs.mock.tsx similarity index 53% rename from react-ui/.storybook/mock/umijs.tsx rename to react-ui/.storybook/mock/umijs.mock.tsx index a6b04409..66ef1e5c 100644 --- a/react-ui/.storybook/mock/umijs.tsx +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -4,9 +4,6 @@ export const Link = ({ to, children, ...props }: any) => ( ); -export const request = () => { - return Promise.resolve({ - success: true, - data: { message: 'Mocked response' }, - }); +export const request = (url: string, options: any) => { + return fetch(url, options).then((res) => res.json()); }; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index 5fc43614..cbdef6b1 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -20,6 +20,7 @@ const preview: Preview = { { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index 13198a78..6a965fd7 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -4,11 +4,11 @@ * @Description: 选择代码 */ +import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; import { type CodeConfigData } from '@/pages/CodeConfig/List'; import { getCodeConfigListReq } from '@/services/codeConfig'; import { to } from '@/utils/promise'; -import { Icon } from '@umijs/max'; import type { ModalProps, PaginationProps } from 'antd'; import { Empty, Input, Pagination } from 'antd'; import { useEffect, useState } from 'react'; @@ -21,6 +21,7 @@ export interface CodeSelectorModalProps extends Omit { onOk?: (params: CodeConfigData | undefined) => void; } +/** 代码配置选择弹窗 */ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [dataList, setDataList] = useState([]); const [total, setTotal] = useState(0); @@ -90,8 +91,11 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { suffix={null} value={inputText} prefix={ - + } + // prefix={ + // + // } /> {dataList?.length !== 0 ? ( <> diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index aa0c7492..be56c96d 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数输入组件,支持手动输入,选择全局参数,选择数据集/模型/镜像 + * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 */ import { CommonTabKeys } from '@/enums'; diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index 28beedd0..1a62819c 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -4,11 +4,11 @@ * @Description: 选择数据集、模型、镜像 */ +import KFIcon from '@/components/KFIcon'; import KFModal from '@/components/KFModal'; import { CommonTabKeys } from '@/enums'; import { ResourceFileData } from '@/pages/Dataset/config'; import { to } from '@/utils/promise'; -import { Icon } from '@umijs/max'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import { Input, Tabs, Tree } from 'antd'; import React, { useEffect, useMemo, useRef, useState } from 'react'; @@ -267,7 +267,14 @@ function ResourceSelectorModal({ variant="borderless" value={searchText} onChange={(e) => setSearchText(e.target.value)} - prefix={} + prefix={ + + } + // prefix={} /> ; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: {}, +}; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx new file mode 100644 index 00000000..2df1b5fa --- /dev/null +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -0,0 +1,81 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/CodeSelectorModal', + component: CodeSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + title: { + description: '标题', + }, + open: { + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + open: false, + }, + render: function Render(args) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onOk() { + updateArgs({ open: false }); + } + + function onCancel() { + updateArgs({ open: false }); + } + + return ( + <> + + + + ); + }, +}; + +const OpenModalByFunction = () => { + const handleOnChange = () => { + const { close } = openAntdModal(CodeSelectorModal, { + onOk: () => { + close(); + }, + }); + }; + return ( + + ); +}; + +export const OpenInFunction: Story = { + render: () => , +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 455108c7..3efe5e99 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -1,5 +1,6 @@ import CreateExperiment from '@/assets/img/create-experiment.png'; import KFModal from '@/components/KFModal'; +import { openAntdModal } from '@/utils/modal'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; @@ -63,3 +64,25 @@ export const Primary: Story = { ); }, }; + +const OpenModalByFunction = () => { + const handleOnChange = () => { + const { close } = openAntdModal(KFModal, { + title: '创建实验', + image: CreateExperiment, + children: '这是一个模态框', + onOk: () => { + close(); + }, + }); + }; + return ( + + ); +}; + +export const OpenInFunction: Story = { + render: () => , +}; From 315cee4c8bc7cb894b6f66238cf5e7e1123cfb2e Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 17 Feb 2025 13:39:04 +0800 Subject: [PATCH 036/127] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/controller/experiment/ExperimentController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java index 7eba2b2c..1faec4d4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java @@ -102,7 +102,7 @@ public class ExperimentController extends BaseController { * @return 删除是否成功 */ @DeleteMapping("{id}") - @ApiOperation("删除流水线") + @ApiOperation("删除实验") public GenericsAjaxResult deleteById(@PathVariable("id") Integer id) throws Exception { return genericsSuccess(this.experimentService.removeById(id)); } From df3973446a2777732ba57006007756650a8e85f1 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 17 Feb 2025 13:43:00 +0800 Subject: [PATCH 037/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/rolebindings.yaml | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 k8s/template-yaml/rolebindings.yaml diff --git a/k8s/template-yaml/rolebindings.yaml b/k8s/template-yaml/rolebindings.yaml new file mode 100644 index 00000000..54c2e13d --- /dev/null +++ b/k8s/template-yaml/rolebindings.yaml @@ -0,0 +1,68 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: custom-workflow + namespace: argo +rules: + - apiGroups: + - argoproj.io + resources: + - workflows + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods + - services + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: custom-workflow-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: custom-workflow +subjects: + - kind: ServiceAccount + name: default From 5e2eba49af066754f2d9097fed440d765202b1cf Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 17 Feb 2025 17:24:47 +0800 Subject: [PATCH 038/127] feat: storybook add msw --- react-ui/.storybook/main.ts | 1 + react-ui/.storybook/mock/umijs.mock.tsx | 9 +- react-ui/.storybook/preview.tsx | 9 + react-ui/.storybook/storybook.css | 13 +- react-ui/package.json | 9 +- react-ui/public/mockServiceWorker.js | 307 ++++++++++ .../components/CodeSelectorModal/index.less | 51 +- .../components/CodeSelectorModal/index.tsx | 10 +- .../components/InfoGroup/InfoGroupTitle.less | 1 - react-ui/src/components/PageTitle/index.tsx | 2 +- .../src/components/ParameterInput/index.tsx | 16 + .../src/components/ResourceSelect/index.tsx | 4 +- .../ResourceSelectorModal/index.tsx | 13 +- react-ui/src/global.less | 2 +- react-ui/src/stories/BasicInfo.stories.tsx | 13 +- react-ui/src/stories/CodeSelect.stories.tsx | 32 +- .../src/stories/CodeSelectorModal.stories.tsx | 54 +- react-ui/src/stories/KFModal.stories.tsx | 50 +- .../src/stories/MenuIconSelector.stories.tsx | 12 +- .../src/stories/ParameterInput.stories.tsx | 108 ++++ .../src/stories/ParameterSelect.stories.tsx | 32 - .../src/stories/ResourceSelect.stories.tsx | 135 +++++ .../stories/ResourceSelectorModal.stories.tsx | 216 +++++++ react-ui/src/stories/mockData.ts | 548 ++++++++++++++++++ 24 files changed, 1508 insertions(+), 139 deletions(-) create mode 100644 react-ui/public/mockServiceWorker.js create mode 100644 react-ui/src/stories/ParameterInput.stories.tsx delete mode 100644 react-ui/src/stories/ParameterSelect.stories.tsx create mode 100644 react-ui/src/stories/ResourceSelect.stories.tsx create mode 100644 react-ui/src/stories/ResourceSelectorModal.stories.tsx create mode 100644 react-ui/src/stories/mockData.ts diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index 2eff8e2c..d0c13b18 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -16,6 +16,7 @@ const config: StorybookConfig = { name: '@storybook/react-webpack5', options: {}, }, + staticDirs: ['../public'], webpackFinal: async (config) => { if (config.resolve) { config.resolve.alias = { diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx index 66ef1e5c..4f25eeb3 100644 --- a/react-ui/.storybook/mock/umijs.mock.tsx +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -5,5 +5,12 @@ export const Link = ({ to, children, ...props }: any) => ( ); export const request = (url: string, options: any) => { - return fetch(url, options).then((res) => res.json()); + return fetch(url, options) + .then((res) => { + if (!res.ok) { + throw new Error(res.statusText); + } + return res; + }) + .then((res) => res.json()); }; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index cbdef6b1..8dc3c3fe 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -4,8 +4,16 @@ import themes from '@/styles/theme.less'; import type { Preview } from '@storybook/react'; import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; +import { initialize, mswLoader } from 'msw-storybook-addon'; import './storybook.css'; +/* + * Initializes MSW + * See https://github.com/mswjs/msw-storybook-addon#configuring-msw + * to learn how to customize it + */ +initialize(); + const preview: Preview = { parameters: { controls: { @@ -77,6 +85,7 @@ const preview: Preview = { ), ], + loaders: [mswLoader], // 👈 Add the MSW loader to all stories }; export default preview; diff --git a/react-ui/.storybook/storybook.css b/react-ui/.storybook/storybook.css index 0084b0f8..6c592a3c 100644 --- a/react-ui/.storybook/storybook.css +++ b/react-ui/.storybook/storybook.css @@ -6,7 +6,14 @@ body, margin: 0; padding: 0; overflow-y: visible; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, - 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; +} + +.ant-input-search-large .ant-input-affix-wrapper, .ant-input-search-large .ant-input-search-button { + height: 46px; +} + +*, +*::before, +*::after { + box-sizing: border-box; } diff --git a/react-ui/package.json b/react-ui/package.json index e69739ad..c98f0286 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -119,6 +119,8 @@ "less-loader": "~12.2.0", "lint-staged": "^13.2.0", "mockjs": "^1.1.0", + "msw": "~2.7.0", + "msw-storybook-addon": "~2.0.4", "prettier": "^2.8.1", "storybook": "~8.5.3", "swagger-ui-dist": "^4.18.2", @@ -158,5 +160,10 @@ "CNAME", "create-umi" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/react-ui/public/mockServiceWorker.js b/react-ui/public/mockServiceWorker.js new file mode 100644 index 00000000..ec47a9a5 --- /dev/null +++ b/react-ui/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.7.0' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/react-ui/src/components/CodeSelectorModal/index.less b/react-ui/src/components/CodeSelectorModal/index.less index cb77da6d..82d73ff8 100644 --- a/react-ui/src/components/CodeSelectorModal/index.less +++ b/react-ui/src/components/CodeSelectorModal/index.less @@ -1,4 +1,4 @@ -.code-selector { +.kf-code-selector-modal { width: 100%; height: 100%; @@ -6,31 +6,6 @@ width: 100%; } - :global { - .ant-input-affix-wrapper { - border-radius: 23px !important; - .ant-input-prefix { - margin-inline-end: 12px; - } - .ant-input-suffix { - margin-inline-end: 12px; - } - .ant-input-clear-icon { - font-size: 16px; - } - } - - .ant-input-group-addon { - display: none; - } - - .ant-pagination { - .ant-select-single { - height: 32px !important; - } - } - } - &__content { display: flex; flex-direction: row; @@ -47,4 +22,28 @@ &__empty { padding-top: 40px; } + + // 覆盖 antd 样式 + .ant-input-affix-wrapper { + border-radius: 23px !important; + .ant-input-prefix { + margin-inline-end: 12px; + } + .ant-input-suffix { + margin-inline-end: 12px; + } + .ant-input-clear-icon { + font-size: 16px; + } + } + + .ant-input-group-addon { + display: none; + } + + .ant-pagination { + .ant-select-single { + height: 32px !important; + } + } } diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index 6a965fd7..6426e8c7 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -13,7 +13,7 @@ import type { ModalProps, PaginationProps } from 'antd'; import { Empty, Input, Pagination } from 'antd'; import { useEffect, useState } from 'react'; import CodeConfigItem from '../CodeConfigItem'; -import styles from './index.less'; +import './index.less'; export { type CodeConfigData }; @@ -80,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { footer={null} destroyOnClose > -
    +
    {dataList?.length !== 0 ? ( <> -
    +
    {dataList?.map((item) => ( ))} @@ -116,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { /> ) : ( -
    +
    )} diff --git a/react-ui/src/components/InfoGroup/InfoGroupTitle.less b/react-ui/src/components/InfoGroup/InfoGroupTitle.less index 26ed375a..b8aff97c 100644 --- a/react-ui/src/components/InfoGroup/InfoGroupTitle.less +++ b/react-ui/src/components/InfoGroup/InfoGroupTitle.less @@ -1,5 +1,4 @@ .kf-info-group-title { - box-sizing: border-box; width: 100%; height: 56px; padding: 0 @content-padding; diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx index ea8a65de..2703e032 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -9,7 +9,7 @@ import './index.less'; type PageTitleProps = { /** 标题 */ - title: string; + title: React.ReactNode; /** 自定义类名 */ className?: string; /** 自定义样式 */ diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index be56c96d..99bb78af 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -26,18 +26,34 @@ export type ParameterInputObject = { export type ParameterInputValue = ParameterInputObject | string; export interface ParameterInputProps { + /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ value?: ParameterInputValue; + /** + * 值变化时的回调 + * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 + */ onChange?: (value?: ParameterInputValue) => void; + /** 点击时的回调 */ onClick?: () => void; + /** 删除时的回调 */ onRemove?: () => void; + /** 是否可以手动输入 */ canInput?: boolean; + /** 是否是文本框 */ textArea?: boolean; + /** 占位符 */ placeholder?: string; + /** 是否允许清除 */ allowClear?: boolean; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 大小 */ size?: 'middle' | 'small' | 'large'; + /** 是否禁用 */ disabled?: boolean; + /** 元素 id */ id?: string; } diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 6e0179d4..bc8d08cf 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -21,14 +21,16 @@ export { requiredValidator, type ParameterInputObject } from '../ParameterInput' export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; type ResourceSelectProps = { + /** 类型,数据集、模型、镜像 */ type: ResourceSelectorType; } & ParameterInputProps; -// 获取选择数据集、模型后面按钮 icon +// 获取选择数据集、模型、镜像后面按钮 icon const getSelectBtnIcon = (type: ResourceSelectorType) => { return ; }; +/** 数据集、模型、镜像选择表单组件 */ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { const [selectedResource, setSelectedResource] = useState( undefined, diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index 1a62819c..e65f1f02 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -16,7 +16,7 @@ import { ResourceSelectorType, selectorTypeConfig } from './config'; import styles from './index.less'; export { ResourceSelectorType, selectorTypeConfig }; -// 选择数据集\模型\镜像的返回类型 +// 选择数据集、模型、镜像的返回类型 export type ResourceSelectorResponse = { activeTab: CommonTabKeys; // 是我的还是公开的 id: string; // 数据集\模型\镜像 id @@ -28,10 +28,18 @@ export type ResourceSelectorResponse = { }; export interface ResourceSelectorModalProps extends Omit { - type: ResourceSelectorType; // 数据集\模型\镜像 + /** 类型,数据集、模型、镜像 */ + type: ResourceSelectorType; + /** 默认展开的节点 */ defaultExpandedKeys?: React.Key[]; + /** 默认展开的节点 */ defaultCheckedKeys?: React.Key[]; + /** 默认激活的 Tab */ defaultActiveTab?: CommonTabKeys; + /** + * 确认回调 + * @param params 选择的数据 + */ onOk?: (params: ResourceSelectorResponse | undefined) => void; } @@ -61,6 +69,7 @@ const getIdAndVersion = (versionKey: string) => { }; }; +/** 选择 数据集、模型、镜像 弹框 */ function ResourceSelectorModal({ type, defaultExpandedKeys = [], diff --git a/react-ui/src/global.less b/react-ui/src/global.less index fbbfa34d..9944c70e 100644 --- a/react-ui/src/global.less +++ b/react-ui/src/global.less @@ -5,7 +5,7 @@ body, height: 100%; margin: 0; padding: 0; - overflow-y: hidden; + overflow-y: visible; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index e6e71255..6fece79a 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -1,20 +1,9 @@ import BasicInfo from '@/components/BasicInfo'; import { formatDate } from '@/utils/date'; +import { formatList } from '@/utils/format'; import type { Meta, StoryObj } from '@storybook/react'; import { Button } from 'antd'; -const formatList = (value: string[] | null | undefined): string => { - if ( - value === undefined || - value === null || - Array.isArray(value) === false || - value.length === 0 - ) { - return '--'; - } - return value.join(','); -}; - // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { title: 'Components/BasicInfo', diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index 3f52e159..8be5b620 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -1,5 +1,9 @@ import CodeSelect from '@/components/CodeSelect'; import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -8,6 +12,13 @@ const meta = { parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], @@ -16,7 +27,7 @@ const meta = { // backgroundColor: { control: 'color' }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - // args: { onClick: fn() }, + args: { onChange: fn() }, } satisfies Meta; export default meta; @@ -24,5 +35,22 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Primary: Story = { - args: {}, + render: ({ onChange }) => { + return ( +
    + +

    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 \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/youtube.svg b/react-ui/src/stories/example/assets/youtube.svg new file mode 100644 index 00000000..a7515d7e --- /dev/null +++ b/react-ui/src/stories/example/assets/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/button.css b/react-ui/src/stories/example/button.css new file mode 100644 index 00000000..4e3620b0 --- /dev/null +++ b/react-ui/src/stories/example/button.css @@ -0,0 +1,30 @@ +.storybook-button { + display: inline-block; + cursor: pointer; + border: 0; + border-radius: 3em; + font-weight: 700; + line-height: 1; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.storybook-button--primary { + background-color: #555ab9; + color: white; +} +.storybook-button--secondary { + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; + background-color: transparent; + color: #333; +} +.storybook-button--small { + padding: 10px 16px; + font-size: 12px; +} +.storybook-button--medium { + padding: 11px 20px; + font-size: 14px; +} +.storybook-button--large { + padding: 12px 24px; + font-size: 16px; +} diff --git a/react-ui/src/stories/example/header.css b/react-ui/src/stories/example/header.css new file mode 100644 index 00000000..5efd46c2 --- /dev/null +++ b/react-ui/src/stories/example/header.css @@ -0,0 +1,32 @@ +.storybook-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-header svg { + display: inline-block; + vertical-align: top; +} + +.storybook-header h1 { + display: inline-block; + vertical-align: top; + margin: 6px 0 6px 10px; + font-weight: 700; + font-size: 20px; + line-height: 1; +} + +.storybook-header button + button { + margin-left: 10px; +} + +.storybook-header .welcome { + margin-right: 10px; + color: #333; + font-size: 14px; +} diff --git a/react-ui/src/stories/example/page.css b/react-ui/src/stories/example/page.css new file mode 100644 index 00000000..77c81d2d --- /dev/null +++ b/react-ui/src/stories/example/page.css @@ -0,0 +1,68 @@ +.storybook-page { + margin: 0 auto; + padding: 48px 20px; + max-width: 600px; + color: #333; + font-size: 14px; + line-height: 24px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-page h2 { + display: inline-block; + vertical-align: top; + margin: 0 0 4px; + font-weight: 700; + font-size: 32px; + line-height: 1; +} + +.storybook-page p { + margin: 1em 0; +} + +.storybook-page a { + color: inherit; +} + +.storybook-page ul { + margin: 1em 0; + padding-left: 30px; +} + +.storybook-page li { + margin-bottom: 8px; +} + +.storybook-page .tip { + display: inline-block; + vertical-align: top; + margin-right: 10px; + border-radius: 1em; + background: #e7fdd8; + padding: 4px 12px; + color: #357a14; + font-weight: 700; + font-size: 11px; + line-height: 12px; +} + +.storybook-page .tip-wrapper { + margin-top: 40px; + margin-bottom: 40px; + font-size: 13px; + line-height: 20px; +} + +.storybook-page .tip-wrapper svg { + display: inline-block; + vertical-align: top; + margin-top: 3px; + margin-right: 4px; + width: 12px; + height: 12px; +} + +.storybook-page .tip-wrapper svg path { + fill: #1ea7fd; +} From 0d297472039c4a0d1f43c2640605c70b922519e7 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 14:07:56 +0800 Subject: [PATCH 020/127] =?UTF-8?q?oauth2=E9=83=A8=E7=BD=B2=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-13oauth2.yaml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 k8s/template-yaml/k8s-13oauth2.yaml diff --git a/k8s/template-yaml/k8s-13oauth2.yaml b/k8s/template-yaml/k8s-13oauth2.yaml new file mode 100644 index 00000000..88be9902 --- /dev/null +++ b/k8s/template-yaml/k8s-13oauth2.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ci4s-oauth2-authenticator-deployment + namespace: argo +spec: + replicas: 1 + selector: + matchLabels: + app: ci4s-oauth2-authenticator + template: + metadata: + labels: + app: ci4s-oauth2-authenticator + spec: + containers: + - name: ci4s-oauth2-authenticator + image: 172.20.32.187/ci4s/spring-oauth2-authenticator:latest + env: + - name: DB_URL + value: mysql.argo.svc:3306 + - name: DB_USERNAME + value: root + - name: DB_PASSWORD + value: qazxc123456. + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: ci4s-oauth2-authenticator-service + namespace: argo +spec: + type: NodePort + ports: + - name: http + port: 8080 + nodePort: 31080 + protocol: TCP + selector: + app: ci4s-oauth2-authenticator + From 1ff139a9600d4f6169fda869bdd826273704f742 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 14:56:21 +0800 Subject: [PATCH 021/127] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E7=8E=AF=E5=A2=83ip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/deploy.sh b/k8s/deploy.sh index d06c05d9..2db3331a 100755 --- a/k8s/deploy.sh +++ b/k8s/deploy.sh @@ -43,7 +43,7 @@ fi # 根据环境设置 IP 地址 if [ "$env" == "dev" ]; then - remote_ip="172.20.32.181" + remote_ip="172.20.32.197" elif [ "$env" == "test" ]; then remote_ip="172.20.32.185" else From 28079f4e45e1966d61ae2e2bfaf0ff2122a921f9 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 15:00:50 +0800 Subject: [PATCH 022/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/build_and_deploy.sh | 2 +- k8s/deploy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/build_and_deploy.sh b/k8s/build_and_deploy.sh index 44ea23fa..6e561cdd 100755 --- a/k8s/build_and_deploy.sh +++ b/k8s/build_and_deploy.sh @@ -19,7 +19,7 @@ show_help() { echo echo "Options:" echo " -b Branch to deploy, default: master" - echo " -s Service to deploy (manage-front, manage, front, all, default: manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" echo " -e Environment (e.g., dev, test, default: dev)" echo " -h Show this help message" } diff --git a/k8s/deploy.sh b/k8s/deploy.sh index 2db3331a..2dffc04f 100755 --- a/k8s/deploy.sh +++ b/k8s/deploy.sh @@ -10,7 +10,7 @@ show_help() { echo "Usage: $0 [-s service] [-e environment]" echo echo "Options:" - echo " -s Service to deploy (manage-front, manage, front, all default: manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system default: manage-front)" echo " -e Environment (e.g., dev, test, default: dev)" echo " -h Show this help message" } From c6cf996f1f7b1d4d0bec45e62ee78afd3f54923e Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:14:07 +0800 Subject: [PATCH 023/127] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/build.sh | 2 +- .../main/java/com/ruoyi/system/mapper/SysDictDataMapper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/build.sh b/k8s/build.sh index 921a2ec5..49041f30 100755 --- a/k8s/build.sh +++ b/k8s/build.sh @@ -13,7 +13,7 @@ show_help() { echo echo "Options:" echo " -b Branch to deploy, default is master" - echo " -s Service to deploy (manage-front, manage, front, all, default is manage-front)" + echo " -s Service to deploy (manage-front, manage, front, all, system, default is manage-front)" echo " -h Show this help message" } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java index b004c598..921390ef 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysDictDataMapper.java @@ -17,7 +17,7 @@ public interface SysDictDataMapper * @param dictData 字典数据信息 * @return 字典数据集合信息 */ - public List selectDictDataList(SysDictData dictData); + List selectDictDataList(SysDictData dictData); /** * 根据字典类型查询字典数据 From 47463a4c2d696d6629b609069dca0096e06a91e7 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:23:43 +0800 Subject: [PATCH 024/127] =?UTF-8?q?=E4=BC=98=E5=8C=96nacos=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-3nacos.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/k8s/template-yaml/k8s-3nacos.yaml b/k8s/template-yaml/k8s-3nacos.yaml index 225c6b23..e7db99c0 100644 --- a/k8s/template-yaml/k8s-3nacos.yaml +++ b/k8s/template-yaml/k8s-3nacos.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: nacos-ci4s - image: nacos/nacos-server:v2.2.0 + image: 172.20.32.187/ci4s/nacos-server:v2.2.0 env: - name: SPRING_DATASOURCE_PLATFORM value: mysql @@ -39,7 +39,7 @@ spec: - containerPort: 9849 initContainers: - name: init-mydb - image: busybox:1.31 + image: 172.20.32.187/ci4s/busybox:1.31 command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] restartPolicy: Always From 65309fe423180e8568c0f73239a177d724f33a42 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:37:57 +0800 Subject: [PATCH 025/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-3nacos.yaml | 2 +- k8s/template-yaml/k8s-7management.yaml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/k8s/template-yaml/k8s-3nacos.yaml b/k8s/template-yaml/k8s-3nacos.yaml index e7db99c0..6b242c2f 100644 --- a/k8s/template-yaml/k8s-3nacos.yaml +++ b/k8s/template-yaml/k8s-3nacos.yaml @@ -38,7 +38,7 @@ spec: - containerPort: 9848 - containerPort: 9849 initContainers: - - name: init-mydb + - name: init-mydb-check image: 172.20.32.187/ci4s/busybox:1.31 command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] restartPolicy: Always diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index edc1c621..80f50756 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -31,6 +31,11 @@ spec: - name: resource-volume hostPath: path: /platform-data + initContainers: + - name: init-fs-check + image: 172.20.32.187/ci4s/busybox:1.31 + command: [ 'sh', '-c', 'if findmnt /platform-data/ | grep 'fuse.juicefs'; then exit 0; else exit 1; fi'] + restartPolicy: Always --- apiVersion: v1 kind: Service From 731ec3945a1e68925858ef8e36d0141fe8584677 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:41:29 +0800 Subject: [PATCH 026/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 80f50756..5bb2c5fb 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -34,7 +34,7 @@ spec: initContainers: - name: init-fs-check image: 172.20.32.187/ci4s/busybox:1.31 - command: [ 'sh', '-c', 'if findmnt /platform-data/ | grep 'fuse.juicefs'; then exit 0; else exit 1; fi'] + command: [ 'sh', '-c', 'findmnt /platform-data/ | grep 'fuse.juicefs''] restartPolicy: Always --- apiVersion: v1 From d1f8f7dfc2027b64b35768a576220184e1f36ff5 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:43:24 +0800 Subject: [PATCH 027/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 5bb2c5fb..4d61f614 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -34,7 +34,7 @@ spec: initContainers: - name: init-fs-check image: 172.20.32.187/ci4s/busybox:1.31 - command: [ 'sh', '-c', 'findmnt /platform-data/ | grep 'fuse.juicefs''] + command: [ 'sh', '-c', "findmnt /platform-data/ | grep 'fuse.juicefs'"] restartPolicy: Always --- apiVersion: v1 From 06522bb54753b670c94203196ebf7a2fe4e9d95f Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:47:25 +0800 Subject: [PATCH 028/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 4d61f614..93df4bf5 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -34,7 +34,7 @@ spec: initContainers: - name: init-fs-check image: 172.20.32.187/ci4s/busybox:1.31 - command: [ 'sh', '-c', "findmnt /platform-data/ | grep 'fuse.juicefs'"] + command: [ 'sh', '-c', "if findmnt /platform-data; then exit 0; else exit 1; fi"] restartPolicy: Always --- apiVersion: v1 From 65ab8f27d4a7e63d8ea97bba3d0c818cb8302973 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Feb 2025 16:59:43 +0800 Subject: [PATCH 029/127] =?UTF-8?q?=E4=BC=98=E5=8C=96management=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 93df4bf5..c2cafd9d 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -34,7 +34,7 @@ spec: initContainers: - name: init-fs-check image: 172.20.32.187/ci4s/busybox:1.31 - command: [ 'sh', '-c', "if findmnt /platform-data; then exit 0; else exit 1; fi"] + command: [ 'sh', '-c', "mount | grep /platform-data"] restartPolicy: Always --- apiVersion: v1 From be41b43f605133a48f39688c362da75cf66bbef6 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 14 Feb 2025 17:00:15 +0800 Subject: [PATCH 030/127] =?UTF-8?q?feat:=20storybook=20=E5=A4=84=E7=90=86l?= =?UTF-8?q?ess=20+=20css=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/main.ts | 23 ++++++- react-ui/.storybook/preview.tsx | 66 +++++++++++++++++-- react-ui/src/app.tsx | 4 +- .../components/BasicInfo/BasicInfoItem.tsx | 22 +++++-- .../BasicInfo/BasicInfoItemValue.tsx | 8 ++- react-ui/src/components/BasicInfo/index.less | 30 +++++++-- react-ui/src/components/BasicInfo/index.tsx | 20 +++++- .../src/components/BasicTableInfo/index.less | 9 ++- .../src/components/BasicTableInfo/index.tsx | 4 +- .../src/components/FullScreenFrame/index.tsx | 15 +++-- react-ui/src/components/IFramePage/index.tsx | 2 +- react-ui/src/components/InfoGroup/index.tsx | 13 +++- .../src/components/InfoGroupTitle/index.less | 4 +- .../src/components/InfoGroupTitle/index.tsx | 6 ++ react-ui/src/components/KFEmpty/index.less | 2 +- react-ui/src/components/KFEmpty/index.tsx | 22 +++++-- react-ui/src/components/KFIcon/index.tsx | 10 ++- .../index.less => KFModal/KFModalTitle.less} | 0 .../index.tsx => KFModal/KFModalTitle.tsx} | 6 +- react-ui/src/components/KFModal/index.tsx | 8 ++- react-ui/src/components/KFRadio/index.tsx | 16 +++-- react-ui/src/components/KFSpin/index.tsx | 7 +- react-ui/src/components/LabelValue/index.less | 19 ------ react-ui/src/components/LabelValue/index.tsx | 20 ------ .../components/MenuIconSelector/index.less | 3 +- .../src/components/MenuIconSelector/index.tsx | 3 + react-ui/src/components/PageTitle/index.tsx | 7 ++ .../src/components/RightContent/index.tsx | 7 +- .../src/components/SubAreaTitle/index.tsx | 11 +++- react-ui/src/pages/404.tsx | 2 +- react-ui/src/pages/CodeConfig/List/index.tsx | 2 +- .../Dataset/components/ResourceList/index.tsx | 2 +- .../DevelopmentEnvironment/Create/index.tsx | 4 +- react-ui/src/pages/Mirror/Create/index.tsx | 4 +- .../ModelDeployment/ServiceInfo/index.tsx | 7 +- .../components/VersionBasicInfo/index.tsx | 2 +- .../components/RobotFrame/index.less | 0 .../components/RobotFrame/index.tsx | 0 react-ui/src/pages/Workspace/index.tsx | 2 +- react-ui/src/pages/missingPage.jsx | 2 +- react-ui/src/stories/BasicInfo.stories.tsx | 22 ++++++- .../src/stories/BasicTableInfo.stories.tsx | 57 +--------------- .../src/stories/FullScreenFrame.stories.tsx | 31 +++++++++ react-ui/src/stories/InfoGroup.stories.tsx | 31 +++++++++ .../src/stories/InfoGroupTitle.stories.tsx | 30 +++++++++ react-ui/src/stories/KFEmpty.stories.tsx | 53 +++++++++++++++ react-ui/src/stories/KFIcon.stories.tsx | 41 ++++++++++++ react-ui/src/stories/KFModal.stories.tsx | 59 +++++++++++++++++ react-ui/src/stories/KFRadio.stories.tsx | 52 +++++++++++++++ react-ui/src/stories/KFSpin.stories.tsx | 35 ++++++++++ .../src/stories/MenuIconSelector.stories.tsx | 65 ++++++++++++++++++ react-ui/src/stories/PageTitle.stories.tsx | 30 +++++++++ react-ui/src/stories/SubAreaTitle.stories.tsx | 32 +++++++++ 53 files changed, 760 insertions(+), 172 deletions(-) rename react-ui/src/components/{ModalTitle/index.less => KFModal/KFModalTitle.less} (100%) rename react-ui/src/components/{ModalTitle/index.tsx => KFModal/KFModalTitle.tsx} (84%) delete mode 100644 react-ui/src/components/LabelValue/index.less delete mode 100644 react-ui/src/components/LabelValue/index.tsx rename react-ui/src/{ => pages/Workspace}/components/RobotFrame/index.less (100%) rename react-ui/src/{ => pages/Workspace}/components/RobotFrame/index.tsx (100%) create mode 100644 react-ui/src/stories/FullScreenFrame.stories.tsx create mode 100644 react-ui/src/stories/InfoGroup.stories.tsx create mode 100644 react-ui/src/stories/InfoGroupTitle.stories.tsx create mode 100644 react-ui/src/stories/KFEmpty.stories.tsx create mode 100644 react-ui/src/stories/KFIcon.stories.tsx create mode 100644 react-ui/src/stories/KFModal.stories.tsx create mode 100644 react-ui/src/stories/KFRadio.stories.tsx create mode 100644 react-ui/src/stories/KFSpin.stories.tsx create mode 100644 react-ui/src/stories/MenuIconSelector.stories.tsx create mode 100644 react-ui/src/stories/PageTitle.stories.tsx create mode 100644 react-ui/src/stories/SubAreaTitle.stories.tsx diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index 551aa73b..ec302bf7 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -10,7 +10,6 @@ const config: StorybookConfig = { '@storybook/addon-essentials', '@chromatic-com/storybook', '@storybook/addon-interactions', - '@storybook/addon-styling-webpack', ], framework: { name: '@storybook/react-webpack5', @@ -31,7 +30,27 @@ const config: StorybookConfig = { test: /\.less$/, use: [ 'style-loader', - 'css-loader', + { + loader: 'css-loader', + options: { + importLoaders: 1, + import: true, + esModule: true, + modules: { + auto: (resourcePath: string) => { + if ( + resourcePath.endsWith('MenuIconSelector/index.less') || + resourcePath.endsWith('theme.less') + ) { + return true; + } else { + return false; + } + }, + localIdentName: '[local]___[hash:base64:5]', + }, + }, + }, { loader: 'less-loader', options: { diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index 9e8c4b60..fc1de258 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -1,7 +1,8 @@ import '@/global.less'; import '@/overrides.less'; +import themes from '@/styles/theme.less'; import type { Preview } from '@storybook/react'; -import { ConfigProvider } from 'antd'; +import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -15,14 +16,69 @@ const preview: Preview = { date: /Date$/i, }, }, + actions: { argTypesRegex: '^on.*' }, }, decorators: [ (Story) => ( - - - - + + + + + + ), diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 275e5e17..dcc4d247 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -41,7 +41,7 @@ export async function getInitialState(): Promise { roleNames: response.user.roles, } as API.CurrentUser; } catch (error) { - console.error('1111', error); + console.error('getInitialState', error); gotoLoginPage(); } return undefined; @@ -215,7 +215,7 @@ export const antd: RuntimeAntdConfig = (memo) => { defaultColor: themes['textColor'], defaultHoverBg: 'rgba(22, 100, 255, 0.06)', defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', - defaultHoverColor: '#3F7FFF ', + defaultHoverColor: '#3F7FFF', defaultActiveBg: 'rgba(22, 100, 255, 0.12)', defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', defaultActiveColor: themes['primaryColor'], diff --git a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx index 871b8fb5..86e63891 100644 --- a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -18,15 +18,23 @@ type BasicInfoItemProps = { classPrefix: string; /** 标题是否显示省略号 */ labelEllipsis?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; }; -function BasicInfoItem({ data, labelWidth, classPrefix, labelEllipsis }: BasicInfoItemProps) { +function BasicInfoItem({ + data, + labelWidth, + classPrefix, + labelEllipsis = true, + labelAlign = 'start', +}: BasicInfoItemProps) { const { label, value, format, ellipsis } = data; const formatValue = format ? format(value) : value; const myClassName = `${classPrefix}__item`; let valueComponent = undefined; if (React.isValidElement(formatValue)) { - valueComponent = formatValue; + valueComponent =

+ + + + + + + ); + }, }; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx index 2df1b5fa..deab369c 100644 --- a/react-ui/src/stories/CodeSelectorModal.stories.tsx +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -4,6 +4,8 @@ import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -12,15 +14,19 @@ const meta = { parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], // More on argTypes: https://storybook.js.org/docs/api/argtypes argTypes: { // backgroundColor: { control: 'color' }, - title: { - description: '标题', - }, open: { description: '对话框是否可见', }, @@ -37,17 +43,19 @@ export const Primary: Story = { args: { open: false, }, - render: function Render(args) { + render: function Render({ onOk, onCancel, ...args }) { const [{ open }, updateArgs] = useArgs(); function onClick() { updateArgs({ open: true }); } - function onOk() { + function onModalOk(res: any) { updateArgs({ open: false }); + onOk?.(res); } - function onCancel() { + function onModalCancel() { updateArgs({ open: false }); + onCancel?.(); } return ( @@ -55,27 +63,27 @@ export const Primary: Story = { - + ); }, }; -const OpenModalByFunction = () => { - const handleOnChange = () => { - const { close } = openAntdModal(CodeSelectorModal, { - onOk: () => { - close(); - }, - }); - }; - return ( - - ); -}; - export const OpenInFunction: Story = { - render: () => , + render: function Render(args) { + const handleOnChange = () => { + const { close } = openAntdModal(CodeSelectorModal, { + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, }; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 3efe5e99..208ced34 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -25,6 +25,10 @@ const meta = { open: { description: '对话框是否可见', }, + children: { + description: '子元素', + type: 'string', + }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args args: { onCancel: fn(), onOk: fn() }, @@ -41,17 +45,19 @@ export const Primary: Story = { open: false, children: '这是一个模态框', }, - render: function Render(args) { + render: function Render({ onOk, onCancel, ...args }) { const [{ open }, updateArgs] = useArgs(); function onClick() { updateArgs({ open: true }); } - function onOk() { + function onModalOk() { updateArgs({ open: false }); + onOk?.(); } - function onCancel() { + function onModalCancel() { updateArgs({ open: false }); + onCancel?.(); } return ( @@ -59,30 +65,28 @@ export const Primary: Story = { - + ); }, }; -const OpenModalByFunction = () => { - const handleOnChange = () => { - const { close } = openAntdModal(KFModal, { - title: '创建实验', - image: CreateExperiment, - children: '这是一个模态框', - onOk: () => { - close(); - }, - }); - }; - return ( - - ); -}; - export const OpenInFunction: Story = { - render: () => , + render: function Render() { + const handleOnChange = () => { + const { close } = openAntdModal(KFModal, { + title: '创建实验', + image: CreateExperiment, + children: '这是一个模态框', + onOk: () => { + close(); + }, + }); + }; + return ( + + ); + }, }; diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx index 45088835..e2950549 100644 --- a/react-ui/src/stories/MenuIconSelector.stories.tsx +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -34,17 +34,19 @@ export const Primary: Story = { selectedIcon: 'manual-icon', open: false, }, - render: function Render(args) { + render: function Render({ onOk, onCancel, ...args }) { const [{ open, selectedIcon }, updateArgs] = useArgs(); function onClick() { updateArgs({ open: true }); } - function onOk(value: string) { + function onModalOk(value: string) { updateArgs({ selectedIcon: value, open: false }); + onOk?.(value); } - function onCancel() { + function onModalCancel() { updateArgs({ open: false }); + onCancel?.(); } return ( @@ -56,8 +58,8 @@ export const Primary: Story = { {...args} open={open} selectedIcon={selectedIcon} - onOk={onOk} - onCancel={onCancel} + onOk={onModalOk} + onCancel={onModalCancel} /> ); diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx new file mode 100644 index 00000000..9d9525e2 --- /dev/null +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -0,0 +1,108 @@ +import ParameterInput, { ParameterInputValue } from '@/components/ParameterInput'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +import { useState } from 'react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ParameterInput', + component: ParameterInput, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Input: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + textArea: false, + allowClear: true, + size: 'large', + }, +}; + +export const Select: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: 'storybook', + canInput: false, + size: 'large', + }, +}; + +export const SelectWithObjctValue: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + }, +}; + +export const Disabled: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + disabled: true, + }, +}; + +export const Application: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + size: 'large', + }, + render: function Render(args) { + const [value, setValue] = useState(''); + + const onClick = () => { + setValue({ + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }); + }; + return ( + <> + setValue(value)} + > + + + ); + }, +}; diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx deleted file mode 100644 index 4a1907ee..00000000 --- a/react-ui/src/stories/ParameterSelect.stories.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import MirrorBasic from '@/assets/img/mirror-basic.png'; -import ParameterSelect from '@/components/ParameterSelect'; -import type { Meta, StoryObj } from '@storybook/react'; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export -const meta = { - title: 'Components/ParameterSelect', - component: ParameterSelect, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - // layout: 'centered', - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - // backgroundColor: { control: 'color' }, - }, - // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - // args: { onClick: fn() }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - title: '基本信息', - image: MirrorBasic, - }, -}; diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx new file mode 100644 index 00000000..b9db1633 --- /dev/null +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -0,0 +1,135 @@ +import ResourceSelect, { + requiredValidator, + ResourceSelectorType, +} from '@/components/ResourceSelect'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelect', + component: ResourceSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: ResourceSelectorType.Dataset, + }, + render: ({ onChange }) => { + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + ); + }, +}; diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx new file mode 100644 index 00000000..e2e0ebd2 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -0,0 +1,216 @@ +import ResourceSelectorModal, { ResourceSelectorType } from '@/components/ResourceSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelectorModal', + component: ResourceSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + open: { + description: '对话框是否可见', + }, + type: { + control: 'select', + options: Object.values(ResourceSelectorType), + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Dataset: Story = { + args: { + type: ResourceSelectorType.Dataset, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onModalOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function onModalCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Model: Story = { + args: { + type: ResourceSelectorType.Model, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onModalOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function onModalCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Mirror: Story = { + args: { + type: ResourceSelectorType.Mirror, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function onModalOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function onModalCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const OpenInFunction: Story = { + args: { + type: ResourceSelectorType.Mirror, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render(args) { + const handleOnChange = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: args.type, + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts new file mode 100644 index 00000000..3d910b06 --- /dev/null +++ b/react-ui/src/stories/mockData.ts @@ -0,0 +1,548 @@ +export const datasetListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + name: '手写体识别训练测试数据集', + identifier: 'admin_dataset_20241213140429', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454047, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '手写体识别', + identifier: 'admin_dataset_20241213140020', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454046, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '生物活性分子数据集', + identifier: 'admin_dataset_20241211151411', + description: '生物活性分子数据集', + is_public: false, + time_ago: '2个月前', + id: 1454004, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '介电材料数据集', + identifier: 'admin_dataset_20241211151330', + description: '介电材料数据集', + is_public: false, + time_ago: '2个月前', + id: 1454003, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const datasetVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.tar.gz', + }, + { + name: 'v3', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.tar.gz', + }, + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.tar.gz', + }, + ], +}; + +export const datasetDetailData = { + msg: '操作成功', + code: 200, + data: { + name: '生物活性分子材料数据集', + identifier: 'admin_dataset_20250217105241', + description: '生物活性分子材料数据集', + is_public: false, + data_type: '自然语言处理', + data_tag: '机器翻译', + version: 'v1', + dataset_version_vos: [ + { + url: '/home/resource/admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset/BBBP.zip', + file_name: 'BBBP.zip', + file_size: '42.14 KB', + }, + ], + id: 1454953, + create_by: '樊帅', + version_desc: '生物活性分子材料数据集', + usage: + '
# 克隆数据集配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/fanshuai/admin_dataset_20250217105241.git\n# 远程拉取配置文件\ndvc pull\n
', + update_time: '2025-02-17 10:52:43', + owner: 'fanshuai', + dataset_source: 'add', + relative_paths: 'admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset', + }, +}; + +export const modelListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + id: 1454208, + name: '介电材料模型1', + create_by: '陈志航', + description: '介电材料模型1', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + }, + { + id: 1454007, + name: '手写体识别部署模型', + create_by: '陈志航', + description: '手写体识别部署模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151713', + is_public: false, + }, + { + id: 1454006, + name: '生物活性分子材料', + create_by: '陈志航', + description: '生物活性分子材料', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151645', + is_public: false, + }, + { + id: 1454005, + name: '介电材料模型', + create_by: '陈志航', + description: '介电材料模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151601', + is_public: false, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const modelVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + ], +}; + +export const modelDetailData = { + msg: '操作成功', + code: 200, + data: { + id: 1454208, + name: '介电材料模型1', + version: 'v1', + version_desc: '介电材料模型1', + create_by: '陈志航', + create_time: '2024-12-24 09:59:31', + update_time: '2024-12-24 09:59:31', + model_size: '101.90 KB', + model_source: 'add', + model_tag: '图像分类', + model_type: 'PyTorch', + description: '介电材料模型1', + usage: + '
# 克隆模型配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/chenzhihang/admin_model_20241224095928.git\n# 远程拉取配置文件\ndvc pull\n
', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + relative_paths: 'admin/model/1454208/admin_model_20241224095928/v1/model', + model_version_vos: [ + { + url: '/home/resource/admin/model/1454208/admin_model_20241224095928/v1/model/sklearn_svr_good.pkl', + file_name: 'sklearn_svr_good.pkl', + file_size: '101.90 KB', + }, + ], + }, +}; + +export const mirrorListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 42, + name: 'ccr.ccs.tencentyun.com/somunslotus/httpserver', + description: 'htttp服务器镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:37.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 44, + name: 'minio-test', + description: 'minio镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-08T15:19:00.000+08:00', + update_by: 'admin', + update_time: '2024-05-08T15:19:00.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 46, + name: 'machine-learning/mnist-model-deploy', + description: 'mnist手写体识别部署镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-28T10:18:30.000+08:00', + update_by: 'admin', + update_time: '2024-05-28T10:18:30.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 49, + name: 'go_httpsever', + description: 'golang httpserver镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-06-25T11:11:41.000+08:00', + update_by: 'admin', + update_time: '2024-06-25T11:11:41.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 51, + name: 'pytorch2_python3_cuda_12', + description: 'pytorch2.1.2_python3.11_cuda_12', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-14T16:16:35.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:16:35.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 53, + name: 'jupyterlab', + description: 'v1', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-15T16:19:29.000+08:00', + update_by: 'admin', + update_time: '2024-10-15T16:19:29.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 55, + name: 'machine-learning/ax-pytorch', + description: '自动机器学习Ax镜像,带pytorch', + image_type: 0, + create_by: 'admin', + create_time: '2024-12-13T11:25:37.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T11:25:37.000+08:00', + state: 1, + version_count: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 7, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 7, + size: 2000, + empty: false, + }, +}; + +export const mirrorVerionData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 54, + image_id: 42, + version: null, + url: '172.20.32.187/testlib/admin/ccr.ccs.tencentyun.com/somunslotus/httpserver:v1', + tag_name: 'v1', + file_size: '6.98 MB', + status: 'available', + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:51.000+08:00', + state: 1, + host_ip: null, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 1, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 1, + size: 2000, + empty: false, + }, +}; + +export const codeListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 2, + code_repo_name: '介电材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/fuli/ML_for_Materials.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-14T16:10:45.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:10:45.000+08:00', + state: 1, + }, + { + id: 3, + code_repo_name: '生物活性材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/zhaoyihan/test_mole_pre.git', + git_branch: 'parse_dataset', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T08:41:39.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T08:41:39.000+08:00', + state: 1, + }, + { + id: 4, + code_repo_name: '数据处理', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T14:51:18.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T14:51:18.000+08:00', + state: 1, + }, + { + id: 5, + code_repo_name: '手写体识别部署', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/somunslotus/mnist-inference.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T16:36:43.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T16:36:43.000+08:00', + state: 1, + }, + { + id: 7, + code_repo_name: '手写体识别训练', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-12-13T13:58:50.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T13:58:50.000+08:00', + state: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 20, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 5, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 5, + size: 20, + empty: false, + }, +}; From cdfe23ef0882fe03112adf1fb277e8731e7b8cbf Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 19 Feb 2025 09:25:53 +0800 Subject: [PATCH 039/127] =?UTF-8?q?feat:=20storybook=20add=20less=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/main.ts | 3 + react-ui/.storybook/mock/umijs.mock.tsx | 3 + react-ui/src/app.tsx | 1 - react-ui/src/components/CodeSelect/index.tsx | 4 +- .../components/CodeSelectorModal/index.tsx | 2 +- react-ui/src/components/KFModal/index.tsx | 2 +- react-ui/src/components/KFSpin/index.tsx | 11 +- .../src/components/ResourceSelect/index.tsx | 4 +- .../ResourceSelectorModal/index.tsx | 2 +- react-ui/src/stories/BasicInfo.stories.tsx | 13 +- .../src/stories/BasicTableInfo.stories.tsx | 2 +- .../src/stories/CodeSelectorModal.stories.tsx | 13 +- .../src/stories/FullScreenFrame.stories.tsx | 3 +- react-ui/src/stories/KFModal.stories.tsx | 13 +- .../src/stories/MenuIconSelector.stories.tsx | 8 +- .../src/stories/ResourceSelectorModal.mdx | 23 ++ .../stories/ResourceSelectorModal.stories.tsx | 25 +-- react-ui/src/stories/docs/Less.mdx | 199 ++++++++++++++++++ 18 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 react-ui/src/stories/ResourceSelectorModal.mdx create mode 100644 react-ui/src/stories/docs/Less.mdx diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index d0c13b18..820a0eeb 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -17,6 +17,9 @@ const config: StorybookConfig = { options: {}, }, staticDirs: ['../public'], + docs: { + defaultName: 'Documentation', + }, webpackFinal: async (config) => { if (config.resolve) { config.resolve.alias = { diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx index 4f25eeb3..ae8a7646 100644 --- a/react-ui/.storybook/mock/umijs.mock.tsx +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -14,3 +14,6 @@ export const request = (url: string, options: any) => { }) .then((res) => res.json()); }; + +export { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +export const history = window.history; diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index dcc4d247..65b4440a 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; import './dayjsConfig'; -import './global.less'; import { removeAllPageCacheState } from './hooks/pageCacheState'; import { getRemoteMenu, diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 0dabae1d..242183e1 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -13,8 +13,10 @@ import './index.less'; export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +type CodeSelectProps = ParameterInputProps; + /** 代码配置选择表单组件 */ -function CodeSelect({ value, onChange, disabled, ...rest }: ParameterInputProps) { +function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index 6426e8c7..c983093e 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -21,7 +21,7 @@ export interface CodeSelectorModalProps extends Omit { onOk?: (params: CodeConfigData | undefined) => void; } -/** 代码配置选择弹窗 */ +/** 选择代码配置的弹窗,推荐使用函数的方式打开 */ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [dataList, setDataList] = useState([]); const [total, setTotal] = useState(0); diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index ec1b7717..8156d6a9 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -14,7 +14,7 @@ export interface KFModalProps extends ModalProps { image?: string; } -/** 自定义 Modal */ +/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ function KFModal({ title, image, diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 161775b9..a3a6c3e5 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -7,12 +7,17 @@ import { Spin, SpinProps } from 'antd'; import './index.less'; +interface KFSpinProps extends SpinProps { + /** 加载文本 */ + label: string; +} + /** 自定义 Spin */ -function KFSpin(props: SpinProps) { +function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { return (
- -
加载中
+ +
{label}
); } diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index bc8d08cf..718d571a 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -20,10 +20,10 @@ import './index.less'; export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; -type ResourceSelectProps = { +interface ResourceSelectProps extends ParameterInputProps { /** 类型,数据集、模型、镜像 */ type: ResourceSelectorType; -} & ParameterInputProps; +} // 获取选择数据集、模型、镜像后面按钮 icon const getSelectBtnIcon = (type: ResourceSelectorType) => { diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index e65f1f02..24e97a4d 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -69,7 +69,7 @@ const getIdAndVersion = (versionKey: string) => { }; }; -/** 选择 数据集、模型、镜像 弹框 */ +/** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ function ResourceSelectorModal({ type, defaultExpandedKeys = [], diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index 6fece79a..80a0337c 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -26,6 +26,7 @@ export default meta; type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 一行两列 */ export const Primary: Story = { args: { datas: [ @@ -86,6 +87,16 @@ export const Primary: Story = { ), }, ], - labelWidth: 100, + labelWidth: 80, + labelAlign: 'justify', + }, +}; + +/** 一行三列 */ +export const ThreeColumn: Story = { + args: { + ...Primary.args, + labelAlign: 'start', + threeColumns: true, }, }; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index 3ed5f332..82581f6a 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -27,6 +27,6 @@ type Story = StoryObj; export const Primary: Story = { args: { ...BasicInfoStories.Primary.args, - labelWidth: 70, + labelWidth: 100, }, }; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx index deab369c..59026ec6 100644 --- a/react-ui/src/stories/CodeSelectorModal.stories.tsx +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -48,12 +48,12 @@ export const Primary: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk(res: any) { + function handleOk(res: any) { updateArgs({ open: false }); onOk?.(res); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -63,15 +63,16 @@ export const Primary: Story = { - + ); }, }; -export const OpenInFunction: Story = { +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { render: function Render(args) { - const handleOnChange = () => { + const handleClick = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { const { onOk } = args; @@ -81,7 +82,7 @@ export const OpenInFunction: Story = { }); }; return ( - ); diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx index 6a2748c2..fa334ed1 100644 --- a/react-ui/src/stories/FullScreenFrame.stories.tsx +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -1,5 +1,6 @@ import FullScreenFrame from '@/components/FullScreenFrame'; import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -16,7 +17,7 @@ const meta = { // backgroundColor: { control: 'color' }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - // args: { onClick: fn() }, + args: { onLoad: fn(), onError: fn() }, } satisfies Meta; export default meta; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 208ced34..763aaf31 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -50,12 +50,12 @@ export const Primary: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk() { + function handleOk() { updateArgs({ open: false }); onOk?.(); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -65,15 +65,16 @@ export const Primary: Story = { - + ); }, }; -export const OpenInFunction: Story = { +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { render: function Render() { - const handleOnChange = () => { + const handleClick = () => { const { close } = openAntdModal(KFModal, { title: '创建实验', image: CreateExperiment, @@ -84,7 +85,7 @@ export const OpenInFunction: Story = { }); }; return ( - ); diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx index e2950549..f02fb77c 100644 --- a/react-ui/src/stories/MenuIconSelector.stories.tsx +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -39,12 +39,12 @@ export const Primary: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk(value: string) { + function handleOk(value: string) { updateArgs({ selectedIcon: value, open: false }); onOk?.(value); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -58,8 +58,8 @@ export const Primary: Story = { {...args} open={open} selectedIcon={selectedIcon} - onOk={onModalOk} - onCancel={onModalCancel} + onOk={handleOk} + onCancel={handleCancel} /> ); diff --git a/react-ui/src/stories/ResourceSelectorModal.mdx b/react-ui/src/stories/ResourceSelectorModal.mdx new file mode 100644 index 00000000..28702123 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas } from '@storybook/blocks'; +import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" + + + +# Usage + +推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 + +```ts +const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: ResourceSelectorType.Dataset, + onOk: (res) => { + // 处理逻辑 + close(); + }, +}); +``` + + + + diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx index e2e0ebd2..9e72efd2 100644 --- a/react-ui/src/stories/ResourceSelectorModal.stories.tsx +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -70,12 +70,12 @@ export const Dataset: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk(res: any) { + function handleOk(res: any) { updateArgs({ open: false }); onOk?.(res); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -85,7 +85,7 @@ export const Dataset: Story = { - + ); }, @@ -116,12 +116,12 @@ export const Model: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk(res: any) { + function handleOk(res: any) { updateArgs({ open: false }); onOk?.(res); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -131,7 +131,7 @@ export const Model: Story = { - + ); }, @@ -159,12 +159,12 @@ export const Mirror: Story = { function onClick() { updateArgs({ open: true }); } - function onModalOk(res: any) { + function handleOk(res: any) { updateArgs({ open: false }); onOk?.(res); } - function onModalCancel() { + function handleCancel() { updateArgs({ open: false }); onCancel?.(); } @@ -174,13 +174,14 @@ export const Mirror: Story = { - + ); }, }; -export const OpenInFunction: Story = { +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { args: { type: ResourceSelectorType.Mirror, }, @@ -197,7 +198,7 @@ export const OpenInFunction: Story = { }, }, render: function Render(args) { - const handleOnChange = () => { + const handleClick = () => { const { close } = openAntdModal(ResourceSelectorModal, { type: args.type, onOk: (res) => { @@ -208,7 +209,7 @@ export const OpenInFunction: Story = { }); }; return ( - ); diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx new file mode 100644 index 00000000..21543267 --- /dev/null +++ b/react-ui/src/stories/docs/Less.mdx @@ -0,0 +1,199 @@ +import { Meta, Controls } from '@storybook/blocks'; + + + +# Less 规范 + +## Theme + +### 自定义主题 + +`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 + +颜色变量还可以在 `js/ts/jsx/tsx` 里使用 + +```js +import themes from "@/styles/theme.less" + +const primaryColor = themes['primaryColor']; // #1664ff +``` + +### Ant Design 主题覆盖 + +Ant Design 可以[定制主题](https://ant-design.antgroup.com/docs/react/customize-theme-cn),Ant Design 是通过 [ConfigProvider](https://ant-design.antgroup.com/components/config-provider-cn) 组件进行主题定制,而 UmiJS 可以在[配置文件](https://umijs.org/docs/max/antd#%E6%9E%84%E5%BB%BA%E6%97%B6%E9%85%8D%E7%BD%AE)或者 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置。我选择在 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置,因为这里可以使用主题颜色变量。 + +```tsx +// 主题修改 +export const antd: RuntimeAntdConfig = (memo) => { + memo.theme ??= {}; + memo.theme.token = { + colorPrimary: themes['primaryColor'], + colorSuccess: themes['successColor'], + colorError: themes['errorColor'], + colorWarning: themes['warningColor'], + colorLink: themes['primaryColor'], + colorText: themes['textColor'], + controlHeightLG: 46, + }; + memo.theme.components ??= {}; + memo.theme.components.Tabs = {}; + memo.theme.components.Button = { + defaultBg: 'rgba(22, 100, 255, 0.06)', + defaultBorderColor: 'rgba(22, 100, 255, 0.11)', + defaultColor: themes['textColor'], + defaultHoverBg: 'rgba(22, 100, 255, 0.06)', + defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', + defaultHoverColor: '#3F7FFF', + defaultActiveBg: 'rgba(22, 100, 255, 0.12)', + defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', + defaultActiveColor: themes['primaryColor'], + contentFontSize: parseInt(themes['fontSize']), + }; + memo.theme.components.Input = { + inputFontSize: parseInt(themes['fontSizeInput']), + inputFontSizeLG: parseInt(themes['fontSizeInputLg']), + paddingBlockLG: 10, + }; + memo.theme.components.Select = { + singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], + }; + memo.theme.components.Table = { + headerBg: 'rgba(242, 244, 247, 0.36)', + headerBorderRadius: 4, + rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + }; + memo.theme.components.Tabs = { + titleFontSize: 16, + }; + memo.theme.components.Form = { + labelColor: 'rgba(29, 29, 32, 0.8);', + }; + memo.theme.components.Breadcrumb = { + iconFontSize: parseInt(themes['fontSize']), + linkColor: 'rgba(29, 29, 32, 0.7)', + separatorColor: 'rgba(29, 29, 32, 0.7)', + }; + + memo.theme.cssVar = true; + + memo.appConfig = { + message: { + // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭 + maxCount: 3, + }, + }; + + return memo; +}; +``` + +覆盖 Ant Design 的默认样式,优先选择这种方式,如果没有相应的变量,才覆盖 Ant Design 的样式,在 `src/overrides.less` 文件里覆盖,请查看 UmiJS 关于[`global.less`](https://umijs.org/docs/guides/directory-structure#globalcsslesssassscss) 与 [`overrides.less`](https://umijs.org/docs/guides/directory-structure#overridescsslesssassscss) 的说明。 + +## BEM + +类名遵循 [BEM - Block, Element, Modifier](https://getbem.com/) 规范 + +### Block + +有意义的独立实体,Block 的类名由小写字母、数字和横线组成,比如 `model`、`form`、`paramneter-input` + +### Element + +块的一部分,Element 的类名由 `Block 的类名` + `双下划线(__)` + `Element 的名称` 组成,比如 `model__title`、`form__input`、`paramneter-input__content` + +### Modifier + +块或元素的变种,Modifier 的类名由 `Block 的类名` 或者 `Element 的类名` + `双横线(--)` + `Modifier 的名称` 组成,比如 `button--active`、`form--large` + +举个 🌰 + +```tsx +// @/components/CodeConfigItem/index.tsx + +import classNames from 'classnames'; +import styles from './index.less'; + +function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { + return ( +
+ + + {item.code_repo_name} + +
+ {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} +
+
+ + {item.git_url} + +
{item.git_branch}
+
+ ); +} + +``` + +### 一些建议 + +如果你陷入嵌套地狱,比如 + +```tsx +function Component() { + return ( +
+
+
+
+
+ // 等等 +
+
+
+
+
+ ) +} +``` + +说明你需要拆分组件了 + +```tsx +function Component1() { + return ( +
+
+
+
+ ) +} + +function Component() { + return ( +
+
+ +
+
+ ) +} +``` + +既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 + + + From 87d806fbba74cfa22cb092421ddf50b8538174bf Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 19 Feb 2025 10:08:56 +0800 Subject: [PATCH 040/127] feat: storybook add configInfo --- react-ui/.storybook/preview.tsx | 5 +++ react-ui/src/components/ConfigInfo/index.tsx | 41 +++++++++++++++++++ .../AutoML/components/AutoMLBasic/index.tsx | 10 ++--- .../AutoML/components/ConfigInfo/index.less | 20 --------- .../AutoML/components/ConfigInfo/index.tsx | 27 ------------ .../components/HyperParameterBasic/index.tsx | 8 ++-- react-ui/src/stories/Config.stories.tsx | 37 +++++++++++++++++ react-ui/src/stories/docs/Less.mdx | 2 +- 8 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 react-ui/src/components/ConfigInfo/index.tsx delete mode 100644 react-ui/src/pages/AutoML/components/ConfigInfo/index.less delete mode 100644 react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx create mode 100644 react-ui/src/stories/Config.stories.tsx diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index 8dc3c3fe..af81cc5c 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -22,6 +22,11 @@ const preview: Preview = { date: /Date$/i, }, }, + options: { + storySort: { + method: 'alphabetical', + }, + }, }, decorators: [ (Story) => ( diff --git a/react-ui/src/components/ConfigInfo/index.tsx b/react-ui/src/components/ConfigInfo/index.tsx new file mode 100644 index 00000000..0f81d46f --- /dev/null +++ b/react-ui/src/components/ConfigInfo/index.tsx @@ -0,0 +1,41 @@ +import BasicInfo, { type BasicInfoData, type BasicInfoProps } from '@/components/BasicInfo'; +import InfoGroup from '@/components/InfoGroup'; +import classNames from 'classnames'; +export type { BasicInfoData }; + +interface ConfigInfoProps extends BasicInfoProps { + /** 标题 */ + title: string; + /** 子元素 */ + children?: React.ReactNode; +} + +/** 详情基本信息块,目前主要用于主动机器学习、超参数寻优、自主学习详情中 */ +function ConfigInfo({ + title, + datas, + labelWidth, + labelAlign = 'start', + labelEllipsis = true, + threeColumns = true, + className, + style, + children, +}: ConfigInfoProps) { + return ( + +
+ + {children} +
+
+ ); +} + +export default ConfigInfo; diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 6a4ceaa7..5f207b7d 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -1,3 +1,4 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; import { AutoMLData } from '@/pages/AutoML/types'; import { experimentStatusInfo } from '@/pages/Experiment/status'; @@ -8,7 +9,6 @@ import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/fo import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; -import ConfigInfo, { type BasicInfoData } from '../ConfigInfo'; import styles from './index.less'; // 格式化优化方向 @@ -236,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {isInstance && runStatus && ( @@ -244,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {!isInstance && ( )} - + ); } diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less b/react-ui/src/pages/AutoML/components/ConfigInfo/index.less deleted file mode 100644 index 33fb3314..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less +++ /dev/null @@ -1,20 +0,0 @@ -.config-info { - :global { - .kf-basic-info { - width: 100%; - - &__item { - width: calc((100% - 80px) / 3); - &__label { - font-size: @font-size; - text-align: left; - text-align-last: left; - } - &__value { - min-width: 0; - font-size: @font-size; - } - } - } - } -} diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx deleted file mode 100644 index 72596581..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; -import InfoGroup from '@/components/InfoGroup'; -import classNames from 'classnames'; -import styles from './index.less'; -export type { BasicInfoData }; - -type ConfigInfoProps = { - title: string; - data: BasicInfoData[]; - labelWidth: number; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -function ConfigInfo({ title, data, labelWidth, className, style, children }: ConfigInfoProps) { - return ( - -
- - {children} -
-
- ); -} - -export default ConfigInfo; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index dc767ee8..eb00288b 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -1,6 +1,6 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { hyperParameterOptimizedMode } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; -import ConfigInfo, { type BasicInfoData } from '@/pages/AutoML/components/ConfigInfo'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { schedulerAlgorithms, @@ -198,7 +198,7 @@ function HyperParameterBasic({ {isInstance && runStatus && ( @@ -206,14 +206,14 @@ function HyperParameterBasic({ {!isInstance && ( )} diff --git a/react-ui/src/stories/Config.stories.tsx b/react-ui/src/stories/Config.stories.tsx new file mode 100644 index 00000000..0287d718 --- /dev/null +++ b/react-ui/src/stories/Config.stories.tsx @@ -0,0 +1,37 @@ +import ConfigInfo from '@/components/ConfigInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import * as BasicInfoStories from './BasicInfo.stories'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ConfigInfo', + component: ConfigInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + datas: BasicInfoStories.Primary.args.datas, + labelAlign: 'start', + labelEllipsis: true, + threeColumns: true, + labelWidth: 80, + children:
I am a child element
, + }, +}; diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 21543267..1b3a6632 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -176,7 +176,7 @@ function Component() { function Component1() { return (
-
+
) From bd4d57f39807213267b1437eff5ba1c92d158f3c Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Wed, 19 Feb 2025 14:29:41 +0800 Subject: [PATCH 041/127] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=B6=85?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AF=BB=E4=BC=98=E5=8F=82=E6=95=B0=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/assets/img/popover-bg.png | Bin 0 -> 48590 bytes react-ui/src/iconfont/iconfont.js | 2 +- .../CreateForm/ParameterRange/index.less | 52 ++++- .../CreateForm/ParameterRange/index.tsx | 207 +++++++++--------- .../CreateForm/PopParameterRange/index.less | 39 +++- .../CreateForm/PopParameterRange/index.tsx | 29 ++- .../components/CreateForm/index.less | 2 +- .../components/CreateForm/utils.ts | 37 +++- 8 files changed, 232 insertions(+), 136 deletions(-) create mode 100644 react-ui/src/assets/img/popover-bg.png diff --git a/react-ui/src/assets/img/popover-bg.png b/react-ui/src/assets/img/popover-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..d783c6378df69fae0d907c0e28259fab437e2e9b GIT binary patch literal 48590 zcmV)AK*Ya^P)!1AD;pz9?+r9Si;qLza&hFm+ zu)Fv8uzNcG?Vx_4;!bacsJ_QU@b>hceilE9pO>Er`Z>|87Io=HPDX&slSh-d_q$#= z=RT)>e+hlwYs^Lbd+F-cCt1(A`qewmQs2n@yY85l*YFa{cqCw340xdXX-e-Hhq)RAMNeNR8*-cx_2n;56pij$i&Npmbq*OuSs2}P~V zazaw-D=(m}_Ga7}dk`kYZ{GX=c3=LnU)sOZ8q2;`!T)eL z+}(YC_x`)Lb{_~1y;Q5GY$uJavs(<67w!%h?3;B2o<-c`A?JujR3!^TW649G)Jtoh3fXoU02-8b^B)A26HjFLl<7*`~2^Oefame zTz|Z|b0dF)fC~YQjH8MM-S=k`TP+5WU*cQNG&bHsa%o|`sD1UT~i+;i_#eK#VXjaV-Ew+v~mez?8+ z#*^>wzVg?1yRYnjeqWVsZUXDS`?rVtFT7{>gNMWJ1EI0IeQnO2#e!&1;@&JYYO~^e zD|*U^&Z&akWXqf6 zxsBI`&@QTJ-U7kpQnzx#B_e~=ZV&tD3k?`iS)>6rJ8Q99``zPr4!b{p2?<|9baVo;==tY)j%Q2XoRzk#+%Y_*G9@AbC3QI?8f`RJstRZ8GR< z@QqX2Re`!UT8{oYXjZ9>cIL$TPkKFZ#;z52deYx%b|o&6C9QgUwr?4OtMzijwOx&g zbecvcM;%(8ZrS%e`{>=KYt$s3tDdFUde*!x6Jzd25RYv!A2b5%OcoTbTt2tTsV<%5 zLg1vrJ*R)MGK(d!!bO=@ZPE2hSy+`TzBu_6P#XDgQdBz{&*QykM|E`X+Ne5Fwbdn8 z5v}@u;~bUdftPv-=i-S(*28yp|L(_sZvSWKSc3?x|NaYym!H|~ek>Tck18G^cNUAz zGI6y%B?<_1QF^lw(X(aM?=^r8yx`Qh(_?&agp@R4>S6CX>NtyQ3UHm+3j1@%L&Kr_ zIMkR!q(cmcEUdf7gZO+S(;b0Y0!ym!C`l?SkGco6u|jT#)x3RH+e)N-*FxBB8vfQR zl&$S;3g(qDsdBcHoJi=V^L!-6LQx0l&2ozcsgYpCZ$MAl*q z5LhoVtu;QNbINGPB?QcEa{n5|-%>E6_7n8vH_5b_u%Kq8i zzaNyIY@T#7!xnlCzZl#QSQfZS^+C!=1McTM0*xVTA5^toaGpeOIP$cKh5cvm;|^dkjITRTXL-f}^l7rnJU zTuR5~;uFr_lH5u%WL%a-PwC%e>#cS2hJt4+?=nrK1Qxk?&tw(3 z$CQOt<$aljpJR0G(5{CBchBw8Y1acDR}@l3=!RonyVtp5IY;5nd5PZV&+eYZ^o)X(m}sCBJMVEwbN9$tI$VE5yv*fPjDZ3R6%ha1b*Zx$ka57A#k7dP;p zfOWo@#7KI&XXWn6bKi3CLcc#pTu0^=H(@w845Y%R^toqS-$)>Zd>?`33BtJ0(Y%e@ zBl>t7&u^@#2sF+SHeB~zg~2j(1NvFp{us@jB=`YwPAVHy!FH@Lz`@Irvm(sb41XKI zLZWeP=C*fT8c2zEq7qf)-g;5|ZKn$ff{T=rmwahB?05g~Py9#w*QMA*0_&fC{_wF| zcXogET*#{!Kuaffx!yEfByScWI(e18?Fmw+3kSCgp{JW@UYEBj+-1Q_E_qOW3X)hyiY2)IiFV6@8XZ5|e*g?w8 zs>MiEqeiSxVYAU?t$C;ERU5kJwV5K$SZnz5~Ofe$4$5! zbFaMaS=UV@S5Dpu9rCcpT&1yVwuf2F-`=&L-2^TpVBD{fg_sxK>IF%4L$8O7{3&fWxIb(<)}XlDDs-*{ zRxgr)@+IXDNM>3!;M|Zlm*12lu*#^YjM)(wBXd`6a~CgWRk8MH zlpVdjJKTP2_iz8&-`#(;RJ$Uu{>h&mo_^)2-A_MxvU{qB4?T3Wc36`S&R^O%TQ>`p zB`T>uw*sZ&qOp~085Q+OiXHN=DgQ5|@ky@{4va?T)kji3M%xX#or(%lP zJ)`O`Ws zw{9<82ONHs_>_bta-BPSjH@mj?K{yLE(4ApY!GEG7Ed1U9^Zav_rLx4f4YB@YF-dn zhr?n28(-M{ty{NtFEp4R()47Dvq}PC$bx0yglI^o6-rg2rl$jy7lt8yCd36{Q5i>1 zD9>5f$*u_oC|l&wxyD{XGh?v@D2`;^0>3j0*@4b@Q98NYqA$irbbc0k;i{1(+(Oc` zxb&cA@gvB@99`6)a*+Q6?&B7LCHVyrTd{E~0Oca2Tybj2iF70c{PsdsU0v14j!hef zj~o~y~`P#ldtUl_h0&@{fyXx!20K}AAaD;t=%VJ4>lBR z>98g@%j5z`q??7y67jk3wgAR?G1$VTG#{J{#V66k>N7K+opChf_ysiPCQ&Aa8^)FM zDD&MTOkc{PKWz`pFItr_0u)eZnLoX9N4p(tb!`D57%s-;XE|Bwh5PVe8V$6r|I03Df*}O_d1hTawf6>A<|wWy=tT7BP@eC$F@sjUI!klbfg->9mnm zHxHcWA!Lb0WFZ`5?wadxEh>qQG*@R{XgbyqSacp)nrT6GK*>Nk6D) z*M2yTEd;^qfs|WioRZ7uK>AjH%BEO`^&8knG~#akq`;VN7QJGxh;hRnUT+8OSeS zM7=r1O(rywrS=#14xySV=%hU1;0439;1}2;cgo48^l!iS=i6cnTu&8!xfsinVeQx5 z8eh+OEZ4+y)8>dk(>m=8$Sv0Y@Zo%~ng;)TpK1!Y$QnskdBN#v^VEqu_4)jx*K{N8 zG=0LN11?4^w`%l*MxI&3T4`@%ZkNX`bjph0DkSOrO-fto{^^-AarxmH7AGUJDld{F z=Nul~umRO5RWbOrbwiCeTVP`KY4U;jQ6msmk6w7Z`(JJP9)y4|CyC05Z@?3ycey&!4M8e@i_xL*~q!&wQ+Z<}sflE{00RE)BER1wr^f+P# z+?7SFGNOTgEb^o&h;fazVOnF=b3Fs*nKdrdw;}mHdVE<xkAZ8bbL}@(Kdj~ zYmo}&7LHYZkVX|QwVLGHI}*0MhxEXxCU0th(0pB0hfc!5w=ewEPw)SqXfF|1|Lva~ z?tS#`?q|cl&UT*Oqf=mjwX30^qzSki0t@b%B&ZDT-XsI+SV_)xCmb9Xw3W|Xxe(Qt zLE{Ll7P|s?(xiRUF^+aDk?WmX=N_*FQNkhoU(+?O0)MW_Rv4Z`teqg6)og{GiWsCl+0{1O}r4MOJcbsP`H_`Hgn`yv$L5UTRn2~{YV73AGexQgS(%P45HNClfa6) zn*rG}_zt#=HHW8r%Szx5-X#^>J&^KmD`KlKoaPM!`W2a%4VcCzJZ8Pqn{sT|tOKNq zv+{FoPNET7(maaubwGD5%?k!jwMG~ouXiw3ByNv+okd_FQ!FG@7lAb?e6rs?{>mG> z|M~Czz5RnkVExn2A3k>b&hAGLgQt?|(J8R++^=6kB2_PJ%7)S}ZWRd($5|st$qvo8 zAUCYfPDNl%G1c6ZYM?%8o5sB3E^x#0VIWY*~JV&yxa_U`mFkT=LLz8-vYN& z#@7KNQ^g1j6Jhjje@Qom@~8MS#YtK;aqemhnmk|QW7b8)sw*Tj%N#%Okgd-neHFANgrG3+A=S*bXzgx z?^jDWr(!xzQJYi_ovR>ZR(T88ZfDU4D&KWzD#}hV&FC7Nx09XuRH`3=7tM|XNXJ5t^XaHQ>|n-EI3!+VKea{GZ^b*SRT zM0We=owZJP%^`hekcllfG831AuqrRyX8TJO7?5vV0mV7Oqb=lWhOQLjkgMQHGZEY79{++HVy?(MJu5tIi^3u%>_th;m4?vAA ziFV7`E4PXn5NFvN18z?Z9e&!TTGd3^J@M&j`PH)cXo`3;+|_maXQD-QsEKV^0z?c6wvQOmX1o(7CGmV@FVOcxU&`pZMhd|JeWQ z!{N@izqI>1fyr7tkr4K1kYI1nJ#Nl}GbB#WMXOs``O`|iN;W36ibZ*KGXR(;MO}e9 zddOao$&F!-qX!;S+}t@`t-q9ba_=l;EAc$#n61TaK-xmCxNgSMjI)FD9aO0CYLx;I zP>n(vy3uhqx@lq<1>DymWk6oySRIM6G(qoT(^@fF-*<6hQX;K9${TL{w{;(oR!p1? zZFO}PtAuutEx6h(jMG4$xV8ZYxeg@cKy?~aHynrOaLpwnmB*{|S|a^O#i<`2lh?qB z`duTyYUde`<64tx96wK&=17D!USK5Ovf7diFaJ$bt9-BBE}FV&YFu8Eww%8-`R~nX zN#5D_r^KhU&EaSHmkF%=W9qCUzq!3T-2cw*-|T<=PY>_Ab$9m@y>RZO#|p5Q9ZpZI zxDsE^8^l77_v+?M+HRN#ex7PEIzv7YirfzwUT9gYf|66_UAO*gRpiWn>4*4MH@0Xm z-IUX$VW0yp0*gA+d1g5(My{yi6k}^td;?QsR&C-1%<4=XTLf~(Hdgsje&DUpfj44cq?(068lCZ_enarvsu3A=5+}))znIQ}mU$^KRX)Zm7oPUjgf)RvnNGcmWdM7UO4Y>%} z2F6QpzYvuZG-++vGGwz2_U2*T;km`L6-Wb=J>o2Hm<{e!H|63OHJPk-QfLTWS0SOJ z0f%VsP~7@B&PD3Ws9eog?*806!QByF;|9_ScjWw}n82oFhgU(T?~BpU42e*1TB_C< z;)9@+-Dbi$c42wa4MRu5X-kc3aTzVi@$y>I4fO34^&qyizU2ryBs$M;K)D;-LzMLA zXrkt%sZ-u(9b=m7s}b`<4xiGtRo$26Eix?ibr#z1{%rpnUpV~ae!qJaNzBnQBfzco zISoQCdFZS=Pnzl9T7z7>#$;7p0T;%j`owcnoG+v(9ycVFI}7toV09x2n3p-=`Bg@9 zgqBR#-i+BaE4*sZq@Ho<6HaP*Z05vg~QK;H}{(dyF&p5(hAJ_-eJ}yx*)K) zpNg|KdR5S7?=I`Ys-Q&&w;B}u-7?YPi>mf>08z4~?>)QYl)6mJiiuFwO@f&`aeMwV z4mE_9sE@k|NTnVUvCFL_%9to|I!)~JnQEuHjzv7-G3HD{GjE*wH1Sa}bae4Ji=w{_ z4gzS9S`msax47~idJ!GkH9KyUAqaRF8)ZSBOi2q+_s{)^txC40i@!8EJd?cW2~&Cf?iHfOA@;J-P0utywzxKKF~L|2Plkw9 zaU1nRO}EavQFN^l==iI&iBs`Qf08d(Qo~pn-MgB~1~cB#0!&p zx0=s-f51=j)4g{=V3C`|?WcARVg=SOgbJ)%j+ncNZUtVg!#x*)RX&u9=UPI+2xMeG z^u%zapH=;HT^ebGpp|HdL>y8UwY{{<1r0|xFyMNIMhmDaq5#!h80QYBTe_`-t#!l53U%e`Fb@O zS{42b9%ty31Ij-^$U|gVINpA;d$RwHFCKnzzuSfXZTs2+td@utbN+Lxwx~&BW&yLs z;BH+zoqLY7v`@sP1nyKu8sOQk8PkHY6?(tRiEf-1w@9Foo>tCS9G zblp3-$8~HV{4J^<3->_jx1<(ZT3aoJYH!Pwz(OxsE%BXl>I4lza$O%rU_}5=2}DZ= zp3MRw|B`NP<6?q6hk3mR$4kwXmOPW%A{RP|l<9(_R`VOa%hgrqTIkUQgApCe==6+{ zR+mQMvSkb^2F1YX_2j=%xGlEa#=xX9hw%B?VBDI7w7us-*VFFCx{xqyT(XFHR42{v zOgw4nNcFbnWxmzWU&^6LEE-&eT!{yy!n-&;b&yj~9HEutrPBa971hYC%`3H{v$X^l z(y>B6&6OTe$C>mc63$u3l!;EBQ%YKr-Vj&@lC2XoZ%3)}lrd;z53D4u_1lYAp4^Y}r#6mL zw$moio8Tl|ExdhuDhxNls8evGF&&M?X#Yi+>FS+aL`KBhT_>~smSd=j!mCahcLr*> z3tV@Q$HrOyKt!z}g&&kC;?}^{J)mr#bx7@M?z1Qv(RvL?x0?yH+rd%#Z(7MUiU_RD zlm&rkl{Rr;mgNDhD}&lVz;+thMOBp3R8W1r7_*>@%DU!Dxl)Xa<0%1VJ3{|;^`jo8 z^KN-gq{^9(L%1V_9TRZ-3lvO57P&CTF`E8)fQt*Ze- zl=qdnAXoba?$lP2Q(SYDz;c>CnYf4WI+>HiFhp!!_@bIFaFFX@^Mc4wKr8$<%K?ex znjE?R!|X2zN8nZ~3Epw~r)IQ$cKH zUPwRmKtBKlp)^+y{q`6J;&h!C{B&;}w8mRAfpr}wke${gr^}3BSt78cUmRQd4Vidw zV6@mgDY#DKJ%|OT$u8ox{o2JisN@`TXiMCB$G@fTP!|s-uuS~8lBn`$tVYlpn?o?V zg-l@F8izHwAXgy_wpcfmY0Wuy-3Y84k`IXZ3)gseOS{i?7hn(M?zm#a!eQ!mC956y+FV` zt&*xDpRy$1MYrl%ERo&pU6jhXjNjoYm zQ#i`kP!dU;3nhug-cracjaV+BLJ0=R4h~1?Mzjq~sI5L1|@<9D!8>=_nj7d9hOBc*(JF z3$Iaqj^$!#r>P6%d5 zm3b@HqbUPLC9y$K}C(*q%GtZ7VsYOkf!^F6&*#Mr~O#C;^V zO8%krZNsiyg>yq-5lB3fPfudh$a0J)%StoKt>oAI8HquQUo{wxY;(!6Y_i~Xf`2Y} zjwXP5#yN7Vy1I|D?WAGPSWACI-5Y~4)Z9n_T^+o9q8E1`I+?2ylR1%2*1e7^#whz# zBAT2KZYQphbJ_wrd&pIRXgF^d*$_);|MuX#&Bc@>!jizEO*5Gl2`-h`x~2@rC=x4T z+`M3u#;2yZ+2~bvNDESK*gMj37Dd-7HM&pUhD)r9z6s;N1}Oi001BWNklG9fHIn{9Xf37#s`#6kCoi88-}_ zPE-+CC!4H}gN=KN`9N#B>Qj(cqZVSnSLhPB$SY0d>cpGi5RH(?gwNI48T^AA$^;qIGmH#z8+xOxHXP9r!-5oi!{u8VGU_5+%BMB zJn>uMdUUbGLz&9bg@axLk^C>^OGi8PQ|a#Db+rrb7U6ImJRZ^=DoSSJs8%6>b$M-G zHR+m&!q~2eZ*gk};#`;jwtT5B0rqN$qJ>QWcfgHh*fntnBVIfH23P_tDwPYTdT$-Y zJmRIKzON#%sEJFMBuT*aoPe}+PHcJNU^)!ZH5Vf{G||xuxx2Cb{C$NrRe5lhu9FP)L8Uw(>s{26PT#5?tj? z*iI>3LUEFAw`(<{wI8unZ!z~NjbByf?L$=!_SC-WMADIKCJY2$0;}~0*pbPG z=)R8Ir=-apf=868SujxD*KP3tuG>&}H6UTo{SO}vMuw`TIlv7p*$UB`Q*{1 z$x1lWHjIqvUd8!nEyTbgtx8gk{5#&CW3^fQD4u1M=06i5(sa2q0kJzQ_=XWPK~)EH zwnIds8(|e4M+qp&!^YrS^2`D);-t;ZPy))blfRcLC#wi7)abd&o!dfRO9W(|P&3JI zU4VdV-g8X>)vp>ndk1Re=&82

a|SrC;t)U0%7?+(zw<$dm5AcXYBvRXS|A_(h){ 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 G+y4VTyNHYc literal 0 HcmV?d00001 diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js index d7576e9c..75c1b755 100644 --- a/react-ui/src/iconfont/iconfont.js +++ b/react-ui/src/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,z,v,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),l()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(v=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,v())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4511447='',(t=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var l,z,v,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(z=function(){document.removeEventListener("DOMContentLoaded",z,!1),l()},document.addEventListener("DOMContentLoaded",z,!1)):document.attachEvent&&(v=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,v())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}})(window); \ No newline at end of file diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less index d49089f5..61091050 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less @@ -1,13 +1,47 @@ .parameter-range { - width: 300px; - &__list { - width: 100%; - max-height: 300px; - overflow-x: visible; - overflow-y: auto; + width: 360px; + &__type { + margin-bottom: 10px; + color: @text-color-secondary; + font-size: @font-size-input; + &::before { + display: inline-block; + color: @error-color; + font-size: 14px; + font-family: SimSun, sans-serif; + line-height: 1; + content: '*'; + margin-inline-end: 4px; + } + } + &__desc { + margin-bottom: 20px; + padding: 4px 8px; + color: @text-color-tertiary; + font-size: 13px; + background: rgba(62, 96, 163, 0.05); + border-radius: 6px; } - &__button { - margin-bottom: 0; - text-align: center; + &__form { + width: 100%; + &__list { + width: 100%; + max-height: 300px; + overflow-x: visible; + overflow-y: auto; + } + &__space { + flex: none; + width: 22px; + color: @text-color-tertiary; + font-size: @font-size-input; + line-height: 32px; + text-align: center; + } + &__button { + width: 100%; + margin-bottom: 0; + text-align: center; + } } } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx index 240e90e6..e4b700c9 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx @@ -1,16 +1,16 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Flex, Form, Input, InputNumber } from 'antd'; -import { ParameterType, getFormOptions } from '../utils'; +import { ParameterType, getFormOptions, parameterTooltip } from '../utils'; import styles from './index.less'; type ParameterRangeProps = { - type?: ParameterType; + type: ParameterType; value?: any[]; onCancel?: () => void; onConfirm?: (value: any[]) => void; }; -function ParameterRange({ type, value, onCancel, onConfirm }: ParameterRangeProps) { +function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { const [form] = Form.useForm(); const isList = type === ParameterType.Choice || type === ParameterType.Grid; const formOptions = getFormOptions(type, value); @@ -33,108 +33,119 @@ function ParameterRange({ type, value, onCancel, onConfirm }: ParameterRangeProp }; return ( -
- {isList ? ( -

- - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }, index) => ( - - - - - - - {index === fields.length - 1 && ( +
+
{type}
+
{parameterTooltip[type]}
+ + {isList ? ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + + + - )} + {index === fields.length - 1 && ( + + )} + - - ))} - {fields.length === 0 && ( - - + ))} + {fields.length === 0 && ( + + + + )} + + )} + +
+ ) : ( + + {formOptions.map((item, index) => { + return ( + <> + + - )} - - )} - -
- ) : ( - formOptions.map((item) => { - return ( - - - - ); - }) - )} - - - - - + {index !== formOptions.length - 1 && ( + + {index === 0 ? '-' : ' '} + + )} + + ); + })} +
+ )} + + + + +
); } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less index 1d8609d3..92080c3e 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -1,14 +1,23 @@ .parameter-range { + border-radius: 18px; + box-shadow: 0px 3px 10px rgba(22, 100, 255, 0.15); :global { - .ant-popover-inner { - padding: 20px 20px 12px; - } - .ant-popconfirm-description { - padding-top: 20px; - } + .ant-popover-content { + .ant-popover-inner { + width: 400px; + padding: 20px 20px 12px; + background-image: url(@/assets/img/popover-bg.png); + background-repeat: no-repeat; + background-position: top left; + background-size: 100% auto; + } + .ant-popconfirm-description { + margin-top: 20px; + } - .ant-popconfirm-buttons { - display: none; + .ant-popconfirm-buttons { + display: none; + } } } @@ -58,3 +67,17 @@ } } } + +.parameter-range-title { + color: @text-color; + font-weight: 500; + font-size: @font-size-content; +} + +.parameter-range-title-icon { + color: @text-color-secondary; + + &:hover { + color: @text-color; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx index eb647f3a..b5db36b9 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx @@ -1,6 +1,6 @@ import KFIcon from '@/components/KFIcon'; import { isEmpty } from '@/utils'; -import { Popconfirm, Typography } from 'antd'; +import { Flex, Popconfirm, Typography } from 'antd'; import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; import ParameterRange from '../ParameterRange'; @@ -8,7 +8,7 @@ import { ParameterType } from '../utils'; import styles from './index.less'; type ParameterRangeProps = { - type?: ParameterType; + type: ParameterType; value?: any[]; onChange?: (value: any[]) => void; }; @@ -58,18 +58,11 @@ function PopParameterRange({ type, value, onChange }: ParameterRangeProps) {
} disabled={disabled} description={ - + } - okText="确定" - cancelText="取消" overlayClassName={styles['parameter-range']} icon={null} open={open} @@ -95,4 +88,18 @@ function PopParameterRange({ type, value, onChange }: ParameterRangeProps) { ); } +function PopconfirmTitle({ title, onClose }: { title: string; onClose: () => void }) { + return ( + + {title} + + + ); +} + export default PopParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less index 5c91d2fc..06bbd5b7 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -41,7 +41,7 @@ &::before { display: inline-block; - color: #c73131; + color: @error-color; font-size: 14px; font-family: SimSun, sans-serif; line-height: 1; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts index f3f92555..558637e9 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts @@ -38,6 +38,27 @@ export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({ value: name, })); +export const parameterTooltip: Record = { + [ParameterType.Uniform]: '在 low 和 high 之间均匀采样浮点数', + [ParameterType.QUniform]: '在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数', + [ParameterType.LogUniform]: '在 low 和 high 之间均匀采样浮点数,对数空间采样', + [ParameterType.QLogUniform]: + '在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数', + [ParameterType.Randn]: '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样', + [ParameterType.QRandn]: + '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数', + [ParameterType.RandInt]: '在 low(包括)到 high(不包括)之间均匀采样整数', + [ParameterType.QRandInt]: + '在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high)', + [ParameterType.LogRandInt]: '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数', + [ParameterType.QLogRandInt]: + '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数', + [ParameterType.Choice]: '从指定的选项中采样一个选项', + [ParameterType.Grid]: '对选项进行网格搜索,每个值都将被采样', + [ParameterType.Range]: '在 low 和 high 范围内采样取值', + [ParameterType.Fixed]: '固定取值', +}; + export type ParameterData = { label: string; name: string; @@ -69,12 +90,12 @@ export const getFormOptions = (type?: ParameterType, value?: number[]): Paramete case ParameterType.Range: return [ { - name: 'min', + name: 'low', label: '最小值', value: numbers?.[0], }, { - name: 'max', + name: 'high', label: '最大值', value: numbers?.[1], }, @@ -85,12 +106,12 @@ export const getFormOptions = (type?: ParameterType, value?: number[]): Paramete case ParameterType.QLogRandInt: return [ { - name: 'min', + name: 'low', label: '最小值', value: numbers?.[0], }, { - name: 'max', + name: 'high', label: '最大值', value: numbers?.[1], }, @@ -103,12 +124,12 @@ export const getFormOptions = (type?: ParameterType, value?: number[]): Paramete case ParameterType.Randn: return [ { - name: 'mean', + name: 'm', label: '均值', value: numbers?.[0], }, { - name: 'std', + name: 's', label: '方差', value: numbers?.[1], }, @@ -116,12 +137,12 @@ export const getFormOptions = (type?: ParameterType, value?: number[]): Paramete case ParameterType.QRandn: return [ { - name: 'mean', + name: 'm', label: '均值', value: numbers?.[0], }, { - name: 'std', + name: 's', label: '方差', value: numbers?.[1], }, From 7e55bda19706295376e8ed388565cebc48863610 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Thu, 20 Feb 2025 10:23:02 +0800 Subject: [PATCH 042/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E4=BB=B6size=E5=8F=82=E6=95=B0=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=97=A0=E7=94=A8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/preview.tsx | 11 +++++ react-ui/package.json | 10 ++-- react-ui/src/components/CodeSelect/index.tsx | 22 +++++++-- .../src/components/ParameterInput/index.less | 21 +++++++-- .../src/components/ParameterInput/index.tsx | 1 + .../src/components/ResourceSelect/index.tsx | 23 +++++++-- react-ui/src/stories/CodeSelect.stories.tsx | 21 ++++++++- react-ui/src/stories/KFEmpty.stories.tsx | 2 +- .../src/stories/ResourceSelect.stories.tsx | 47 ++++++++++++++----- 9 files changed, 127 insertions(+), 31 deletions(-) diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index af81cc5c..61e82aaa 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -17,14 +17,25 @@ initialize(); const preview: Preview = { parameters: { controls: { + expanded: true, + sort: 'requiredFirst', matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + backgrounds: { + values: [ + { name: 'Dark', value: '#000' }, + { name: 'Gray', value: '#f9fafb' }, + { name: 'Light', value: '#FFF' }, + ], + default: 'Light', + }, options: { storySort: { method: 'alphabetical', + order: ['Documentation', 'Components'], }, }, }, diff --git a/react-ui/package.json b/react-ui/package.json index c98f0286..818d2144 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -36,12 +36,14 @@ "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", + "storybook": "storybook dev -p 6006", + "storybook-build": "storybook build", + "storybook-docs": "storybook dev --docs", + "storybook-docs-build": "storybook build --docs", "test": "jest", "test:coverage": "npm run jest -- --coverage", "test:update": "npm run jest -- -u", - "tsc": "tsc --noEmit", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "tsc": "tsc --noEmit" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", @@ -166,4 +168,4 @@ "public" ] } -} \ No newline at end of file +} diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 242183e1..d2afbb94 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -8,15 +8,28 @@ import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; +import classNames from 'classnames'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; -export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +export { + requiredValidator, + type ParameterInputObject, + type ParameterInputValue, +} from '../ParameterInput'; type CodeSelectProps = ParameterInputProps; /** 代码配置选择表单组件 */ -function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { +function CodeSelect({ + value, + size, + disabled, + className, + style, + onChange, + ...rest +}: CodeSelectProps) { const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { @@ -47,9 +60,10 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { }; return ( -
+
- + - + Date: Thu, 20 Feb 2025 10:28:38 +0800 Subject: [PATCH 043/127] =?UTF-8?q?chore:=20=E4=BF=AE=E6=94=B9=E8=B6=85?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/HyperParameter/Create/index.tsx | 12 ++++++------ react-ui/src/pages/HyperParameter/Info/index.tsx | 4 ++-- react-ui/src/pages/HyperParameter/Instance/index.tsx | 4 ++-- .../components/HyperParameterBasic/index.tsx | 4 ++-- .../components/ParameterInfo/index.tsx | 4 ++-- react-ui/src/pages/HyperParameter/types.ts | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index bd83783a..79e45582 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -13,10 +13,10 @@ import { useEffect } from 'react'; import BasicConfig from '../components/CreateForm/BasicConfig'; import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; import { getReqParamName } from '../components/CreateForm/utils'; -import { FormData, HyperparameterData } from '../types'; +import { FormData, HyperParameterData } from '../types'; import styles from './index.less'; -function CreateHyperparameter() { +function CreateHyperParameter() { const navigate = useNavigate(); const [form] = Form.useForm(); const { message } = App.useApp(); @@ -28,15 +28,15 @@ function CreateHyperparameter() { useEffect(() => { // 编辑,复制 if (id && !Number.isNaN(id)) { - getHyperparameterInfo(id); + getHyperParameterInfo(id); } }, [id]); // 获取服务详情 - const getHyperparameterInfo = async (id: number) => { + const getHyperParameterInfo = async (id: number) => { const [res] = await to(getRayInfoReq({ id })); if (res && res.data) { - const info: HyperparameterData = res.data; + const info: HyperParameterData = res.data; const { name: name_str, parameters, points_to_evaluate, ...rest } = info; const name = isCopy ? `${name_str}-copy` : name_str; if (parameters && Array.isArray(parameters)) { @@ -164,4 +164,4 @@ function CreateHyperparameter() { ); } -export default CreateHyperparameter; +export default CreateHyperParameter; diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx index 9a37a68f..3d5c5c04 100644 --- a/react-ui/src/pages/HyperParameter/Info/index.tsx +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -10,13 +10,13 @@ import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; import { useEffect, useState } from 'react'; import HyperParameterBasic from '../components/HyperParameterBasic'; -import { HyperparameterData } from '../types'; +import { HyperParameterData } from '../types'; import styles from './index.less'; function HyperparameterInfo() { const params = useParams(); const hyperparameterId = safeInvoke(Number)(params.id); - const [hyperparameterInfo, setHyperparameterInfo] = useState( + const [hyperparameterInfo, setHyperparameterInfo] = useState( undefined, ); diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 8f7faa33..8d29521b 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; import ExperimentResult from '../components/ExperimentResult'; import HyperParameterBasic from '../components/HyperParameterBasic'; -import { AutoMLInstanceData, HyperparameterData } from '../types'; +import { AutoMLInstanceData, HyperParameterData } from '../types'; import styles from './index.less'; enum TabKeys { @@ -24,7 +24,7 @@ enum TabKeys { function AutoMLInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); - const [autoMLInfo, setAutoMLInfo] = useState(undefined); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); const [instanceInfo, setInstanceInfo] = useState(undefined); const params = useParams(); // const autoMLId = safeInvoke(Number)(params.autoMLId); diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index eb00288b..817d8418 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -6,7 +6,7 @@ import { schedulerAlgorithms, searchAlgorithms, } from '@/pages/HyperParameter/components/CreateForm/utils'; -import { HyperparameterData } from '@/pages/HyperParameter/types'; +import { HyperParameterData } from '@/pages/HyperParameter/types'; import { type NodeStatus } from '@/types'; import { elapsedTime } from '@/utils/date'; import { @@ -29,7 +29,7 @@ const formatOptimizeMode = (value: string) => { }; type HyperParameterBasicProps = { - info?: HyperparameterData; + info?: HyperParameterData; className?: string; isInstance?: boolean; runStatus?: NodeStatus; diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index 95a24e1f..0e5ced40 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -2,14 +2,14 @@ import { getReqParamName, type FormParameter, } from '@/pages/HyperParameter/components/CreateForm/utils'; -import { HyperparameterData } from '@/pages/HyperParameter/types'; +import { HyperParameterData } from '@/pages/HyperParameter/types'; import tableCellRender, { TableCellValueType } from '@/utils/table'; import { Table, Tooltip, type TableProps } from 'antd'; import { useMemo } from 'react'; import styles from './index.less'; type ParameterInfoProps = { - info: HyperparameterData; + info: HyperParameterData; }; function ParameterInfo({ info }: ParameterInfoProps) { diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 8c6a35d0..1e610f20 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -29,7 +29,7 @@ export type FormData = { points_to_evaluate: { [key: string]: any }[]; }; -export type HyperparameterData = { +export type HyperParameterData = { id: number; progress: number; run_state: string; From cdee50eb5fcfba91d3b7bbeaee943f8c33934c80 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 21 Feb 2025 10:19:02 +0800 Subject: [PATCH 044/127] =?UTF-8?q?chore:=20=E6=94=B9=E5=9B=9E=E6=9D=90?= =?UTF-8?q?=E6=96=99=E5=B9=B3=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/config/config.ts | 2 +- react-ui/config/defaultSettings.ts | 2 +- react-ui/config/routes.ts | 2 +- react-ui/package.json | 1 + react-ui/public/favicon-cc.ico | Bin 0 -> 4158 bytes react-ui/public/favicon-cl.ico | Bin 4286 -> 0 bytes react-ui/public/favicon.ico | Bin 4158 -> 4286 bytes react-ui/src/assets/img/logo-cc.png | Bin 0 -> 9164 bytes react-ui/src/assets/img/logo-cl.png | Bin 5270 -> 0 bytes react-ui/src/assets/img/logo.png | Bin 9164 -> 5270 bytes react-ui/src/components/IFramePage/index.tsx | 4 ++-- .../RightContent/AvatarDropdown.tsx | 12 +++++++----- react-ui/src/iconfont/iconfont-menu.json | 2 +- react-ui/src/pages/User/Login/login.tsx | 6 +++--- .../components/WorkspaceIntro/index.less | 1 - .../components/WorkspaceIntro/index.tsx | 10 ++++++---- 16 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 react-ui/public/favicon-cc.ico delete mode 100644 react-ui/public/favicon-cl.ico create mode 100644 react-ui/src/assets/img/logo-cc.png delete mode 100644 react-ui/src/assets/img/logo-cl.png diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts index f64c8c77..04ab330d 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -75,7 +75,7 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: '复杂智能软件', + title: '智能材料科研平台', layout: { ...defaultSettings, }, diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index 306c89db..c4c59d2f 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -17,7 +17,7 @@ const Settings: ProLayoutProps & { fixSiderbar: false, splitMenus: false, colorWeak: false, - title: '复杂智能软件', + title: '智能材料科研平台', pwa: true, token: { // 参见ts声明,demo 见文档,通过token 修改样式 diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index 45c65760..e9363f91 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -44,7 +44,7 @@ export default [ { name: 'login', path: '/user/login', - component: './User/Login/login', + component: process.env.NO_SSO ? './User/Login/login' : './User/Login', }, ], }, diff --git a/react-ui/package.json b/react-ui/package.json index 818d2144..2b2cfd4b 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -8,6 +8,7 @@ "build": "max build", "deploy": "npm run build && npm run gh-pages", "dev": "npm run start:dev", + "dev-no-sso": "NO_SSO=true npm run start:dev", "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", diff --git a/react-ui/public/favicon-cc.ico b/react-ui/public/favicon-cc.ico new file mode 100644 index 0000000000000000000000000000000000000000..4d544cb1436b15f817076d1d553c1e5c6107ff29 GIT binary patch literal 4158 zcmb`K32anV6oy{`H7ICXI`f7_t!R-=1*DY4tpX85Yhg#Qb=1{ ziUGk`1Cojci-CfHsE{aXKrk+76g5~e27?h16j5RB`Tg%OZJ*=3c4%gruXjKHJ$HNe z8b%!dl9LVn-{>1}7*4}5`VwXt4-lOn{IRU9AH2?bWI1x-b|j*>LtE8jUQYdQ$VGAn zvlCxJmYIf_tJoW)?ib`d`6GzOc^VTFZ?j^LH4l-WLY!oH z5*~AyeT&$ZQ13nReTYVejc}VoW2<6sje7gY*CCEEJccaS5c?LfEur3Ca@4|S$aW3B zQWfMnBPXFV=BFg1B)tdLW}4XOF|jGn#MS~+wwIY| zNBPy^TH=;Cb8kH>c`3L|ABJSn0LaSG|Iu&uzkcN&*|I+3T7%bR(Est&pN{E{JFHrf z;>f*er!4G`7O1PW3x_r72SWY}{XZ#NmsF~4%ezLh)s%o9#^uei=z7!!=k;=y`Mj!W6ty;Ak~K3PBQEve0^kvjhn)cNhq1@oynnUs@N zq?GfKVAnwW14pP;4_#8L$DxJs5RQqS6Y7KdpgyEF7pgA*qHHUkXKOFBOC0?#qHYbT zA{Fp^_l*dMXNKA|Fu5U&Z@ko_STSkGs$N1B6)$7 z@C<14VyjBhyQ+$eHGwsg&joniwvY)`GsQaom-QPei>$gj4tJu9dis0v3aKMoh&#Mq zYiW7<&Ujgo{s{ZAAW-wf3ROL+SgI!#$hv8M&c9#k^8Hd@>Q@bwDONl1CUwL-M;Y~Y zkk^U+uD(jH_%30!8%>(72K%OJ*+QZT4>N;4~sRQSC26f&g?~wiE zZKCg0qocLco_bi&H6Gaxy~p+a=|ggm=(`O2VwUsUW_#|1>xsv48}B>AE=93c177nw z`kcvquKoRzd`>bqa0@jwaJl*jBzTN{af7YYdw{YL$Z=)B*_Z$$U=XL5i%2A?bH s`HrM*&EtFA;GNXLb(BMP)4ulcPx3c8#re@|?HQJhg3sB>+P1a$zj4dWqW}N^ literal 0 HcmV?d00001 diff --git a/react-ui/public/favicon-cl.ico b/react-ui/public/favicon-cl.ico deleted file mode 100644 index 408b8a236709dc94679a96a769597c3d94443a93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`GS!|V65XYytR1jqqq0-WpZj`OGKuZgy6ezoi`e5+IG%mEgU7>sFN(<_XhD0La zL5)5Tj88~3(S*gMf-%NKBqqjah(r{l2vMND-!lHr*WTJ*Z`&*B*Zex?o0&8JnRC7? z62)Iql9>N;YPQIH5y=2tQUu2DJ;Wk;26}c%pm&D^4)2uyS9V7QUfR7DUWRkbVNB;?{=}!wt9E^=%ECSAHfA3!S}?TqmO8#@6g9&J%OHW zgY)<9CbX0bY{*;y28b#6KXItG1KS5X{z`jc|`6me^}i8)U+#p)U{-}C^Cj1A{AA-CG?Dh>0+uemx#utFGo&YmB?HlO< zI!0i$+Sbp7^PtdYx8=_V`KxX*TDkO*E83YSRtsyR-L^i4_>4)h z`C-4&b~rPGIcM!zM$J6KUbqW3`0dtpVjDe=9I=~oJ^g#-sV27^Zh?;AGV8oyp$Hb* zo#XZ4X|=AKf&GA46~2XbsD@m6h_Rd3(#u-0T65#@T?UgE%G*I%ZOE4XmMn1^v&F7o zB~~5tm~~^8hc;x2!ERd3Y-_MR% zpy&1A`G@oy277RfHQIxeU~hwl!NK<{41|dV^G!~#)38c{FzMOC>Fw5<6C_XzJ_<<8*&}2@%O_?i1%tYW=)~~Y|mYVX4o& zL(a?O=3*P+B6&{O+>tfvGt14nIn8`C!}n?KG~2slbI(rQid|0K%IlyLFhKnG335i( zxJhmSm>I=OohQ@8v)yu+T`W9)N@zrk$Nu>77fgFmzX zSZDvzo1hVb-m(&yN&Zx9nX`Y{cdVU|{eSbm$Jw7DZcVy4HA}EdM61)qs?8AHvtYOn zr#2%18X-5x%QSV-7HwK1yr;wS_}Z`s;VMvX$w{|%Ng~>YShpr)id&r~L7b{ouLt`3 z!Kq2t2>F)0I1uI=O)c++n)I%rT6eME!&SHfC)}E}0`z=tbt-+Nh?b>bQ(prYGy;y< z)oIZVb7*av*fpGkInU~)gZ2E0QPijhYm8dI;x9!z+UUDg$&iu^7g+1~q+DUo3bW7N zk3KU5AGac5s8(_THLj0eC;2zxTR?R0g5bVYZsp>S!3GV(C47aBIkSqz;+FlZt(EhH zn79>-ec0n+_J*tXrdyd5?N%hA&o^QAS0+8`RwS0f`|u@v1!u^qgGHv_!MtSdI3+l` zc={EI=qJvAKF|pGf}FX)e$KNp&ggsf)xYQg*Pl37D-!0p{)A~>kHq@Ry&k#uq+xBd z(3&~o`WG2yKp$uXTq0)P25*m!YFM|pYzvp z_Le31-17KipcA3x@h6zmhaFWLS-1uohM)00r~4i=d7}Qf`_c>Dq~+8? z->doJe4p-Huo+ImHL&4NIIa6)i?#GopO(fB=K2$*UlMhYS=dlaY!*2)#YR*OS4A#h b$9Oj3kA|q>xLFyGVJgfLx!Oe??e_QwVjhSf diff --git a/react-ui/public/favicon.ico b/react-ui/public/favicon.ico index 4d544cb1436b15f817076d1d553c1e5c6107ff29..408b8a236709dc94679a96a769597c3d94443a93 100644 GIT binary patch literal 4286 zcmb`GS!|V65XYytR1jqqq0-WpZj`OGKuZgy6ezoi`e5+IG%mEgU7>sFN(<_XhD0La zL5)5Tj88~3(S*gMf-%NKBqqjah(r{l2vMND-!lHr*WTJ*Z`&*B*Zex?o0&8JnRC7? z62)Iql9>N;YPQIH5y=2tQUu2DJ;Wk;26}c%pm&D^4)2uyS9V7QUfR7DUWRkbVNB;?{=}!wt9E^=%ECSAHfA3!S}?TqmO8#@6g9&J%OHW zgY)<9CbX0bY{*;y28b#6KXItG1KS5X{z`jc|`6me^}i8)U+#p)U{-}C^Cj1A{AA-CG?Dh>0+uemx#utFGo&YmB?HlO< zI!0i$+Sbp7^PtdYx8=_V`KxX*TDkO*E83YSRtsyR-L^i4_>4)h z`C-4&b~rPGIcM!zM$J6KUbqW3`0dtpVjDe=9I=~oJ^g#-sV27^Zh?;AGV8oyp$Hb* zo#XZ4X|=AKf&GA46~2XbsD@m6h_Rd3(#u-0T65#@T?UgE%G*I%ZOE4XmMn1^v&F7o zB~~5tm~~^8hc;x2!ERd3Y-_MR% zpy&1A`G@oy277RfHQIxeU~hwl!NK<{41|dV^G!~#)38c{FzMOC>Fw5<6C_XzJ_<<8*&}2@%O_?i1%tYW=)~~Y|mYVX4o& zL(a?O=3*P+B6&{O+>tfvGt14nIn8`C!}n?KG~2slbI(rQid|0K%IlyLFhKnG335i( zxJhmSm>I=OohQ@8v)yu+T`W9)N@zrk$Nu>77fgFmzX zSZDvzo1hVb-m(&yN&Zx9nX`Y{cdVU|{eSbm$Jw7DZcVy4HA}EdM61)qs?8AHvtYOn zr#2%18X-5x%QSV-7HwK1yr;wS_}Z`s;VMvX$w{|%Ng~>YShpr)id&r~L7b{ouLt`3 z!Kq2t2>F)0I1uI=O)c++n)I%rT6eME!&SHfC)}E}0`z=tbt-+Nh?b>bQ(prYGy;y< z)oIZVb7*av*fpGkInU~)gZ2E0QPijhYm8dI;x9!z+UUDg$&iu^7g+1~q+DUo3bW7N zk3KU5AGac5s8(_THLj0eC;2zxTR?R0g5bVYZsp>S!3GV(C47aBIkSqz;+FlZt(EhH zn79>-ec0n+_J*tXrdyd5?N%hA&o^QAS0+8`RwS0f`|u@v1!u^qgGHv_!MtSdI3+l` zc={EI=qJvAKF|pGf}FX)e$KNp&ggsf)xYQg*Pl37D-!0p{)A~>kHq@Ry&k#uq+xBd z(3&~o`WG2yKp$uXTq0)P25*m!YFM|pYzvp z_Le31-17KipcA3x@h6zmhaFWLS-1uohM)00r~4i=d7}Qf`_c>Dq~+8? z->doJe4p-Huo+ImHL&4NIIa6)i?#GopO(fB=K2$*UlMhYS=dlaY!*2)#YR*OS4A#h b$9Oj3kA|q>xLFyGVJgfLx!Oe??e_QwVjhSf literal 4158 zcmb`K32anV6oy{`H7ICXI`f7_t!R-=1*DY4tpX85Yhg#Qb=1{ ziUGk`1Cojci-CfHsE{aXKrk+76g5~e27?h16j5RB`Tg%OZJ*=3c4%gruXjKHJ$HNe z8b%!dl9LVn-{>1}7*4}5`VwXt4-lOn{IRU9AH2?bWI1x-b|j*>LtE8jUQYdQ$VGAn zvlCxJmYIf_tJoW)?ib`d`6GzOc^VTFZ?j^LH4l-WLY!oH z5*~AyeT&$ZQ13nReTYVejc}VoW2<6sje7gY*CCEEJccaS5c?LfEur3Ca@4|S$aW3B zQWfMnBPXFV=BFg1B)tdLW}4XOF|jGn#MS~+wwIY| zNBPy^TH=;Cb8kH>c`3L|ABJSn0LaSG|Iu&uzkcN&*|I+3T7%bR(Est&pN{E{JFHrf z;>f*er!4G`7O1PW3x_r72SWY}{XZ#NmsF~4%ezLh)s%o9#^uei=z7!!=k;=y`Mj!W6ty;Ak~K3PBQEve0^kvjhn)cNhq1@oynnUs@N zq?GfKVAnwW14pP;4_#8L$DxJs5RQqS6Y7KdpgyEF7pgA*qHHUkXKOFBOC0?#qHYbT zA{Fp^_l*dMXNKA|Fu5U&Z@ko_STSkGs$N1B6)$7 z@C<14VyjBhyQ+$eHGwsg&joniwvY)`GsQaom-QPei>$gj4tJu9dis0v3aKMoh&#Mq zYiW7<&Ujgo{s{ZAAW-wf3ROL+SgI!#$hv8M&c9#k^8Hd@>Q@bwDONl1CUwL-M;Y~Y zkk^U+uD(jH_%30!8%>(72K%OJ*+QZT4>N;4~sRQSC26f&g?~wiE zZKCg0qocLco_bi&H6Gaxy~p+a=|ggm=(`O2VwUsUW_#|1>xsv48}B>AE=93c177nw z`kcvquKoRzd`>bqa0@jwaJl*jBzTN{af7YYdw{YL$Z=)B*_Z$$U=XL5i%2A?bH s`HrM*&EtFA;GNXLb(BMP)4ulcPx3c8#re@|?HQJhg3sB>+P1a$zj4dWqW}N^ diff --git a/react-ui/src/assets/img/logo-cc.png b/react-ui/src/assets/img/logo-cc.png new file mode 100644 index 0000000000000000000000000000000000000000..cae91fe524c40541f346a56ac105410f98cd8825 GIT binary patch literal 9164 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 diff --git a/react-ui/src/assets/img/logo.png b/react-ui/src/assets/img/logo.png index cae91fe524c40541f346a56ac105410f98cd8825..e2fbcfe56a63546e5be260aebc6053c124e30301 100644 GIT binary patch literal 5270 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 literal 9164 zcmV;-BQxBIP)r$;Xf(fRYI)w@V_s_8hrmQw!bDb;$@K+3vtXVO>4wUZ{8u-_4C^fE)VzGzm`$c z)XoK8OhV&+8dtVE{_iMrg}{Rs`qY0*p#Y*XH%74WcUY&dX@&lI4(q=x2yy?4dGgu* z)dJD0=@$Tvn63_x1#yh@#K~yI@#Py#H!}WZ5Eu-PKp_L-#@F8m+ZDEBe>#G;YfB43 zRca-+-zy5@=SvsJU-vh#HXA_by1HN~GteM2&~#g*Xqdnn0&6jrLrsrvEd-6eht(LH zBCM?Cgce8JeXL3is`>q;u-vu{hia{aEVG9W@$AZJV(Q(*r;i4}jK+%SzEfMFj7N^h0+u^sAxPIAud25>Mtk*t$ z22l4jbu{k!3fu_!l}byZZ*gqGbZbsmrTaB)4cese>2zxv1>Q~|cm`B+27spbHrC|+ z{#)4a!{zhkf=U-%kL`5_sQYxakIIOvaq*uhdjr%wPIpt}TgXDlB(!fSwj64F2b3bq z!WRUJPvDT{B%IH+GzQc4ZAz_Bp1fxHTzPXnfGBm&x&btLs@f4O=}P?Y#MNcvN$@QPsf$A zT=GTOpU>l1E-WpN)7_Nw#5Hf-DBrIqP&EapYq}bp@x^VpnS)DQbg^%7*jM-z8wVa? zVx_Hx?ZPymF@=DFGhs-w9P5#Id-UA;+0g_dV!3$g^!bAC7yF+0@`@YfbM*qLW&m|f zQAcGwaVzd>XyjszZ!u3`aokeIw^#?B%5>U%iRmtt3@S(Mv-*lq$l+PEd^TM~SPeOk3O z1y3}0PgpBDPh@}ReA>(+*5W=i=*B|o%`uX*fBaYjj=gq1j}lMjTExu7*UFW(M5xyQ z0h~K!A-q{V_wC!Mbz-r9;UHdu-=ZkKu>^TaQ3ZJ#jaZo2=F0-GoPVKmZj(ReRIPU)3cIqY+)(`yym;i9+l zl-Ld}btryxx6pUlrcJh7+Nya-ET}D36hcdjTdw9M+DFALO<%}5Uy8F=&XrHs!jgIf z(1_F2D4A6c;&vxmhiNSbSGW?ba1TBoR88W+<=4rzwYbny=NNmc8osGXd>XBO2p)n1 z(Lin5MMPO)qHi(XTHsssk7(aw*qsX|8Bj$ZWh~9l=g(PjQ|^)8-h0&mbx&1Wp*wmM z2ad7e;FcEQrf$VkeczJ#@~PflRBw)lVlfGi_;I+NPnvEljDM^Tv%VCkL#YgabG2JXX7_NZhAk>E**sc{xoET4rshT zf$0dYmZ}?0sePIOO~=OAon<0^DAviWIBm%d^0l7ab1Hx|Ay9YWuAazx;`@l6SM=n^ zHuo_jXQhx7N4_#Gl8>9lLd&;K6 z);DFJd-rR0IEPU^wdo;ws(E`!GuqJ4H zl*CA&D}J(k zo}6FFiW&eVwUACaUIYk^F6Ud!Hb#qOAY*A1VOhD!vN#u14w|LN7YbtfiUsnmav(JT zD$_dM)76*I>L)P+(7|iyTeSQ`A< z$f@e{*gTyvG75op`1EP~N?8`C30|mN@G%zDjGVLdM)`+Qm(Z}F6yQN1J_X^;3`#M? ze2dOM85xKglR@L^B0h0MabV(6R9)o1wQPQd)Q!ZY0Zid+~RR^I_0X6VZR%swj z&WV4lpWbkR%>66xxNTe=Cpm>u1=k;Je zpN_lwg2`E`LaNPzi|w8&u7uU>5GdM~#j)ctL;^m?#!9C9(A;e?n)AJhHRZ-?cd>M(Nx=YgR_OGSOaa9aJ>j4g32rcsOIeOFA%vyRi*V z-8ewz?mYO!c4T}3ivB(m-_9V)pI&gRcVcbO(PuGb0N3IwoA^Q_)j%e1(i*{j?i( zo+WubDewQhCMDs*!}OjNZ69dwyGrnBAuW?Pe*7&LB~M>5H9%3#80+Ek#v0O_s-Yj7=u z!7|-j2##YxuCLM{|J%$-*J=pQ<_AoWvQOhA&TjEKfz=?d3D+B4U z$;bVAKL(q7M7MB?W!IUieaukxvyK-3uxedMh>q#TlCfl~*^t-3gy0nW6Sg4J_{82c zb1UP6%s$*rX6gsfu&HVsDo<=dNT3iTD-Izzra7XOt4chFp!G+o0NVYcH6zA!wms64 zBVS`JrstnYPHKJL6YF6)M4?@9f^|a-h@C|(BkGe%`yhQ@&-3=4IjJ^9?TK9-395o; zL}8zUs|$S#;tCH>60 zB4SR)FzATiFXwaCZ=!C^&}ic`IL?X1HOcHO$p=}Y5Q~>^zV?mYptDb^Uorpi>1qdz zFq~x_eJXwx?^0m)N zrfY%EF&rCi-uNi3&GWkRxqg!;-d|5aXWTMeORAX_c4!hicu%p#KJT#C}flUdg64+4kcCwtc z>%#yv%Fe>>F3EDL@~o}YqUtGLrZ-js5o`woTyf3QhHYDHdQ}NyqVqI z0lH1K@Wso1m#VLHbrh~*i%59#iXBLgUhtSCU;vUv@=7w zd+USHeSG)Q6EcbEAa9u6e6lP_!DZlzZRl^YX1Ye z)kA|?{Qb+uI~9CVVz-E^pt`Z;tI_H>KCVKCKB!C_)Ds_+l?(l5x$Ep>>N^`q63`>3 zu(wCo@eM-+pw)NJ!Fz9W2Xo+XpZjIFHpoUJTy@M1(AfVzq3Sewp z)SBn%bo8>;l@LcG)jr93I*R|Rz`FFP`3s}ffWOz9QXxO!%f?fne_9T#gV18=EQdx zAWF7vKVCi2+apb0o)F^|swVB#45o>g!YBhlaF2Plq0((*cP)}78( zXO0=_UtgVb;=|_`%PMV9e`)tDSwH0ejM$`#L z6WN(%bg8vC@yAR?-QAq^ZiU7CvzqJbi+#J#P+P+k&g9sJeT%jnwix_r@<)191b_;B zlmS!a#SL=o*=qPU!_^DfjOx};e%M`wL1>u9X2#9u3-dkUdOV|wiL2jJVjVQML>1bT z@1UmQsccp*_oTl(>v?2rB8%Jr)<2TXcw=;p0sEwtu)tSvd#~a9SK)x4ym5wnw=WvH zr@t}cEY%@4_?L2=V-y0n@ZPdG3R(#ut0-{uV*gyL_d_`zQzCCY#(xxzTvC@yyug@` z3X*Q-dmBbAH{F{Pj4nlYbQ6Z{-uL93FG0`zgphe z9~-h>*93L@EHvjBcT_|`y_wA!qF|KaD47dIb zK1mfwQQ#^cM+UOKi1I=?aT{i4YaV1 zpjt<31h)U;&opW6qzr0Ccn3dmrLK@1s0&p`t@FY{O3Z%d+nLJ;GC)J8tHC*6 zOtsw_TMQb6#s`ouvg3tp;XPYgLHsl*o}YN0|8i4Spvb{;FKtM6-8GcZBk69h^r9^h z|Nb4_=b29r;BJoIG?vf%r%DW-DwcIjGdZB4(bl=|Dw5lu<3HWjEI#I1M1<8P`5;awcW4{W>eQ>HMBZo6#VzhzR3!4|bGtg!WN`Hn z(PUgm>f&W_70I4+D^$LV5!+6gFDKug!x+xTugGd{GGiJI6*?WhJjh%ceF;Lz-$Jc>*BgZ?D2$WF2f@`8U^!$l1=PRc6Aex*4g(B-^{;vI&G@+_ z`p8?bPYW8cyQ4*MWlvmB;Dbt~hg_4gXOEqLEMqYAa$UdXSWcA|WXGhsLsl^K-CB}8 zKh$|~!JLyb*KKZ)KoGidEt4#jwrVwjLm*MG%1!HwRTz_cPtx^Doa9y<*g_Tnx_tzm z=+0`??TQm4n0TqZMTNeId{4v&X-i@YGQNk`O{FONDrbK?UxHgkoM2g(&;D#?_T~hs zdy1L}g(7wOQ>QVG)i+70amU zOM%F8hT*;Ty|Ezcqll$i%LzY2KNsvL#U+=XnEB@B0twmFgE5Ku)?6*h@ejNXEXS>X z!{4iy&J#ap1ukJBfNsOVt^95Q+!-dQ*cZNNRG?xz`JIxgwpA7?uk!`>?U zpum!x8AKu$lw?_EnVn%kQN&4-Y4)VX3OR4Vgo&JzwO)+VSYr%(dJ1DzUe{kNT^suGJU_CcmD zn|)bHANT6;72XarX2?x_2@+{=sH#0m$3L(f5CYc6XhHXelme}-#wfb`%3T*&oPRrl z#uhyxAorDETw*+i-tU&=iz@n@3aWTpvl7Dx#eIxnAr1-^3OCM@;gVbU>QVe@w#*yu9g$}VemUWE(@p9skr8mfBX?}|U zy2rA((1iHyi2T5#9Xp#c$QKbvCB(XF;wHDdB$a!@2nA+W90!$C3{u8HajkC+CbH;E z(36hcy>Q*&TgoG)HR*Y>Bhzd>sD&w)e0;*t&|OM|@zlP=BB`x0Nf# ziASq>DLVmlFB+>=Q(@B7Y(_L(Hyd)foZJOF3{7J(Np*5zxv+-Eo(DA%3{nRklip}? zfn~XIkfZfs2*^P*C(B3EKD|BJ4}*sf!R>SrA;2dq2Q&|wuL>JF3Yp)e@jLGClA8Ss zAZu7$=u>EVM;^>&7HT+(WubhJhv{w&Fgf&1A;l%%XH46pr+zF7kjD-y&F!%h{qORD_2$vI;q+JM_m}RI0B*6 zr;U@a9XH*H``!Vg+}et(f#Q}JuNEr5y|Yz`9qwhEKci@F{BVY*viU zZ!BPpt&V@tt^N7!Yty~$v77~wkQj}F(q5I_&!Q&don2--jWP@2LpPG5Br`c7F7qTA2iw9bt1J~orf_OIm9)_O$EOl|e zAr=>m?v583L`=tYZu;d!CA+iSMlmVhEOy5phr%EyqKDZQyYE=(0wK~0@Ei^ZKvgx2(`gUo>qLpB@MRq4(Jr zO)&y*Y+3lPo5YKyvZB=h65p4KMjk_}C&V*>qM+XrPnLwEExU@W^Y>w`c2l!FvL``V zZ*HWoj~y{({kAgOyj50#Mq~Qqdi_3G5Z*qhz`9tiuG-&3E{sqEfygz9BRA|+ zaZ4A9zl(?Kn`5Fl=m{MM>HdvIy3$K6!gXD%5Ra4WJK3C)1;&`xo0`QZKg2A&{r}|Zs;d-E`(*CweY`D-Y000AbNklK))j>=DuI$iY0t<58vxY^Ah5XS z(Y>LhFd)%^>SKSRNabeZ377czAz07>4S;F|&=mDYj)RKWn`U$N`aXFu3(@l;81uxo-oY+B{ek2fY9W&}Ls7XxUtZtlcri~Bq5p75kRQTehFGzf(2EfAn1rSU~`#**rNeZjR1lLy^K?VB&1<+%;qXGIk`dX zzVy~~wa2x&jSufxh{Y{r7FQ%obAK1sVwI^u&?>gp=6LqtoZKyKarlR~X=`(51kgXx zz@eI53Y5wg7FZTWkL}t3sCEFo2F0K~WO2?=5^EXkW5PHImJ+f#M+XX2@SK>~0H{U) zLCt<6WO4SSVd}w#J=}YRn}hk}BHPUl7*@Vh1E3lKgsPrJXnRzlIcjy7Kn>OEB1)a8 zdTYzdiX9sO)d(Q4IGQ#E?@UdzM3_QJ)f-B}M;bIcO?y_}hemn74WAOw?D)I7fr5(G zD8y?PqN+D!qYaqWWMt)^ZtRh!1*od$ZBXMuJc)rD;UpDHmyCcy%R;pJG+xkpXqq2y z>{Gb~sH$fflmupRc8ShW5z0TdGETqczC@fP#Hq{X4ea+dRBrb4dyoZD-loJz1Y;St z_Mk$r3dn9>Hcyb@4#}d9@x!U z`@1KA-m!6z6Xrx~4v0ECzwOL}@B}J?Zo&wz+mYh_K+Ppu`=+eV8EJF_-VuyAC>XVn zQ12G8ELfbBx1xk${))NsrTSb}{(dSe7rRHws&sq-*5P>-R<}PGZ8@F#%2B(6q)Md8cRQGf&T|$ W*H$`*UBlx50000 Promise) => { switch (type) { case IframePageType.DatasetAnnotation: // 数据标注 - return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; + return getLabelStudioUrl; case IframePageType.AppDevelopment: // 应用开发 return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); case IframePageType.DevEnv: // 开发环境 diff --git a/react-ui/src/components/RightContent/AvatarDropdown.tsx b/react-ui/src/components/RightContent/AvatarDropdown.tsx index 9efc837a..d93d4f74 100644 --- a/react-ui/src/components/RightContent/AvatarDropdown.tsx +++ b/react-ui/src/components/RightContent/AvatarDropdown.tsx @@ -1,6 +1,8 @@ import { clearSessionToken } from '@/access'; import { setRemoteMenu } from '@/services/session'; import { logout } from '@/services/system/auth'; +import { ClientInfo } from '@/types'; +import SessionStorage from '@/utils/sessionStorage'; import { gotoLoginPage } from '@/utils/ui'; import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { setAlpha } from '@ant-design/pro-components'; @@ -64,11 +66,11 @@ const AvatarDropdown: React.FC = ({ menu }) => { clearSessionToken(); setRemoteMenu(null); gotoLoginPage(); - // const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); - // if (clientInfo) { - // const { logoutUri } = clientInfo; - // location.replace(logoutUri); - // } + const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); + if (clientInfo) { + const { logoutUri } = clientInfo; + location.replace(logoutUri); + } }; const actionClassName = useEmotionCss(({ token }) => { return { diff --git a/react-ui/src/iconfont/iconfont-menu.json b/react-ui/src/iconfont/iconfont-menu.json index 297c7837..15215226 100644 --- a/react-ui/src/iconfont/iconfont-menu.json +++ b/react-ui/src/iconfont/iconfont-menu.json @@ -1,6 +1,6 @@ { "id": "4511326", - "name": "复杂智能软件-导航", + "name": "智能材料科研平台-导航", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", diff --git a/react-ui/src/pages/User/Login/login.tsx b/react-ui/src/pages/User/Login/login.tsx index c22f8be9..9ef18148 100644 --- a/react-ui/src/pages/User/Login/login.tsx +++ b/react-ui/src/pages/User/Login/login.tsx @@ -119,10 +119,10 @@ const Login = () => { draggable={false} alt="" /> - 复杂智能软件 + 智能材料科研平台
- 复杂智能软件 + 智能材料科研平台 {
欢迎登录 - 复杂智能软件 + 智能材料科研平台
账号登录
diff --git a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less index 59a0cb5b..26a67688 100644 --- a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less +++ b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less @@ -37,7 +37,6 @@ } &__icon { - // width: 363px; height: 176px; } } diff --git a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx index f3543705..81ab8f54 100644 --- a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx +++ b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx @@ -1,14 +1,16 @@ +import { Button } from 'antd'; import styles from './index.less'; function WorkspaceIntro() { return (
-
复杂智能软件
+
自主实验平台
- 复杂智能软件平台构建一套完整的版本迭代升级机制、开发与运行态版本依赖关系分析,以及整合开发部署和持续优化的一体化流程,涵盖数据管理、模型建模、服务开发和系统运行等关键环节,以实现高效、稳定的软件生命周期管理。 + 材料领域的自主实验系统是一种用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, + 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作
- {/*
+
-
*/} +
Date: Fri, 21 Feb 2025 11:12:13 +0800 Subject: [PATCH 045/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/build.sh b/k8s/build.sh index 49041f30..5e77bdf9 100755 --- a/k8s/build.sh +++ b/k8s/build.sh @@ -54,7 +54,7 @@ compile_front() { # 编译前端 docker run -v ${baseDir}:${baseDir} \ -e http_proxy=http://172.20.32.253:3128 -e https_proxy=http://172.20.32.253:3128 \ - 172.20.32.187/ci4s/node:16.16.0 ${baseDir}/k8s/build-node.sh + 172.20.32.187/tempimagefile/node:18.16.0 ${baseDir}/k8s/build-node.sh if [ $? -ne 0 ]; then echo "编译失败,请检查代码!" exit 1 From f8ff7861d77a89548ce6862321615e985c1f5f84 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Feb 2025 14:27:19 +0800 Subject: [PATCH 046/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/k8s-10gen.yaml | 36 ----------- k8s/k8s-11visual.yaml | 36 ----------- k8s/k8s-12front.yaml | 36 ----------- k8s/k8s-3nacos.yaml | 62 ------------------- k8s/k8s-4gateway.yaml | 36 ----------- k8s/k8s-5auth.yaml | 36 ----------- k8s/k8s-6system.yaml | 36 ----------- k8s/k8s-7management.yaml | 44 ------------- k8s/k8s-8file.yaml | 36 ----------- k8s/k8s-9job.yaml | 36 ----------- k8s/template-yaml/deploy/k8s-12front.yaml | 36 ----------- k8s/template-yaml/deploy/k8s-7management.yaml | 53 ---------------- 12 files changed, 483 deletions(-) delete mode 100644 k8s/k8s-10gen.yaml delete mode 100644 k8s/k8s-11visual.yaml delete mode 100644 k8s/k8s-12front.yaml delete mode 100644 k8s/k8s-3nacos.yaml delete mode 100644 k8s/k8s-4gateway.yaml delete mode 100644 k8s/k8s-5auth.yaml delete mode 100644 k8s/k8s-6system.yaml delete mode 100644 k8s/k8s-7management.yaml delete mode 100644 k8s/k8s-8file.yaml delete mode 100644 k8s/k8s-9job.yaml delete mode 100644 k8s/template-yaml/deploy/k8s-12front.yaml delete mode 100644 k8s/template-yaml/deploy/k8s-7management.yaml diff --git a/k8s/k8s-10gen.yaml b/k8s/k8s-10gen.yaml deleted file mode 100644 index aaec3d8a..00000000 --- a/k8s/k8s-10gen.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-gen-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-gen - template: - metadata: - labels: - app: ci4s-gen - spec: - containers: - - name: ci4s-gen - image: ci4s-gen:v1.0 - ports: - - containerPort: 9202 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-gen-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9202 - nodePort: 31211 - protocol: TCP - selector: - app: ci4s-gen - diff --git a/k8s/k8s-11visual.yaml b/k8s/k8s-11visual.yaml deleted file mode 100644 index 3c2b25fb..00000000 --- a/k8s/k8s-11visual.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-visual-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-visual - template: - metadata: - labels: - app: ci4s-visual - spec: - containers: - - name: ci4s-visual - image: ci4s-visual:v1.0 - ports: - - containerPort: 9100 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-visual-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9100 - nodePort: 31212 - protocol: TCP - selector: - app: ci4s-visual - diff --git a/k8s/k8s-12front.yaml b/k8s/k8s-12front.yaml deleted file mode 100644 index fb24fc2d..00000000 --- a/k8s/k8s-12front.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-front-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-front - template: - metadata: - labels: - app: ci4s-front - spec: - containers: - - name: ci4s-front - image: 172.20.32.187/ci4s/ci4s-front:20240401 - ports: - - containerPort: 8000 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-front-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8000 - nodePort: 31213 - protocol: TCP - selector: - app: ci4s-front - diff --git a/k8s/k8s-3nacos.yaml b/k8s/k8s-3nacos.yaml deleted file mode 100644 index 0c293016..00000000 --- a/k8s/k8s-3nacos.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - namespace: argo - name: nacos-ci4s - labels: - app: nacos-ci4s -spec: - replicas: 1 - selector: - matchLabels: - app: nacos-ci4s - template: - metadata: - labels: - app: nacos-ci4s - spec: - containers: - - name: nacos-ci4s - image: nacos/nacos-server:v2.2.0 - env: - - name: SPRING_DATASOURCE_PLATFORM - value: mysql - - name: MODE - value: standalone - - name: MYSQL_SERVICE_HOST - value: mysql.argo.svc - - name: MYSQL_SERVICE_PORT - value: "3306" - - name: MYSQL_SERVICE_DB_NAME - value: nacos-ci4s-config - - name: MYSQL_SERVICE_USER - value: root - - name: MYSQL_SERVICE_PASSWORD - value: qazxc123456. - ports: - - containerPort: 8848 - - containerPort: 9848 - restartPolicy: Always - ---- - -apiVersion: v1 -kind: Service -metadata: - namespace: argo - name: nacos-ci4s - labels: - app: nacos-ci4s -spec: - type: NodePort - selector: - app: nacos-ci4s - ports: - - port: 8848 - targetPort: 8848 - nodePort: 31203 - name: web - - port: 9848 - targetPort: 9848 - nodePort: 31204 - name: podsa diff --git a/k8s/k8s-4gateway.yaml b/k8s/k8s-4gateway.yaml deleted file mode 100644 index b0cf7991..00000000 --- a/k8s/k8s-4gateway.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-gateway-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-gateway - template: - metadata: - labels: - app: ci4s-gateway - spec: - containers: - - name: ci4s-gateway - image: 172.20.32.187/ci4s/ci4s-gateway:20240401 - ports: - - containerPort: 8082 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-gateway-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8082 - nodePort: 31205 - protocol: TCP - selector: - app: ci4s-gateway - diff --git a/k8s/k8s-5auth.yaml b/k8s/k8s-5auth.yaml deleted file mode 100644 index 2066bd5d..00000000 --- a/k8s/k8s-5auth.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-auth-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-auth - template: - metadata: - labels: - app: ci4s-auth - spec: - containers: - - name: ci4s-auth - image: 172.20.32.187/ci4s/ci4s-auth:20240401 - ports: - - containerPort: 9200 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-auth-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9200 - nodePort: 31206 - protocol: TCP - selector: - app: ci4s-auth - diff --git a/k8s/k8s-6system.yaml b/k8s/k8s-6system.yaml deleted file mode 100644 index 8c6830bf..00000000 --- a/k8s/k8s-6system.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-system-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-system - template: - metadata: - labels: - app: ci4s-system - spec: - containers: - - name: ci4s-system - image: 172.20.32.187/ci4s/ci4s-system:20240401 - ports: - - containerPort: 9201 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-system-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9201 - nodePort: 31207 - protocol: TCP - selector: - app: ci4s-system - diff --git a/k8s/k8s-7management.yaml b/k8s/k8s-7management.yaml deleted file mode 100644 index cb07a130..00000000 --- a/k8s/k8s-7management.yaml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-management-platform-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-management-platform - template: - metadata: - labels: - app: ci4s-management-platform - spec: - containers: - - name: ci4s-management-platform - image: 172.20.32.187/ci4s/managent:20240401 - ports: - - containerPort: 9213 - volumeMounts: - - name: resource - mountPath: /home/resource/ - volumes: - - name: resource - hostPath: - path: /home/resource/ - type: DirectoryOrCreate - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-management-platform-service - namespace: argo -spec: - type: NodePort - ports: - - port: 9213 - nodePort: 31208 - protocol: TCP - selector: - app: ci4s-management-platform - diff --git a/k8s/k8s-8file.yaml b/k8s/k8s-8file.yaml deleted file mode 100644 index 3f54b8d0..00000000 --- a/k8s/k8s-8file.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-file-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-file - template: - metadata: - labels: - app: ci4s-file - spec: - containers: - - name: ci4s-file - image: ci4s-file:v1.0 - ports: - - containerPort: 9300 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-file-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9300 - nodePort: 31209 - protocol: TCP - selector: - app: ci4s-file - diff --git a/k8s/k8s-9job.yaml b/k8s/k8s-9job.yaml deleted file mode 100644 index b52cb355..00000000 --- a/k8s/k8s-9job.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-job-deployment - namespace: ci4s-test -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-job - template: - metadata: - labels: - app: ci4s-job - spec: - containers: - - name: ci4s-job - image: ci4s-job:v1.0 - ports: - - containerPort: 9203 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-job-service - namespace: ci4s-test -spec: - type: NodePort - ports: - - port: 9203 - nodePort: 31210 - protocol: TCP - selector: - app: ci4s-job - diff --git a/k8s/template-yaml/deploy/k8s-12front.yaml b/k8s/template-yaml/deploy/k8s-12front.yaml deleted file mode 100644 index 565b12ec..00000000 --- a/k8s/template-yaml/deploy/k8s-12front.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-front-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-front - template: - metadata: - labels: - app: ci4s-front - spec: - containers: - - name: ci4s-front - image: 172.20.32.187/ci4s/ci4s-front:202406120836 - ports: - - containerPort: 8000 - ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-front-service - namespace: argo -spec: - type: NodePort - ports: - - port: 8000 - nodePort: 31213 - protocol: TCP - selector: - app: ci4s-front - diff --git a/k8s/template-yaml/deploy/k8s-7management.yaml b/k8s/template-yaml/deploy/k8s-7management.yaml deleted file mode 100644 index 75f1b522..00000000 --- a/k8s/template-yaml/deploy/k8s-7management.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ci4s-management-platform-deployment - namespace: argo -spec: - replicas: 1 - selector: - matchLabels: - app: ci4s-management-platform - template: - metadata: - labels: - app: ci4s-management-platform - spec: - containers: - - name: ci4s-management-platform - image: 172.20.32.187/ci4s/ci4s-managent:202409201355 - env: - - name: TZ - value: Asia/Shanghai - - name: JAVA_TOOL_OPTIONS - value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" - ports: - - containerPort: 9213 - volumeMounts: - - name: resource-volume - mountPath: /home/resource/ - volumes: - - name: resource-volume - persistentVolumeClaim: - claimName: platform-data-pvc-nfs ---- -apiVersion: v1 -kind: Service -metadata: - name: ci4s-management-platform-service - namespace: argo -spec: - type: NodePort - ports: - - name: http - port: 9213 - nodePort: 31208 - protocol: TCP - - name: debug - nodePort: 34567 - port: 5005 - protocol: TCP - targetPort: 5005 - selector: - app: ci4s-management-platform - From fb71c682163403f6060cfbc37361d5e598e834f6 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 24 Feb 2025 08:48:00 +0800 Subject: [PATCH 047/127] =?UTF-8?q?chore:=20=E9=A2=9C=E8=89=B2=E6=95=B4?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/tsconfig.json | 1 + .../src/components/BasicTableInfo/index.less | 4 +- react-ui/src/components/InfoGroup/index.less | 2 +- .../ResourceSelectorModal/index.less | 14 ++-- .../pages/Experiment/Comparison/index.less | 4 +- .../CreateForm/ParameterRange/index.less | 2 +- .../Model/components/ModelMetrics/index.less | 2 +- .../components/RobotFrame/index.less | 2 +- react-ui/src/stories/KFModal.mdx | 45 +++++++++++ react-ui/src/stories/KFModal.stories.tsx | 5 +- react-ui/src/stories/docs/Colors.mdx | 77 +++++++++++++++++++ react-ui/src/stories/docs/Git-Commit.mdx | 25 ++++++ react-ui/src/stories/docs/Less.mdx | 2 +- react-ui/src/styles/theme.less | 12 +-- react-ui/tsconfig.json | 1 + 15 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 react-ui/src/stories/KFModal.mdx create mode 100644 react-ui/src/stories/docs/Colors.mdx create mode 100644 react-ui/src/stories/docs/Git-Commit.mdx diff --git a/react-ui/.storybook/tsconfig.json b/react-ui/.storybook/tsconfig.json index 601d7708..e30a508b 100644 --- a/react-ui/.storybook/tsconfig.json +++ b/react-ui/.storybook/tsconfig.json @@ -21,6 +21,7 @@ "incremental": true, // 通过读写磁盘上的文件来启用增量编译 "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 "strictNullChecks": true, // 启用严格的null检查 + "importHelpers": true, "baseUrl": "./" } } diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index 479fe332..1207d033 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -4,7 +4,7 @@ flex-wrap: wrap; align-items: stretch; width: 100%; - border: 1px solid @border-color-base; + border: 1px solid @border-color; border-bottom: none; border-radius: 4px; @@ -12,7 +12,7 @@ display: flex; align-items: stretch; width: 25%; - border-bottom: 1px solid @border-color-base; + border-bottom: 1px solid @border-color; &__label { flex: none; diff --git a/react-ui/src/components/InfoGroup/index.less b/react-ui/src/components/InfoGroup/index.less index 4dccf4c7..94c56187 100644 --- a/react-ui/src/components/InfoGroup/index.less +++ b/react-ui/src/components/InfoGroup/index.less @@ -4,7 +4,7 @@ &__content { padding: 20px @content-padding; background-color: white; - border: 1px solid @border-color-base; + border: 1px solid @border-color; border-top: none; border-radius: 0 0 4px 4px; } diff --git a/react-ui/src/components/ResourceSelectorModal/index.less b/react-ui/src/components/ResourceSelectorModal/index.less index cffe4caf..1581e510 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.less +++ b/react-ui/src/components/ResourceSelectorModal/index.less @@ -22,8 +22,8 @@ height: 398px; margin-right: 15px; padding: 15px; - background-color: @background-color-primary; - border: 1px solid @border-color; + background-color: rgba(22, 100, 255, 0.03); + border: 1px solid rgba(22, 100, 255, 0.3); border-radius: 8px; &__search { @@ -31,7 +31,7 @@ padding-left: 0; background-color: transparent; border-width: 0; - border-bottom: 1px solid @border-color-secondary; + border-bottom: 1px solid rgba(22, 100, 255, 0.1); border-radius: 0; } @@ -45,8 +45,8 @@ width: calc(100% - 488px - 15px); height: 398px; padding: 15px; - background-color: @background-color-primary; - border: 1px solid @border-color; + background-color: rgba(22, 100, 255, 0.03); + border: 1px solid rgba(22, 100, 255, 0.3); border-radius: 8px; &__title { @@ -56,7 +56,7 @@ color: @text-color; font-size: @font-size; line-height: 46px; - border-bottom: 1px solid @border-color-secondary; + border-bottom: 1px solid rgba(22, 100, 255, 0.1); } &__files { height: calc(100% - 75px); @@ -68,7 +68,7 @@ color: @text-color-secondary; font-size: 13px; word-break: break-all; - background: @background-color-gray; + background: rgba(4, 3, 3, 0.06); border-radius: 4px; } } diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index 3be69ed9..4dce8268 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -29,7 +29,7 @@ div { flex: 1; height: 1px; - background-color: @border-color-base; + background-color: @border-color; } p { @@ -45,7 +45,7 @@ .ant-table-thead { .ant-table-cell { background-color: rgb(247, 247, 247); - border-color: @border-color-base !important; + border-color: @border-color !important; } } .ant-table-tbody { diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less index 61091050..25edd4bc 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.less @@ -15,7 +15,7 @@ } } &__desc { - margin-bottom: 20px; + margin-bottom: 15px; padding: 4px 8px; color: @text-color-tertiary; font-size: 13px; diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.less b/react-ui/src/pages/Model/components/ModelMetrics/index.less index 77b763d8..03123746 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.less +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.less @@ -12,7 +12,7 @@ .ant-table-thead { .ant-table-cell { background-color: rgb(247, 247, 247); - border-color: @border-color-base !important; + border-color: @border-color !important; } } .ant-table-tbody { diff --git a/react-ui/src/pages/Workspace/components/RobotFrame/index.less b/react-ui/src/pages/Workspace/components/RobotFrame/index.less index a203ecc3..f6ecbe07 100644 --- a/react-ui/src/pages/Workspace/components/RobotFrame/index.less +++ b/react-ui/src/pages/Workspace/components/RobotFrame/index.less @@ -23,7 +23,7 @@ width: 100%; height: 60px; padding: 0 15px; - border-bottom: 1px solid @border-color-base; + border-bottom: 1px solid @border-color; } &__iframe { diff --git a/react-ui/src/stories/KFModal.mdx b/react-ui/src/stories/KFModal.mdx new file mode 100644 index 00000000..8bd711bf --- /dev/null +++ b/react-ui/src/stories/KFModal.mdx @@ -0,0 +1,45 @@ +import { Meta, Title, Subtitle, Description, Primary, Controls, Stories } from '@storybook/blocks'; +import * as KFModalStories from "./KFModal.stories" + + + + +<Subtitle /> +<Description /> + +## Usage + +为了风格统一,应用中的其它 Modal 应该使用 **KFModal** 进行封装,例如 **CodeSelectorModal** + +```ts +export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { + onOk?: (params: CodeConfigData | undefined) => void; +} + +function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { + return ( + <KFModal + {...rest} + title="选择代码配置" + image={require('@/assets/img/modal-code-config.png')} + width={920} + footer={null} + destroyOnClose + > + <div>children</div> + </KFModal> + ); +} + +export default CodeSelectorModal; + +``` + +## Primary + +<Primary /> + +<Controls /> + +<Stories /> + diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 763aaf31..d1c30a91 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -15,7 +15,7 @@ const meta = { layout: 'centered', }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], + tags: ['!autodocs'], // More on argTypes: https://storybook.js.org/docs/api/argtypes argTypes: { // backgroundColor: { control: 'color' }, @@ -38,6 +38,7 @@ export default meta; type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 作为子组件时,通过 `open` 属性打开或关闭 */ export const Primary: Story = { args: { title: '创建实验', @@ -71,7 +72,7 @@ export const Primary: Story = { }, }; -/** 通过 `openAntdModal` 函数打开 */ +/** 推荐通过 `openAntdModal` 函数打开 */ export const OpenByFunction: Story = { render: function Render() { const handleClick = () => { diff --git a/react-ui/src/stories/docs/Colors.mdx b/react-ui/src/stories/docs/Colors.mdx new file mode 100644 index 00000000..c90bddc8 --- /dev/null +++ b/react-ui/src/stories/docs/Colors.mdx @@ -0,0 +1,77 @@ + +import { Meta, ColorPalette, ColorItem } from '@storybook/blocks'; + +<Meta title="Documentation/Colors" /> + +# Colors + +<ColorPalette> + <ColorItem + title="theme.color.primary" + subtitle="主题色" + colors={{ "primary-color": '#1664ff' }} + /> + <ColorItem + title="theme.color.primary-hover" + subtitle="主题色:hover" + colors={{ "primary-color-hover": "#69b1ff" }} + /> + <ColorItem + title="theme.color.underline" + subtitle="链接的下划线" + colors={{ "underline-color": "#5d93ff" }} + /> + <ColorItem + title="theme.color.success" + subtitle="成功色" + colors={{ "success-color": '#6ac21d' }} + /> + <ColorItem + title="theme.color.error" + subtitle="失败色" + colors={{ "error-color": '#c73131' }} + /> + <ColorItem + title="theme.color.warning" + subtitle="警告色" + colors={{ "warning-color": '#f98e1b' }} + /> + <ColorItem + title="theme.color.warning" + subtitle="终止" + colors={{ "abort-color": '#8a8a8a' }} + /> + <ColorItem + title="theme.color.warning" + subtitle="等待运行" + colors={{ "pending-color": '#ecb934' }} + /> + <ColorItem + title="theme.color.background" + subtitle="背景色" + colors={{ + "background-color": '#f9fafb', + "sider-background-color": '#f2f5f7' + }} + /> + <ColorItem + title="theme.color.font" + subtitle="字体颜色" + colors={{ + "text-color": '#1d1d20', + "text-color-secondary": '#575757', + "text-color-tertiary": '#8a8a8a', + "text-disabled-color": 'rgba(0, 0, 0, 0.25)', + }} + /> + <ColorItem + title="theme.color.placeholder" + subtitle="placeholder 颜色" + colors={{ "text-placeholder-color": 'rgba(0, 0, 0, 0.25)' }} + /> + <ColorItem + title="theme.color.border" + subtitle="边框色" + colors={{ "border-color": '#eaeaea' }} + /> +</ColorPalette> \ No newline at end of file diff --git a/react-ui/src/stories/docs/Git-Commit.mdx b/react-ui/src/stories/docs/Git-Commit.mdx new file mode 100644 index 00000000..26e81138 --- /dev/null +++ b/react-ui/src/stories/docs/Git-Commit.mdx @@ -0,0 +1,25 @@ +import { Meta } from '@storybook/blocks'; + +<Meta title="Documentation/Git Commit" /> + +# Git 提交规范 + +遵循 [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines) 规范: + +- feat: 增加新功能 +- fix: 修复 bug +- build: 更改构建系统或外部依赖(例如:gulp, broccoli, npm) +- ci: 更改CI配置文件和脚本(例如:Travis, Circle, BrowserStack, SauceLabs) +- docs: 更改文档 +- perf: 提高性能的修改 +- refactor: 重构,但是即不是新增功能,也不是修复 bug +- style: 修改格式或样式,但是不会影响代码的运行(例如:删除空白、添加分号等) +- test: 添加测试 +- chore: 其它不修改 src 或测试文件的更改 + +例如 + +```sh +$ git commit -m "feat: 集成 Storybook 框架" +``` + diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 1b3a6632..2fc5b9bd 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -1,4 +1,4 @@ -import { Meta, Controls } from '@storybook/blocks'; +import { Meta } from '@storybook/blocks'; <Meta title="Documentation/Less" /> diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index ff7813f9..d044889f 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -8,6 +8,7 @@ @primary-color: #1664ff; // 主色调 @primary-color-secondary: #4e89ff; @primary-color-hover: #69b1ff; +@sider-background-color: #f2f5f7; // 侧边栏背景颜色 @background-color: #f9fafb; // 页面背景颜色 @text-color: #1d1d20; @text-color-secondary: #575757; @@ -20,19 +21,12 @@ @abort-color: #8a8a8a; @pending-color: #ecb934; @underline-color: #5d93ff; +@border-color: #eaeaea; -@border-color-base: #eaeaea; -@border-color: rgba(22, 100, 255, 0.3); -@border-color-secondary: rgba(22, 100, 255, 0.1); -@background-color-primary: rgba(22, 100, 255, 0.03); -@background-color-gray: rgba(4, 3, 3, 0.06); - +@link-hover-color: #69b1ff; @heading-color: rgba(0, 0, 0, 0.85); @input-icon-hover-color: rgba(0, 0, 0, 0.85); -@link-hover-color: #69b1ff; -@sider-background-color: #f2f5f7; - @workspace-background: linear-gradient( 179.03deg, rgba(138, 138, 138, 0.06) 0%, diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index 55ce7f74..4eb2830b 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -21,6 +21,7 @@ "incremental": true, // 通过读写磁盘上的文件来启用增量编译 "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 "strictNullChecks": true, // 启用严格的null检查 + "importHelpers": true, "baseUrl": "./", "paths": { "@/*": ["src/*"], From df55816eb96fcc590dc597b3cb8ad41e7c5e47b9 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 25 Feb 2025 09:09:40 +0800 Subject: [PATCH 048/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/domain/Ray.java | 4 ++-- .../com/ruoyi/platform/service/impl/RayServiceImpl.java | 8 ++++---- .../src/main/java/com/ruoyi/platform/vo/RayVo.java | 2 +- .../resources/mapper/managementPlatform/RayDaoMapper.xml | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index ce8257ab..674c6e34 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -26,8 +26,8 @@ public class Ray { @ApiModelProperty(value = "数据集挂载路径") private String model; - @ApiModelProperty(value = "代码") - private String code; + @ApiModelProperty(value = "代码配置") + private String codeConfig; @ApiModelProperty(value = "镜像") private String image; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index fc7ef4be..32c19964 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -47,7 +47,7 @@ public class RayServiceImpl implements RayService { ray.setCreateBy(username); ray.setUpdateBy(username); ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); - ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); + ray.setCodeConfig(JacksonUtil.toJSONString(rayVo.getCodeConfig())); ray.setModel(JacksonUtil.toJSONString(rayVo.getModel())); ray.setImage(JacksonUtil.toJSONString(rayVo.getImage())); ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); @@ -68,7 +68,7 @@ public class RayServiceImpl implements RayService { ray.setParameters(JacksonUtil.toJSONString(rayVo.getParameters())); ray.setPointsToEvaluate(JacksonUtil.toJSONString(rayVo.getPointsToEvaluate())); ray.setDataset(JacksonUtil.toJSONString(rayVo.getDataset())); - ray.setCode(JacksonUtil.toJSONString(rayVo.getCode())); + ray.setCodeConfig(JacksonUtil.toJSONString(rayVo.getCodeConfig())); ray.setModel(JacksonUtil.toJSONString(rayVo.getModel())); ray.setImage(JacksonUtil.toJSONString(rayVo.getImage())); rayDao.edit(ray); @@ -93,8 +93,8 @@ public class RayServiceImpl implements RayService { if (StringUtils.isNotEmpty(ray.getDataset())) { rayVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); } - if (StringUtils.isNotEmpty(ray.getCode())) { - rayVo.setCode(JsonUtils.jsonToMap(ray.getCode())); + if (StringUtils.isNotEmpty(ray.getCodeConfig())) { + rayVo.setCodeConfig(JsonUtils.jsonToMap(ray.getCodeConfig())); } if (StringUtils.isNotEmpty(ray.getModel())) { rayVo.setModel(JsonUtils.jsonToMap(ray.getModel())); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 6e8963ee..4cf000e5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -70,7 +70,7 @@ public class RayVo { private String runState; @ApiModelProperty(value = "代码") - private Map<String, Object> code; + private Map<String, Object> codeConfig; private Map<String, Object> dataset; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index a23ba911..236141c4 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -2,10 +2,10 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.RayDao"> <insert id="save"> - insert into ray(name, description, dataset, model, code, main_py, num_samples, parameters, points_to_evaluate, storage_path, + insert into ray(name, description, dataset, model, code_config, main_py, num_samples, parameters, points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, min_samples_required, resource, image, create_by, update_by) - values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.model}, #{ray.code}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.model}, #{ray.codeConfig}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, #{ray.resource}, #{ray.image}, #{ray.createBy}, #{ray.updateBy}) @@ -26,8 +26,8 @@ <if test="ray.model != null and ray.model !=''"> model = #{ray.model}, </if> - <if test="ray.code != null and ray.code !=''"> - code = #{ray.code}, + <if test="ray.codeConfig != null and ray.codeConfig !=''"> + code_config = #{ray.codeConfig}, </if> <if test="ray.image != null and ray.image !=''"> image = #{ray.image}, From 1e97edd8d65dd475165b1f978821622b45261493 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 25 Feb 2025 09:47:47 +0800 Subject: [PATCH 049/127] =?UTF-8?q?docs:=20=E4=BF=AE=E6=94=B9msw=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/main.ts | 2 +- react-ui/package.json | 2 +- .../src/components/DisabledInput/index.tsx | 21 --- .../{DisabledInput => FormInfo}/index.less | 13 +- react-ui/src/components/FormInfo/index.tsx | 41 ++++++ .../components/ExperimentInstance/index.tsx | 2 +- .../components/ExperimentInstance/index.tsx | 18 +-- .../components/ExperimentParameter/index.tsx | 25 ++-- react-ui/src/pages/Experiment/index.jsx | 2 +- .../src/stories/CodeSelectorModal.stories.tsx | 1 + react-ui/src/stories/FormInfo.stories.tsx | 124 ++++++++++++++++++ react-ui/src/stories/KFModal.stories.tsx | 9 +- .../stories/ResourceSelectorModal.stories.tsx | 1 + react-ui/src/stories/docs/Less.mdx | 2 +- react-ui/static/favicon.ico | Bin 0 -> 4286 bytes .../{public => static}/mockServiceWorker.js | 0 16 files changed, 213 insertions(+), 50 deletions(-) delete mode 100644 react-ui/src/components/DisabledInput/index.tsx rename react-ui/src/components/{DisabledInput => FormInfo}/index.less (55%) create mode 100644 react-ui/src/components/FormInfo/index.tsx create mode 100644 react-ui/src/stories/FormInfo.stories.tsx create mode 100644 react-ui/static/favicon.ico rename react-ui/{public => static}/mockServiceWorker.js (100%) diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index 820a0eeb..54824837 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -16,7 +16,7 @@ const config: StorybookConfig = { name: '@storybook/react-webpack5', options: {}, }, - staticDirs: ['../public'], + staticDirs: ['../static'], docs: { defaultName: 'Documentation', }, diff --git a/react-ui/package.json b/react-ui/package.json index 2b2cfd4b..56a4b735 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -166,7 +166,7 @@ }, "msw": { "workerDirectory": [ - "public" + "static" ] } } diff --git a/react-ui/src/components/DisabledInput/index.tsx b/react-ui/src/components/DisabledInput/index.tsx deleted file mode 100644 index 3a31def8..00000000 --- a/react-ui/src/components/DisabledInput/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Typography } from 'antd'; -import styles from './index.less'; - -type DisabledInputProps = { - value?: any; - valuePropName?: string; -}; - -/** - * 模拟禁用的输入框,但是完全显示内容 - */ -function DisabledInput({ value, valuePropName }: DisabledInputProps) { - const data = valuePropName ? value[valuePropName] : value; - return ( - <div className={styles['disabled-input']}> - <Typography.Text ellipsis={{ tooltip: data }}>{data}</Typography.Text> - </div> - ); -} - -export default DisabledInput; diff --git a/react-ui/src/components/DisabledInput/index.less b/react-ui/src/components/FormInfo/index.less similarity index 55% rename from react-ui/src/components/DisabledInput/index.less rename to react-ui/src/components/FormInfo/index.less index 06808c5a..868404c7 100644 --- a/react-ui/src/components/DisabledInput/index.less +++ b/react-ui/src/components/FormInfo/index.less @@ -1,4 +1,5 @@ -.disabled-input { +.form-info { + min-height: 32px; padding: 4px 11px; color: @text-disabled-color; font-size: @font-size-input; @@ -6,4 +7,14 @@ border: 1px solid #d9d9d9; border-radius: 6px; cursor: not-allowed; + + .ant-typography { + margin: 0 !important; + } +} + +.form-info--multiline { + .ant-typography { + white-space: pre-wrap; + } } diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx new file mode 100644 index 00000000..a784e433 --- /dev/null +++ b/react-ui/src/components/FormInfo/index.tsx @@ -0,0 +1,41 @@ +import { Typography } from 'antd'; +import classNames from 'classnames'; +import './index.less'; + +type FormInfoProps = { + /** 自定义类名 */ + value?: any; + /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ + valuePropName?: string; + /** 是否是多行 */ + multiline?: boolean; + /** 自定义类名 */ + className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; +}; + +/** + * 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容 + */ +function FormInfo({ value, valuePropName, className, style, multiline = false }: FormInfoProps) { + const data = value && typeof value === 'object' && valuePropName ? value[valuePropName] : value; + return ( + <div + className={classNames( + 'form-info', + { + 'form-info--multiline': multiline, + }, + className, + )} + style={style} + > + <Typography.Paragraph ellipsis={multiline ? false : { tooltip: data }}> + {data} + </Typography.Paragraph> + </div> + ); +} + +export default FormInfo; diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx index ea319dc8..ab713f48 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -107,7 +107,7 @@ function ExperimentInstanceComponent({ }; if (!experimentInsList || experimentInsList.length === 0) { - return null; + return <div style={{ textAlign: 'center' }}>暂无实验实例</div>; } return ( diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 4d68d93c..9b37dba8 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -20,7 +20,7 @@ import TensorBoardStatusCell from '../TensorBoardStatus'; import styles from './index.less'; type ExperimentInstanceProps = { - experimentInList?: ExperimentInstance[]; + experimentInsList?: ExperimentInstance[]; experimentInsTotal: number; onClickInstance?: (instance: ExperimentInstance) => void; onClickTensorBoard?: (instance: ExperimentInstance) => void; @@ -30,7 +30,7 @@ type ExperimentInstanceProps = { }; function ExperimentInstanceComponent({ - experimentInList, + experimentInsList, experimentInsTotal, onClickInstance, onClickTensorBoard, @@ -40,8 +40,8 @@ function ExperimentInstanceComponent({ }: ExperimentInstanceProps) { const { message } = App.useApp(); const allIntanceIds = useMemo(() => { - return experimentInList?.map((item) => item.id) || []; - }, [experimentInList]); + return experimentInsList?.map((item) => item.id) || []; + }, [experimentInsList]); const [ selectedIns, setSelectedIns, @@ -57,7 +57,7 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInList]); + }, [experimentInsList]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { @@ -118,8 +118,8 @@ function ExperimentInstanceComponent({ } }; - if (!experimentInList || experimentInList.length === 0) { - return null; + if (!experimentInsList || experimentInsList.length === 0) { + return <div style={{ textAlign: 'center' }}>暂无数据</div>; } return ( @@ -152,7 +152,7 @@ function ExperimentInstanceComponent({ </div> </div> - {experimentInList.map((item, index) => ( + {experimentInsList.map((item, index) => ( <div key={item.id} className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} @@ -244,7 +244,7 @@ function ExperimentInstanceComponent({ </div> </div> ))} - {experimentInsTotal > experimentInList.length ? ( + {experimentInsTotal > experimentInsList.length ? ( <div className={styles.loadMoreBox}> <Button type="link" onClick={onLoadMore}> 更多 diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index 82f75e2c..235e3087 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -1,11 +1,10 @@ -import ParameterInput from '@/components/ParameterInput'; +import FormInfo from '@/components/FormInfo'; import ParameterSelect from '@/components/ParameterSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; import { PipelineNodeModelSerialize } from '@/types'; -import { Form, Input, Select } from 'antd'; +import { Form, Select } from 'antd'; import styles from './index.less'; -const { TextArea } = Input; type ExperimentParameterProps = { nodeData: PipelineNodeModelSerialize; @@ -64,7 +63,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { }, ]} > - <Input disabled /> + <FormInfo /> </Form.Item> <Form.Item label="任务ID" @@ -76,7 +75,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { }, ]} > - <Input disabled /> + <FormInfo /> </Form.Item> <div className={styles['experiment-parameter__title']}> <SubAreaTitle @@ -94,14 +93,14 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { }, ]} > - <Input disabled /> + <FormInfo /> </Form.Item> <Form.Item label="工作目录" name="working_directory"> - <Input disabled /> + <FormInfo /> </Form.Item> <Form.Item label="启动命令" name="command"> - <TextArea disabled /> + <FormInfo multiline /> </Form.Item> <Form.Item label="资源规格" @@ -123,14 +122,14 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { /> </Form.Item> <Form.Item label="挂载路径" name="mount_path"> - <Input disabled /> + <FormInfo /> </Form.Item> <Form.Item label="环境变量" name="env_variables"> - <TextArea disabled /> + <FormInfo multiline /> </Form.Item> {controlStrategyList.map((item) => ( <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> - <ParameterInput disabled /> + <FormInfo valuePropName="showValue" /> </Form.Item> ))} <div className={styles['experiment-parameter__title']}> @@ -149,7 +148,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { {item.value.type === 'select' ? ( <ParameterSelect disabled /> ) : ( - <ParameterInput disabled /> + <FormInfo valuePropName="showValue" /> )} </Form.Item> ))} @@ -166,7 +165,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { label={item.value.label + '(' + item.key + ')'} rules={[{ required: item.value.require ? true : false }]} > - <ParameterInput disabled /> + <FormInfo valuePropName="showValue" /> </Form.Item> ))} </Form> diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index de8504f5..70345689 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -537,7 +537,7 @@ function Experiment() { expandable={{ expandedRowRender: (record) => ( <ExperimentInstance - experimentInList={experimentInList} + experimentInsList={experimentInList} experimentInsTotal={experimentInsTotal} onClickInstance={(item) => gotoInstanceInfo(item, record)} onClickTensorBoard={handleTensorboard} diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx index 59026ec6..a042f238 100644 --- a/react-ui/src/stories/CodeSelectorModal.stories.tsx +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -71,6 +71,7 @@ export const Primary: Story = { /** 通过 `openAntdModal` 函数打开 */ export const OpenByFunction: Story = { + name: '通过函数的方式打开', render: function Render(args) { const handleClick = () => { const { close } = openAntdModal(CodeSelectorModal, { diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx new file mode 100644 index 00000000..b822427f --- /dev/null +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -0,0 +1,124 @@ +import FormInfo from '@/components/FormInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Form, Input, Select, Typography } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/FormInfo', + component: FormInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta<typeof FormInfo>; + +export default meta; +type Story = StoryObj<typeof meta>; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const InForm: Story = { + render: () => { + return ( + <Form + name="form" + style={{ width: 300 }} + labelCol={{ flex: '100px' }} + initialValues={{ + text: '文本', + large_text: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + multiline_text: `多行文本\n超长文本超长文本超长文本超长文本\n 多行文本`, + object_text: { + value: 1, + showValue: '对象文本', + }, + input_text: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + antd_select: 1, + select_text: 1, + select_large_text: 1, + }} + > + <Form.Item label="文本" name="text"> + <FormInfo /> + </Form.Item> + <Form.Item label="超长文本" name="large_text"> + <FormInfo /> + </Form.Item> + <Form.Item label="多行文本" name="multiline_text"> + <FormInfo multiline /> + </Form.Item> + <Form.Item label="对象" name="object_text"> + <FormInfo valuePropName="showValue" /> + </Form.Item> + <Form.Item label="无内容" name="empty_text"> + <FormInfo /> + </Form.Item> + <Form.Item label="Input" name="input_text"> + <Input disabled /> + </Form.Item> + <Form.Item label="Select" name="antd_select"> + <Select + disabled + options={[ + { + label: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + value: 1, + }, + ]} + /> + </Form.Item> + <Form.Item label="Select" name="select_text"> + <Select + labelRender={(props) => { + return ( + <div style={{ width: '100%', lineHeight: 'normal' }}> + <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> + {props.label} + </Typography.Text> + </div> + ); + }} + disabled + options={[ + { + label: '选择文本', + value: 1, + }, + ]} + /> + </Form.Item> + <Form.Item label="Long Select" name="select_large_text"> + <Select + labelRender={(props) => { + return ( + <div style={{ width: '100%', lineHeight: 'normal' }}> + <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> + {props.label} + </Typography.Text> + </div> + ); + }} + disabled + options={[ + { + label: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + value: 1, + }, + ]} + /> + </Form.Item> + </Form> + ); + }, +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index d1c30a91..024a2708 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -3,7 +3,7 @@ import KFModal from '@/components/KFModal'; import { openAntdModal } from '@/utils/modal'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; +import { expect, fn, screen, userEvent, within } from '@storybook/test'; import { Button } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export @@ -70,10 +70,17 @@ export const Primary: Story = { </> ); }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole('button', { name: /打开 KFModal/i })); + const dialog = await screen.findByRole('dialog'); + await expect(dialog).toBeInTheDocument(); + }, }; /** 推荐通过 `openAntdModal` 函数打开 */ export const OpenByFunction: Story = { + name: '通过函数的方式打开', render: function Render() { const handleClick = () => { const { close } = openAntdModal(KFModal, { diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx index 9e72efd2..b39cb45e 100644 --- a/react-ui/src/stories/ResourceSelectorModal.stories.tsx +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -182,6 +182,7 @@ export const Mirror: Story = { /** 通过 `openAntdModal` 函数打开 */ export const OpenByFunction: Story = { + name: '通过函数的方式打开', args: { type: ResourceSelectorType.Mirror, }, diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 2fc5b9bd..9098d96d 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -148,7 +148,7 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { ``` -### 一些建议 +### 一点建议 如果你陷入嵌套地狱,比如 diff --git a/react-ui/static/favicon.ico b/react-ui/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..408b8a236709dc94679a96a769597c3d94443a93 GIT binary patch literal 4286 zcmb`GS!|V65XYytR1jqqq0-WpZj`OGKuZgy6ezoi`e5+IG%mEgU7>sFN(<_XhD0La zL5)5Tj88~3(S*gMf-%NKBqqjah(r{l2vMND-!lHr*WTJ*Z`&*B*Zex?o0&8JnRC7? z62)Iql9>N;YPQIH5y=2tQUu2DJ;Wk;26}c%pm&D^4)2uyS9V7QUfR7DUWRk<DZCaq zvU|;8iUp4BB5#-2FYSq>bVNB;?{=}!wt9E^=%ECSAHfA3!S}?TqmO8#@6g9&J%OHW zgY)<9C<q+haUFCFe&d|BnzOo9ohfr4jG2@cEcET3VjuQ+pm)2DOV75qtV6|9%pUD7 znk4;Vv3iOhg7ehY3AjjnCQP3Gp)Fz^+VUWLWOZ-R3HXHgbeJ@K`%sYtI_RNma|5`b z<IrmIiBF>bX0bY{*;y28b#6KXItG1KS5X{z`jc|`6me^}i8)U+#p)<bfY;$Ct7GF` zxC8e<;jGnBFdM{fTQ3fTK6ZQlaLj7oAa?tD4?UVH1FaiF)4g|X-HLYMzjPGDptTg8 z9oS;Ky`TvOtoDL_YL;Nd?xJ>U{-}C^Cj1A{AA-CG?Dh>0+uemx#utFGo&YmB?HlO< zI!0i$+Sbp7^PtdYx8=_V`KxX*TDkO*E83YSRtsyR-L^i4_><W8;2i6t*1`NNw>4)h z`C-4&b~rPGIcM!zM$J6KUbqW3`0dtpVjDe=9I=~oJ^g#-sV27^Zh?;AGV8oyp$Hb* zo#XZ4X|=AKf&GA46~2XbsD@m6h_Rd3(#u-0T65#@T?UgE%G*I%ZOE4XmMn1^v&F7o zB~~5tm~~^8hc;x2!ERd3Y-_M<W9*ikqo5(&K=)Pjv)~zGr=UM1C%EUVHKcK`Wu>R% zpy&1A`G@oy277RfHQIxeU~hwl!NK<{41|dV^G!~#)38c{Fz<A<SHcslpN2f<f5c&K zlVcm2=&31t9(>MOC>Fw5<6C_XzJ_<<8*&}2@%O_?i1%tYW=)~~<IHy&u5$(hV8Kn^ zKO^=mOmpf@AJCY!0sa7;fMZTmcC6c+9nCB$<h}sS@D$8;xJ##grH{IP>Y|mYVX4o& zL(a?O=3*P+B6&{O+>tfvGt14nIn8`C!}n?KG~2slbI(rQid|0K%IlyLFhKnG335i( zxJhmSm>I=<XXg8H8tJDIpLsX)A6Wh&=>OohQ@8v)yu+T`W9)N@zrk$Nu>77fgFmzX zSZDvzo1hVb-m(&yN&Zx9nX`Y{cdVU|{eSbm$Jw7DZcVy4HA}EdM61)qs?8AHvtYOn zr#2%18X-5x%QSV-7HwK1yr;wS_}Z`s;VMvX$w{|%Ng~>YShpr)id&r~L7b{ouLt`3 z!Kq2t2>F)0I1uI=O)c++n)I%rT6eME!&SHfC)}E}0`z=tbt-+Nh?b>bQ(prYGy;y< z)oIZVb7*av*fpGkInU~)gZ2E0QPijhYm8dI;x9!z+UUDg$&iu^7g+1~q+DUo3bW7N zk3KU5AGac5s8(_THLj0eC;2zxTR?R0g5bVYZsp>S!3GV(C47aBIkSqz;+FlZt(EhH zn79>-ec0n+_J*tXrdyd5?N%hA&o^QAS0+8`RwS0f`|u@v1!u^qgGHv_!MtSdI3+l` zc={EI=qJvAKF|pGf}FX)e$KNp&ggsf)xYQg*Pl37D-!0p{)A~>kHq@Ry&k#uq+xBd z(3&~o`WG2yKp$uXTq0)<h<0rfyOutxSXae!zTBnhYsk7)N%Tl>P25*m!YFM|pYzvp z_Le31-17KipcA3x@h6zmhaFW<E9UcEF%#Na2?wZM9Xl!~^d-heK6Tu(IC_Z7*RqAz zK*!-1@(PKIQ<5B7?!Ml&jB_hpDDJ-aC@p>LS-1uohM)00r~4i=d7}Qf`_c>Dq~+8? z->doJe4p-Huo+ImHL&4NIIa6)i?#GopO(fB=K2$*UlMhYS=dlaY!*2)#YR*OS4A#h b$9Oj3kA|q>xLFyGVJgfLx!Oe??e_QwVjhSf literal 0 HcmV?d00001 diff --git a/react-ui/public/mockServiceWorker.js b/react-ui/static/mockServiceWorker.js similarity index 100% rename from react-ui/public/mockServiceWorker.js rename to react-ui/static/mockServiceWorker.js From 8d6f0e2cb52fe23cf9c1338c19948a87cb3b8e6a Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 26 Feb 2025 10:38:39 +0800 Subject: [PATCH 050/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/platform/mapper/RayInsDao.java | 2 + .../scheduling/AutoMlInsStatusTask.java | 3 +- .../platform/scheduling/RayInsStatusTask.java | 95 ++++++++++++ .../ruoyi/platform/service/RayInsService.java | 4 + .../service/impl/RayInsServiceImpl.java | 146 ++++++++++++++++-- .../platform/service/impl/RayServiceImpl.java | 83 +++++++++- .../com/ruoyi/platform/vo/RayParamVo.java | 50 ++++++ .../managementPlatform/RayInsDaoMapper.xml | 8 + 8 files changed, 379 insertions(+), 12 deletions(-) create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java index ceeb581d..86801d04 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/RayInsDao.java @@ -18,4 +18,6 @@ public interface RayInsDao { int insert(@Param("rayIns") RayIns rayIns); int update(@Param("rayIns") RayIns rayIns); + + List<RayIns> queryByRayInsIsNotTerminated(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/AutoMlInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/AutoMlInsStatusTask.java index 5c94284f..fa07acf6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/AutoMlInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/AutoMlInsStatusTask.java @@ -1,5 +1,6 @@ package com.ruoyi.platform.scheduling; +import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.AutoMl; import com.ruoyi.platform.domain.AutoMlIns; import com.ruoyi.platform.mapper.AutoMlDao; @@ -41,7 +42,7 @@ public class AutoMlInsStatusTask { try { autoMlIns = autoMlInsService.queryStatusFromArgo(autoMlIns); } catch (Exception e) { - autoMlIns.setStatus("Failed"); + autoMlIns.setStatus(Constant.Failed); } // 线程安全的添加操作 synchronized (autoMlIds) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java new file mode 100644 index 00000000..23d03164 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -0,0 +1,95 @@ +package com.ruoyi.platform.scheduling; + +import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.domain.RayIns; +import com.ruoyi.platform.mapper.RayDao; +import com.ruoyi.platform.mapper.RayInsDao; +import com.ruoyi.platform.service.RayInsService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +@Component() +public class RayInsStatusTask { + + @Resource + private RayInsService rayInsService; + @Resource + private RayInsDao rayInsDao; + @Resource + private RayDao rayDao; + + private List<Long> rayIds = new ArrayList<>(); + + @Scheduled(cron = "0/30 * * * * ?") // 每30S执行一次 + public void executeRayInsStatus() { + List<RayIns> rayInsList = rayInsService.queryByRayInsIsNotTerminated(); + + // 去argo查询状态 + List<RayIns> updateList = new ArrayList<>(); + if (rayInsList != null && rayInsList.size() > 0) { + for (RayIns rayIns : rayInsList) { + //当原本状态为null或非终止态时才调用argo接口 + try { + rayIns = rayInsService.queryStatusFromArgo(rayIns); + } catch (Exception e) { + rayIns.setStatus(Constant.Failed); + } + // 线程安全的添加操作 + synchronized (rayIds) { + rayIds.add(rayIns.getRayId()); + } + updateList.add(rayIns); + } + if (updateList.size() > 0) { + for (RayIns rayIns : updateList) { + rayInsDao.update(rayIns); + } + } + } + } + + @Scheduled(cron = "0/30 * * * * ?") // / 每30S执行一次 + public void executeRayStatus() { + if (rayIds.size() == 0) { + return; + } + // 存储需要更新的实验对象列表 + List<Ray> updateRays = new ArrayList<>(); + for (Long rayId : rayIds) { + // 获取当前实验的所有实例列表 + List<RayIns> insList = rayInsDao.getByRayId(rayId); + List<String> statusList = new ArrayList<>(); + // 更新实验状态列表 + for (int i = 0; i < insList.size(); i++) { + statusList.add(insList.get(i).getStatus()); + } + String subStatus = statusList.toString().substring(1, statusList.toString().length() - 1); + Ray ray = rayDao.getRayById(rayId); + if (!StringUtils.equals(ray.getStatusList(), subStatus)) { + ray.setStatusList(subStatus); + updateRays.add(ray); + rayDao.edit(ray); + } + } + + if (!updateRays.isEmpty()) { + // 使用Iterator进行安全的删除操作 + Iterator<Long> iterator = rayIds.iterator(); + while (iterator.hasNext()) { + Long rayId = iterator.next(); + for (Ray ray : updateRays) { + if (ray.getId().equals(rayId)) { + iterator.remove(); + } + } + } + } + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java index 867dd0a9..63559ffe 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java @@ -18,4 +18,8 @@ public interface RayInsService { RayIns getDetailById(Long id) throws IOException; void updateRayStatus(Long rayId); + + RayIns queryStatusFromArgo(RayIns ins); + + List<RayIns> queryByRayInsIsNotTerminated(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 976a43b5..f881faa4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -6,8 +6,11 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; +import com.ruoyi.platform.utils.DateUtils; +import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.JsonUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -23,6 +26,13 @@ import java.util.stream.Collectors; @Service("rayInsService") public class RayInsServiceImpl implements RayInsService { + @Value("${argo.url}") + private String argoUrl; + @Value("${argo.workflowStatus}") + private String argoWorkflowStatus; + @Value("${argo.workflowTermination}") + private String argoWorkflowTermination; + @Resource private RayInsDao rayInsDao; @@ -49,7 +59,7 @@ public class RayInsServiceImpl implements RayInsService { return "实验实例不存在"; } if (StringUtils.isEmpty(rayIns.getStatus())) { - //todo queryStatusFromArgo + rayIns = queryStatusFromArgo(rayIns); } if (StringUtils.equals(rayIns.getStatus(), Constant.Running)) { return "实验实例正在运行,不可删除"; @@ -89,7 +99,7 @@ public class RayInsServiceImpl implements RayInsService { // 获取当前状态,如果为空,则从Argo查询 if (StringUtils.isEmpty(currentStatus)) { - // todo queryStatusFromArgo + currentStatus = queryStatusFromArgo(rayIns).getStatus(); } // 只有状态是"Running"时才能终止实例 @@ -97,20 +107,64 @@ public class RayInsServiceImpl implements RayInsService { throw new Exception("终止错误,只有运行状态的实例才能终止"); // 如果不是"Running"状态,则不执行终止操作 } - //todo terminateFromArgo + // 创建请求数据map + Map<String, Object> requestData = new HashMap<>(); + requestData.put("namespace", namespace); + requestData.put("name", name); + // 创建发送数据map,将请求数据作为"data"键的值 + Map<String, Object> res = new HashMap<>(); + res.put("data", requestData); + + try { + // 发送POST请求到Argo工作流状态查询接口,并将请求数据转换为JSON + String req = HttpUtils.sendPost(argoUrl + argoWorkflowTermination, null, JsonUtils.mapToJson(res)); + // 检查响应是否为空或无内容 + if (StringUtils.isEmpty(req)) { + throw new RuntimeException("终止响应内容为空。"); + + } + // 将响应的JSON字符串转换为Map对象 + Map<String, Object> runResMap = JsonUtils.jsonToMap(req); + // 从响应Map中直接获取"errCode"的值 + Integer errCode = (Integer) runResMap.get("errCode"); + if (errCode != null && errCode == 0) { + //更新autoMlIns,确保状态更新被保存到数据库 + RayIns ins = queryStatusFromArgo(rayIns); + String nodeStatus = ins.getNodeStatus(); + Map<String, Object> nodeMap = JsonUtils.jsonToMap(nodeStatus); - rayIns.setStatus(Constant.Terminated); - rayIns.setFinishTime(new Date()); - this.rayInsDao.update(rayIns); - updateRayStatus(rayIns.getRayId()); - return true; + // 遍历 map + for (Map.Entry<String, Object> entry : nodeMap.entrySet()) { + // 获取每个 Map 中的值并强制转换为 Map + Map<String, Object> innerMap = (Map<String, Object>) entry.getValue(); + // 检查 phase 的值 + if (innerMap.containsKey("phase")) { + String phaseValue = (String) innerMap.get("phase"); + // 如果值不等于 Succeeded,则赋值为 Failed + if (!StringUtils.equals("Succeeded", phaseValue)) { + innerMap.put("phase", "Failed"); + } + } + } + ins.setNodeStatus(JsonUtils.mapToJson(nodeMap)); + ins.setStatus(Constant.Terminated); + ins.setUpdateTime(new Date()); + rayInsDao.update(ins); + updateRayStatus(rayIns.getRayId()); + return true; + } else { + return false; + } + } catch (Exception e) { + throw new RuntimeException("终止实例错误: " + e.getMessage(), e); + } } @Override public RayIns getDetailById(Long id) throws IOException { RayIns rayIns = rayInsDao.queryById(id); if (Constant.Running.equals(rayIns.getStatus()) || Constant.Pending.equals(rayIns.getStatus())) { - //todo queryStatusFromArgo + rayIns = queryStatusFromArgo(rayIns); } rayIns.setTrialList(getTrialList(rayIns.getResultPath())); return rayIns; @@ -132,6 +186,80 @@ public class RayInsServiceImpl implements RayInsService { } } + @Override + public RayIns queryStatusFromArgo(RayIns ins) { + String namespace = ins.getArgoInsNs(); + String name = ins.getArgoInsName(); + + // 创建请求数据map + Map<String, Object> requestData = new HashMap<>(); + requestData.put("namespace", namespace); + requestData.put("name", name); + + // 创建发送数据map,将请求数据作为"data"键的值 + Map<String, Object> res = new HashMap<>(); + res.put("data", requestData); + + try { + // 发送POST请求到Argo工作流状态查询接口,并将请求数据转换为JSON + String req = HttpUtils.sendPost(argoUrl + argoWorkflowStatus, null, JsonUtils.mapToJson(res)); + // 检查响应是否为空或无内容 + if (req == null || StringUtils.isEmpty(req)) { + throw new RuntimeException("工作流状态响应为空。"); + } + // 将响应的JSON字符串转换为Map对象 + Map<String, Object> runResMap = JsonUtils.jsonToMap(req); + // 从响应Map中获取"data"部分 + Map<String, Object> data = (Map<String, Object>) runResMap.get("data"); + if (data == null || data.isEmpty()) { + throw new RuntimeException("工作流数据为空."); + } + // 从"data"中获取"status"部分,并返回"phase"的值 + Map<String, Object> status = (Map<String, Object>) data.get("status"); + if (status == null || status.isEmpty()) { + throw new RuntimeException("工作流状态为空。"); + } + + //解析流水线结束时间 + String finishedAtString = (String) status.get("finishedAt"); + if (finishedAtString != null && !finishedAtString.isEmpty()) { + Date finishTime = DateUtils.convertUTCtoShanghaiDate(finishedAtString); + ins.setFinishTime(finishTime); + } + + // 解析nodes字段,提取节点状态并转换为JSON字符串 + Map<String, Object> nodes = (Map<String, Object>) status.get("nodes"); + Map<String, Object> modifiedNodes = new LinkedHashMap<>(); + if (nodes != null) { + for (Map.Entry<String, Object> nodeEntry : nodes.entrySet()) { + Map<String, Object> nodeDetails = (Map<String, Object>) nodeEntry.getValue(); + String templateName = (String) nodeDetails.get("displayName"); + modifiedNodes.put(templateName, nodeDetails); + } + } + + + String nodeStatusJson = JsonUtils.mapToJson(modifiedNodes); + ins.setNodeStatus(nodeStatusJson); + + //终止态为终止不改 + if (!StringUtils.equals(ins.getStatus(), Constant.Terminated)) { + ins.setStatus(StringUtils.isNotEmpty((String) status.get("phase")) ? (String) status.get("phase") : Constant.Pending); + } + if (StringUtils.equals(ins.getStatus(), "Error")) { + ins.setStatus(Constant.Failed); + } + return ins; + } catch (Exception e) { + throw new RuntimeException("查询状态失败: " + e.getMessage(), e); + } + } + + @Override + public List<RayIns> queryByRayInsIsNotTerminated() { + return rayInsDao.queryByRayInsIsNotTerminated(); + } + public ArrayList<Map<String, Object>> getTrialList(String directoryPath) throws IOException { // 获取指定路径下的所有文件 Path dirPath = Paths.get(directoryPath); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 32c19964..1d812659 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -4,14 +4,22 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.AutoMlIns; import com.ruoyi.platform.domain.Ray; +import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; +import com.ruoyi.platform.mapper.RayInsDao; +import com.ruoyi.platform.service.RayInsService; import com.ruoyi.platform.service.RayService; +import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.utils.JsonUtils; +import com.ruoyi.platform.vo.RayParamVo; import com.ruoyi.platform.vo.RayVo; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -20,13 +28,30 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @Service("rayService") public class RayServiceImpl implements RayService { + + @Value("${argo.url}") + private String argoUrl; + @Value("${argo.convertRay}") + String convertRay; + @Value("${argo.workflowRun}") + private String argoWorkflowRun; + + @Value("${minio.endpoint}") + private String minioEndpoint; + @Resource private RayDao rayDao; + @Resource + private RayInsDao rayInsDao; + @Resource + private RayInsService rayInsService; @Override public Page<Ray> queryByPage(String name, PageRequest pageRequest) { @@ -126,7 +151,61 @@ public class RayServiceImpl implements RayService { if (ray == null) { throw new Exception("自动超参数寻优配置不存在"); } - //todo argo - return null; + + RayParamVo rayParamVo = new RayParamVo(); + BeanUtils.copyProperties(ray, rayParamVo); + rayParamVo.setCodeConfig(JsonUtils.jsonToMap(ray.getCodeConfig())); + rayParamVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); + rayParamVo.setModel(JsonUtils.jsonToMap(ray.getModel())); + rayParamVo.setImage(JsonUtils.jsonToMap(ray.getImage())); + String param = JsonUtils.objectToJson(rayParamVo); + + // 调argo转换接口 + try { + String convertRes = HttpUtils.sendPost(argoUrl + convertRay, param); + if (convertRes == null || StringUtils.isEmpty(convertRes)) { + throw new RuntimeException("转换流水线失败"); + } + Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes); + // 组装运行接口json + Map<String, Object> output = (Map<String, Object>) converMap.get("output"); + Map<String, Object> runReqMap = new HashMap<>(); + runReqMap.put("data", converMap.get("data")); + // 调argo运行接口 + String runRes = HttpUtils.sendPost(argoUrl + argoWorkflowRun, JsonUtils.mapToJson(runReqMap)); + if (runRes == null || StringUtils.isEmpty(runRes)) { + throw new RuntimeException("Failed to run workflow."); + } + Map<String, Object> runResMap = JsonUtils.jsonToMap(runRes); + Map<String, Object> data = (Map<String, Object>) runResMap.get("data"); + //判断data为空 + if (data == null || MapUtils.isEmpty(data)) { + throw new RuntimeException("Failed to run workflow."); + } + Map<String, Object> metadata = (Map<String, Object>) data.get("metadata"); + + // 插入记录到实验实例表 + RayIns rayIns = new RayIns(); + rayIns.setRayId(ray.getId()); + rayIns.setArgoInsNs((String) metadata.get("namespace")); + rayIns.setArgoInsName((String) metadata.get("name")); + rayIns.setParam(param); + rayIns.setStatus(Constant.Pending); + //替换argoInsName + String outputString = JsonUtils.mapToJson(output); + rayIns.setNodeResult(outputString.replace("{{workflow.name}}", (String) metadata.get("name"))); + + Map<String, Object> param_output = (Map<String, Object>) output.get("param_output"); + List output1 = (ArrayList) param_output.values().toArray()[0]; + Map<String, String> output2 = (Map<String, String>) output1.get(0); + String outputPath = minioEndpoint + "/" + output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/"; + + rayIns.setResultPath(outputPath); + rayInsDao.insert(rayIns); + rayInsService.updateRayStatus(id); + } catch (Exception e) { + throw new RuntimeException(e); + } + return "执行成功"; } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java new file mode 100644 index 00000000..4b6e1c5a --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java @@ -0,0 +1,50 @@ +package com.ruoyi.platform.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModel; +import lombok.Data; + +import java.util.Map; + +@Data +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@ApiModel(description = "超参数寻优参数") +public class RayParamVo { + + private Map<String,Object> codeConfig; + + private Map<String,Object> dataset; + + private Map<String,Object> image; + + private Map<String,Object> model; + + private String mainPy; + + private String name; + + private Integer numSamples; + + private String parameters; + + private String pointsToEvaluate; + + private String storagePath; + + private String searchAlg; + + private String scheduler; + + private String metric; + + private String mode; + + private Integer maxT; + + private Integer minSamplesRequired; + + private String resource; +} diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml index ca4b7231..2b9c12ac 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml @@ -66,4 +66,12 @@ and state = 1 order by update_time DESC limit 5 </select> + + <select id="queryByRayInsIsNotTerminated" resultType="com.ruoyi.platform.domain.RayIns"> + select * + from ray_ins + where (status NOT IN ('Terminated', 'Succeeded', 'Failed') + OR status IS NULL) + and state = 1 + </select> </mapper> \ No newline at end of file From f3da9e5c3f80804df2133a3f5bb31f99e5a44a90 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 26 Feb 2025 11:24:32 +0800 Subject: [PATCH 051/127] =?UTF-8?q?feat:=20storybook=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/blocks/StoryName.tsx | 19 ++ react-ui/.storybook/main.ts | 2 +- react-ui/.storybook/manager.ts | 6 + react-ui/.storybook/theme.ts | 7 + react-ui/package.json | 4 +- .../{static => public}/mockServiceWorker.js | 0 react-ui/src/components/BasicInfo/index.tsx | 6 + react-ui/src/stories/BasicInfo.stories.tsx | 2 +- .../src/stories/BasicTableInfo.stories.tsx | 2 +- react-ui/src/stories/CodeSelect.stories.tsx | 2 +- .../src/stories/CodeSelectorModal.stories.tsx | 38 +--- react-ui/src/stories/Config.stories.tsx | 2 +- react-ui/src/stories/FormInfo.stories.tsx | 2 +- .../src/stories/FullScreenFrame.stories.tsx | 2 +- react-ui/src/stories/IFramePage.stories.tsx | 2 +- react-ui/src/stories/InfoGroup.stories.tsx | 2 +- react-ui/src/stories/KFEmpty.stories.tsx | 2 +- react-ui/src/stories/KFIcon.stories.tsx | 2 +- react-ui/src/stories/KFModal.stories.tsx | 2 +- react-ui/src/stories/KFRadio.stories.tsx | 2 +- react-ui/src/stories/KFSpin.stories.tsx | 2 +- .../src/stories/MenuIconSelector.stories.tsx | 2 +- react-ui/src/stories/PageTitle.stories.tsx | 2 +- .../src/stories/ParameterInput.stories.tsx | 2 +- .../src/stories/ResourceSelect.stories.tsx | 2 +- .../src/stories/ResourceSelectorModal.mdx | 26 ++- .../stories/ResourceSelectorModal.stories.tsx | 197 ++++++------------ react-ui/src/stories/SubAreaTitle.stories.tsx | 2 +- react-ui/static/favicon.ico | Bin 4286 -> 0 bytes 29 files changed, 145 insertions(+), 196 deletions(-) create mode 100644 react-ui/.storybook/blocks/StoryName.tsx create mode 100644 react-ui/.storybook/manager.ts create mode 100644 react-ui/.storybook/theme.ts rename react-ui/{static => public}/mockServiceWorker.js (100%) delete mode 100644 react-ui/static/favicon.ico diff --git a/react-ui/.storybook/blocks/StoryName.tsx b/react-ui/.storybook/blocks/StoryName.tsx new file mode 100644 index 00000000..074c73cb --- /dev/null +++ b/react-ui/.storybook/blocks/StoryName.tsx @@ -0,0 +1,19 @@ +import { Of, useOf } from '@storybook/blocks'; + +/** + * A block that displays the story name or title from the of prop + * - if a story reference is passed, it renders the story name + * - if a meta reference is passed, it renders the stories' title + * - if nothing is passed, it defaults to the primary story + */ +export const StoryName = ({ of }: { of?: Of }) => { + const resolvedOf = useOf(of || 'story', ['story', 'meta']); + switch (resolvedOf.type) { + case 'story': { + return <h3 className="css-wzniqs">{resolvedOf.story.name}</h3>; + } + case 'meta': { + return <h3 className="css-wzniqs">{resolvedOf.preparedMeta.title}</h3>; + } + } +}; diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts index 54824837..820a0eeb 100644 --- a/react-ui/.storybook/main.ts +++ b/react-ui/.storybook/main.ts @@ -16,7 +16,7 @@ const config: StorybookConfig = { name: '@storybook/react-webpack5', options: {}, }, - staticDirs: ['../static'], + staticDirs: ['../public'], docs: { defaultName: 'Documentation', }, diff --git a/react-ui/.storybook/manager.ts b/react-ui/.storybook/manager.ts new file mode 100644 index 00000000..baf80b25 --- /dev/null +++ b/react-ui/.storybook/manager.ts @@ -0,0 +1,6 @@ +import { addons } from '@storybook/manager-api'; +import theme from './theme'; + +addons.setConfig({ + theme: theme, +}); diff --git a/react-ui/.storybook/theme.ts b/react-ui/.storybook/theme.ts new file mode 100644 index 00000000..7b624111 --- /dev/null +++ b/react-ui/.storybook/theme.ts @@ -0,0 +1,7 @@ +import { create } from '@storybook/theming'; +export default create({ + base: 'light', + brandTitle: '组件库文档', + brandUrl: 'https://storybook.js.org/docs', + brandTarget: '_blank', +}); diff --git a/react-ui/package.json b/react-ui/package.json index 56a4b735..f2583d6b 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -96,9 +96,11 @@ "@storybook/addon-webpack5-compiler-babel": "~3.0.5", "@storybook/addon-webpack5-compiler-swc": "~2.0.0", "@storybook/blocks": "~8.5.3", + "@storybook/manager-api": "~8.6.0", "@storybook/react": "~8.5.3", "@storybook/react-webpack5": "~8.5.3", "@storybook/test": "~8.5.3", + "@storybook/theming": "~8.6.0", "@testing-library/react": "^14.0.0", "@types/antd": "^1.0.0", "@types/express": "^4.17.14", @@ -166,7 +168,7 @@ }, "msw": { "workerDirectory": [ - "static" + "public" ] } } diff --git a/react-ui/static/mockServiceWorker.js b/react-ui/public/mockServiceWorker.js similarity index 100% rename from react-ui/static/mockServiceWorker.js rename to react-ui/public/mockServiceWorker.js diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 11eacfde..68622d7c 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -24,6 +24,12 @@ export type BasicInfoProps = { /** * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 + * + * ### usage + * ```tsx + * import { BasicInfo } from '@/components/BasicInfo'; + * <BasicInfo datas={datas} labelWidth={80} /> + * ``` */ export default function BasicInfo({ datas, diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index 80a0337c..eddbec12 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -6,7 +6,7 @@ import { Button } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/BasicInfo', + title: 'Components/BasicInfo 基本信息', component: BasicInfo, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index 82581f6a..cdde73fc 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -4,7 +4,7 @@ import * as BasicInfoStories from './BasicInfo.stories'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/BasicTableInfo', + title: 'Components/BasicTableInfo 表格基本信息', component: BasicTableInfo, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index b5520c96..08520603 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -8,7 +8,7 @@ import { codeListData } from './mockData'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/CodeSelect', + title: 'Components/CodeSelect 代码配置选择器', component: CodeSelect, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx index a042f238..9faae0ae 100644 --- a/react-ui/src/stories/CodeSelectorModal.stories.tsx +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -1,6 +1,5 @@ import CodeSelectorModal from '@/components/CodeSelectorModal'; import { openAntdModal } from '@/utils/modal'; -import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; import { Button } from 'antd'; @@ -9,7 +8,7 @@ import { codeListData } from './mockData'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/CodeSelectorModal', + title: 'Components/CodeSelectorModal 代码选择对话框', component: CodeSelectorModal, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout @@ -39,39 +38,8 @@ export default meta; type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - open: false, - }, - render: function Render({ onOk, onCancel, ...args }) { - const [{ open }, updateArgs] = useArgs(); - function onClick() { - updateArgs({ open: true }); - } - function handleOk(res: any) { - updateArgs({ open: false }); - onOk?.(res); - } - - function handleCancel() { - updateArgs({ open: false }); - onCancel?.(); - } - return ( - <> - <Button type="primary" onClick={onClick}> - 选择代码配置 - </Button> - <CodeSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> - </> - ); - }, -}; - -/** 通过 `openAntdModal` 函数打开 */ -export const OpenByFunction: Story = { - name: '通过函数的方式打开', +export const Primary: Story = { render: function Render(args) { const handleClick = () => { const { close } = openAntdModal(CodeSelectorModal, { @@ -84,7 +52,7 @@ export const OpenByFunction: Story = { }; return ( <Button type="primary" onClick={handleClick}> - 以函数的方式打开 + 选择代码配置 </Button> ); }, diff --git a/react-ui/src/stories/Config.stories.tsx b/react-ui/src/stories/Config.stories.tsx index 0287d718..5c2a42ce 100644 --- a/react-ui/src/stories/Config.stories.tsx +++ b/react-ui/src/stories/Config.stories.tsx @@ -4,7 +4,7 @@ import * as BasicInfoStories from './BasicInfo.stories'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/ConfigInfo', + title: 'Components/ConfigInfo 配置信息', component: ConfigInfo, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index b822427f..8e2d89de 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -4,7 +4,7 @@ import { Form, Input, Select, Typography } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/FormInfo', + title: 'Components/FormInfo 表单信息', component: FormInfo, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx index fa334ed1..74dae50a 100644 --- a/react-ui/src/stories/FullScreenFrame.stories.tsx +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -4,7 +4,7 @@ import { fn } from '@storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/FullScreenFrame', + title: 'Components/FullScreenFrame 全屏iframe', component: FullScreenFrame, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/IFramePage.stories.tsx b/react-ui/src/stories/IFramePage.stories.tsx index 3d1714e8..d5c6725b 100644 --- a/react-ui/src/stories/IFramePage.stories.tsx +++ b/react-ui/src/stories/IFramePage.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/IFramePage', + title: 'Components/IFramePage iframe页面', component: IFramePage, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/InfoGroup.stories.tsx b/react-ui/src/stories/InfoGroup.stories.tsx index f3a6def8..3fe86b80 100644 --- a/react-ui/src/stories/InfoGroup.stories.tsx +++ b/react-ui/src/stories/InfoGroup.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/InfoGroup', + title: 'Components/InfoGroup 信息分组', component: InfoGroup, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/KFEmpty.stories.tsx b/react-ui/src/stories/KFEmpty.stories.tsx index fef1d5bf..369695be 100644 --- a/react-ui/src/stories/KFEmpty.stories.tsx +++ b/react-ui/src/stories/KFEmpty.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/KFEmpty', + title: 'Components/KFEmpty 空状态', component: KFEmpty, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/KFIcon.stories.tsx b/react-ui/src/stories/KFIcon.stories.tsx index 44cb274e..7367b302 100644 --- a/react-ui/src/stories/KFIcon.stories.tsx +++ b/react-ui/src/stories/KFIcon.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Button } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/KFIcon', + title: 'Components/KFIcon 图标', component: KFIcon, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx index 024a2708..054634f2 100644 --- a/react-ui/src/stories/KFModal.stories.tsx +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -8,7 +8,7 @@ import { Button } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/KFModal', + title: 'Components/KFModal 对话框', component: KFModal, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/KFRadio.stories.tsx b/react-ui/src/stories/KFRadio.stories.tsx index c48c16e9..14bfd07c 100644 --- a/react-ui/src/stories/KFRadio.stories.tsx +++ b/react-ui/src/stories/KFRadio.stories.tsx @@ -5,7 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/KFRadio', + title: 'Components/KFRadio 单选框', component: KFRadio, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/KFSpin.stories.tsx b/react-ui/src/stories/KFSpin.stories.tsx index 7f3185d2..75f9a872 100644 --- a/react-ui/src/stories/KFSpin.stories.tsx +++ b/react-ui/src/stories/KFSpin.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/KFSpin', + title: 'Components/KFSpin 加载器', component: KFSpin, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx index f02fb77c..d89e7a6d 100644 --- a/react-ui/src/stories/MenuIconSelector.stories.tsx +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -6,7 +6,7 @@ import { Button } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/MenuIconSelector', + title: 'Components/MenuIconSelector 菜单图标选择器', component: MenuIconSelector, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/PageTitle.stories.tsx b/react-ui/src/stories/PageTitle.stories.tsx index 216591b1..b8eb31c7 100644 --- a/react-ui/src/stories/PageTitle.stories.tsx +++ b/react-ui/src/stories/PageTitle.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/PageTitle', + title: 'Components/PageTitle 页面标题', component: PageTitle, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx index 9d9525e2..914b6196 100644 --- a/react-ui/src/stories/ParameterInput.stories.tsx +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -5,7 +5,7 @@ import { useState } from 'react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/ParameterInput', + title: 'Components/ParameterInput 参数输入框', component: ParameterInput, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx index 28c314e3..93474d7f 100644 --- a/react-ui/src/stories/ResourceSelect.stories.tsx +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -21,7 +21,7 @@ import { // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/ResourceSelect', + title: 'Components/ResourceSelect 资源选择器', component: ResourceSelect, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/ResourceSelectorModal.mdx b/react-ui/src/stories/ResourceSelectorModal.mdx index 28702123..f7e0c569 100644 --- a/react-ui/src/stories/ResourceSelectorModal.mdx +++ b/react-ui/src/stories/ResourceSelectorModal.mdx @@ -1,13 +1,22 @@ -import { Meta, Canvas } from '@storybook/blocks'; +import { Meta, Title, Subtitle, Description, Primary, Controls, Stories } from '@storybook/blocks'; import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" +import { StoryName } from "../../.storybook/blocks/StoryName" + +<Meta of={ResourceSelectorModalStories} /> + +<Title /> +<Subtitle /> +<Description /> -<Meta of={ResourceSelectorModalStories} name="Usage" /> - -# Usage +### Usage 推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 ```ts +import { openAntdModal } from '@/utils/modal'; +import ResourceSelectorModal } from '@/components/ResourceSelectorModal'; + +// 打开资源选择对话框 const handleClick = () => { const { close } = openAntdModal(ResourceSelectorModal, { type: ResourceSelectorType.Dataset, @@ -18,6 +27,11 @@ const handleClick = () => { }); ``` - -<Canvas of={ResourceSelectorModalStories.OpenByFunction} /> +### Primary + +<Primary /> + +<Controls /> + +<Stories of={ResourceSelectorModalStories} /> diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx index b39cb45e..b43a8e5b 100644 --- a/react-ui/src/stories/ResourceSelectorModal.stories.tsx +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -1,6 +1,5 @@ import ResourceSelectorModal, { ResourceSelectorType } from '@/components/ResourceSelectorModal'; import { openAntdModal } from '@/utils/modal'; -import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; import { Button } from 'antd'; @@ -18,14 +17,42 @@ import { // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/ResourceSelectorModal', + title: 'Components/ResourceSelectorModal 资源选择对话框', component: ResourceSelectorModal, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, }, // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], + tags: ['!autodocs'], // More on argTypes: https://storybook.js.org/docs/api/argtypes argTypes: { // backgroundColor: { control: 'color' }, @@ -45,158 +72,58 @@ export default meta; type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 选择数据集 */ export const Dataset: Story = { args: { type: ResourceSelectorType.Dataset, - open: false, }, - parameters: { - msw: { - handlers: [ - http.get('/api/mmp/newdataset/queryDatasets', () => { - return HttpResponse.json(datasetListData); - }), - http.get('/api/mmp/newdataset/getVersionList', () => { - return HttpResponse.json(datasetVersionData); - }), - http.get('/api/mmp/newdataset/getDatasetDetail', () => { - return HttpResponse.json(datasetDetailData); - }), - ], - }, - }, - render: function Render({ onOk, onCancel, ...args }) { - const [{ open }, updateArgs] = useArgs(); - function onClick() { - updateArgs({ open: true }); - } - function handleOk(res: any) { - updateArgs({ open: false }); - onOk?.(res); - } - - function handleCancel() { - updateArgs({ open: false }); - onCancel?.(); - } - + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: args.type, + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; return ( - <> - <Button type="primary" onClick={onClick}> - 选择数据集 - </Button> - <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> - </> + <Button type="primary" onClick={handleClick}> + 选择数据集 + </Button> ); }, }; +/** 选择模型 */ export const Model: Story = { args: { type: ResourceSelectorType.Model, - open: false, - }, - parameters: { - msw: { - handlers: [ - http.get('/api/mmp/newmodel/queryModels', () => { - return HttpResponse.json(modelListData); - }), - http.get('/api/mmp/newmodel/getVersionList', () => { - return HttpResponse.json(modelVersionData); - }), - http.get('/api/mmp/newmodel/getModelDetail', () => { - return HttpResponse.json(modelDetailData); - }), - ], - }, }, - render: function Render({ onOk, onCancel, ...args }) { - const [{ open }, updateArgs] = useArgs(); - function onClick() { - updateArgs({ open: true }); - } - function handleOk(res: any) { - updateArgs({ open: false }); - onOk?.(res); - } - - function handleCancel() { - updateArgs({ open: false }); - onCancel?.(); - } - + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: args.type, + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; return ( - <> - <Button type="primary" onClick={onClick}> - 选择模型 - </Button> - <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> - </> + <Button type="primary" onClick={handleClick}> + 选择模型 + </Button> ); }, }; +/** 选择镜像 */ export const Mirror: Story = { args: { type: ResourceSelectorType.Mirror, - open: false, - }, - parameters: { - msw: { - handlers: [ - http.get('/api/mmp/image', () => { - return HttpResponse.json(mirrorListData); - }), - http.get('/api/mmp/imageVersion', () => { - return HttpResponse.json(mirrorVerionData); - }), - ], - }, - }, - render: function Render({ onOk, onCancel, ...args }) { - const [{ open }, updateArgs] = useArgs(); - function onClick() { - updateArgs({ open: true }); - } - function handleOk(res: any) { - updateArgs({ open: false }); - onOk?.(res); - } - - function handleCancel() { - updateArgs({ open: false }); - onCancel?.(); - } - - return ( - <> - <Button type="primary" onClick={onClick}> - 选择镜像 - </Button> - <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> - </> - ); - }, -}; - -/** 通过 `openAntdModal` 函数打开 */ -export const OpenByFunction: Story = { - name: '通过函数的方式打开', - args: { - type: ResourceSelectorType.Mirror, - }, - parameters: { - msw: { - handlers: [ - http.get('/api/mmp/image', () => { - return HttpResponse.json(mirrorListData); - }), - http.get('/api/mmp/imageVersion', () => { - return HttpResponse.json(mirrorVerionData); - }), - ], - }, }, render: function Render(args) { const handleClick = () => { @@ -211,7 +138,7 @@ export const OpenByFunction: Story = { }; return ( <Button type="primary" onClick={handleClick}> - 以函数的方式打开 + 选择镜像 </Button> ); }, diff --git a/react-ui/src/stories/SubAreaTitle.stories.tsx b/react-ui/src/stories/SubAreaTitle.stories.tsx index 9fad7d71..30506448 100644 --- a/react-ui/src/stories/SubAreaTitle.stories.tsx +++ b/react-ui/src/stories/SubAreaTitle.stories.tsx @@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/SubAreaTitle', + title: 'Components/SubAreaTitle 子区域标题', component: SubAreaTitle, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/static/favicon.ico b/react-ui/static/favicon.ico deleted file mode 100644 index 408b8a236709dc94679a96a769597c3d94443a93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmb`GS!|V65XYytR1jqqq0-WpZj`OGKuZgy6ezoi`e5+IG%mEgU7>sFN(<_XhD0La zL5)5Tj88~3(S*gMf-%NKBqqjah(r{l2vMND-!lHr*WTJ*Z`&*B*Zex?o0&8JnRC7? z62)Iql9>N;YPQIH5y=2tQUu2DJ;Wk;26}c%pm&D^4)2uyS9V7QUfR7DUWRk<DZCaq zvU|;8iUp4BB5#-2FYSq>bVNB;?{=}!wt9E^=%ECSAHfA3!S}?TqmO8#@6g9&J%OHW zgY)<9C<q+haUFCFe&d|BnzOo9ohfr4jG2@cEcET3VjuQ+pm)2DOV75qtV6|9%pUD7 znk4;Vv3iOhg7ehY3AjjnCQP3Gp)Fz^+VUWLWOZ-R3HXHgbeJ@K`%sYtI_RNma|5`b z<IrmIiBF>bX0bY{*;y28b#6KXItG1KS5X{z`jc|`6me^}i8)U+#p)<bfY;$Ct7GF` zxC8e<;jGnBFdM{fTQ3fTK6ZQlaLj7oAa?tD4?UVH1FaiF)4g|X-HLYMzjPGDptTg8 z9oS;Ky`TvOtoDL_YL;Nd?xJ>U{-}C^Cj1A{AA-CG?Dh>0+uemx#utFGo&YmB?HlO< zI!0i$+Sbp7^PtdYx8=_V`KxX*TDkO*E83YSRtsyR-L^i4_><W8;2i6t*1`NNw>4)h z`C-4&b~rPGIcM!zM$J6KUbqW3`0dtpVjDe=9I=~oJ^g#-sV27^Zh?;AGV8oyp$Hb* zo#XZ4X|=AKf&GA46~2XbsD@m6h_Rd3(#u-0T65#@T?UgE%G*I%ZOE4XmMn1^v&F7o zB~~5tm~~^8hc;x2!ERd3Y-_M<W9*ikqo5(&K=)Pjv)~zGr=UM1C%EUVHKcK`Wu>R% zpy&1A`G@oy277RfHQIxeU~hwl!NK<{41|dV^G!~#)38c{Fz<A<SHcslpN2f<f5c&K zlVcm2=&31t9(>MOC>Fw5<6C_XzJ_<<8*&}2@%O_?i1%tYW=)~~<IHy&u5$(hV8Kn^ zKO^=mOmpf@AJCY!0sa7;fMZTmcC6c+9nCB$<h}sS@D$8;xJ##grH{IP>Y|mYVX4o& zL(a?O=3*P+B6&{O+>tfvGt14nIn8`C!}n?KG~2slbI(rQid|0K%IlyLFhKnG335i( zxJhmSm>I=<XXg8H8tJDIpLsX)A6Wh&=>OohQ@8v)yu+T`W9)N@zrk$Nu>77fgFmzX zSZDvzo1hVb-m(&yN&Zx9nX`Y{cdVU|{eSbm$Jw7DZcVy4HA}EdM61)qs?8AHvtYOn zr#2%18X-5x%QSV-7HwK1yr;wS_}Z`s;VMvX$w{|%Ng~>YShpr)id&r~L7b{ouLt`3 z!Kq2t2>F)0I1uI=O)c++n)I%rT6eME!&SHfC)}E}0`z=tbt-+Nh?b>bQ(prYGy;y< z)oIZVb7*av*fpGkInU~)gZ2E0QPijhYm8dI;x9!z+UUDg$&iu^7g+1~q+DUo3bW7N zk3KU5AGac5s8(_THLj0eC;2zxTR?R0g5bVYZsp>S!3GV(C47aBIkSqz;+FlZt(EhH zn79>-ec0n+_J*tXrdyd5?N%hA&o^QAS0+8`RwS0f`|u@v1!u^qgGHv_!MtSdI3+l` zc={EI=qJvAKF|pGf}FX)e$KNp&ggsf)xYQg*Pl37D-!0p{)A~>kHq@Ry&k#uq+xBd z(3&~o`WG2yKp$uXTq0)<h<0rfyOutxSXae!zTBnhYsk7)N%Tl>P25*m!YFM|pYzvp z_Le31-17KipcA3x@h6zmhaFW<E9UcEF%#Na2?wZM9Xl!~^d-heK6Tu(IC_Z7*RqAz zK*!-1@(PKIQ<5B7?!Ml&jB_hpDDJ-aC@p>LS-1uohM)00r~4i=d7}Qf`_c>Dq~+8? z->doJe4p-Huo+ImHL&4NIIa6)i?#GopO(fB=K2$*UlMhYS=dlaY!*2)#YR*OS4A#h b$9Oj3kA|q>xLFyGVJgfLx!Oe??e_QwVjhSf From 0eb5606731a8396a647c77feaf382e9aa9678d3c Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 26 Feb 2025 13:39:50 +0800 Subject: [PATCH 052/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/service/impl/RayServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 1d812659..db003344 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -4,7 +4,6 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; -import com.ruoyi.platform.domain.AutoMlIns; import com.ruoyi.platform.domain.Ray; import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; @@ -198,7 +197,7 @@ public class RayServiceImpl implements RayService { Map<String, Object> param_output = (Map<String, Object>) output.get("param_output"); List output1 = (ArrayList) param_output.values().toArray()[0]; Map<String, String> output2 = (Map<String, String>) output1.get(0); - String outputPath = minioEndpoint + "/" + output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/"; + String outputPath = minioEndpoint + "/" + output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/" + ray.getName(); rayIns.setResultPath(outputPath); rayInsDao.insert(rayIns); From 6c29c2b332f388580a5763a16afa045ddbac78af Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 26 Feb 2025 15:06:55 +0800 Subject: [PATCH 053/127] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=B6=85?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AF=BB=E4=BC=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/package.json | 2 +- react-ui/src/components/FormInfo/index.tsx | 31 ++++++++-- react-ui/src/hooks/resource.ts | 6 +- react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- .../components/CreateMirrorModal/index.tsx | 4 +- .../Experiment/components/LogGroup/index.tsx | 2 +- .../pages/HyperParameter/Instance/index.tsx | 19 +++--- .../components/CreateForm/ExecuteConfig.tsx | 2 +- react-ui/src/pages/HyperParameter/types.ts | 2 +- react-ui/src/pages/Mirror/Create/index.tsx | 4 +- .../ModelDeployment/CreateVersion/index.tsx | 2 +- react-ui/src/stories/FormInfo.stories.tsx | 60 ++++++------------- react-ui/src/utils/format.ts | 6 +- 13 files changed, 70 insertions(+), 72 deletions(-) diff --git a/react-ui/package.json b/react-ui/package.json index f2583d6b..1a8d7ebc 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -8,7 +8,7 @@ "build": "max build", "deploy": "npm run build && npm run gh-pages", "dev": "npm run start:dev", - "dev-no-sso": "NO_SSO=true npm run start:dev", + "dev-no-sso": "cross-env NO_SSO=true npm run start:dev", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index a784e433..7e8e95ad 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -1,3 +1,4 @@ +import { formatEnum } from '@/utils/format'; import { Typography } from 'antd'; import classNames from 'classnames'; import './index.less'; @@ -7,8 +8,12 @@ type FormInfoProps = { value?: any; /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ valuePropName?: string; - /** 是否是多行 */ - multiline?: boolean; + /** 是否是多行文本 */ + textArea?: boolean; + /** 是否是下拉框 */ + select?: boolean; + /** 下拉框数据 */ + options?: { label: string; value: any }[]; /** 自定义类名 */ className?: string; /** 自定义样式 */ @@ -18,20 +23,34 @@ type FormInfoProps = { /** * 模拟禁用的输入框,但是内容超长时,hover 时显示所有内容 */ -function FormInfo({ value, valuePropName, className, style, multiline = false }: FormInfoProps) { - const data = value && typeof value === 'object' && valuePropName ? value[valuePropName] : value; +function FormInfo({ + value, + valuePropName, + className, + select, + options, + style, + textArea = false, +}: FormInfoProps) { + let data = value; + if (value && typeof value === 'object' && valuePropName) { + data = value[valuePropName]; + } else if (select === true && options) { + data = formatEnum(options)(value); + } + return ( <div className={classNames( 'form-info', { - 'form-info--multiline': multiline, + 'form-info--multiline': textArea, }, className, )} style={style} > - <Typography.Paragraph ellipsis={multiline ? false : { tooltip: data }}> + <Typography.Paragraph ellipsis={textArea ? false : { tooltip: data }}> {data} </Typography.Paragraph> </div> diff --git a/react-ui/src/hooks/resource.ts b/react-ui/src/hooks/resource.ts index 0b491aeb..82a3ee78 100644 --- a/react-ui/src/hooks/resource.ts +++ b/react-ui/src/hooks/resource.ts @@ -15,11 +15,11 @@ import { useSnapshot } from 'umi'; // 获取资源规格 export function useComputingResource() { const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); - const computingResourceSnap = useSnapshot(computingResourceState); + const snap = useSnapshot(computingResourceState); useEffect(() => { - if (computingResourceSnap.computingResource.length > 0) { - setResourceStandardList(computingResourceSnap.computingResource as ComputingResource[]); + if (snap.computingResource.length > 0) { + setResourceStandardList(snap.computingResource as ComputingResource[]); } else { getComputingResource(); } diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 947ac63d..aefee532 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -83,7 +83,7 @@ function AutoMLInstance() { const setupSSE = (name: string, namespace: string) => { let { origin } = location; if (process.env.NODE_ENV === 'development') { - origin = 'http://172.20.32.181:31213'; + origin = 'http://172.20.32.197:31213'; } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( diff --git a/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx index 6af530e1..8d2b27fa 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx @@ -67,8 +67,8 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) { message: '请输入镜像Tag', }, { - pattern: /^[a-zA-Z0-9_-]*$/, - message: '只支持字母、数字、下划线(_)、中横线(-)', + pattern: /^[a-zA-Z0-9._-]+$/, + message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', }, ]} > diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 1b448930..6dd31166 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -135,7 +135,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.181:31213'; + host = '172.20.32.197:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 8d29521b..6a1d3931 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,7 +1,7 @@ import KFIcon from '@/components/KFIcon'; import { AutoMLTaskType, ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; -import { getExperimentInsReq } from '@/services/autoML'; +import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; import { safeInvoke } from '@/utils/functional'; @@ -22,12 +22,11 @@ enum TabKeys { History = 'history', } -function AutoMLInstance() { +function HyperParameterInstance() { const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); - const [autoMLInfo, setAutoMLInfo] = useState<HyperParameterData | undefined>(undefined); + const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); const params = useParams(); - // const autoMLId = safeInvoke(Number)(params.autoMLId); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef<EventSource | null>(null); @@ -42,14 +41,14 @@ function AutoMLInstance() { // 获取实验实例详情 const getExperimentInsInfo = async (isStatusDetermined: boolean) => { - const [res] = await to(getExperimentInsReq(instanceId)); + const [res] = await to(getRayInsReq(instanceId)); if (res && res.data) { const info = res.data as AutoMLInstanceData; const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; // 解析配置参数 const paramJson = parseJsonText(param); if (paramJson) { - setAutoMLInfo(paramJson); + setExperimentInfo(paramJson); } // 这个接口返回的状态有延时,SSE 返回的状态是最新的 @@ -83,7 +82,7 @@ function AutoMLInstance() { const setupSSE = (name: string, namespace: string) => { let { origin } = location; if (process.env.NODE_ENV === 'development') { - origin = 'http://172.20.32.181:31213'; + origin = 'http://172.20.32.197:31213'; } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( @@ -142,7 +141,7 @@ function AutoMLInstance() { children: ( <HyperParameterBasic className={styles['auto-ml-instance__basic']} - info={autoMLInfo} + info={experimentInfo} runStatus={instanceInfo?.nodeStatus} isInstance /> @@ -189,7 +188,7 @@ function AutoMLInstance() { children: ( <ExperimentHistory fileUrl={instanceInfo?.run_history_path} - isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} + isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification} /> ), }, @@ -212,4 +211,4 @@ function AutoMLInstance() { ); } -export default AutoMLInstance; +export default HyperParameterInstance; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 1609328c..672f9094 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -109,7 +109,7 @@ function ExecuteConfig() { <Col span={10}> <Form.Item label="代码配置" - name="code" + name="code_config" rules={[ { validator: requiredValidator, diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 1e610f20..f8508a88 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -12,7 +12,7 @@ export enum OperationType { export type FormData = { name: string; // 实验名称 description: string; // 实验描述 - code: ParameterInputObject; // 代码 + code_config: ParameterInputObject; // 代码 dataset: ParameterInputObject; // 数据集 model: ParameterInputObject; // 模型 image: ParameterInputObject; // 镜像 diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx index 01a22031..681103bf 100644 --- a/react-ui/src/pages/Mirror/Create/index.tsx +++ b/react-ui/src/pages/Mirror/Create/index.tsx @@ -177,8 +177,8 @@ function MirrorCreate() { message: '请输入镜像Tag', }, { - pattern: /^[a-zA-Z0-9_-]*$/, - message: '只支持字母、数字、下划线(_)、中横线(-)', + pattern: /^[a-zA-Z0-9._-]+$/, + message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', }, ]} > diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index 38171380..430eae87 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -247,7 +247,7 @@ function CreateServiceVersion() { }, { pattern: /^[a-zA-Z0-9._-]+$/, - message: '版本只支持字母、数字、点、下划线、中横线', + message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', }, ]} > diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index 8e2d89de..09466a46 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -1,6 +1,6 @@ import FormInfo from '@/components/FormInfo'; import type { Meta, StoryObj } from '@storybook/react'; -import { Form, Input, Select, Typography } from 'antd'; +import { Form, Input, Select } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -24,6 +24,14 @@ export default meta; type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + style: { width: '200px' }, + value: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + }, +}; + export const InForm: Story = { render: () => { return ( @@ -40,11 +48,10 @@ export const InForm: Story = { value: 1, showValue: '对象文本', }, - input_text: - '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', - antd_select: 1, select_text: 1, - select_large_text: 1, + ant_input_text: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + ant_select_text: 1, }} > <Form.Item label="文本" name="text"> @@ -54,7 +61,7 @@ export const InForm: Story = { <FormInfo /> </Form.Item> <Form.Item label="多行文本" name="multiline_text"> - <FormInfo multiline /> + <FormInfo textArea /> </Form.Item> <Form.Item label="对象" name="object_text"> <FormInfo valuePropName="showValue" /> @@ -62,12 +69,9 @@ export const InForm: Story = { <Form.Item label="无内容" name="empty_text"> <FormInfo /> </Form.Item> - <Form.Item label="Input" name="input_text"> - <Input disabled /> - </Form.Item> - <Form.Item label="Select" name="antd_select"> - <Select - disabled + <Form.Item label="Select" name="select_text"> + <FormInfo + select options={[ { label: @@ -77,37 +81,11 @@ export const InForm: Story = { ]} /> </Form.Item> - <Form.Item label="Select" name="select_text"> - <Select - labelRender={(props) => { - return ( - <div style={{ width: '100%', lineHeight: 'normal' }}> - <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> - {props.label} - </Typography.Text> - </div> - ); - }} - disabled - options={[ - { - label: '选择文本', - value: 1, - }, - ]} - /> + <Form.Item label="Input" name="ant_input_text"> + <Input disabled /> </Form.Item> - <Form.Item label="Long Select" name="select_large_text"> + <Form.Item label="Select" name="ant_select_text"> <Select - labelRender={(props) => { - return ( - <div style={{ width: '100%', lineHeight: 'normal' }}> - <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> - {props.label} - </Typography.Text> - </div> - ); - }} disabled options={[ { diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index c540e441..40d46fdc 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -122,10 +122,12 @@ export const formatBoolean = (value: boolean): string => { return value ? '是' : '否'; }; -type FormatEnum = (value: string | number) => string; +type FormatEnumFunc = (value: string | number) => string; // 格式化枚举 -export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { +export const formatEnum = ( + options: { value: string | number; label: string }[], +): FormatEnumFunc => { return (value: string | number) => { const option = options.find((item) => item.value === value); return option ? option.label : '--'; From a80a13e0d923227632abf6cc991607118d17bdd2 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 26 Feb 2025 15:08:19 +0800 Subject: [PATCH 054/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/controller/ray/RayInsController.java | 2 +- .../com/ruoyi/platform/service/RayInsService.java | 2 +- .../platform/service/impl/RayInsServiceImpl.java | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java index d2c34074..1a358418 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java @@ -54,7 +54,7 @@ public class RayInsController extends BaseController { @GetMapping("{id}") @ApiOperation("查看实验实例详情") - public GenericsAjaxResult<RayIns> getDetailById(@PathVariable("id") Long id) throws IOException { + public GenericsAjaxResult<RayIns> getDetailById(@PathVariable("id") Long id) throws Exception { return genericsSuccess(this.rayInsService.getDetailById(id)); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java index 63559ffe..391b112f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java @@ -15,7 +15,7 @@ public interface RayInsService { boolean terminateRayIns(Long id) throws Exception; - RayIns getDetailById(Long id) throws IOException; + RayIns getDetailById(Long id) throws Exception; void updateRayStatus(Long rayId); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index f881faa4..0860d783 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -9,6 +9,7 @@ import com.ruoyi.platform.service.RayInsService; import com.ruoyi.platform.utils.DateUtils; import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.JsonUtils; +import com.ruoyi.platform.utils.MinioUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; @@ -39,6 +40,9 @@ public class RayInsServiceImpl implements RayInsService { @Resource private RayDao rayDao; + @Resource + private MinioUtil minioUtil; + @Override public Page<RayIns> queryByPage(Long rayId, PageRequest pageRequest) throws IOException { long total = this.rayInsDao.count(rayId); @@ -161,7 +165,7 @@ public class RayInsServiceImpl implements RayInsService { } @Override - public RayIns getDetailById(Long id) throws IOException { + public RayIns getDetailById(Long id) throws Exception { RayIns rayIns = rayInsDao.queryById(id); if (Constant.Running.equals(rayIns.getStatus()) || Constant.Pending.equals(rayIns.getStatus())) { rayIns = queryStatusFromArgo(rayIns); @@ -260,8 +264,13 @@ public class RayInsServiceImpl implements RayInsService { return rayInsDao.queryByRayInsIsNotTerminated(); } - public ArrayList<Map<String, Object>> getTrialList(String directoryPath) throws IOException { + public ArrayList<Map<String, Object>> getTrialList(String directoryPath) throws Exception { // 获取指定路径下的所有文件 + String bucketName = directoryPath.substring(0, directoryPath.indexOf("/")); + String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; + + List<Map> maps = minioUtil.listFilesInDirectory(bucketName, prefix); + Path dirPath = Paths.get(directoryPath); Path experimentState = Files.list(dirPath).filter(path -> Files.isRegularFile(path) && path.getFileName().toString().startsWith("experiment_state")).collect(Collectors.toList()).get(0); String content = new String(Files.readAllBytes(experimentState)); From 29124867f5b254d64590b244982df11a62bec160 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 26 Feb 2025 15:43:28 +0800 Subject: [PATCH 055/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/RayInsServiceImpl.java | 50 ++++++++++--------- .../platform/service/impl/RayServiceImpl.java | 3 +- .../com/ruoyi/platform/vo/RayParamVo.java | 2 - 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 0860d783..3f852dc9 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -19,7 +19,6 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; @@ -33,6 +32,8 @@ public class RayInsServiceImpl implements RayInsService { private String argoWorkflowStatus; @Value("${argo.workflowTermination}") private String argoWorkflowTermination; + @Value("${minio.dataReleaseBucketName}") + private String bucketName; @Resource private RayInsDao rayInsDao; @@ -266,37 +267,40 @@ public class RayInsServiceImpl implements RayInsService { public ArrayList<Map<String, Object>> getTrialList(String directoryPath) throws Exception { // 获取指定路径下的所有文件 - String bucketName = directoryPath.substring(0, directoryPath.indexOf("/")); String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; - List<Map> maps = minioUtil.listFilesInDirectory(bucketName, prefix); - Path dirPath = Paths.get(directoryPath); - Path experimentState = Files.list(dirPath).filter(path -> Files.isRegularFile(path) && path.getFileName().toString().startsWith("experiment_state")).collect(Collectors.toList()).get(0); - String content = new String(Files.readAllBytes(experimentState)); - Map<String, Object> result = JsonUtils.jsonToMap(content); - ArrayList<ArrayList> trial_data_list = (ArrayList<ArrayList>) result.get("trial_data"); + if (!maps.isEmpty()) { + List<Map> collect = maps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); + if (!collect.isEmpty()) { + Path experimentState = Paths.get(collect.get(0).get("name").toString()); + String content = minioUtil.readObjectAsString(bucketName, prefix + "/" + experimentState); - ArrayList<Map<String, Object>> trialList = new ArrayList<>(); + Map<String, Object> result = JsonUtils.jsonToMap(content); + ArrayList<ArrayList> trial_data_list = (ArrayList<ArrayList>) result.get("trial_data"); + ArrayList<Map<String, Object>> trialList = new ArrayList<>(); - for (ArrayList trial_data : trial_data_list) { - Map<String, Object> trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); - Map<String, Object> trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); + for (ArrayList trial_data : trial_data_list) { + Map<String, Object> trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); + Map<String, Object> trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); - Map<String, Object> trial = new HashMap<>(); - trial.put("trial_id", trial_data_0.get("trial_id")); - trial.put("config", trial_data_0.get("config")); - trial.put("status", trial_data_0.get("status")); + Map<String, Object> trial = new HashMap<>(); + trial.put("trial_id", trial_data_0.get("trial_id")); + trial.put("config", trial_data_0.get("config")); + trial.put("status", trial_data_0.get("status")); - Map<String, Object> last_result = (Map<String, Object>) trial_data_1.get("last_result"); - Map<String, Object> metric_analysis = (Map<String, Object>) trial_data_1.get("metric_analysis"); - Map<String, Object> time_total_s = (Map<String, Object>) metric_analysis.get("time_total_s"); + Map<String, Object> last_result = (Map<String, Object>) trial_data_1.get("last_result"); + Map<String, Object> metric_analysis = (Map<String, Object>) trial_data_1.get("metric_analysis"); + Map<String, Object> time_total_s = (Map<String, Object>) metric_analysis.get("time_total_s"); - trial.put("training_iteration", last_result.get("training_iteration")); - trial.put("time", time_total_s.get("avg")); + trial.put("training_iteration", last_result.get("training_iteration")); + trial.put("time", time_total_s.get("avg")); - trialList.add(trial); + trialList.add(trial); + } + return trialList; + } } - return trialList; + return null; } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index db003344..67f30d29 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -197,8 +197,7 @@ public class RayServiceImpl implements RayService { Map<String, Object> param_output = (Map<String, Object>) output.get("param_output"); List output1 = (ArrayList) param_output.values().toArray()[0]; Map<String, String> output2 = (Map<String, String>) output1.get(0); - String outputPath = minioEndpoint + "/" + output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/" + ray.getName(); - + String outputPath = output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/hpo"; rayIns.setResultPath(outputPath); rayInsDao.insert(rayIns); rayInsService.updateRayStatus(id); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java index 4b6e1c5a..86c77018 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java @@ -24,8 +24,6 @@ public class RayParamVo { private String mainPy; - private String name; - private Integer numSamples; private String parameters; From d984d585cb9f47b1294adb3a455bcea76d38d83f Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 26 Feb 2025 16:16:05 +0800 Subject: [PATCH 056/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/ruoyi/platform/domain/RayIns.java | 4 ++++ .../platform/service/impl/RayInsServiceImpl.java | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java index 3de2e1a9..54a41d08 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java @@ -9,6 +9,7 @@ import lombok.Data; import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; @Data @@ -45,6 +46,9 @@ public class RayIns { private Date finishTime; + @TableField(exist = false) + private List<Map> fileList; + @TableField(exist = false) private ArrayList<Map<String, Object>> trialList; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 3f852dc9..6b388c57 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -171,7 +171,9 @@ public class RayInsServiceImpl implements RayInsService { if (Constant.Running.equals(rayIns.getStatus()) || Constant.Pending.equals(rayIns.getStatus())) { rayIns = queryStatusFromArgo(rayIns); } - rayIns.setTrialList(getTrialList(rayIns.getResultPath())); + getTrialList(rayIns); + +// rayIns.setTrialList(getTrialList(rayIns.getResultPath())); return rayIns; } @@ -265,12 +267,14 @@ public class RayInsServiceImpl implements RayInsService { return rayInsDao.queryByRayInsIsNotTerminated(); } - public ArrayList<Map<String, Object>> getTrialList(String directoryPath) throws Exception { + public void getTrialList(RayIns rayIns) throws Exception { // 获取指定路径下的所有文件 + String directoryPath = rayIns.getResultPath(); String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; List<Map> maps = minioUtil.listFilesInDirectory(bucketName, prefix); if (!maps.isEmpty()) { + rayIns.setFileList(maps); List<Map> collect = maps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); if (!collect.isEmpty()) { Path experimentState = Paths.get(collect.get(0).get("name").toString()); @@ -298,9 +302,8 @@ public class RayInsServiceImpl implements RayInsService { trialList.add(trial); } - return trialList; + rayIns.setTrialList(trialList); } } - return null; } } From f0beb4718f548e63bba0cbbe5b68b8b6be7497b4 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 27 Feb 2025 08:41:05 +0800 Subject: [PATCH 057/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/domain/RayIns.java | 3 +++ .../com/ruoyi/platform/service/impl/RayInsServiceImpl.java | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java index 54a41d08..aff4158e 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java @@ -22,6 +22,9 @@ public class RayIns { private String resultPath; + @TableField(exist = false) + private String resultTxt; + private Integer state; private String status; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 6b388c57..cd2bc5e9 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -32,6 +32,8 @@ public class RayInsServiceImpl implements RayInsService { private String argoWorkflowStatus; @Value("${argo.workflowTermination}") private String argoWorkflowTermination; + @Value("${minio.endpoint}") + String endpoint; @Value("${minio.dataReleaseBucketName}") private String bucketName; @@ -270,6 +272,10 @@ public class RayInsServiceImpl implements RayInsService { public void getTrialList(RayIns rayIns) throws Exception { // 获取指定路径下的所有文件 String directoryPath = rayIns.getResultPath(); + + rayIns.setResultPath(endpoint + "/" + directoryPath); + rayIns.setResultTxt(endpoint + "/" + directoryPath + "/result.txt"); + String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; List<Map> maps = minioUtil.listFilesInDirectory(bucketName, prefix); From f48aa65fce145c3567e3813fad898626593126b1 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 27 Feb 2025 09:59:41 +0800 Subject: [PATCH 058/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java | 2 +- .../java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java index dfddf71f..c1b567f8 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/AutoMlServiceImpl.java @@ -45,7 +45,7 @@ public class AutoMlServiceImpl implements AutoMlService { @Value("${argo.workflowRun}") private String argoWorkflowRun; - @Value("${minio.endpoint}") + @Value("${minio.endpointIp}") private String minioEndpoint; @Resource diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index cd2bc5e9..c922f093 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -32,7 +32,7 @@ public class RayInsServiceImpl implements RayInsService { private String argoWorkflowStatus; @Value("${argo.workflowTermination}") private String argoWorkflowTermination; - @Value("${minio.endpoint}") + @Value("${minio.endpointIp}") String endpoint; @Value("${minio.dataReleaseBucketName}") private String bucketName; From 718ccbe5ef6b848820d9de86f171ded6f95baf3c Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 27 Feb 2025 11:40:12 +0800 Subject: [PATCH 059/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/RayInsServiceImpl.java | 7 +++- .../com/ruoyi/platform/utils/MinioUtil.java | 39 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index c922f093..c5f02452 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -277,7 +277,7 @@ public class RayInsServiceImpl implements RayInsService { rayIns.setResultTxt(endpoint + "/" + directoryPath + "/result.txt"); String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; - List<Map> maps = minioUtil.listFilesInDirectory(bucketName, prefix); + List<Map> maps = minioUtil.listRayFilesInDirectory(bucketName, prefix); if (!maps.isEmpty()) { rayIns.setFileList(maps); @@ -304,7 +304,10 @@ public class RayInsServiceImpl implements RayInsService { Map<String, Object> time_total_s = (Map<String, Object>) metric_analysis.get("time_total_s"); trial.put("training_iteration", last_result.get("training_iteration")); - trial.put("time", time_total_s.get("avg")); + trial.put("time_avg", time_total_s.get("avg")); + + Map<String, Object> param = JsonUtils.jsonToMap(rayIns.getParam()); + trial.put("metric_analysis", metric_analysis.get((String) param.get("metric"))); trialList.add(trial); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java index f311065a..3e2b8c9d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java @@ -1,7 +1,6 @@ package com.ruoyi.platform.utils; -import com.ruoyi.common.security.utils.SecurityUtils; import io.minio.*; import io.minio.errors.MinioException; import io.minio.messages.DeleteObject; @@ -30,6 +29,8 @@ public class MinioUtil { @Value("${git.localPath}") String localPath; + @Value("${minio.endpointIp}") + String minioEndpoint; @Autowired public MinioUtil(@Value("${minio.endpoint}") String minioEndpoint, @Value("${minio.accessKey}") String minioAccessKey, @Value("${minio.secretKey}") String minioSecretKey) { @@ -315,12 +316,48 @@ public class MinioUtil { Item item = result.get(); String fullPath = item.objectName(); Path path = Paths.get(fullPath); + + String fileName = path.getFileName().toString(); + long fileSize = item.size(); + String formattedSize = FileUtil.formatFileSize(fileSize); // 格式化文件大小 + Map map = new HashMap<>(); + map.put("name", fileName); + map.put("size", formattedSize); + fileInfoList.add(map); + } + + return fileInfoList; + } + + + public List<Map> listRayFilesInDirectory(String bucketName, String prefix) throws Exception { + List<Map> fileInfoList = new ArrayList<>(); + Iterable<Result<Item>> results = minioClient.listObjects( + ListObjectsArgs.builder() + .prefix(prefix) + .bucket(bucketName) + .build()); + + for (Result<Item> result : results) { + Item item = result.get(); + String fullPath = item.objectName(); + Path path = Paths.get(fullPath); + String fileName = path.getFileName().toString(); long fileSize = item.size(); String formattedSize = FileUtil.formatFileSize(fileSize); // 格式化文件大小 Map map = new HashMap<>(); map.put("name", fileName); map.put("size", formattedSize); + + if ((fileName.startsWith("run") || fileName.startsWith("checkpoint")) && fileSize == 0) { + map.put("isDirectory", true); + map.put("children", listRayFilesInDirectory(bucketName, fullPath)); + } else { + map.put("isFile", true); + map.put("url", minioEndpoint + "/" + bucketName + "/" + fullPath); + } + fileInfoList.add(map); } From dac303f5e7783b0ccd89acbbf036aeaf32910337 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 27 Feb 2025 11:48:00 +0800 Subject: [PATCH 060/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index c5f02452..f7f5811e 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -308,6 +308,7 @@ public class RayInsServiceImpl implements RayInsService { Map<String, Object> param = JsonUtils.jsonToMap(rayIns.getParam()); trial.put("metric_analysis", metric_analysis.get((String) param.get("metric"))); + trial.put("metric", param.get("metric")); trialList.add(trial); } From ed0bb92c11334016e691049a36fedb08b8343ed4 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 27 Feb 2025 13:59:36 +0800 Subject: [PATCH 061/127] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-=E5=AE=9E=E9=AA=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.tsx | 14 +- react-ui/src/pages/AutoML/Instance/index.tsx | 6 +- .../components/ExperimentResult/index.less | 1 + .../pages/Experiment/Comparison/index.less | 20 --- .../Experiment/components/LogGroup/index.less | 3 +- .../pages/HyperParameter/Create/index.less | 7 +- .../src/pages/HyperParameter/Create/index.tsx | 4 +- .../src/pages/HyperParameter/Info/index.less | 2 +- .../src/pages/HyperParameter/Info/index.tsx | 4 +- .../pages/HyperParameter/Instance/index.less | 2 +- .../pages/HyperParameter/Instance/index.tsx | 84 ++++++--- .../components/ExperimentHistory/index.less | 21 +++ .../components/ExperimentHistory/index.tsx | 168 ++++++++---------- .../components/ExperimentLog/index.tsx | 83 +++++++++ .../components/ExperimentResult/index.less | 41 +---- .../components/ExperimentResult/index.tsx | 93 +++++----- .../components/HyperParameterBasic/index.tsx | 2 +- react-ui/src/pages/HyperParameter/types.ts | 27 ++- 18 files changed, 330 insertions(+), 252 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index 7e8e95ad..c1e23cbe 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -4,9 +4,9 @@ import classNames from 'classnames'; import './index.less'; type FormInfoProps = { - /** 自定义类名 */ + /** 值 */ value?: any; - /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ + /** 如果 `value` 是对象,取对象的哪个属性作为值 */ valuePropName?: string; /** 是否是多行文本 */ textArea?: boolean; @@ -32,11 +32,11 @@ function FormInfo({ style, textArea = false, }: FormInfoProps) { - let data = value; + let showValue = value; if (value && typeof value === 'object' && valuePropName) { - data = value[valuePropName]; + showValue = value[valuePropName]; } else if (select === true && options) { - data = formatEnum(options)(value); + showValue = formatEnum(options)(value); } return ( @@ -50,8 +50,8 @@ function FormInfo({ )} style={style} > - <Typography.Paragraph ellipsis={textArea ? false : { tooltip: data }}> - {data} + <Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}> + {showValue} </Typography.Paragraph> </div> ); diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index aefee532..19a6414d 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -22,6 +22,8 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function AutoMLInstance() { const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); @@ -66,7 +68,7 @@ function AutoMLInstance() { const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { + if (key.startsWith(NodePrefix)) { const value = nodeStatusJson[key]; info.nodeStatus = value; } @@ -100,7 +102,7 @@ function AutoMLInstance() { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { const statusData = Object.values(nodes).find((node: any) => - node.displayName.startsWith('auto-ml'), + node.displayName.startsWith(NodePrefix), ) as NodeStatus; if (statusData) { setInstanceInfo((prev) => ({ diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index 342817c3..bcb52314 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -25,6 +25,7 @@ } &__text { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; white-space: pre-wrap; } diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index 4dce8268..b0984b91 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -18,26 +18,6 @@ background-color: white; border-radius: 10px; - &__footer { - display: flex; - align-items: center; - padding-top: 20px; - color: @text-color-secondary; - font-size: 12px; - background-color: white; - - div { - flex: 1; - height: 1px; - background-color: @border-color; - } - - p { - flex: none; - margin: 0 8px; - } - } - :global { .ant-table-container { border: none !important; diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.less b/react-ui/src/pages/Experiment/components/LogGroup/index.less index 2962a2c6..48012951 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.less +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.less @@ -20,7 +20,8 @@ padding: 15px; color: white; font-size: 14px; - white-space: pre-line; + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; + white-space: pre-wrap; text-align: left; word-break: break-all; background: #19253b; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less index 145be0d1..ae065195 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.less +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -1,4 +1,4 @@ -.create-hyperparameter { +.create-hyper-parameter { height: 100%; &__content { @@ -11,11 +11,6 @@ background-color: white; border-radius: 10px; - &__type { - color: @text-color; - font-size: @font-size-input-lg; - } - :global { .ant-input-number { width: 100%; diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index 79e45582..bd7aedb9 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -118,9 +118,9 @@ function CreateHyperParameter() { } return ( - <div className={styles['create-hyperparameter']}> + <div className={styles['create-hyper-parameter']}> <PageTitle title={title}></PageTitle> - <div className={styles['create-hyperparameter__content']}> + <div className={styles['create-hyper-parameter__content']}> <div> <Form name="create-hyperparameter" diff --git a/react-ui/src/pages/HyperParameter/Info/index.less b/react-ui/src/pages/HyperParameter/Info/index.less index e27756ef..d0c2398a 100644 --- a/react-ui/src/pages/HyperParameter/Info/index.less +++ b/react-ui/src/pages/HyperParameter/Info/index.less @@ -1,4 +1,4 @@ -.auto-ml-info { +.hyper-parameter-info { position: relative; height: 100%; &__tabs { diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx index 3d5c5c04..2a175d8d 100644 --- a/react-ui/src/pages/HyperParameter/Info/index.tsx +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -35,9 +35,9 @@ function HyperparameterInfo() { }; return ( - <div className={styles['auto-ml-info']}> + <div className={styles['hyper-parameter-info']}> <PageTitle title="实验详情"></PageTitle> - <div className={styles['auto-ml-info__content']}> + <div className={styles['hyper-parameter-info__content']}> <HyperParameterBasic info={hyperparameterInfo} /> </div> </div> diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less index 889faeb5..8d57a98d 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.less +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -1,4 +1,4 @@ -.auto-ml-instance { +.hyper-parameter-instance { height: 100%; &__tabs { diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 6a1d3931..5cc1fded 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,5 +1,5 @@ import KFIcon from '@/components/KFIcon'; -import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import { ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; @@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; import ExperimentResult from '../components/ExperimentResult'; import HyperParameterBasic from '../components/HyperParameterBasic'; -import { AutoMLInstanceData, HyperParameterData } from '../types'; +import { HyperParameterData, HyperParameterInstanceData } from '../types'; import styles from './index.less'; enum TabKeys { @@ -22,10 +22,16 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function HyperParameterInstance() { const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); - const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); + const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( + undefined, + ); + // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 + const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef<EventSource | null>(null); @@ -43,11 +49,26 @@ function HyperParameterInstance() { const getExperimentInsInfo = async (isStatusDetermined: boolean) => { const [res] = await to(getRayInsReq(instanceId)); if (res && res.data) { - const info = res.data as AutoMLInstanceData; + const info = res.data as HyperParameterInstanceData; const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; // 解析配置参数 const paramJson = parseJsonText(param); if (paramJson) { + // 实例详情返回的参数是字符串,需要转换 + if (typeof paramJson.parameters === 'string') { + paramJson.parameters = parseJsonText(paramJson.parameters); + } + if (!Array.isArray(paramJson.parameters)) { + paramJson.parameters = []; + } + + // 实例详情返回的运行参数是字符串,需要转换 + if (typeof paramJson.points_to_evaluate === 'string') { + paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate); + } + if (!Array.isArray(paramJson.points_to_evaluate)) { + paramJson.points_to_evaluate = []; + } setExperimentInfo(paramJson); } @@ -65,13 +86,17 @@ function HyperParameterInstance() { const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { - const value = nodeStatusJson[key]; - info.nodeStatus = value; + if (key.startsWith(NodePrefix)) { + const nodeStatus = nodeStatusJson[key]; + info.nodeStatus = nodeStatus; + } else if (key.startsWith('workflow')) { + const workflowStatus = nodeStatusJson[key]; + setWorkflowStatus(workflowStatus); } }); } setInstanceInfo(info); + // 运行中或者等待中,开启 SSE if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { setupSSE(argo_ins_name, argo_ins_ns); @@ -98,19 +123,29 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - const statusData = Object.values(nodes).find((node: any) => - node.displayName.startsWith('auto-ml'), + const nodeStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith(NodePrefix), ) as NodeStatus; - if (statusData) { + const workflowStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith('workflow'), + ) as NodeStatus; + + // 节点状态 + if (nodeStatus) { setInstanceInfo((prev) => ({ ...prev!, - nodeStatus: statusData, + nodeStatus: nodeStatus, })); + } + + // 设置工作流状态 + if (workflowStatus) { + setWorkflowStatus(workflowStatus); // 实验结束,关闭 SSE if ( - statusData.phase !== ExperimentStatus.Pending && - statusData.phase !== ExperimentStatus.Running + workflowStatus.phase !== ExperimentStatus.Pending && + workflowStatus.phase !== ExperimentStatus.Running ) { closeSSE(); getExperimentInsInfo(true); @@ -140,9 +175,9 @@ function HyperParameterInstance() { icon: <KFIcon type="icon-jibenxinxi" />, children: ( <HyperParameterBasic - className={styles['auto-ml-instance__basic']} + className={styles['hyper-parameter-instance__basic']} info={experimentInfo} - runStatus={instanceInfo?.nodeStatus} + runStatus={workflowStatus} isInstance /> ), @@ -152,7 +187,7 @@ function HyperParameterInstance() { label: '日志', icon: <KFIcon type="icon-rizhi1" />, children: ( - <div className={styles['auto-ml-instance__log']}> + <div className={styles['hyper-parameter-instance__log']}> {instanceInfo && instanceInfo.nodeStatus && ( <LogList instanceName={instanceInfo.argo_ins_name} @@ -174,23 +209,14 @@ function HyperParameterInstance() { label: '实验结果', icon: <KFIcon type="icon-shiyanjieguo1" />, children: ( - <ExperimentResult - fileUrl={instanceInfo?.result_path} - imageUrl={instanceInfo?.img_path} - modelPath={instanceInfo?.model_path} - /> + <ExperimentResult fileUrl={instanceInfo?.result_txt} fileList={instanceInfo?.file_list} /> ), }, { key: TabKeys.History, label: 'Trial 列表', icon: <KFIcon type="icon-Trialliebiao" />, - children: ( - <ExperimentHistory - fileUrl={instanceInfo?.run_history_path} - isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification} - /> - ), + children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, }, ]; @@ -200,9 +226,9 @@ function HyperParameterInstance() { : basicTabItems; return ( - <div className={styles['auto-ml-instance']}> + <div className={styles['hyper-parameter-instance']}> <Tabs - className={styles['auto-ml-instance__tabs']} + className={styles['hyper-parameter-instance__tabs']} items={tabItems} activeKey={activeTab} onChange={setActiveTab} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index beac2a8a..b30e30b2 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -10,5 +10,26 @@ &__table { height: 100%; } + + :global { + .ant-table-container { + border: none !important; + } + .ant-table-thead { + .ant-table-cell { + background-color: rgb(247, 247, 247); + border-color: @border-color !important; + } + } + .ant-table-tbody { + .ant-table-cell { + border-right: none !important; + border-left: none !important; + } + } + .ant-table-tbody-virtual::after { + border-bottom: none !important; + } + } } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index e95ccd42..2d57a845 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,101 +1,44 @@ -import { getFileReq } from '@/services/file'; -import { to } from '@/utils/promise'; -import tableCellRender from '@/utils/table'; -import { Table, type TableProps } from 'antd'; +import { HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, Tooltip, type TableProps } from 'antd'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentHistoryProps = { - fileUrl?: string; - isClassification: boolean; + trialList?: HyperParameterTrialList[]; }; -type TableData = { - id?: string; - accuracy?: number; - duration?: number; - train_loss?: number; - status?: string; - feature?: string; - althorithm?: string; -}; - -function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { - const [tableData, setTableData] = useState<TableData[]>([]); - useEffect(() => { - if (fileUrl) { - getHistoryFile(); - } - }, [fileUrl]); - - // 获取实验运行历史记录 - const getHistoryFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - const data: any[] = res.data; - const list: TableData[] = data.map((item) => { - return { - id: item[0]?.[0], - accuracy: item[1]?.[5]?.accuracy, - duration: item[1]?.[5]?.duration, - train_loss: item[1]?.[5]?.train_loss, - status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], - }; - }); - list.forEach((item) => { - if (!item.id) return; - const config = (res as any).configs?.[item.id]; - item.feature = config?.['feature_preprocessor:__choice__']; - item.althorithm = isClassification - ? config?.['classifier:__choice__'] - : config?.['regressor:__choice__']; - }); - setTableData(list); - } - }; +function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { + const first: HyperParameterTrialList | undefined = trialList[0]; + const config: Record<string, any> = first?.config ?? {}; + const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; + const paramsNames = Object.keys(config); + const metricNames = Object.keys(metricAnalysis); - const columns: TableProps<TableData>['columns'] = [ - { - title: 'ID', - dataIndex: 'id', - key: 'id', - width: 80, - render: tableCellRender(false), - }, - { - title: '准确率', - dataIndex: 'accuracy', - key: 'accuracy', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, + const columns: TableProps<HyperParameterTrialList>['columns'] = [ { - title: '耗时', - dataIndex: 'duration', - key: 'duration', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '序号', + dataIndex: 'index', + key: 'index', + width: 100, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), }, { - title: '训练损失', - dataIndex: 'train_loss', - key: 'train_loss', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, - { - title: '特征处理', - dataIndex: 'feature', - key: 'feature', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '运行次数', + dataIndex: 'training_iteration', + key: 'training_iteration', + width: 120, + render: tableCellRender(false), }, { - title: '算法', - dataIndex: 'althorithm', - key: 'althorithm', - render: tableCellRender(true), + title: '平均时长(秒)', + dataIndex: 'time_avg', + key: 'time_avg', + width: 150, + render: tableCellRender(false, TableCellValueType.Custom, { + format: (value = 0) => Number(value).toFixed(2), + }), ellipsis: { showTitle: false }, }, { @@ -107,6 +50,52 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps }, ]; + if (paramsNames.length) { + columns.push({ + title: '运行参数', + dataIndex: 'config', + key: 'config', + align: 'center', + children: paramsNames.map((name) => ({ + title: ( + <Tooltip title={name}> + <span>{name}</span> + </Tooltip> + ), + dataIndex: ['config', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + + if (metricNames.length) { + columns.push({ + title: `指标分析(${first.metric ?? ''})`, + dataIndex: 'metrics', + key: 'metrics', + align: 'center', + children: metricNames.map((name) => ({ + title: ( + <Tooltip title={name}> + <span>{name}</span> + </Tooltip> + ), + dataIndex: ['metric_analysis', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + return ( <div className={styles['experiment-history']}> <div className={styles['experiment-history__content']}> @@ -117,11 +106,12 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps )} > <Table - dataSource={tableData} + dataSource={trialList} columns={columns} pagination={false} - scroll={{ y: 'calc(100% - 55px)' }} - rowKey="id" + bordered={true} + scroll={{ y: 'calc(100% - 110px)', x: '100%' }} + rowKey="trial_id" /> </div> </div> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index e69de29b..9991a65c 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -0,0 +1,83 @@ +import KFIcon from '@/components/KFIcon'; +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; +import { Tabs } from 'antd'; +import { useEffect } from 'react'; +import styles from './index.less'; + +type ExperimentLogProps = { + instanceInfo: HyperParameterInstanceData; +}; + +function ExperimentLog({ instanceInfo }: ExperimentLogProps) { + const tabItems = [ + { + key: 'git-clone-1', + label: '框架代码日志', + icon: <KFIcon type="icon-jibenxinxi" />, + children: ( + <div className={styles['auto-ml-instance__log']}> + {instanceInfo && instanceInfo.nodeStatus && ( + <LogList + instanceName={instanceInfo.argo_ins_name} + instanceNamespace={instanceInfo.argo_ins_ns} + pipelineNodeId={instanceInfo.nodeStatus.displayName} + workflowId={instanceInfo.nodeStatus.id} + instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} + instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + ></LogList> + )} + </div> + ), + }, + { + key: 'git-clone-2', + label: '训练代码日志', + icon: <KFIcon type="icon-rizhi1" />, + children: ( + <div className={styles['auto-ml-instance__log']}> + {instanceInfo && instanceInfo.nodeStatus && ( + <LogList + instanceName={instanceInfo.argo_ins_name} + instanceNamespace={instanceInfo.argo_ins_ns} + pipelineNodeId={instanceInfo.nodeStatus.displayName} + workflowId={instanceInfo.nodeStatus.id} + instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} + instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + ></LogList> + )} + </div> + ), + }, + { + key: 'auto-hpo', + label: '超参寻优日志', + icon: <KFIcon type="icon-rizhi1" />, + children: ( + <div className={styles['auto-ml-instance__log']}> + {instanceInfo && instanceInfo.nodeStatus && ( + <LogList + instanceName={instanceInfo.argo_ins_name} + instanceNamespace={instanceInfo.argo_ins_ns} + pipelineNodeId={instanceInfo.nodeStatus.displayName} + workflowId={instanceInfo.nodeStatus.id} + instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} + instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + ></LogList> + )} + </div> + ), + }, + ]; + + useEffect(() => {}, []); + + return ( + <div className={styles['experiment-log']}> + <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> + </div> + ); +} + +export default ExperimentLog; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less index 342817c3..55ea8aed 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -6,47 +6,12 @@ background-color: white; border-radius: 10px; - &__download { - padding-top: 16px; - padding-bottom: 16px; - - padding-left: @content-padding; - color: @text-color; - font-size: 13px; - background-color: #f8f8f9; - border-radius: 4px; - - &__btn { - display: block; - height: 36px; - margin-top: 15px; - font-size: 14px; - } + &__table { + height: 400px; } &__text { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; white-space: pre-wrap; } - - &__images { - display: flex; - align-items: flex-start; - width: 100%; - overflow-x: auto; - - :global { - .ant-image { - margin-right: 20px; - - &:last-child { - margin-right: 0; - } - } - } - - &__item { - height: 248px; - border: 1px solid rgba(96, 107, 122, 0.3); - } - } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index a826155d..7fa22912 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,25 +1,44 @@ import InfoGroup from '@/components/InfoGroup'; +import { HyperParameterFileList } from '@/pages/HyperParameter/types'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import { Button, Image } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { + fileList?: HyperParameterFileList[]; fileUrl?: string; - imageUrl?: string; - modelPath?: string; }; -function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { +function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { const [result, setResult] = useState<string | undefined>(''); - const images = useMemo(() => { - if (imageUrl) { - return imageUrl.split(',').map((item) => item.trim()); - } - return []; - }, [imageUrl]); + const columns: TableProps<HyperParameterFileList>['columns'] = [ + { + title: '序号', + dataIndex: 'index', + key: 'index', + width: 120, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), + }, + { + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), + }, + ]; useEffect(() => { if (fileUrl) { @@ -37,45 +56,25 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp return ( <div className={styles['experiment-result']}> + <InfoGroup title="文件列表" style={{ margin: '16px 0' }}> + <div + className={classNames( + 'vertical-scroll-table-no-page', + styles['experiment-result__table'], + )} + > + <Table + dataSource={fileList} + columns={columns} + pagination={false} + scroll={{ y: 'calc(100% - 55px)', x: '100%' }} + rowKey="name" + /> + </div> + </InfoGroup> <InfoGroup title="实验结果" height={420} width="100%"> <div className={styles['experiment-result__text']}>{result}</div> </InfoGroup> - <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> - <div className={styles['experiment-result__images']}> - <Image.PreviewGroup - preview={{ - onChange: (current, prev) => - console.log(`current index: ${current}, prev index: ${prev}`), - }} - > - {images.map((item) => ( - <Image - key={item} - className={styles['experiment-result__images__item']} - src={item} - height={248} - draggable={false} - alt="" - /> - ))} - </Image.PreviewGroup> - </div> - </InfoGroup> - {modelPath && ( - <div className={styles['experiment-result__download']}> - <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> - <span>save_model.joblib</span> - <Button - type="primary" - className={styles['experiment-result__download__btn']} - onClick={() => { - window.location.href = modelPath; - }} - > - 模型下载 - </Button> - </div> - )} </div> ); } diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 817d8418..47e5534e 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -90,7 +90,7 @@ function HyperParameterBasic({ return [ { label: '代码', - value: info.code, + value: info.code_config, format: formatCodeConfig, }, { diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index f8508a88..3a14861d 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -42,13 +42,11 @@ export type HyperParameterData = { } & FormData; // 自动机器学习实验实例 -export type AutoMLInstanceData = { +export type HyperParameterInstanceData = { id: number; - auto_ml_id: number; + ray_id: number; result_path: string; - model_path: string; - img_path: string; - run_history_path: string; + result_txt: string; state: number; status: string; node_status: string; @@ -60,5 +58,22 @@ export type AutoMLInstanceData = { create_time: string; update_time: string; finish_time: string; - nodeStatus?: NodeStatus; + nodeStatus?: NodeStatus; // json之后的节点状态 + trial_list?: HyperParameterTrialList[]; + file_list?: HyperParameterFileList[]; +}; + +export type HyperParameterTrialList = { + trial_id?: string; + training_iteration?: number; + time?: number; + status?: string; + config?: Record<string, any>; + metric_analysis?: Record<string, any>; + metric: string; +}; + +export type HyperParameterFileList = { + name?: string; + size?: string; }; From 3430691d08733a370371dac937cbb82f0e567a89 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 28 Feb 2025 11:01:39 +0800 Subject: [PATCH 062/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../minio/MinioStorageController.java | 11 +++- .../com/ruoyi/platform/domain/RayIns.java | 5 +- .../ruoyi/platform/service/MinioService.java | 3 ++ .../service/impl/MinioServiceImpl.java | 26 +++++++++ .../service/impl/RayInsServiceImpl.java | 53 +++++++++++++++---- .../com/ruoyi/platform/utils/MinioUtil.java | 3 +- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/minio/MinioStorageController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/minio/MinioStorageController.java index 66643ef4..a531b21c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/minio/MinioStorageController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/minio/MinioStorageController.java @@ -23,11 +23,20 @@ public class MinioStorageController { @Resource private MinioService minioService; + + @GetMapping("/downloadFile") + @ApiOperation("下载单个文件") + public ResponseEntity<InputStreamResource> downloadFile(@RequestParam("path") String path) throws Exception { + String bucketName = path.substring(0, path.indexOf("/")); + String prefix = path.substring(path.indexOf("/")+1,path.length()); + return minioService.downloadFile(bucketName, prefix); + } + @GetMapping("/download") @ApiOperation(value = "minio存储下载", notes = "minio存储下载文件为zip包") public ResponseEntity<InputStreamResource> downloadDataset(@RequestParam("path") String path) { String bucketName = path.substring(0, path.indexOf("/")); - String prefix = path.substring(path.indexOf("/")+1,path.length())+"/"; + String prefix = path.substring(path.indexOf("/")+1,path.length()); return minioService.downloadZipFile(bucketName,prefix); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java index aff4158e..e704b1a5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/RayIns.java @@ -22,9 +22,6 @@ public class RayIns { private String resultPath; - @TableField(exist = false) - private String resultTxt; - private Integer state; private String status; @@ -50,7 +47,7 @@ public class RayIns { private Date finishTime; @TableField(exist = false) - private List<Map> fileList; + private String resultTxt; @TableField(exist = false) private ArrayList<Map<String, Object>> trialList; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java index 3b990a86..1af311c7 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java @@ -9,6 +9,9 @@ import java.util.List; import java.util.Map; public interface MinioService { + + ResponseEntity<InputStreamResource> downloadFile(String bucketName , String path) throws Exception; + ResponseEntity<InputStreamResource> downloadZipFile(String bucketName , String path); Map<String, String> uploadFile(String bucketName, String objectName, MultipartFile file ) throws Exception; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java index 380f96c6..5adf760c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java @@ -31,6 +31,32 @@ public class MinioServiceImpl implements MinioService { public MinioServiceImpl(MinioUtil minioUtil) { this.minioUtil = minioUtil; } + + @Override + public ResponseEntity<InputStreamResource> downloadFile(String bucketName , String url) throws Exception { + try { + // 使用ByteArrayOutputStream来捕获下载的数据 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + minioUtil.downloadObject(bucketName, url, outputStream); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + InputStreamResource resource = new InputStreamResource(inputStream); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + extractFileName(url) + "\"") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + } catch (Exception e) { + e.printStackTrace(); + throw new Exception("下载文件错误"); + } + } + + private String extractFileName(String urlStr) { + return urlStr.substring(urlStr.lastIndexOf('/') + 1); + } + + @Override public ResponseEntity<InputStreamResource> downloadZipFile(String bucketName,String path) { try { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index f7f5811e..7cec3cfd 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -11,6 +11,8 @@ import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.JsonUtils; import com.ruoyi.platform.utils.MinioUtil; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -26,6 +28,8 @@ import java.util.stream.Collectors; @Service("rayInsService") public class RayInsServiceImpl implements RayInsService { + private static final Logger logger = LoggerFactory.getLogger(RayInsServiceImpl.class); + @Value("${argo.url}") private String argoUrl; @Value("${argo.workflowStatus}") @@ -34,8 +38,6 @@ public class RayInsServiceImpl implements RayInsService { private String argoWorkflowTermination; @Value("${minio.endpointIp}") String endpoint; - @Value("${minio.dataReleaseBucketName}") - private String bucketName; @Resource private RayInsDao rayInsDao; @@ -174,8 +176,6 @@ public class RayInsServiceImpl implements RayInsService { rayIns = queryStatusFromArgo(rayIns); } getTrialList(rayIns); - -// rayIns.setTrialList(getTrialList(rayIns.getResultPath())); return rayIns; } @@ -276,12 +276,12 @@ public class RayInsServiceImpl implements RayInsService { rayIns.setResultPath(endpoint + "/" + directoryPath); rayIns.setResultTxt(endpoint + "/" + directoryPath + "/result.txt"); + String bucketName = directoryPath.substring(0, directoryPath.indexOf("/")); String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; - List<Map> maps = minioUtil.listRayFilesInDirectory(bucketName, prefix); + List<Map> fileMaps = minioUtil.listRayFilesInDirectory(bucketName, prefix); - if (!maps.isEmpty()) { - rayIns.setFileList(maps); - List<Map> collect = maps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); + if (!fileMaps.isEmpty()) { + List<Map> collect = fileMaps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); if (!collect.isEmpty()) { Path experimentState = Paths.get(collect.get(0).get("name").toString()); String content = minioUtil.readObjectAsString(bucketName, prefix + "/" + experimentState); @@ -294,8 +294,9 @@ public class RayInsServiceImpl implements RayInsService { Map<String, Object> trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); Map<String, Object> trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); + String trialId = (String) trial_data_0.get("trial_id"); Map<String, Object> trial = new HashMap<>(); - trial.put("trial_id", trial_data_0.get("trial_id")); + trial.put("trial_id", trialId); trial.put("config", trial_data_0.get("config")); trial.put("status", trial_data_0.get("status")); @@ -310,10 +311,44 @@ public class RayInsServiceImpl implements RayInsService { trial.put("metric_analysis", metric_analysis.get((String) param.get("metric"))); trial.put("metric", param.get("metric")); + for (Map fileMap : fileMaps) { + if (fileMap.get("name").toString().contains(trialId)) { + trial.put("file", fileMap); + } + } + + try { + String resultTxt = minioUtil.readObjectAsString(bucketName, prefix + "result.txt"); + String bestTrialId = getStringBetween(resultTxt, "'trial_id': '", "'"); + if (bestTrialId.equals(trialId)) { + trial.put("is_best", true); + } + } catch (Exception e) { + logger.error("未找到结果文件:result.txt"); + } trialList.add(trial); } rayIns.setTrialList(trialList); } } } + + String getStringBetween(String input, String startMarker, String endMarker) { + int startIndex = input.indexOf(startMarker); + if (startIndex == -1) { + return ""; // 如果未找到起始标记,返回空字符串 + } + + // 跳过起始标记 + startIndex += startMarker.length(); + + int endIndex = input.indexOf(endMarker, startIndex); + if (endIndex == -1) { + return ""; // 如果未找到结束标记,返回空字符串 + } + + return input.substring(startIndex, endIndex); + } } + + diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java index 3e2b8c9d..7649ff60 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java @@ -355,9 +355,8 @@ public class MinioUtil { map.put("children", listRayFilesInDirectory(bucketName, fullPath)); } else { map.put("isFile", true); - map.put("url", minioEndpoint + "/" + bucketName + "/" + fullPath); } - + map.put("url", bucketName + "/" + fullPath); fileInfoList.add(map); } From 9190a238176a7597fcdfab63488a8429c7d8854c Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 28 Feb 2025 11:46:34 +0800 Subject: [PATCH 063/127] =?UTF-8?q?docs:=20storybook=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20ParameterSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParameterSelect/index.tsx | 39 ++- react-ui/src/stories/CodeSelect.stories.tsx | 8 +- react-ui/src/stories/FormInfo.stories.tsx | 26 +- .../src/stories/ParameterSelect.stories.tsx | 126 +++++++++ react-ui/src/stories/mockData.ts | 247 ++++++++++++++++++ 5 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 react-ui/src/stories/ParameterSelect.stories.tsx diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 2c9f862f..a6a911e5 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -4,22 +4,35 @@ * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { PipelineNodeModelParameter } from '@/types'; import { to } from '@/utils/promise'; -import { Select } from 'antd'; +import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; import { paramSelectConfig } from './config'; -type ParameterSelectProps = { - value?: PipelineNodeModelParameter; - onChange?: (value: PipelineNodeModelParameter) => void; - disabled?: boolean; +/** 值类型 */ +export type ParameterSelectValue = { + /** 类型,参数名是和后台保持一致的 */ + item_type: 'dataset' | 'model' | 'service' | 'resource'; + /** 值 */ + value?: any; + /** 占位符 */ + placeholder?: string; + /** 其它属性 */ + [key: string]: any; }; -function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { +interface ParameterSelectProps extends SelectProps { + /** 值 */ + value?: ParameterSelectValue; + /** 修改后回调 */ + onChange?: (value: ParameterSelectValue) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; + const valueNotNullable = value ?? ({} as ParameterSelectValue); + const { item_type } = valueNotNullable; const propsConfig = paramSelectConfig[item_type]; useEffect(() => { @@ -28,7 +41,7 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP const hangleChange = (e: string) => { onChange?.({ - ...valueNonNullable, + ...valueNotNullable, value: e, }); }; @@ -47,14 +60,14 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP return ( <Select - placeholder={valueNonNullable.placeholder} + {...rest} + placeholder={valueNotNullable.placeholder || rest.placeholder} filterOption={propsConfig?.filterOption} options={options} fieldNames={propsConfig?.fieldNames} - value={valueNonNullable.value} + value={valueNotNullable.value} optionFilterProp={propsConfig.optionFilterProp} onChange={hangleChange} - disabled={disabled} showSearch allowClear /> diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index 08520603..ce4b2c90 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -56,7 +56,13 @@ export const Primary: Story = { export const InForm: Story = { render: ({ onChange }) => { return ( - <Form name="code-select-form" size="large"> + <Form + name="code-select-form" + labelCol={{ flex: '80px' }} + labelAlign="left" + size="large" + autoComplete="off" + > <Row gutter={8}> <Col span={10}> <Form.Item label="代码配置" name="code_config"> diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index 09466a46..a214abae 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -1,6 +1,6 @@ import FormInfo from '@/components/FormInfo'; import type { Meta, StoryObj } from '@storybook/react'; -import { Form, Input, Select } from 'antd'; +import { Form, Input, Select, Typography } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -52,6 +52,7 @@ export const InForm: Story = { ant_input_text: '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', ant_select_text: 1, + ant_select_ellipsis_text: 1, }} > <Form.Item label="文本" name="text"> @@ -69,7 +70,7 @@ export const InForm: Story = { <Form.Item label="无内容" name="empty_text"> <FormInfo /> </Form.Item> - <Form.Item label="Select" name="select_text"> + <Form.Item label="模拟 Select" name="select_text"> <FormInfo select options={[ @@ -96,6 +97,27 @@ export const InForm: Story = { ]} /> </Form.Item> + <Form.Item label="Select Ellipsis" name="ant_select_ellipsis_text"> + <Select + labelRender={(props) => { + return ( + <div style={{ width: '100%', lineHeight: 'normal' }}> + <Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}> + {props.label} + </Typography.Text> + </div> + ); + }} + disabled + options={[ + { + label: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + value: 1, + }, + ]} + /> + </Form.Item> </Form> ); }, diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx new file mode 100644 index 00000000..6aed4e31 --- /dev/null +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -0,0 +1,126 @@ +import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSelect'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { computeResourceData, datasetListData, modelListData, serviceListData } from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ParameterSelect 参数选择器', + component: ParameterSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/service', () => { + return HttpResponse.json(serviceListData); + }), + http.get('/api/mmp/computingResource', () => { + return HttpResponse.json(computeResourceData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta<typeof ParameterSelect>; + +export default meta; +type Story = StoryObj<typeof meta>; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + value: { + item_type: 'dataset', + placeholder: '请选择数据集', + }, + style: { width: 400 }, + size: 'large', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterSelectValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return <ParameterSelect {...args} value={value} onChange={handleChange}></ParameterSelect>; + }, +}; + +export const InForm: Story = { + render: ({ onChange }) => { + return ( + <Form + name="parameter-select-form" + labelCol={{ flex: '80px' }} + labelAlign="left" + size="large" + autoComplete="off" + initialValues={{ + dataset: { + item_type: 'dataset', + placeholder: '请选择数据集', + }, + model: { + item_type: 'model', + placeholder: '请选择模型', + }, + service: { + item_type: 'service', + placeholder: '请选择服务', + }, + resource: { + item_type: 'resource', + placeholder: '请选择计算资源', + }, + }} + > + <Row gutter={8}> + <Col span={10}> + <Form.Item label="数据集" name="dataset"> + <ParameterSelect onChange={onChange} /> + </Form.Item> + </Col> + </Row> + <Row gutter={8}> + <Col span={10}> + <Form.Item label="模型" name="model"> + <ParameterSelect onChange={onChange} /> + </Form.Item> + </Col> + </Row> + <Row gutter={8}> + <Col span={10}> + <Form.Item label="服务" name="service"> + <ParameterSelect onChange={onChange} /> + </Form.Item> + </Col> + </Row> + <Row gutter={8}> + <Col span={10}> + <Form.Item label="计算资源" name="resource"> + <ParameterSelect onChange={onChange} /> + </Form.Item> + </Col> + </Row> + </Form> + ); + }, +}; diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts index 3d910b06..11b5ffa2 100644 --- a/react-ui/src/stories/mockData.ts +++ b/react-ui/src/stories/mockData.ts @@ -546,3 +546,250 @@ export const codeListData = { empty: false, }, }; + +export const serviceListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 25, + service_name: '测试1224', + service_type: 'video', + service_type_name: '视频', + description: '测试', + create_by: 'admin', + update_by: 'admin', + create_time: '2024-12-24T16:01:02.000+08:00', + update_time: '2024-12-24T16:01:02.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 12, + service_name: '介电材料', + service_type: 'text', + service_type_name: '文本', + description: 'test', + create_by: 'admin', + update_by: 'admin', + create_time: '2024-11-27T09:30:23.000+08:00', + update_time: '2024-11-27T09:30:23.000+08:00', + state: 1, + version_count: 0, + }, + { + id: 7, + service_name: '手写体识别', + service_type: 'image', + service_type_name: '图片', + description: '手写体识别服务', + create_by: 'admin', + update_by: 'admin', + create_time: '2024-10-10T10:14:00.000+08:00', + update_time: '2024-10-10T10:14:00.000+08:00', + state: 1, + version_count: 5, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageNumber: 0, + pageSize: 10, + offset: 0, + paged: true, + unpaged: false, + }, + last: true, + totalPages: 1, + totalElements: 3, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 3, + size: 10, + empty: false, + }, +}; + +export const computeResourceData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 15, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":1,"memory":"2GB"}}', + description: 'GPU: 0, CPU:1, 内存: 2GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 16, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":2,"memory":"4GB"}}', + description: 'GPU: 0, CPU:2, 内存: 4GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 17, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":4,"memory":"8GB"}}', + description: 'GPU: 0, CPU:4, 内存: 8GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 18, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":1,"memory":"2GB"}}', + description: 'GPU: 1, CPU:1, 内存: 2GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 19, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":2,"memory":"4GB"}}', + description: 'GPU: 1, CPU:2, 内存: 4GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 20, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":4,"memory":"8GB"}}', + description: 'GPU: 1, CPU:4, 内存: 8GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 21, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":2,"cpu":2,"memory":"4GB"}}', + description: 'GPU: 2, CPU:2, 内存: 4GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 22, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":2,"cpu":4,"memory":"8GB"}}', + description: 'GPU: 2, CPU:4, 内存: 8GB', + create_by: 'admin', + create_time: '2024-04-19T00:00:00.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T00:00:00.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 23, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"RTX 3080 Ti","gpu":1,"cpu":1,"memory":"2GB"}}', + description: 'GPU: 1, CPU:1, 内存: 2GB, 显存: 12GB', + create_by: 'admin', + create_time: '2024-04-19T11:38:07.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T11:38:07.000+08:00', + state: 1, + used_state: null, + node: null, + }, + { + id: 24, + computing_resource: 'GPU', + standard: + '{"name":"CPU-GPU","value":{"detail_type":"RTX 3080","gpu":1,"cpu":2,"memory":"4GB"}}', + description: 'GPU: 1, CPU:2, 内存: 4GB, 显存: 10GB', + create_by: 'admin', + create_time: '2024-04-19T11:39:40.000+08:00', + update_by: 'admin', + update_time: '2024-04-19T11:39:40.000+08:00', + state: 1, + used_state: null, + node: null, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageNumber: 0, + pageSize: 1000, + offset: 0, + paged: true, + unpaged: false, + }, + last: true, + totalPages: 1, + totalElements: 10, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 10, + size: 1000, + empty: false, + }, +}; From 607a8b71efd45fe435c499a90b086a307efce1d9 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 28 Feb 2025 11:50:23 +0800 Subject: [PATCH 064/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/platform/service/impl/RayInsServiceImpl.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 7cec3cfd..8ff9b06d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -286,6 +286,11 @@ public class RayInsServiceImpl implements RayInsService { Path experimentState = Paths.get(collect.get(0).get("name").toString()); String content = minioUtil.readObjectAsString(bucketName, prefix + "/" + experimentState); + String resultTxt = minioUtil.readObjectAsString(bucketName, prefix + "result.txt"); + String bestMetrics = getStringBetween(resultTxt, "Best metrics:", "Best result_df"); + Map<String, Object> bestMetricsMap = JsonUtils.jsonToMap(bestMetrics); + String bestTrialId = (String)bestMetricsMap.get("trial_id"); + Map<String, Object> result = JsonUtils.jsonToMap(content); ArrayList<ArrayList> trial_data_list = (ArrayList<ArrayList>) result.get("trial_data"); ArrayList<Map<String, Object>> trialList = new ArrayList<>(); @@ -318,8 +323,6 @@ public class RayInsServiceImpl implements RayInsService { } try { - String resultTxt = minioUtil.readObjectAsString(bucketName, prefix + "result.txt"); - String bestTrialId = getStringBetween(resultTxt, "'trial_id': '", "'"); if (bestTrialId.equals(trialId)) { trial.put("is_best", true); } From f0d1a13cad22854af0aab0cc9c06fd6892999f99 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 28 Feb 2025 15:38:30 +0800 Subject: [PATCH 065/127] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-trail=E5=88=97=E8=A1=A8=20=E2=80=9D=20'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- .../components/ExperimentResult/index.less | 2 +- .../Experiment/components/LogGroup/index.tsx | 29 +++---- .../Experiment/components/LogList/index.tsx | 11 ++- .../pages/HyperParameter/Instance/index.less | 2 +- .../pages/HyperParameter/Instance/index.tsx | 57 ++++--------- .../components/ExperimentHistory/index.less | 23 ++++++ .../components/ExperimentHistory/index.tsx | 72 +++++++++++++++-- .../components/ExperimentLog/index.less | 16 ++++ .../components/ExperimentLog/index.tsx | 81 +++++++++++++------ .../components/ExperimentResult/index.less | 2 +- .../components/ExperimentResult/index.tsx | 49 +---------- react-ui/src/pages/HyperParameter/types.ts | 9 ++- 13 files changed, 212 insertions(+), 143 deletions(-) diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 19a6414d..677cc791 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -22,7 +22,7 @@ enum TabKeys { History = 'history', } -const NodePrefix = 'auto-hpo'; +const NodePrefix = 'auto-ml'; function AutoMLInstance() { const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index bcb52314..27034da0 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -26,7 +26,7 @@ &__text { font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; - white-space: pre-wrap; + white-space: pre; } &__images { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 6dd31166..9625118a 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -17,6 +17,7 @@ import styles from './index.less'; export type LogGroupProps = ExperimentLog & { status?: ExperimentStatus; // 实验状态 + listId: string; }; type Log = { @@ -25,25 +26,13 @@ type Log = { pod_name: string; // pod名称 }; -// 滚动到底部 -const scrollToBottom = (smooth: boolean = true) => { - const element = document.getElementById('log-list'); - if (element) { - const optons: ScrollToOptions = { - top: element.scrollHeight, - behavior: smooth ? 'smooth' : 'instant', - }; - - element.scrollTo(optons); - } -}; - function LogGroup({ log_type = 'normal', pod_name = '', log_content = '', start_time, status, + listId, }: LogGroupProps) { const [collapse, setCollapse] = useState(true); const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); @@ -135,7 +124,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.197:31213'; + host = '172.20.32.181:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, @@ -210,6 +199,18 @@ function LogGroup({ } }; + // 滚动到底部 + const scrollToBottom = (smooth: boolean = true) => { + const element = document.getElementById(listId); + if (element) { + const optons: ScrollToOptions = { + top: element.scrollHeight, + behavior: smooth ? 'smooth' : 'instant', + }; + + element.scrollTo(optons); + } + }; const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 8beebc49..b45fdae4 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -14,6 +14,7 @@ export type ExperimentLog = { }; type LogListProps = { + idPrefix?: string; // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 instanceName: string; // 实验实例 name instanceNamespace: string; // 实验实例 namespace pipelineNodeId: string; // 流水线节点 id @@ -23,6 +24,7 @@ type LogListProps = { }; function LogList({ + idPrefix, instanceName, instanceNamespace, pipelineNodeId, @@ -86,10 +88,15 @@ function LogList({ } }; + // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 + const listId = idPrefix ? `${idPrefix}-log-list` : 'log-list'; + return ( - <div className={styles['log-list']} id="log-list"> + <div className={styles['log-list']} id={listId}> {logList.length > 0 ? ( - logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) + logList.map((v) => ( + <LogGroup key={v.pod_name} {...v} listId={listId} status={instanceNodeStatus} /> + )) ) : ( <div className={styles['log-list__empty']}>暂无日志</div> )} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less index 8d57a98d..9a2f8bfb 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.less +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -34,7 +34,7 @@ &__log { height: calc(100% - 10px); margin-top: 10px; - padding: 20px calc(@content-padding - 8px); + padding: 8px calc(@content-padding - 8px) 20px; overflow-y: visible; background-color: white; border-radius: 10px; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 5cc1fded..d38b0ee6 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,6 +1,5 @@ import KFIcon from '@/components/KFIcon'; import { ExperimentStatus } from '@/enums'; -import LogList from '@/pages/Experiment/components/LogList'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; @@ -10,6 +9,7 @@ import { useParams } from '@umijs/max'; import { Tabs } from 'antd'; import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentLog from '../components/ExperimentLog'; import ExperimentResult from '../components/ExperimentResult'; import HyperParameterBasic from '../components/HyperParameterBasic'; import { HyperParameterData, HyperParameterInstanceData } from '../types'; @@ -22,8 +22,6 @@ enum TabKeys { History = 'history', } -const NodePrefix = 'auto-hpo'; - function HyperParameterInstance() { const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); @@ -32,6 +30,7 @@ function HyperParameterInstance() { ); // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); + const [nodes, setNodes] = useState<Record<string, NodeStatus> | undefined>(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef<EventSource | null>(null); @@ -72,30 +71,27 @@ function HyperParameterInstance() { setExperimentInfo(paramJson); } + setInstanceInfo(info); + // 这个接口返回的状态有延时,SSE 返回的状态是最新的 - // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE + // SSE 调用时,不需要解析 node_status,也不要重新建立 SSE if (isStatusDetermined) { - setInstanceInfo((prev) => ({ - ...info, - nodeStatus: prev!.nodeStatus, - })); return; } // 进行节点状态 const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { - Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith(NodePrefix)) { - const nodeStatus = nodeStatusJson[key]; - info.nodeStatus = nodeStatus; - } else if (key.startsWith('workflow')) { + setNodes(nodeStatusJson); + Object.keys(nodeStatusJson).some((key) => { + if (key.startsWith('workflow')) { const workflowStatus = nodeStatusJson[key]; setWorkflowStatus(workflowStatus); + return true; } + return false; }); } - setInstanceInfo(info); // 运行中或者等待中,开启 SSE if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { @@ -106,9 +102,9 @@ function HyperParameterInstance() { const setupSSE = (name: string, namespace: string) => { let { origin } = location; - if (process.env.NODE_ENV === 'development') { - origin = 'http://172.20.32.197:31213'; - } + // if (process.env.NODE_ENV === 'development') { + // origin = 'http://172.20.32.197:31213'; + // } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, @@ -123,20 +119,12 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - const nodeStatus = Object.values(nodes).find((node: any) => - node.displayName.startsWith(NodePrefix), - ) as NodeStatus; const workflowStatus = Object.values(nodes).find((node: any) => node.displayName.startsWith('workflow'), ) as NodeStatus; - // 节点状态 - if (nodeStatus) { - setInstanceInfo((prev) => ({ - ...prev!, - nodeStatus: nodeStatus, - })); - } + // 节点 + setNodes(nodes); // 设置工作流状态 if (workflowStatus) { @@ -188,16 +176,7 @@ function HyperParameterInstance() { icon: <KFIcon type="icon-rizhi1" />, children: ( <div className={styles['hyper-parameter-instance__log']}> - {instanceInfo && instanceInfo.nodeStatus && ( - <LogList - instanceName={instanceInfo.argo_ins_name} - instanceNamespace={instanceInfo.argo_ins_ns} - pipelineNodeId={instanceInfo.nodeStatus.displayName} - workflowId={instanceInfo.nodeStatus.id} - instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} - instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} - ></LogList> - )} + {instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />} </div> ), }, @@ -208,9 +187,7 @@ function HyperParameterInstance() { key: TabKeys.Result, label: '实验结果', icon: <KFIcon type="icon-shiyanjieguo1" />, - children: ( - <ExperimentResult fileUrl={instanceInfo?.result_txt} fileList={instanceInfo?.file_list} /> - ), + children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />, }, { key: TabKeys.History, diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index b30e30b2..8e3e29a2 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -33,3 +33,26 @@ } } } + +.cell-index { + position: relative; + width: 100%; + padding-left: 20px; + text-align: left; + + &__best-tag { + margin-left: 8px; + padding: 1px 10px; + color: @primary-color; + font-weight: normal; + font-size: 13px; + background-color: .addAlpha(@primary-color, 0.1) []; + border: 1px solid .addAlpha(@primary-color, 0.5) []; + border-radius: 2px; + } +} + +.table-best-row { + color: @primary-color; + font-weight: bold; +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 2d57a845..41a897d1 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,6 +1,8 @@ -import { HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import KFIcon from '@/components/KFIcon'; +import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import { downLoadZip } from '@/utils/downloadfile'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Table, Tooltip, type TableProps } from 'antd'; +import { Button, Table, Tooltip, type TableProps } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; @@ -15,14 +17,21 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { const paramsNames = Object.keys(config); const metricNames = Object.keys(metricAnalysis); - const columns: TableProps<HyperParameterTrialList>['columns'] = [ + const trialColumns: TableProps<HyperParameterTrialList>['columns'] = [ { title: '序号', dataIndex: 'index', key: 'index', - width: 100, + width: 120, align: 'center', - render: tableCellRender(false, TableCellValueType.Index), + render: (_text, record, index: number) => { + return ( + <div className={styles['cell-index']}> + <span className={styles['cell-index__text']}>{index + 1}</span> + {record.is_best && <span className={styles['cell-index__best-tag']}>最佳</span>} + </div> + ); + }, }, { title: '运行次数', @@ -51,7 +60,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { ]; if (paramsNames.length) { - columns.push({ + trialColumns.push({ title: '运行参数', dataIndex: 'config', key: 'config', @@ -74,7 +83,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { } if (metricNames.length) { - columns.push({ + trialColumns.push({ title: `指标分析(${first.metric ?? ''})`, dataIndex: 'metrics', key: 'metrics', @@ -96,6 +105,51 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }); } + const fileColumns: TableProps<HyperParameterFileList>['columns'] = [ + { + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), + }, + { + title: '操作', + dataIndex: 'option', + width: 160, + key: 'option', + render: (_: any, record: HyperParameterFileList) => { + return ( + <Button + type="link" + size="small" + key="download" + icon={<KFIcon type="icon-xiazai" />} + onClick={() => { + if (record.isFile) { + downLoadZip(`/api/mmp/minioStorage/downloadFile`, { path: record.url }); + } else { + downLoadZip(`/api/mmp/minioStorage/download`, { path: record.url }); + } + }} + > + 下载 + </Button> + ); + }, + }, + ]; + + const expandedRowRender = (record: HyperParameterTrialList) => ( + <Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" /> + ); + return ( <div className={styles['experiment-history']}> <div className={styles['experiment-history__content']}> @@ -106,12 +160,14 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { )} > <Table + rowClassName={(record) => (record.is_best ? styles['table-best-row'] : '')} dataSource={trialList} - columns={columns} + columns={trialColumns} pagination={false} bordered={true} scroll={{ y: 'calc(100% - 110px)', x: '100%' }} rowKey="trial_id" + expandable={{ expandedRowRender }} /> </div> </div> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less index e69de29b..6eb6f074 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less @@ -0,0 +1,16 @@ +.experiment-log { + height: 100%; + &__tabs { + height: 100%; + :global { + .ant-tabs-nav-list { + padding-left: 0 !important; + background: none !important; + } + } + + &__log { + height: 100%; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index 9991a65c..90da4ff5 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -1,50 +1,78 @@ -import KFIcon from '@/components/KFIcon'; import { ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; +import { NodeStatus } from '@/types'; import { Tabs } from 'antd'; import { useEffect } from 'react'; import styles from './index.less'; type ExperimentLogProps = { instanceInfo: HyperParameterInstanceData; + nodes: Record<string, NodeStatus>; }; -function ExperimentLog({ instanceInfo }: ExperimentLogProps) { +function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { + let hpoNodeStatus: NodeStatus | undefined; + let frameworkCloneNodeStatus: NodeStatus | undefined; + let trainCloneNodeStatus: NodeStatus | undefined; + + Object.keys(nodes) + .sort((key1, key2) => { + const node1 = nodes[key1]; + const node2 = nodes[key2]; + return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime(); + }) + .forEach((key) => { + const node = nodes[key]; + if (node.displayName.startsWith('auto-hpo')) { + hpoNodeStatus = node; + } else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) { + frameworkCloneNodeStatus = node; + } else if ( + node.displayName.startsWith('git-clone') && + frameworkCloneNodeStatus && + node.displayName !== frameworkCloneNodeStatus?.displayName + ) { + trainCloneNodeStatus = node; + } + }); + const tabItems = [ { - key: 'git-clone-1', + key: 'git-clone-framework', label: '框架代码日志', - icon: <KFIcon type="icon-jibenxinxi" />, + // icon: <KFIcon type="icon-rizhi1" />, children: ( - <div className={styles['auto-ml-instance__log']}> - {instanceInfo && instanceInfo.nodeStatus && ( + <div className={styles['experiment-log__tabs__log']}> + {frameworkCloneNodeStatus && ( <LogList + idPrefix="git-clone-framework" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} - pipelineNodeId={instanceInfo.nodeStatus.displayName} - workflowId={instanceInfo.nodeStatus.id} - instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} - instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + pipelineNodeId={frameworkCloneNodeStatus.displayName} + workflowId={frameworkCloneNodeStatus.id} + instanceNodeStartTime={frameworkCloneNodeStatus.startedAt} + instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus} ></LogList> )} </div> ), }, { - key: 'git-clone-2', + key: 'git-clone-train', label: '训练代码日志', - icon: <KFIcon type="icon-rizhi1" />, + // icon: <KFIcon type="icon-rizhi1" />, children: ( - <div className={styles['auto-ml-instance__log']}> - {instanceInfo && instanceInfo.nodeStatus && ( + <div className={styles['experiment-log__tabs__log']}> + {trainCloneNodeStatus && ( <LogList + idPrefix="git-clone-train" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} - pipelineNodeId={instanceInfo.nodeStatus.displayName} - workflowId={instanceInfo.nodeStatus.id} - instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} - instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + pipelineNodeId={trainCloneNodeStatus.displayName} + workflowId={trainCloneNodeStatus.id} + instanceNodeStartTime={trainCloneNodeStatus.startedAt} + instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} ></LogList> )} </div> @@ -53,17 +81,18 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) { { key: 'auto-hpo', label: '超参寻优日志', - icon: <KFIcon type="icon-rizhi1" />, + // icon: <KFIcon type="icon-rizhi1" />, children: ( - <div className={styles['auto-ml-instance__log']}> - {instanceInfo && instanceInfo.nodeStatus && ( + <div className={styles['experiment-log__tabs__log']}> + {hpoNodeStatus && ( <LogList + idPrefix="auto-hpo" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} - pipelineNodeId={instanceInfo.nodeStatus.displayName} - workflowId={instanceInfo.nodeStatus.id} - instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} - instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} + pipelineNodeId={hpoNodeStatus.displayName} + workflowId={hpoNodeStatus.id} + instanceNodeStartTime={hpoNodeStatus.startedAt} + instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} ></LogList> )} </div> @@ -75,7 +104,7 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) { return ( <div className={styles['experiment-log']}> - <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> + <Tabs className={styles['experiment-log__tabs']} items={tabItems} /> </div> ); } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less index 55ea8aed..239a3abf 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -12,6 +12,6 @@ &__text { font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; - white-space: pre-wrap; + white-space: pre; } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index 7fa22912..dfb60b04 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,45 +1,16 @@ import InfoGroup from '@/components/InfoGroup'; -import { HyperParameterFileList } from '@/pages/HyperParameter/types'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Table, type TableProps } from 'antd'; -import classNames from 'classnames'; import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { - fileList?: HyperParameterFileList[]; fileUrl?: string; }; -function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { +function ExperimentResult({ fileUrl }: ExperimentResultProps) { const [result, setResult] = useState<string | undefined>(''); - const columns: TableProps<HyperParameterFileList>['columns'] = [ - { - title: '序号', - dataIndex: 'index', - key: 'index', - width: 120, - align: 'center', - render: tableCellRender(false, TableCellValueType.Index), - }, - { - title: '文件名称', - dataIndex: 'name', - key: 'name', - render: tableCellRender(false), - }, - { - title: '文件大小', - dataIndex: 'size', - key: 'size', - width: 200, - render: tableCellRender(false), - }, - ]; - useEffect(() => { if (fileUrl) { getResultFile(); @@ -56,23 +27,7 @@ function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { return ( <div className={styles['experiment-result']}> - <InfoGroup title="文件列表" style={{ margin: '16px 0' }}> - <div - className={classNames( - 'vertical-scroll-table-no-page', - styles['experiment-result__table'], - )} - > - <Table - dataSource={fileList} - columns={columns} - pagination={false} - scroll={{ y: 'calc(100% - 55px)', x: '100%' }} - rowKey="name" - /> - </div> - </InfoGroup> - <InfoGroup title="实验结果" height={420} width="100%"> + <InfoGroup title="最佳实验结果" width="100%"> <div className={styles['experiment-result__text']}>{result}</div> </InfoGroup> </div> diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 3a14861d..bff6d046 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -71,9 +71,14 @@ export type HyperParameterTrialList = { config?: Record<string, any>; metric_analysis?: Record<string, any>; metric: string; + file: HyperParameterFileList; + is_best?: boolean; }; export type HyperParameterFileList = { - name?: string; - size?: string; + name: string; + size: string; + url: string; + isFile: boolean; + children?: HyperParameterFileList[]; }; From d376779cd638618056c8181017e5854940da8220 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 28 Feb 2025 16:54:14 +0800 Subject: [PATCH 066/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ray/RayInsController.java | 8 ++++++ .../ruoyi/platform/service/RayInsService.java | 2 ++ .../service/impl/RayInsServiceImpl.java | 27 ++++++++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java index 1a358418..c13354a3 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/ray/RayInsController.java @@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.service.RayInsService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.springframework.web.bind.annotation.*; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -57,4 +58,11 @@ public class RayInsController extends BaseController { public GenericsAjaxResult<RayIns> getDetailById(@PathVariable("id") Long id) throws Exception { return genericsSuccess(this.rayInsService.getDetailById(id)); } + + @PostMapping("/getExpMetrics") + @ApiOperation("获取当前实验的指标对比地址") + @ApiResponse + public GenericsAjaxResult<String> getExpMetrics(@RequestBody String trailIds) throws Exception { + return genericsSuccess(rayInsService.getExpMetrics(trailIds)); + } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java index 391b112f..42da3ec2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/RayInsService.java @@ -22,4 +22,6 @@ public interface RayInsService { RayIns queryStatusFromArgo(RayIns ins); List<RayIns> queryByRayInsIsNotTerminated(); + + String getExpMetrics(String trailIds) throws Exception; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 8ff9b06d..8c81cb9a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -6,10 +6,7 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; -import com.ruoyi.platform.utils.DateUtils; -import com.ruoyi.platform.utils.HttpUtils; -import com.ruoyi.platform.utils.JsonUtils; -import com.ruoyi.platform.utils.MinioUtil; +import com.ruoyi.platform.utils.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +18,7 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.io.IOException; +import java.net.URLEncoder; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; @@ -39,6 +37,11 @@ public class RayInsServiceImpl implements RayInsService { @Value("${minio.endpointIp}") String endpoint; + @Value("${aim.url}") + private String aimUrl; + @Value("${aim.proxyUrl}") + private String aimProxyUrl; + @Resource private RayInsDao rayInsDao; @@ -269,6 +272,22 @@ public class RayInsServiceImpl implements RayInsService { return rayInsDao.queryByRayInsIsNotTerminated(); } + @Override + public String getExpMetrics(String trailIds) throws Exception { + String encodedUrlString = URLEncoder.encode("run.trial_id in " + trailIds, "UTF-8"); + String url = aimProxyUrl + "/api/runs/search/run?query=" + encodedUrlString; + String s = HttpUtils.sendGet(url, null); + List<Map<String, Object>> responses = JacksonUtil.parseJSONStr2MapList(s); + + List<String> runIds = new ArrayList<>(); + for (Map response:responses) { + runIds.add((String)response.get("run_hash")); + } + + String decode = AIM64EncoderUtil.decode(runIds); + return aimUrl + "/metrics?select=" + decode; + } + public void getTrialList(RayIns rayIns) throws Exception { // 获取指定路径下的所有文件 String directoryPath = rayIns.getResultPath(); From 2ba233d3d99a51387b9e2908cdb8b1fc70bc6d19 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 3 Mar 2025 11:42:16 +0800 Subject: [PATCH 067/127] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-trail=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/app.tsx | 3 + react-ui/src/enums/index.ts | 20 ++++ react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.tsx | 3 +- .../components/TrialStatusCell/index.less | 3 + .../components/TrialStatusCell/index.tsx | 67 +++++++++++ .../src/pages/Experiment/Comparison/index.tsx | 4 +- .../pages/HyperParameter/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.less | 26 ++++- .../components/ExperimentHistory/index.tsx | 110 ++++++++++++++++-- .../components/TrialStatusCell/index.less | 3 + .../components/TrialStatusCell/index.tsx | 67 +++++++++++ react-ui/src/pages/HyperParameter/types.ts | 12 +- react-ui/src/services/hyperParameter/index.js | 7 ++ 14 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 react-ui/src/pages/AutoML/components/TrialStatusCell/index.less create mode 100644 react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 65b4440a..857d2650 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -245,6 +245,9 @@ export const antd: RuntimeAntdConfig = (memo) => { linkColor: 'rgba(29, 29, 32, 0.7)', separatorColor: 'rgba(29, 29, 32, 0.7)', }; + memo.theme.components.Tree = { + directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)', + }; memo.theme.cssVar = true; // memo.theme.hashed = false; diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index bdfe9e00..afba79b8 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -129,3 +129,23 @@ export const hyperParameterOptimizedModeOptions = [ { label: '越大越好', value: hyperParameterOptimizedMode.Max }, { label: '越小越好', value: hyperParameterOptimizedMode.Min }, ]; + +// 超参数 Trail 运行状态 +export enum HyperParameterTrailStatus { + PENDING = 'PENDING', // 挂起 + RUNNING = 'RUNNING', // 运行中 + TERMINATED = 'TERMINATED', // 成功 + ERROR = 'ERROR', // 错误 + PAUSED = 'PAUSED', // 暂停 + RESTORING = 'RESTORING', // 恢复中 +} + +// 自动 Trail 运行状态 +export enum AutoMLTrailStatus { + TIMEOUT = 'TIMEOUT', // 超时 + SUCCESS = 'SUCCESS', // 成功 + FAILURE = 'FAILURE', // 失败 + CRASHED = 'CRASHED', // 崩溃 + STOP = 'STOP', // 停止 + CANCELLED = 'CANCELLED', // 取消 +} diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 677cc791..2ccde91f 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -186,7 +186,7 @@ function AutoMLInstance() { }, { key: TabKeys.History, - label: 'Trial 列表', + label: '试验列表', icon: <KFIcon type="icon-Trialliebiao" />, children: ( <ExperimentHistory diff --git a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx index e95ccd42..223f768d 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx @@ -4,6 +4,7 @@ import tableCellRender from '@/utils/table'; import { Table, type TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; +import TrialStatusCell from '../TrialStatusCell'; import styles from './index.less'; type ExperimentHistoryProps = { @@ -103,7 +104,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps dataIndex: 'status', key: 'status', width: 120, - render: tableCellRender(false), + render: TrialStatusCell, }, ]; diff --git a/react-ui/src/pages/AutoML/components/TrialStatusCell/index.less b/react-ui/src/pages/AutoML/components/TrialStatusCell/index.less new file mode 100644 index 00000000..6bdaf5bc --- /dev/null +++ b/react-ui/src/pages/AutoML/components/TrialStatusCell/index.less @@ -0,0 +1,3 @@ +.trial-status-cell { + height: 100%; +} diff --git a/react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx b/react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx new file mode 100644 index 00000000..89b4e074 --- /dev/null +++ b/react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx @@ -0,0 +1,67 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-18 18:35:41 + * @Description: 实验状态 + */ + +import { AutoMLTrailStatus } from '@/enums'; +import { ExperimentStatusInfo } from '@/pages/Experiment/status'; +import themes from '@/styles/theme.less'; +import styles from './index.less'; + +export const statusInfo: Record<AutoMLTrailStatus, ExperimentStatusInfo> = { + [AutoMLTrailStatus.SUCCESS]: { + label: '成功', + color: themes.successColor, + icon: '/assets/images/experiment-status/success-icon.png', + }, + [AutoMLTrailStatus.TIMEOUT]: { + label: '超时', + color: themes.pendingColor, + icon: '/assets/images/experiment-status/pending-icon.png', + }, + [AutoMLTrailStatus.FAILURE]: { + label: '失败', + color: themes.errorColor, + icon: '/assets/images/experiment-status/fail-icon.png', + }, + [AutoMLTrailStatus.CRASHED]: { + label: '崩溃', + color: themes.errorColor, + icon: '/assets/images/experiment-status/fail-icon.png', + }, + [AutoMLTrailStatus.CANCELLED]: { + label: '取消', + color: themes.abortColor, + icon: '/assets/images/experiment-status/omitted-icon.png', + }, + [AutoMLTrailStatus.STOP]: { + label: '停止', + color: themes.textColor, + icon: '/assets/images/experiment-status/omitted-icon.png', + }, +}; + +function TrialStatusCell(status?: AutoMLTrailStatus | null) { + if (status === null || status === undefined) { + return <span>--</span>; + } + return ( + <div className={styles['trial-status-cell']}> + {/* <img + style={{ width: '17px', marginRight: '7px' }} + src={statusInfo[status]?.icon} + draggable={false} + alt="" + /> */} + <span + style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }} + className={styles['trial-status-cell__label']} + > + {statusInfo[status] ? statusInfo[status].label : status} + </span> + </div> + ); +} + +export default TrialStatusCell; diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index b900842c..2a75467c 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -77,7 +77,7 @@ function ExperimentComparison() { }; // 对比按钮 click - const hanldeComparisonClick = () => { + const handleComparisonClick = () => { if (selectedRowKeys.length < 2) { message.error('请至少选择两项进行对比'); return; @@ -202,7 +202,7 @@ function ExperimentComparison() { return ( <div className={styles['experiment-comparison']}> <div className={styles['experiment-comparison__header']}> - <Button type="default" onClick={hanldeComparisonClick}> + <Button type="default" onClick={handleComparisonClick}> 可视化对比 </Button> </div> diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index d38b0ee6..21f5dfe3 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -191,7 +191,7 @@ function HyperParameterInstance() { }, { key: TabKeys.History, - label: 'Trial 列表', + label: '寻优列表', icon: <KFIcon type="icon-Trialliebiao" />, children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, }, diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index 8e3e29a2..04160e95 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -8,7 +8,8 @@ border-radius: 10px; &__table { - height: 100%; + height: calc(100% - 52px); + margin-top: 20px; } :global { @@ -43,16 +44,31 @@ &__best-tag { margin-left: 8px; padding: 1px 10px; - color: @primary-color; + color: @success-color; font-weight: normal; font-size: 13px; - background-color: .addAlpha(@primary-color, 0.1) []; - border: 1px solid .addAlpha(@primary-color, 0.5) []; + background-color: .addAlpha(@success-color, 0.1) []; + // border: 1px solid .addAlpha(@success-color, 0.5) []; border-radius: 2px; } } .table-best-row { - color: @primary-color; + color: @success-color; font-weight: bold; } + +.trail-result { + :global { + .ant-tree-node-selected { + .trail-result__icon { + color: white; + } + } + + .trail-result__icon { + margin-left: 8px; + color: @primary-color; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 41a897d1..0263bd69 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,23 +1,33 @@ +import InfoGroup from '@/components/InfoGroup'; import KFIcon from '@/components/KFIcon'; -import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; +import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types'; +import { getExpMetricsReq } from '@/services/hyperParameter'; import { downLoadZip } from '@/utils/downloadfile'; +import { to } from '@/utils/promise'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Button, Table, Tooltip, type TableProps } from 'antd'; +import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; import classNames from 'classnames'; +import { useState } from 'react'; import styles from './index.less'; +const { DirectoryTree } = Tree; + type ExperimentHistoryProps = { - trialList?: HyperParameterTrialList[]; + trialList?: HyperParameterTrial[]; }; function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { - const first: HyperParameterTrialList | undefined = trialList[0]; + const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); + const { message } = App.useApp(); + + const first: HyperParameterTrial | undefined = trialList[0]; const config: Record<string, any> = first?.config ?? {}; const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; const paramsNames = Object.keys(config); const metricNames = Object.keys(metricAnalysis); - const trialColumns: TableProps<HyperParameterTrialList>['columns'] = [ + const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ { title: '序号', dataIndex: 'index', @@ -55,7 +65,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { dataIndex: 'status', key: 'status', width: 120, - render: tableCellRender(false), + render: TrialStatusCell, }, ]; @@ -105,7 +115,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }); } - const fileColumns: TableProps<HyperParameterFileList>['columns'] = [ + const fileColumns: TableProps<HyperParameterFile>['columns'] = [ { title: '文件名称', dataIndex: 'name', @@ -124,7 +134,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { dataIndex: 'option', width: 160, key: 'option', - render: (_: any, record: HyperParameterFileList) => { + render: (_: any, record: HyperParameterFile) => { return ( <Button type="link" @@ -146,13 +156,92 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }, ]; - const expandedRowRender = (record: HyperParameterTrialList) => ( + const expandedRowRender = (record: HyperParameterTrial) => ( <Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" /> ); + const expandedRowRender2 = (record: HyperParameterTrial) => { + const filesToTreeData = ( + files: HyperParameterFile[], + parent?: HyperParameterFile, + ): TreeDataNode[] => + files.map((file) => { + const key = parent ? `${parent.name}/${file.name}` : file.name; + return { + ...file, + key, + title: file.name, + children: file.children ? filesToTreeData(file.children, file) : undefined, + }; + }); + + const treeData: TreeDataNode[] = filesToTreeData([record.file]); + return ( + <InfoGroup title="寻优结果" className={styles['trail-result']}> + <DirectoryTree + // @ts-ignore + treeData={treeData} + defaultExpandAll + titleRender={(record: TreeDataNode & HyperParameterFile) => { + const label = record.title + (record.isFile ? `(${record.size})` : ''); + return ( + <> + <span style={{ fontSize: 14 }}>{label}</span> + <KFIcon + type="icon-xiazai" + className="trail-result__icon" + onClick={(e) => { + e.stopPropagation(); + downLoadZip( + record.isFile + ? `/api/mmp/minioStorage/downloadFile` + : `/api/mmp/minioStorage/download`, + { path: record.url }, + ); + }} + /> + </> + ); + }} + /> + </InfoGroup> + ); + }; + + // 选择行 + const rowSelection: TableProps<HyperParameterTrial>['rowSelection'] = { + type: 'checkbox', + columnWidth: 48, + fixed: 'left', + selectedRowKeys, + onChange: (selectedRowKeys: React.Key[]) => { + setSelectedRowKeys(selectedRowKeys); + }, + }; + + const handleComparisonClick = () => { + if (selectedRowKeys.length < 1) { + message.error('请至少选择一项'); + return; + } + getExpMetrics(); + }; + + // 获取对比 url + const getExpMetrics = async () => { + const [res] = await to(getExpMetricsReq(selectedRowKeys)); + if (res && res.data) { + const url = res.data; + window.open(url, '_blank'); + } + }; + return ( <div className={styles['experiment-history']}> <div className={styles['experiment-history__content']}> + <Button type="default" onClick={handleComparisonClick}> + 可视化对比 + </Button> <div className={classNames( 'vertical-scroll-table-no-page', @@ -167,7 +256,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { bordered={true} scroll={{ y: 'calc(100% - 110px)', x: '100%' }} rowKey="trial_id" - expandable={{ expandedRowRender }} + expandable={{ expandedRowRender: expandedRowRender2 }} + rowSelection={rowSelection} /> </div> </div> diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less new file mode 100644 index 00000000..6bdaf5bc --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less @@ -0,0 +1,3 @@ +.trial-status-cell { + height: 100%; +} diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx new file mode 100644 index 00000000..8838e175 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx @@ -0,0 +1,67 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-18 18:35:41 + * @Description: 实验状态 + */ + +import { HyperParameterTrailStatus } from '@/enums'; +import { ExperimentStatusInfo } from '@/pages/Experiment/status'; +import themes from '@/styles/theme.less'; +import styles from './index.less'; + +export const statusInfo: Record<HyperParameterTrailStatus, ExperimentStatusInfo> = { + [HyperParameterTrailStatus.RUNNING]: { + label: '运行中', + color: themes.primaryColor, + icon: '/assets/images/experiment-status/running-icon.png', + }, + [HyperParameterTrailStatus.TERMINATED]: { + label: '成功', + color: themes.successColor, + icon: '/assets/images/experiment-status/success-icon.png', + }, + [HyperParameterTrailStatus.PENDING]: { + label: '挂起', + color: themes.pendingColor, + icon: '/assets/images/experiment-status/pending-icon.png', + }, + [HyperParameterTrailStatus.ERROR]: { + label: '失败', + color: themes.errorColor, + icon: '/assets/images/experiment-status/fail-icon.png', + }, + [HyperParameterTrailStatus.PAUSED]: { + label: '暂停', + color: themes.abortColor, + icon: '/assets/images/experiment-status/omitted-icon.png', + }, + [HyperParameterTrailStatus.RESTORING]: { + label: '恢复中', + color: themes.textColor, + icon: '/assets/images/experiment-status/omitted-icon.png', + }, +}; + +function TrialStatusCell(status?: HyperParameterTrailStatus | null) { + if (status === null || status === undefined) { + return <span>--</span>; + } + return ( + <div className={styles['trial-status-cell']}> + {/* <img + style={{ width: '17px', marginRight: '7px' }} + src={statusInfo[status]?.icon} + draggable={false} + alt="" + /> */} + <span + style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }} + className={styles['trial-status-cell__label']} + > + {statusInfo[status] ? statusInfo[status].label : status} + </span> + </div> + ); +} + +export default TrialStatusCell; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index bff6d046..fc32bf3f 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -59,11 +59,11 @@ export type HyperParameterInstanceData = { update_time: string; finish_time: string; nodeStatus?: NodeStatus; // json之后的节点状态 - trial_list?: HyperParameterTrialList[]; - file_list?: HyperParameterFileList[]; + trial_list?: HyperParameterTrial[]; + file_list?: HyperParameterFile[]; }; -export type HyperParameterTrialList = { +export type HyperParameterTrial = { trial_id?: string; training_iteration?: number; time?: number; @@ -71,14 +71,14 @@ export type HyperParameterTrialList = { config?: Record<string, any>; metric_analysis?: Record<string, any>; metric: string; - file: HyperParameterFileList; + file: HyperParameterFile; is_best?: boolean; }; -export type HyperParameterFileList = { +export type HyperParameterFile = { name: string; size: string; url: string; isFile: boolean; - children?: HyperParameterFileList[]; + children: HyperParameterFile[]; }; diff --git a/react-ui/src/services/hyperParameter/index.js b/react-ui/src/services/hyperParameter/index.js index c97e617d..96ea52e1 100644 --- a/react-ui/src/services/hyperParameter/index.js +++ b/react-ui/src/services/hyperParameter/index.js @@ -91,3 +91,10 @@ export function batchDeleteRayInsReq(data) { }); } +// 获取当前实验的指标对比地址 +export function getExpMetricsReq(data) { + return request(`/api/mmp/rayIns/getExpMetrics`, { + method: 'POST', + data + }); +} From 3b6d948c3ea53878e57b939ce637d7c101c8c764 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 3 Mar 2025 11:43:26 +0800 Subject: [PATCH 068/127] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9FormInfo=20&?= =?UTF-8?q?=20ParameterSelect=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.tsx | 26 ++++++-- .../src/components/ParameterSelect/index.tsx | 65 ++++++++++++------- .../components/ExperimentParameter/index.tsx | 14 ++-- .../components/PipelineNodeDrawer/index.tsx | 17 +++-- react-ui/src/stories/FormInfo.stories.tsx | 19 +++++- .../src/stories/ParameterSelect.stories.tsx | 62 +++++++++++++++--- react-ui/src/utils/format.ts | 6 +- 7 files changed, 156 insertions(+), 53 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index c1e23cbe..d33d615a 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -1,5 +1,5 @@ import { formatEnum } from '@/utils/format'; -import { Typography } from 'antd'; +import { Typography, type SelectProps } from 'antd'; import classNames from 'classnames'; import './index.less'; @@ -13,7 +13,9 @@ type FormInfoProps = { /** 是否是下拉框 */ select?: boolean; /** 下拉框数据 */ - options?: { label: string; value: any }[]; + options?: SelectProps['options']; + /** 自定义节点 label、value 的字段 */ + fieldNames?: SelectProps['fieldNames']; /** 自定义类名 */ className?: string; /** 自定义样式 */ @@ -26,17 +28,29 @@ type FormInfoProps = { function FormInfo({ value, valuePropName, - className, - select, + textArea = false, + select = false, options, + fieldNames, + className, style, - textArea = false, }: FormInfoProps) { let showValue = value; if (value && typeof value === 'object' && valuePropName) { showValue = value[valuePropName]; } else if (select === true && options) { - showValue = formatEnum(options)(value); + let _options: SelectProps['options'] = options; + if (fieldNames) { + _options = options.map((v) => { + return { + ...v, + label: fieldNames.label && v[fieldNames.label], + value: fieldNames.value && v[fieldNames.value], + options: fieldNames.options && v[fieldNames.options], + }; + }); + } + showValue = formatEnum(_options)(value); } return ( diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index a6a911e5..9b6389d8 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -7,43 +7,50 @@ import { to } from '@/utils/promise'; import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; +import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -/** 值类型 */ -export type ParameterSelectValue = { - /** 类型,参数名是和后台保持一致的 */ - item_type: 'dataset' | 'model' | 'service' | 'resource'; - /** 值 */ - value?: any; - /** 占位符 */ - placeholder?: string; - /** 其它属性 */ +type ParameterSelectObject = { + value: any; [key: string]: any; }; interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: 'dataset' | 'model' | 'service' | 'resource'; + /** 是否只是展示信息 */ + isInfo?: boolean; /** 值 */ - value?: ParameterSelectValue; + value?: string | ParameterSelectObject; /** 修改后回调 */ - onChange?: (value: ParameterSelectValue) => void; + onChange?: (value: string | ParameterSelectObject) => void; } /** 参数选择器,支持资源规格、数据集、模型、服务 */ -function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { +function ParameterSelect({ + dataType, + isInfo = false, + value, + onChange, + ...rest +}: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNotNullable = value ?? ({} as ParameterSelectValue); - const { item_type } = valueNotNullable; - const propsConfig = paramSelectConfig[item_type]; + const propsConfig = paramSelectConfig[dataType]; + const valueText = typeof value === 'object' && value !== null ? value.value : value; useEffect(() => { getSelectOptions(); }, []); - const hangleChange = (e: string) => { - onChange?.({ - ...valueNotNullable, - value: e, - }); + const handleChange = (text: string) => { + if (typeof value === 'object' && value !== null) { + onChange?.({ + ...value, + value: text, + }); + } else { + onChange?.(text); + } }; // 获取下拉数据 @@ -58,16 +65,26 @@ function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { } }; + if (isInfo) { + return ( + <FormInfo + select + value={valueText} + options={options} + fieldNames={propsConfig?.fieldNames} + ></FormInfo> + ); + } + return ( <Select {...rest} - placeholder={valueNotNullable.placeholder || rest.placeholder} filterOption={propsConfig?.filterOption} options={options} fieldNames={propsConfig?.fieldNames} - value={valueNotNullable.value} - optionFilterProp={propsConfig.optionFilterProp} - onChange={hangleChange} + optionFilterProp={propsConfig?.optionFilterProp} + value={valueText} + onChange={handleChange} showSearch allowClear /> diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index 235e3087..f3b6ef42 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -3,7 +3,7 @@ import ParameterSelect from '@/components/ParameterSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { useComputingResource } from '@/hooks/resource'; import { PipelineNodeModelSerialize } from '@/types'; -import { Form, Select } from 'antd'; +import { Form } from 'antd'; import styles from './index.less'; type ExperimentParameterProps = { @@ -100,7 +100,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { </Form.Item> <Form.Item label="启动命令" name="command"> - <FormInfo multiline /> + <FormInfo textArea /> </Form.Item> <Form.Item label="资源规格" @@ -112,9 +112,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { }, ]} > - <Select + <FormInfo + select options={resourceStandardList} - disabled fieldNames={{ label: 'description', value: 'standard', @@ -125,7 +125,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { <FormInfo /> </Form.Item> <Form.Item label="环境变量" name="env_variables"> - <FormInfo multiline /> + <FormInfo textArea /> </Form.Item> {controlStrategyList.map((item) => ( <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> @@ -146,7 +146,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { rules={[{ required: item.value.require ? true : false }]} > {item.value.type === 'select' ? ( - <ParameterSelect disabled /> + ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + <ParameterSelect dataType={item.value.item_type as any} isInfo /> + ) : null ) : ( <FormInfo valuePropName="showValue" /> )} diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index de041c72..6314ea76 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -502,7 +502,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete label={getLabel(item, 'control_strategy')} rules={getFormRules(item)} > - <ParameterInput allowClear></ParameterInput> + <ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput> </Form.Item> ))} {/* 输入参数 */} @@ -523,9 +523,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete <div className={styles['pipeline-drawer__ref-row']}> <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> {item.value.type === 'select' ? ( - <ParameterSelect /> + ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + <ParameterSelect + dataType={item.value.item_type as any} + placeholder={item.value.placeholder} + /> + ) : null ) : ( - <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> + <ParameterInput + canInput={canInput(item.value)} + placeholder={item.value.placeholder} + allowClear + ></ParameterInput> )} </Form.Item> {item.value.type === 'ref' && ( @@ -563,7 +572,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete label={getLabel(item, 'out_parameters')} rules={getFormRules(item)} > - <ParameterInput allowClear></ParameterInput> + <ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput> </Form.Item> ))} </> diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index a214abae..abdf7b5e 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -38,7 +38,7 @@ export const InForm: Story = { <Form name="form" style={{ width: 300 }} - labelCol={{ flex: '100px' }} + labelCol={{ flex: '150px' }} initialValues={{ text: '文本', large_text: @@ -49,6 +49,7 @@ export const InForm: Story = { showValue: '对象文本', }, select_text: 1, + select_map_text: 1, ant_input_text: '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', ant_select_text: 1, @@ -82,6 +83,22 @@ export const InForm: Story = { ]} /> </Form.Item> + <Form.Item label="模拟 Select Map" name="select_map_text"> + <FormInfo + select + options={[ + { + otherLabel: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + otherValue: 1, + }, + ]} + fieldNames={{ + label: 'otherLabel', + value: 'otherValue', + }} + /> + </Form.Item> <Form.Item label="Input" name="ant_input_text"> <Input disabled /> </Form.Item> diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx index 6aed4e31..924ab423 100644 --- a/react-ui/src/stories/ParameterSelect.stories.tsx +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -2,7 +2,7 @@ import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSel import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; -import { Col, Form, Row } from 'antd'; +import { Button, Col, Form, Row } from 'antd'; import { http, HttpResponse } from 'msw'; import { computeResourceData, datasetListData, modelListData, serviceListData } from './mockData'; @@ -46,12 +46,32 @@ type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Primary: Story = { args: { - value: { - item_type: 'dataset', - placeholder: '请选择数据集', - }, + placeholder: '请选择', + dataType: 'dataset', + style: { width: 400 }, + size: 'large', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterSelectValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return <ParameterSelect {...args} value={value} onChange={handleChange}></ParameterSelect>; + }, +}; + +/** 值可以是一个对象,典型的是流水线节点对象 **PipelineNodeModelParameter** */ +export const Object: Story = { + args: { + placeholder: '请选择', + dataType: 'dataset', style: { width: 400 }, size: 'large', + value: { + value: undefined, + }, }, render: function Render(args) { const [{ value }, updateArgs] = useArgs(); @@ -65,6 +85,9 @@ export const Primary: Story = { }; export const InForm: Story = { + args: { + dataType: 'dataset', + }, render: ({ onChange }) => { return ( <Form @@ -72,54 +95,75 @@ export const InForm: Story = { labelCol={{ flex: '80px' }} labelAlign="left" size="large" + onFinish={(values) => { + console.log('onFinish', values); + }} autoComplete="off" initialValues={{ dataset: { + type: 'select', item_type: 'dataset', placeholder: '请选择数据集', + label: '数据集', }, model: { + type: 'select', item_type: 'model', placeholder: '请选择模型', + label: '模型', }, service: { + type: 'select', item_type: 'service', placeholder: '请选择服务', + label: '服务', }, resource: { + type: 'select', item_type: 'resource', placeholder: '请选择计算资源', + label: '计算资源', }, + test: '1234', }} > <Row gutter={8}> <Col span={10}> <Form.Item label="数据集" name="dataset"> - <ParameterSelect onChange={onChange} /> + <ParameterSelect dataType="dataset" placeholder="请选择数据集" onChange={onChange} /> </Form.Item> </Col> </Row> <Row gutter={8}> <Col span={10}> <Form.Item label="模型" name="model"> - <ParameterSelect onChange={onChange} /> + <ParameterSelect dataType="model" placeholder="请选择模型" onChange={onChange} /> </Form.Item> </Col> </Row> <Row gutter={8}> <Col span={10}> <Form.Item label="服务" name="service"> - <ParameterSelect onChange={onChange} /> + <ParameterSelect dataType="service" placeholder="请选择服务" onChange={onChange} /> </Form.Item> </Col> </Row> <Row gutter={8}> <Col span={10}> <Form.Item label="计算资源" name="resource"> - <ParameterSelect onChange={onChange} /> + <ParameterSelect + dataType="resource" + placeholder="请选择计算资源" + onChange={onChange} + /> </Form.Item> </Col> </Row> + <Row gutter={8}> + <Button htmlType="submit" type="primary"> + 提交 + </Button> + </Row> </Form> ); }, diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index 40d46fdc..7d37fbf5 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -122,14 +122,14 @@ export const formatBoolean = (value: boolean): string => { return value ? '是' : '否'; }; -type FormatEnumFunc = (value: string | number) => string; +type FormatEnumFunc = (value: string | number) => React.ReactNode; // 格式化枚举 export const formatEnum = ( - options: { value: string | number; label: string }[], + options: { value?: string | number | null; label?: React.ReactNode }[], ): FormatEnumFunc => { return (value: string | number) => { const option = options.find((item) => item.value === value); - return option ? option.label : '--'; + return option && option.label ? option.label : '--'; }; }; From 649e9f9992c016b64cf71471868b400b8a76b1fc Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 3 Mar 2025 14:12:23 +0800 Subject: [PATCH 069/127] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20ParameterS?= =?UTF-8?q?elect=20=E7=BB=84=E4=BB=B6=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/ParameterSelect/index.tsx | 6 +++--- .../Experiment/components/ExperimentParameter/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 9b6389d8..04e6b87f 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -19,7 +19,7 @@ interface ParameterSelectProps extends SelectProps { /** 类型 */ dataType: 'dataset' | 'model' | 'service' | 'resource'; /** 是否只是展示信息 */ - isInfo?: boolean; + display?: boolean; /** 值 */ value?: string | ParameterSelectObject; /** 修改后回调 */ @@ -29,7 +29,7 @@ interface ParameterSelectProps extends SelectProps { /** 参数选择器,支持资源规格、数据集、模型、服务 */ function ParameterSelect({ dataType, - isInfo = false, + display = false, value, onChange, ...rest @@ -65,7 +65,7 @@ function ParameterSelect({ } }; - if (isInfo) { + if (display) { return ( <FormInfo select diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index f3b6ef42..a81cf106 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -147,7 +147,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { > {item.value.type === 'select' ? ( ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( - <ParameterSelect dataType={item.value.item_type as any} isInfo /> + <ParameterSelect dataType={item.value.item_type as any} display /> ) : null ) : ( <FormInfo valuePropName="showValue" /> From b510df0f15f700ead05af17f49dcf84d098e3350 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 4 Mar 2025 16:37:19 +0800 Subject: [PATCH 070/127] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20BasicTable?= =?UTF-8?q?Info=20=E7=BB=84=E4=BB=B6=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/BasicInfo/index.tsx | 12 ++--- .../src/components/BasicTableInfo/index.tsx | 8 ++-- .../Experiment/components/LogGroup/index.tsx | 1 + react-ui/src/stories/BasicInfo.stories.tsx | 2 + .../src/stories/BasicTableInfo.stories.tsx | 47 ++++++++++++++++++- react-ui/src/stories/docs/Less.mdx | 42 ++++++++++++++++- 6 files changed, 96 insertions(+), 16 deletions(-) diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 68622d7c..a918b8ee 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -24,21 +24,15 @@ export type BasicInfoProps = { /** * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 - * - * ### usage - * ```tsx - * import { BasicInfo } from '@/components/BasicInfo'; - * <BasicInfo datas={datas} labelWidth={80} /> - * ``` */ export default function BasicInfo({ datas, - className, - style, labelWidth, labelEllipsis = true, - threeColumns = false, labelAlign = 'start', + threeColumns = false, + className, + style, }: BasicInfoProps) { return ( <div diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index 68c350bd..357a82fb 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -5,16 +5,18 @@ import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; export type { BasicInfoData, BasicInfoLink }; +export type BasicTableInfoProps = Omit<BasicInfoProps, 'labelAlign' | 'threeColumns'>; + /** * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 */ export default function BasicTableInfo({ datas, - className, - style, labelWidth, labelEllipsis, -}: BasicInfoProps) { + className, + style, +}: BasicTableInfoProps) { const remainder = datas.length % 4; const array = []; if (remainder > 0) { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 9625118a..655e0b92 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -211,6 +211,7 @@ function LogGroup({ element.scrollTo(optons); } }; + const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index eddbec12..f669104e 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -89,6 +89,8 @@ export const Primary: Story = { ], labelWidth: 80, labelAlign: 'justify', + threeColumns: false, + labelEllipsis: true, }, }; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index cdde73fc..3d261d03 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -14,7 +14,49 @@ const meta = { tags: ['autodocs'], // More on argTypes: https://storybook.js.org/docs/api/argtypes argTypes: { - // backgroundColor: { control: 'color' }, + datas: { + description: '基础信息', + table: { + type: { summary: 'BasicInfoData[]' }, + }, + type: { + required: true, + name: 'array', + value: { + name: 'object', + value: {}, + }, + }, + }, + labelWidth: { + description: '标题宽度', + type: { + required: true, + name: 'number', + }, + }, + labelEllipsis: { + description: '标题是否显示省略号', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'true' }, + }, + control: 'boolean', + }, + className: { + description: '自定义类名', + table: { + type: { summary: 'string' }, + }, + control: 'text', + }, + style: { + description: '自定义样式', + table: { + type: { summary: 'ReactCSSProperties' }, + }, + control: 'object', + }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args // args: { onClick: fn() }, @@ -26,7 +68,8 @@ type Story = StoryObj<typeof meta>; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Primary: Story = { args: { - ...BasicInfoStories.Primary.args, + datas: BasicInfoStories.Primary.args.datas, labelWidth: 100, + labelEllipsis: true, }, }; diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 9098d96d..2caed9ba 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -8,7 +8,45 @@ import { Meta } from '@storybook/blocks'; ### 自定义主题 -`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 +`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。 + +在开发过程中使用这个文件的定义的变量、函数以及混合。通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 + +```css +// 颜色 +@primary-color: #1664ff; // 主色调 +@primary-color-secondary: #4e89ff; +@primary-color-hover: #69b1ff; +@sider-background-color: #f2f5f7; // 侧边栏背景颜色 +@background-color: #f9fafb; // 页面背景颜色 +@text-color: #1d1d20; +@text-color-secondary: #575757; +@text-color-tertiary: #8a8a8a; +@text-placeholder-color: rgba(0, 0, 0, 0.25); +@text-disabled-color: rgba(0, 0, 0, 0.25); +@success-color: #6ac21d; +@error-color: #c73131; +@warning-color: #f98e1b; +@abort-color: #8a8a8a; +@pending-color: #ecb934; +@underline-color: #5d93ff; +@border-color: #eaeaea; +@link-hover-color: #69b1ff; +@heading-color: rgba(0, 0, 0, 0.85); +@input-icon-hover-color: rgba(0, 0, 0, 0.85); + +// 字体大小 +@font-size-title: 18px; +@font-size-content: 16px; +@font-size: 15px; +@font-size-input: 14px; +@font-size-input-lg: @font-size-content; + +// padding +@content-padding: 25px; +``` + + 颜色变量还可以在 `js/ts/jsx/tsx` 里使用 @@ -193,7 +231,7 @@ function Component() { } ``` -既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 +既减少了类名的嵌套,又减少了 HTML 的嵌套,使代码逻辑更加清晰,易于理解与维护,同时实现模块化和组件化 From 0295112a6d88a6dd5c2c5ea92bacc33600bcd066 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 6 Mar 2025 10:50:21 +0800 Subject: [PATCH 071/127] docs: add onfinish action --- react-ui/src/components/ParameterSelect/index.tsx | 4 ++-- react-ui/src/stories/CodeSelect.stories.tsx | 9 ++++++++- react-ui/src/stories/ParameterSelect.stories.tsx | 14 +++++--------- react-ui/src/stories/ResourceSelect.stories.tsx | 9 ++++++++- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 04e6b87f..182db352 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -10,12 +10,12 @@ import { useEffect, useState } from 'react'; import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -type ParameterSelectObject = { +export type ParameterSelectObject = { value: any; [key: string]: any; }; -interface ParameterSelectProps extends SelectProps { +export interface ParameterSelectProps extends SelectProps { /** 类型 */ dataType: 'dataset' | 'model' | 'service' | 'resource'; /** 是否只是展示信息 */ diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index ce4b2c90..415d05da 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -1,8 +1,9 @@ import CodeSelect, { type ParameterInputValue } from '@/components/CodeSelect'; +import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; -import { Col, Form, Row } from 'antd'; +import { Button, Col, Form, Row } from 'antd'; import { http, HttpResponse } from 'msw'; import { codeListData } from './mockData'; @@ -62,6 +63,7 @@ export const InForm: Story = { labelAlign="left" size="large" autoComplete="off" + onFinish={action('onFinish')} > <Row gutter={8}> <Col span={10}> @@ -75,6 +77,11 @@ export const InForm: Story = { </Form.Item> </Col> </Row> + <Row gutter={8}> + <Button htmlType="submit" type="primary"> + 提交 + </Button> + </Row> </Form> ); }, diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx index 924ab423..d6399d5d 100644 --- a/react-ui/src/stories/ParameterSelect.stories.tsx +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -1,4 +1,5 @@ -import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSelect'; +import ParameterSelect, { type ParameterSelectObject } from '@/components/ParameterSelect'; +import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; @@ -33,9 +34,6 @@ const meta = { // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs tags: ['autodocs'], // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - // backgroundColor: { control: 'color' }, - }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args args: { onChange: fn() }, } satisfies Meta<typeof ParameterSelect>; @@ -53,7 +51,7 @@ export const Primary: Story = { }, render: function Render(args) { const [{ value }, updateArgs] = useArgs(); - function handleChange(value?: ParameterSelectValue) { + function handleChange(value?: string | ParameterSelectObject) { updateArgs({ value: value }); args.onChange?.(value); } @@ -75,7 +73,7 @@ export const Object: Story = { }, render: function Render(args) { const [{ value }, updateArgs] = useArgs(); - function handleChange(value?: ParameterSelectValue) { + function handleChange(value?: string | ParameterSelectObject) { updateArgs({ value: value }); args.onChange?.(value); } @@ -95,9 +93,7 @@ export const InForm: Story = { labelCol={{ flex: '80px' }} labelAlign="left" size="large" - onFinish={(values) => { - console.log('onFinish', values); - }} + onFinish={action('onFinish')} autoComplete="off" initialValues={{ dataset: { diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx index 93474d7f..8b87f990 100644 --- a/react-ui/src/stories/ResourceSelect.stories.tsx +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -3,10 +3,11 @@ import ResourceSelect, { requiredValidator, ResourceSelectorType, } from '@/components/ResourceSelect'; +import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; -import { Col, Form, Row } from 'antd'; +import { Button, Col, Form, Row } from 'antd'; import { http, HttpResponse } from 'msw'; import { datasetDetailData, @@ -100,6 +101,7 @@ export const InForm: Story = { labelAlign="left" size="large" autoComplete="off" + onFinish={action('onFinish')} > <Row gutter={8}> <Col span={10}> @@ -150,6 +152,11 @@ export const InForm: Story = { </Form.Item> </Col> </Row> + <Row gutter={8}> + <Button htmlType="submit" type="primary"> + 提交 + </Button> + </Row> </Form> ); }, From 756967a6fd52aa83cb190e089896e6e5a078640b Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 6 Mar 2025 15:45:46 +0800 Subject: [PATCH 072/127] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B6=85=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AF=BB=E4=BC=98=E5=AE=9E=E9=AA=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/RayInsServiceImpl.java | 105 +++++++++--------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 8c81cb9a..0f6b7412 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -280,8 +280,8 @@ public class RayInsServiceImpl implements RayInsService { List<Map<String, Object>> responses = JacksonUtil.parseJSONStr2MapList(s); List<String> runIds = new ArrayList<>(); - for (Map response:responses) { - runIds.add((String)response.get("run_hash")); + for (Map response : responses) { + runIds.add((String) response.get("run_hash")); } String decode = AIM64EncoderUtil.decode(runIds); @@ -294,64 +294,65 @@ public class RayInsServiceImpl implements RayInsService { rayIns.setResultPath(endpoint + "/" + directoryPath); rayIns.setResultTxt(endpoint + "/" + directoryPath + "/result.txt"); - - String bucketName = directoryPath.substring(0, directoryPath.indexOf("/")); - String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; - List<Map> fileMaps = minioUtil.listRayFilesInDirectory(bucketName, prefix); - - if (!fileMaps.isEmpty()) { - List<Map> collect = fileMaps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); - if (!collect.isEmpty()) { - Path experimentState = Paths.get(collect.get(0).get("name").toString()); - String content = minioUtil.readObjectAsString(bucketName, prefix + "/" + experimentState); - - String resultTxt = minioUtil.readObjectAsString(bucketName, prefix + "result.txt"); - String bestMetrics = getStringBetween(resultTxt, "Best metrics:", "Best result_df"); - Map<String, Object> bestMetricsMap = JsonUtils.jsonToMap(bestMetrics); - String bestTrialId = (String)bestMetricsMap.get("trial_id"); - - Map<String, Object> result = JsonUtils.jsonToMap(content); - ArrayList<ArrayList> trial_data_list = (ArrayList<ArrayList>) result.get("trial_data"); - ArrayList<Map<String, Object>> trialList = new ArrayList<>(); - - for (ArrayList trial_data : trial_data_list) { - Map<String, Object> trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); - Map<String, Object> trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); - - String trialId = (String) trial_data_0.get("trial_id"); - Map<String, Object> trial = new HashMap<>(); - trial.put("trial_id", trialId); - trial.put("config", trial_data_0.get("config")); - trial.put("status", trial_data_0.get("status")); - - Map<String, Object> last_result = (Map<String, Object>) trial_data_1.get("last_result"); - Map<String, Object> metric_analysis = (Map<String, Object>) trial_data_1.get("metric_analysis"); - Map<String, Object> time_total_s = (Map<String, Object>) metric_analysis.get("time_total_s"); - - trial.put("training_iteration", last_result.get("training_iteration")); - trial.put("time_avg", time_total_s.get("avg")); - - Map<String, Object> param = JsonUtils.jsonToMap(rayIns.getParam()); - trial.put("metric_analysis", metric_analysis.get((String) param.get("metric"))); - trial.put("metric", param.get("metric")); - - for (Map fileMap : fileMaps) { - if (fileMap.get("name").toString().contains(trialId)) { - trial.put("file", fileMap); + try { + String bucketName = directoryPath.substring(0, directoryPath.indexOf("/")); + String prefix = directoryPath.substring(directoryPath.indexOf("/") + 1, directoryPath.length()) + "/"; + List<Map> fileMaps = minioUtil.listRayFilesInDirectory(bucketName, prefix); + + if (!fileMaps.isEmpty()) { + List<Map> collect = fileMaps.stream().filter(map -> map.get("name").toString().startsWith("experiment_state")).collect(Collectors.toList()); + if (!collect.isEmpty()) { + Path experimentState = Paths.get(collect.get(0).get("name").toString()); + String content = minioUtil.readObjectAsString(bucketName, prefix + "/" + experimentState); + + String resultTxt = minioUtil.readObjectAsString(bucketName, prefix + "result.txt"); + String bestMetrics = getStringBetween(resultTxt, "Best metrics:", "Best result_df"); + Map<String, Object> bestMetricsMap = JsonUtils.jsonToMap(bestMetrics); + String bestTrialId = (String) bestMetricsMap.get("trial_id"); + + Map<String, Object> result = JsonUtils.jsonToMap(content); + ArrayList<ArrayList> trial_data_list = (ArrayList<ArrayList>) result.get("trial_data"); + ArrayList<Map<String, Object>> trialList = new ArrayList<>(); + + for (ArrayList trial_data : trial_data_list) { + Map<String, Object> trial_data_0 = JsonUtils.jsonToMap((String) trial_data.get(0)); + Map<String, Object> trial_data_1 = JsonUtils.jsonToMap((String) trial_data.get(1)); + + String trialId = (String) trial_data_0.get("trial_id"); + Map<String, Object> trial = new HashMap<>(); + trial.put("trial_id", trialId); + trial.put("config", trial_data_0.get("config")); + trial.put("status", trial_data_0.get("status")); + + Map<String, Object> last_result = (Map<String, Object>) trial_data_1.get("last_result"); + Map<String, Object> metric_analysis = (Map<String, Object>) trial_data_1.get("metric_analysis"); + Map<String, Object> time_total_s = (Map<String, Object>) metric_analysis.get("time_total_s"); + + trial.put("training_iteration", last_result.get("training_iteration")); + trial.put("time_avg", time_total_s.get("avg")); + + Map<String, Object> param = JsonUtils.jsonToMap(rayIns.getParam()); + trial.put("metric_analysis", metric_analysis.get((String) param.get("metric"))); + trial.put("metric", param.get("metric")); + + for (Map fileMap : fileMaps) { + if (fileMap.get("name").toString().contains(trialId)) { + trial.put("file", fileMap); + } } - } - try { if (bestTrialId.equals(trialId)) { trial.put("is_best", true); + trialList.add(0, trial); + } else { + trialList.add(trial); } - } catch (Exception e) { - logger.error("未找到结果文件:result.txt"); } - trialList.add(trial); + rayIns.setTrialList(trialList); } - rayIns.setTrialList(trialList); } + } catch (Exception e) { + logger.error("未找到结果文件:result.txt"); } } From 9b9e76099c7f25e4dcf2935a6b55439185e363e0 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 7 Mar 2025 09:10:13 +0800 Subject: [PATCH 073/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E7=BB=84=E6=97=A5=E5=BF=97=E6=BB=91=E5=8A=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Experiment/components/LogGroup/index.tsx | 27 +++++++++---------- .../Experiment/components/LogList/index.tsx | 18 ++++++------- .../components/ExperimentHistory/index.less | 5 ++-- .../components/ExperimentHistory/index.tsx | 3 +-- .../components/ExperimentLog/index.tsx | 3 --- react-ui/src/stories/docs/Less.mdx | 14 +++++----- 6 files changed, 31 insertions(+), 39 deletions(-) diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 655e0b92..5123fae1 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -17,7 +17,6 @@ import styles from './index.less'; export type LogGroupProps = ExperimentLog & { status?: ExperimentStatus; // 实验状态 - listId: string; }; type Log = { @@ -32,7 +31,6 @@ function LogGroup({ log_content = '', start_time, status, - listId, }: LogGroupProps) { const [collapse, setCollapse] = useState(true); const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); @@ -42,6 +40,7 @@ function LogGroup({ const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); const socketRef = useRef<WebSocket | undefined>(undefined); const retryRef = useRef(2); // 等待 2 秒,重试 3 次 + const elementRef = useRef<HTMLDivElement | null>(null); useEffect(() => { scrollToBottom(false); @@ -124,7 +123,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.181:31213'; + host = '172.20.32.197:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, @@ -150,7 +149,7 @@ function LogGroup({ }); socket.addEventListener('message', (event) => { - console.log('message received.', event); + // console.log('message received.', event); if (!event.data) { return; } @@ -201,15 +200,15 @@ function LogGroup({ // 滚动到底部 const scrollToBottom = (smooth: boolean = true) => { - const element = document.getElementById(listId); - if (element) { - const optons: ScrollToOptions = { - top: element.scrollHeight, - behavior: smooth ? 'smooth' : 'instant', - }; - - element.scrollTo(optons); - } + // const element = document.getElementById(listId); + // if (element) { + // const optons: ScrollToOptions = { + // top: element.scrollHeight, + // behavior: smooth ? 'smooth' : 'instant', + // }; + // element.scrollTo(optons); + // } + elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' }); }; const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; @@ -217,7 +216,7 @@ function LogGroup({ const showMoreBtn = status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; return ( - <div className={styles['log-group']}> + <div className={styles['log-group']} ref={elementRef}> {log_type === 'resource' && ( <div className={styles['log-group__pod']} onClick={handleCollapse}> <div className={styles['log-group__pod__name']}>{pod_name}</div> diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index b45fdae4..86c97d15 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -1,8 +1,9 @@ import { ExperimentStatus } from '@/enums'; import { getQueryByExperimentLog } from '@/services/experiment/index.js'; import { to } from '@/utils/promise'; +import classNames from 'classnames'; import dayjs from 'dayjs'; -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import LogGroup from '../LogGroup'; import styles from './index.less'; @@ -14,23 +15,25 @@ export type ExperimentLog = { }; type LogListProps = { - idPrefix?: string; // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 instanceName: string; // 实验实例 name instanceNamespace: string; // 实验实例 namespace pipelineNodeId: string; // 流水线节点 id workflowId?: string; // 实验实例工作流 id instanceNodeStartTime?: string; // 实验实例节点开始运行时间 instanceNodeStatus?: ExperimentStatus; + className?: string; + style?: React.CSSProperties; }; function LogList({ - idPrefix, instanceName, instanceNamespace, pipelineNodeId, workflowId, instanceNodeStartTime, instanceNodeStatus, + className, + style, }: LogListProps) { const [logList, setLogList] = useState<ExperimentLog[]>([]); const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); @@ -88,15 +91,10 @@ function LogList({ } }; - // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 - const listId = idPrefix ? `${idPrefix}-log-list` : 'log-list'; - return ( - <div className={styles['log-list']} id={listId}> + <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> {logList.length > 0 ? ( - logList.map((v) => ( - <LogGroup key={v.pod_name} {...v} listId={listId} status={instanceNodeStatus} /> - )) + logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) ) : ( <div className={styles['log-list__empty']}>暂无日志</div> )} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index 04160e95..69ef78cc 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -38,8 +38,7 @@ .cell-index { position: relative; width: 100%; - padding-left: 20px; - text-align: left; + white-space: nowrap; &__best-tag { margin-left: 8px; @@ -47,8 +46,8 @@ color: @success-color; font-weight: normal; font-size: 13px; + white-space: nowrap; background-color: .addAlpha(@success-color, 0.1) []; - // border: 1px solid .addAlpha(@success-color, 0.5) []; border-radius: 2px; } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 0263bd69..3b3822d9 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -32,8 +32,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { title: '序号', dataIndex: 'index', key: 'index', - width: 120, - align: 'center', + width: 110, render: (_text, record, index: number) => { return ( <div className={styles['cell-index']}> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index 90da4ff5..b27c20fe 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -46,7 +46,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { <div className={styles['experiment-log__tabs__log']}> {frameworkCloneNodeStatus && ( <LogList - idPrefix="git-clone-framework" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} pipelineNodeId={frameworkCloneNodeStatus.displayName} @@ -66,7 +65,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { <div className={styles['experiment-log__tabs__log']}> {trainCloneNodeStatus && ( <LogList - idPrefix="git-clone-train" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} pipelineNodeId={trainCloneNodeStatus.displayName} @@ -86,7 +84,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { <div className={styles['experiment-log__tabs__log']}> {hpoNodeStatus && ( <LogList - idPrefix="auto-hpo" instanceName={instanceInfo.argo_ins_name} instanceNamespace={instanceInfo.argo_ins_ns} pipelineNodeId={hpoNodeStatus.displayName} diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 2caed9ba..24d4dd1b 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -211,20 +211,20 @@ function Component() { 说明你需要拆分组件了 ```tsx -function Component1() { +function Component() { return ( - <div className="component1"> - <div className="component1__element1"> + <div className="component"> + <div className="component__element1"> + <Component1></Component1> </div> </div> ) } -function Component() { +function SubComponent() { return ( - <div className="component"> - <div className="component__element1"> - <Component1></Component1> + <div className="sub-component"> + <div className="sub-component__element1"> </div> </div> ) From d52da06d9eca59e8019358c1d016e02bcb7c142b Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 7 Mar 2025 17:06:05 +0800 Subject: [PATCH 074/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Tabs=20?= =?UTF-8?q?=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.less | 1 - react-ui/src/components/KFSpin/index.tsx | 2 +- react-ui/src/pages/AutoML/Instance/index.tsx | 16 ++-- .../components/ExperimentList/index.tsx | 5 +- react-ui/src/pages/Experiment/index.jsx | 5 +- .../pages/HyperParameter/Instance/index.tsx | 10 +-- .../components/CreateForm/ExecuteConfig.tsx | 16 ++-- .../components/ExperimentHistory/index.tsx | 79 +++++++------------ .../components/HyperParameterBasic/index.tsx | 4 +- .../components/ParameterInfo/index.tsx | 4 +- react-ui/src/pages/HyperParameter/types.ts | 4 +- .../ModelDeployment/VersionInfo/index.tsx | 12 +-- 12 files changed, 57 insertions(+), 101 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.less b/react-ui/src/components/FormInfo/index.less index 868404c7..322fb082 100644 --- a/react-ui/src/components/FormInfo/index.less +++ b/react-ui/src/components/FormInfo/index.less @@ -6,7 +6,6 @@ background-color: rgba(0, 0, 0, 0.04); border: 1px solid #d9d9d9; border-radius: 6px; - cursor: not-allowed; .ant-typography { margin: 0 !important; diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index a3a6c3e5..ee054aea 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -9,7 +9,7 @@ import './index.less'; interface KFSpinProps extends SpinProps { /** 加载文本 */ - label: string; + label?: string; } /** 自定义 Spin */ diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 2ccde91f..8b525bdd 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -25,7 +25,6 @@ enum TabKeys { const NodePrefix = 'auto-ml'; function AutoMLInstance() { - const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); const params = useParams(); @@ -83,10 +82,10 @@ function AutoMLInstance() { }; const setupSSE = (name: string, namespace: string) => { - let { origin } = location; - if (process.env.NODE_ENV === 'development') { - origin = 'http://172.20.32.197:31213'; - } + const { origin } = location; + // if (process.env.NODE_ENV === 'development') { + // origin = 'http://172.20.32.197:31213'; + // } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, @@ -204,12 +203,7 @@ function AutoMLInstance() { return ( <div className={styles['auto-ml-instance']}> - <Tabs - className={styles['auto-ml-instance__tabs']} - items={tabItems} - activeKey={activeTab} - onChange={setActiveTab} - /> + <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> </div> ); } diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index 0da5ca9c..01f6b899 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -412,11 +412,8 @@ function ExperimentList({ type }: ExperimentListProps) { onLoadMore={() => loadMoreExperimentIns()} ></ExperimentInstance> ), - onExpand: (e, a) => { - handleExpandChange(e, a); - }, + onExpand: handleExpandChange, expandedRowKeys: expandedRowKeys, - rowExpandable: () => true, }} rowKey="id" /> diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 70345689..efd18344 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -549,11 +549,8 @@ function Experiment() { onLoadMore={() => loadMoreExperimentIns()} ></ExperimentInstance> ), - onExpand: (e, a) => { - expandChange(e, a); - }, + onExpand: expandChange, expandedRowKeys: [expandedRowKeys], - rowExpandable: (record) => true, }} /> </div> diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 21f5dfe3..df25cc18 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -23,7 +23,6 @@ enum TabKeys { } function HyperParameterInstance() { - const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( undefined, @@ -101,7 +100,7 @@ function HyperParameterInstance() { }; const setupSSE = (name: string, namespace: string) => { - let { origin } = location; + const { origin } = location; // if (process.env.NODE_ENV === 'development') { // origin = 'http://172.20.32.197:31213'; // } @@ -204,12 +203,7 @@ function HyperParameterInstance() { return ( <div className={styles['hyper-parameter-instance']}> - <Tabs - className={styles['hyper-parameter-instance__tabs']} - items={tabItems} - activeKey={activeTab} - onChange={setActiveTab} - /> + <Tabs className={styles['hyper-parameter-instance__tabs']} items={tabItems} /> </div> ); } diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 672f9094..307bc5dd 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -202,16 +202,16 @@ function ExecuteConfig() { <Row gutter={8}> <Col span={10}> <Form.Item - label="总实验次数" + label="总试验次数" name="num_samples" rules={[ { required: true, - message: '请输入总实验次数', + message: '请输入总试验次数', }, ]} > - <InputNumber placeholder="请输入总实验次数" min={0} precision={0} /> + <InputNumber placeholder="请输入总试验次数" min={0} precision={0} /> </Form.Item> </Col> </Row> @@ -297,7 +297,7 @@ function ExecuteConfig() { <Row gutter={8}> <Col span={10}> <Form.Item - label="参数" + label="超参数" style={{ marginBottom: 0, marginTop: '-14px' }} required ></Form.Item> @@ -460,7 +460,7 @@ function ExecuteConfig() { ); if (arr.length > 0 && arr.length < runParameters.length) { return Promise.reject( - new Error(`手动运行参数 ${name} 必须全部填写或者都不填写`), + new Error(`手动运行超参数 ${name} 必须全部填写或者都不填写`), ); } } @@ -475,7 +475,7 @@ function ExecuteConfig() { <Row gutter={8}> <Col span={10}> <Form.Item - label="手动运行参数" + label="手动运行超参数" style={{ marginBottom: 0, marginTop: '-14px' }} ></Form.Item> </Col> @@ -567,9 +567,9 @@ function ExecuteConfig() { <Row gutter={0}> <Col span={24}> <Form.Item - label="优化方向" + label="指标优化方向" name="mode" - rules={[{ required: true, message: '请选择优化方向' }]} + rules={[{ required: true, message: '请选择指标优化方向' }]} > <Radio.Group options={hyperParameterOptimizedModeOptions}></Radio.Group> </Form.Item> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 3b3822d9..0e79687b 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -18,9 +18,19 @@ type ExperimentHistoryProps = { }; function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { + const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const { message } = App.useApp(); + const [tableData, setTableData] = useState<HyperParameterTrial[]>([]); + const [loading, setLoading] = useState(true); + // 防止 Tabs 卡顿 + setTimeout(() => { + setTableData(trialList); + setLoading(false); + }, 100); + + // 计算 column const first: HyperParameterTrial | undefined = trialList[0]; const config: Record<string, any> = first?.config ?? {}; const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; @@ -32,7 +42,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { title: '序号', dataIndex: 'index', key: 'index', - width: 110, + width: 100, render: (_text, record, index: number) => { return ( <div className={styles['cell-index']}> @@ -114,52 +124,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }); } - const fileColumns: TableProps<HyperParameterFile>['columns'] = [ - { - title: '文件名称', - dataIndex: 'name', - key: 'name', - render: tableCellRender(false), - }, - { - title: '文件大小', - dataIndex: 'size', - key: 'size', - width: 200, - render: tableCellRender(false), - }, - { - title: '操作', - dataIndex: 'option', - width: 160, - key: 'option', - render: (_: any, record: HyperParameterFile) => { - return ( - <Button - type="link" - size="small" - key="download" - icon={<KFIcon type="icon-xiazai" />} - onClick={() => { - if (record.isFile) { - downLoadZip(`/api/mmp/minioStorage/downloadFile`, { path: record.url }); - } else { - downLoadZip(`/api/mmp/minioStorage/download`, { path: record.url }); - } - }} - > - 下载 - </Button> - ); - }, - }, - ]; - - const expandedRowRender = (record: HyperParameterTrial) => ( - <Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" /> - ); - - const expandedRowRender2 = (record: HyperParameterTrial) => { + // 自定义展开视图 + const expandedRowRender = (record: HyperParameterTrial) => { const filesToTreeData = ( files: HyperParameterFile[], parent?: HyperParameterFile, @@ -207,6 +173,15 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { ); }; + // 展开实例 + const handleExpandChange = (expanded: boolean, record: HyperParameterTrial) => { + if (expanded) { + setExpandedRowKeys([record.trial_id]); + } else { + setExpandedRowKeys([]); + } + }; + // 选择行 const rowSelection: TableProps<HyperParameterTrial>['rowSelection'] = { type: 'checkbox', @@ -218,6 +193,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }, }; + // 对比 const handleComparisonClick = () => { if (selectedRowKeys.length < 1) { message.error('请至少选择一项'); @@ -248,14 +224,19 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { )} > <Table + loading={loading} rowClassName={(record) => (record.is_best ? styles['table-best-row'] : '')} - dataSource={trialList} + dataSource={tableData} columns={trialColumns} pagination={false} bordered={true} scroll={{ y: 'calc(100% - 110px)', x: '100%' }} rowKey="trial_id" - expandable={{ expandedRowRender: expandedRowRender2 }} + expandable={{ + expandedRowRender: expandedRowRender, + onExpand: handleExpandChange, + expandedRowKeys: expandedRowKeys, + }} rowSelection={rowSelection} /> </div> diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 47e5534e..df2ca0ad 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -113,7 +113,7 @@ function HyperParameterBasic({ format: formatModel, }, { - label: '总实验次数', + label: '总试验次数', value: info.num_samples, }, { @@ -135,7 +135,7 @@ function HyperParameterBasic({ value: info.min_samples_required, }, { - label: '优化方向', + label: '指标优化方向', value: info.mode, format: formatOptimizeMode, }, diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index 0e5ced40..740010cc 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -88,9 +88,9 @@ function ParameterInfo({ info }: ParameterInfoProps) { return ( <div className={styles['parameter-info']}> - <div className={styles['parameter-info__title']}>参数</div> + <div className={styles['parameter-info__title']}>超参数</div> <Table dataSource={parameters} columns={columns} rowKey="name" bordered pagination={false} /> - <div className={styles['parameter-info__title']}>手动运行参数</div> + <div className={styles['parameter-info__title']}>手动运行超参数</div> <Table dataSource={runParameters} columns={runColumns} diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index fc32bf3f..e0c02957 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -21,7 +21,7 @@ export type FormData = { mode: string; // 优化方向 search_alg?: string; // 搜索算法 scheduler?: string; // 调度算法 - num_samples: number; // 总实验次数 + num_samples: number; // 总试验次数 max_t: number; // 单次试验最大时间 min_samples_required: number; // 计算中位数的最小试验数 resource: string; // 资源规格 @@ -64,7 +64,7 @@ export type HyperParameterInstanceData = { }; export type HyperParameterTrial = { - trial_id?: string; + trial_id: string; training_iteration?: number; time?: number; status?: string; diff --git a/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx b/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx index d3eeaff5..ec6092ba 100644 --- a/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx @@ -9,11 +9,11 @@ import SubAreaTitle from '@/components/SubAreaTitle'; import { getServiceVersionInfoReq } from '@/services/modelDeployment'; import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; -import { Tabs, type TabsProps } from 'antd'; +import { Tabs } from 'antd'; import { useEffect, useState } from 'react'; -import VersionBasicInfo from '../components/VersionBasicInfo'; import ServerLog from '../components/ServerLog'; import UserGuide from '../components/UserGuide'; +import VersionBasicInfo from '../components/VersionBasicInfo'; import { ServiceVersionData } from '../types'; import styles from './index.less'; @@ -24,7 +24,6 @@ export enum ModelDeploymentTabKey { } function ServiceVersionInfo() { - const [activeTab, setActiveTab] = useState<string>(ModelDeploymentTabKey.Predict); const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined); const params = useParams(); const id = params.id; @@ -61,11 +60,6 @@ function ServiceVersionInfo() { }, ]; - // 切换 Tab,重置数据 - const hanleTabChange: TabsProps['onChange'] = (value) => { - setActiveTab(value); - }; - return ( <div className={styles['service-version-info']}> <PageTitle title="服务版本详情"></PageTitle> @@ -77,7 +71,7 @@ function ServiceVersionInfo() { ></SubAreaTitle> <VersionBasicInfo info={versionInfo} /> <div className={styles['service-version-info__content__tabs']}> - <Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} /> + <Tabs items={tabItems} /> </div> </div> </div> From 6b2b4dffc362b6cc1d7330f8e1358c9b5b1aa7bc Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 10 Mar 2025 16:08:15 +0800 Subject: [PATCH 075/127] =?UTF-8?q?docs:=20=E5=88=A0=E9=99=A4example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stories/{example => docs}/Configure.mdx | 0 .../assets/accessibility.png | Bin .../assets/accessibility.svg | 0 .../assets/addon-library.png | Bin .../{example => docs}/assets/assets.png | Bin .../assets/avif-test-image.avif | Bin .../{example => docs}/assets/context.png | Bin .../{example => docs}/assets/discord.svg | 0 .../stories/{example => docs}/assets/docs.png | Bin .../{example => docs}/assets/figma-plugin.png | Bin .../{example => docs}/assets/github.svg | 0 .../{example => docs}/assets/share.png | Bin .../{example => docs}/assets/styling.png | Bin .../{example => docs}/assets/testing.png | Bin .../{example => docs}/assets/theming.png | Bin .../{example => docs}/assets/tutorials.svg | 0 .../{example => docs}/assets/youtube.svg | 0 .../src/stories/example/Button.stories.ts | 53 ------------- react-ui/src/stories/example/Button.tsx | 37 --------- .../src/stories/example/Header.stories.ts | 33 -------- react-ui/src/stories/example/Header.tsx | 56 -------------- react-ui/src/stories/example/Page.stories.ts | 32 -------- react-ui/src/stories/example/Page.tsx | 73 ------------------ react-ui/src/stories/example/button.css | 30 ------- react-ui/src/stories/example/header.css | 32 -------- react-ui/src/stories/example/page.css | 68 ---------------- 26 files changed, 414 deletions(-) rename react-ui/src/stories/{example => docs}/Configure.mdx (100%) rename react-ui/src/stories/{example => docs}/assets/accessibility.png (100%) rename react-ui/src/stories/{example => docs}/assets/accessibility.svg (100%) rename react-ui/src/stories/{example => docs}/assets/addon-library.png (100%) rename react-ui/src/stories/{example => docs}/assets/assets.png (100%) rename react-ui/src/stories/{example => docs}/assets/avif-test-image.avif (100%) rename react-ui/src/stories/{example => docs}/assets/context.png (100%) rename react-ui/src/stories/{example => docs}/assets/discord.svg (100%) rename react-ui/src/stories/{example => docs}/assets/docs.png (100%) rename react-ui/src/stories/{example => docs}/assets/figma-plugin.png (100%) rename react-ui/src/stories/{example => docs}/assets/github.svg (100%) rename react-ui/src/stories/{example => docs}/assets/share.png (100%) rename react-ui/src/stories/{example => docs}/assets/styling.png (100%) rename react-ui/src/stories/{example => docs}/assets/testing.png (100%) rename react-ui/src/stories/{example => docs}/assets/theming.png (100%) rename react-ui/src/stories/{example => docs}/assets/tutorials.svg (100%) rename react-ui/src/stories/{example => docs}/assets/youtube.svg (100%) delete mode 100644 react-ui/src/stories/example/Button.stories.ts delete mode 100644 react-ui/src/stories/example/Button.tsx delete mode 100644 react-ui/src/stories/example/Header.stories.ts delete mode 100644 react-ui/src/stories/example/Header.tsx delete mode 100644 react-ui/src/stories/example/Page.stories.ts delete mode 100644 react-ui/src/stories/example/Page.tsx delete mode 100644 react-ui/src/stories/example/button.css delete mode 100644 react-ui/src/stories/example/header.css delete mode 100644 react-ui/src/stories/example/page.css diff --git a/react-ui/src/stories/example/Configure.mdx b/react-ui/src/stories/docs/Configure.mdx similarity index 100% rename from react-ui/src/stories/example/Configure.mdx rename to react-ui/src/stories/docs/Configure.mdx diff --git a/react-ui/src/stories/example/assets/accessibility.png b/react-ui/src/stories/docs/assets/accessibility.png similarity index 100% rename from react-ui/src/stories/example/assets/accessibility.png rename to react-ui/src/stories/docs/assets/accessibility.png diff --git a/react-ui/src/stories/example/assets/accessibility.svg b/react-ui/src/stories/docs/assets/accessibility.svg similarity index 100% rename from react-ui/src/stories/example/assets/accessibility.svg rename to react-ui/src/stories/docs/assets/accessibility.svg diff --git a/react-ui/src/stories/example/assets/addon-library.png b/react-ui/src/stories/docs/assets/addon-library.png similarity index 100% rename from react-ui/src/stories/example/assets/addon-library.png rename to react-ui/src/stories/docs/assets/addon-library.png diff --git a/react-ui/src/stories/example/assets/assets.png b/react-ui/src/stories/docs/assets/assets.png similarity index 100% rename from react-ui/src/stories/example/assets/assets.png rename to react-ui/src/stories/docs/assets/assets.png diff --git a/react-ui/src/stories/example/assets/avif-test-image.avif b/react-ui/src/stories/docs/assets/avif-test-image.avif similarity index 100% rename from react-ui/src/stories/example/assets/avif-test-image.avif rename to react-ui/src/stories/docs/assets/avif-test-image.avif diff --git a/react-ui/src/stories/example/assets/context.png b/react-ui/src/stories/docs/assets/context.png similarity index 100% rename from react-ui/src/stories/example/assets/context.png rename to react-ui/src/stories/docs/assets/context.png diff --git a/react-ui/src/stories/example/assets/discord.svg b/react-ui/src/stories/docs/assets/discord.svg similarity index 100% rename from react-ui/src/stories/example/assets/discord.svg rename to react-ui/src/stories/docs/assets/discord.svg diff --git a/react-ui/src/stories/example/assets/docs.png b/react-ui/src/stories/docs/assets/docs.png similarity index 100% rename from react-ui/src/stories/example/assets/docs.png rename to react-ui/src/stories/docs/assets/docs.png diff --git a/react-ui/src/stories/example/assets/figma-plugin.png b/react-ui/src/stories/docs/assets/figma-plugin.png similarity index 100% rename from react-ui/src/stories/example/assets/figma-plugin.png rename to react-ui/src/stories/docs/assets/figma-plugin.png diff --git a/react-ui/src/stories/example/assets/github.svg b/react-ui/src/stories/docs/assets/github.svg similarity index 100% rename from react-ui/src/stories/example/assets/github.svg rename to react-ui/src/stories/docs/assets/github.svg diff --git a/react-ui/src/stories/example/assets/share.png b/react-ui/src/stories/docs/assets/share.png similarity index 100% rename from react-ui/src/stories/example/assets/share.png rename to react-ui/src/stories/docs/assets/share.png diff --git a/react-ui/src/stories/example/assets/styling.png b/react-ui/src/stories/docs/assets/styling.png similarity index 100% rename from react-ui/src/stories/example/assets/styling.png rename to react-ui/src/stories/docs/assets/styling.png diff --git a/react-ui/src/stories/example/assets/testing.png b/react-ui/src/stories/docs/assets/testing.png similarity index 100% rename from react-ui/src/stories/example/assets/testing.png rename to react-ui/src/stories/docs/assets/testing.png diff --git a/react-ui/src/stories/example/assets/theming.png b/react-ui/src/stories/docs/assets/theming.png similarity index 100% rename from react-ui/src/stories/example/assets/theming.png rename to react-ui/src/stories/docs/assets/theming.png diff --git a/react-ui/src/stories/example/assets/tutorials.svg b/react-ui/src/stories/docs/assets/tutorials.svg similarity index 100% rename from react-ui/src/stories/example/assets/tutorials.svg rename to react-ui/src/stories/docs/assets/tutorials.svg diff --git a/react-ui/src/stories/example/assets/youtube.svg b/react-ui/src/stories/docs/assets/youtube.svg similarity index 100% rename from react-ui/src/stories/example/assets/youtube.svg rename to react-ui/src/stories/docs/assets/youtube.svg diff --git a/react-ui/src/stories/example/Button.stories.ts b/react-ui/src/stories/example/Button.stories.ts deleted file mode 100644 index 2a05e01b..00000000 --- a/react-ui/src/stories/example/Button.stories.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; - -import { Button } from './Button'; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export -const meta = { - title: 'Example/Button', - component: Button, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - backgroundColor: { control: 'color' }, - }, - // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - args: { onClick: fn() }, -} satisfies Meta<typeof Button>; - -export default meta; -type Story = StoryObj<typeof meta>; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - primary: true, - label: 'Button', - }, -}; - -export const Secondary: Story = { - args: { - label: 'Button', - }, -}; - -export const Large: Story = { - args: { - size: 'large', - label: 'Button', - }, -}; - -export const Small: Story = { - args: { - size: 'small', - label: 'Button', - }, -}; diff --git a/react-ui/src/stories/example/Button.tsx b/react-ui/src/stories/example/Button.tsx deleted file mode 100644 index f35dafdc..00000000 --- a/react-ui/src/stories/example/Button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import './button.css'; - -export interface ButtonProps { - /** Is this the principal call to action on the page? */ - primary?: boolean; - /** What background color to use */ - backgroundColor?: string; - /** How large should the button be? */ - size?: 'small' | 'medium' | 'large'; - /** Button contents */ - label: string; - /** Optional click handler */ - onClick?: () => void; -} - -/** Primary UI component for user interaction */ -export const Button = ({ - primary = false, - size = 'medium', - backgroundColor, - label, - ...props -}: ButtonProps) => { - const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; - return ( - <button - type="button" - className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} - style={{ backgroundColor }} - {...props} - > - {label} - </button> - ); -}; diff --git a/react-ui/src/stories/example/Header.stories.ts b/react-ui/src/stories/example/Header.stories.ts deleted file mode 100644 index 80c71d0f..00000000 --- a/react-ui/src/stories/example/Header.stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; - -import { Header } from './Header'; - -const meta = { - title: 'Example/Header', - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, - args: { - onLogin: fn(), - onLogout: fn(), - onCreateAccount: fn(), - }, -} satisfies Meta<typeof Header>; - -export default meta; -type Story = StoryObj<typeof meta>; - -export const LoggedIn: Story = { - args: { - user: { - name: 'Jane Doe', - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/react-ui/src/stories/example/Header.tsx b/react-ui/src/stories/example/Header.tsx deleted file mode 100644 index 1bf981a4..00000000 --- a/react-ui/src/stories/example/Header.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; - -import { Button } from './Button'; -import './header.css'; - -type User = { - name: string; -}; - -export interface HeaderProps { - user?: User; - onLogin?: () => void; - onLogout?: () => void; - onCreateAccount?: () => void; -} - -export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( - <header> - <div className="storybook-header"> - <div> - <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> - <g fill="none" fillRule="evenodd"> - <path - d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" - fill="#FFF" - /> - <path - d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" - fill="#555AB9" - /> - <path - d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" - fill="#91BAF8" - /> - </g> - </svg> - <h1>Acme</h1> - </div> - <div> - {user ? ( - <> - <span className="welcome"> - Welcome, <b>{user.name}</b>! - </span> - <Button size="small" onClick={onLogout} label="Log out" /> - </> - ) : ( - <> - <Button size="small" onClick={onLogin} label="Log in" /> - <Button primary size="small" onClick={onCreateAccount} label="Sign up" /> - </> - )} - </div> - </div> - </header> -); diff --git a/react-ui/src/stories/example/Page.stories.ts b/react-ui/src/stories/example/Page.stories.ts deleted file mode 100644 index 5d2c688a..00000000 --- a/react-ui/src/stories/example/Page.stories.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { expect, userEvent, within } from '@storybook/test'; - -import { Page } from './Page'; - -const meta = { - title: 'Example/Page', - component: Page, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, -} satisfies Meta<typeof Page>; - -export default meta; -type Story = StoryObj<typeof meta>; - -export const LoggedOut: Story = {}; - -// More on component testing: https://storybook.js.org/docs/writing-tests/component-testing -export const LoggedIn: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const loginButton = canvas.getByRole('button', { name: /Log in/i }); - await expect(loginButton).toBeInTheDocument(); - await userEvent.click(loginButton); - await expect(loginButton).not.toBeInTheDocument(); - - const logoutButton = canvas.getByRole('button', { name: /Log out/i }); - await expect(logoutButton).toBeInTheDocument(); - }, -}; diff --git a/react-ui/src/stories/example/Page.tsx b/react-ui/src/stories/example/Page.tsx deleted file mode 100644 index e1174830..00000000 --- a/react-ui/src/stories/example/Page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import { Header } from './Header'; -import './page.css'; - -type User = { - name: string; -}; - -export const Page: React.FC = () => { - const [user, setUser] = React.useState<User>(); - - return ( - <article> - <Header - user={user} - onLogin={() => setUser({ name: 'Jane Doe' })} - onLogout={() => setUser(undefined)} - onCreateAccount={() => setUser({ name: 'Jane Doe' })} - /> - - <section className="storybook-page"> - <h2>Pages in Storybook</h2> - <p> - We recommend building UIs with a{' '} - <a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> - <strong>component-driven</strong> - </a>{' '} - process starting with atomic components and ending with pages. - </p> - <p> - Render pages with mock data. This makes it easy to build and review page states without - needing to navigate to them in your app. Here are some handy patterns for managing page - data in Storybook: - </p> - <ul> - <li> - Use a higher-level connected component. Storybook helps you compose such data from the - "args" of child component stories - </li> - <li> - Assemble data in the page component from your services. You can mock these services out - using Storybook. - </li> - </ul> - <p> - Get a guided tutorial on component-driven development at{' '} - <a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> - Storybook tutorials - </a> - . Read more in the{' '} - <a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> - docs - </a> - . - </p> - <div className="tip-wrapper"> - <span className="tip">Tip</span> Adjust the width of the canvas with the{' '} - <svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> - <g fill="none" fillRule="evenodd"> - <path - d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" - id="a" - fill="#999" - /> - </g> - </svg> - Viewports addon in the toolbar - </div> - </section> - </article> - ); -}; diff --git a/react-ui/src/stories/example/button.css b/react-ui/src/stories/example/button.css deleted file mode 100644 index 4e3620b0..00000000 --- a/react-ui/src/stories/example/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - display: inline-block; - cursor: pointer; - border: 0; - border-radius: 3em; - font-weight: 700; - line-height: 1; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} -.storybook-button--primary { - background-color: #555ab9; - color: white; -} -.storybook-button--secondary { - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; - background-color: transparent; - color: #333; -} -.storybook-button--small { - padding: 10px 16px; - font-size: 12px; -} -.storybook-button--medium { - padding: 11px 20px; - font-size: 14px; -} -.storybook-button--large { - padding: 12px 24px; - font-size: 16px; -} diff --git a/react-ui/src/stories/example/header.css b/react-ui/src/stories/example/header.css deleted file mode 100644 index 5efd46c2..00000000 --- a/react-ui/src/stories/example/header.css +++ /dev/null @@ -1,32 +0,0 @@ -.storybook-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-header svg { - display: inline-block; - vertical-align: top; -} - -.storybook-header h1 { - display: inline-block; - vertical-align: top; - margin: 6px 0 6px 10px; - font-weight: 700; - font-size: 20px; - line-height: 1; -} - -.storybook-header button + button { - margin-left: 10px; -} - -.storybook-header .welcome { - margin-right: 10px; - color: #333; - font-size: 14px; -} diff --git a/react-ui/src/stories/example/page.css b/react-ui/src/stories/example/page.css deleted file mode 100644 index 77c81d2d..00000000 --- a/react-ui/src/stories/example/page.css +++ /dev/null @@ -1,68 +0,0 @@ -.storybook-page { - margin: 0 auto; - padding: 48px 20px; - max-width: 600px; - color: #333; - font-size: 14px; - line-height: 24px; - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.storybook-page h2 { - display: inline-block; - vertical-align: top; - margin: 0 0 4px; - font-weight: 700; - font-size: 32px; - line-height: 1; -} - -.storybook-page p { - margin: 1em 0; -} - -.storybook-page a { - color: inherit; -} - -.storybook-page ul { - margin: 1em 0; - padding-left: 30px; -} - -.storybook-page li { - margin-bottom: 8px; -} - -.storybook-page .tip { - display: inline-block; - vertical-align: top; - margin-right: 10px; - border-radius: 1em; - background: #e7fdd8; - padding: 4px 12px; - color: #357a14; - font-weight: 700; - font-size: 11px; - line-height: 12px; -} - -.storybook-page .tip-wrapper { - margin-top: 40px; - margin-bottom: 40px; - font-size: 13px; - line-height: 20px; -} - -.storybook-page .tip-wrapper svg { - display: inline-block; - vertical-align: top; - margin-top: 3px; - margin-right: 4px; - width: 12px; - height: 12px; -} - -.storybook-page .tip-wrapper svg path { - fill: #1ea7fd; -} From 3d0a1f59fd1b3924904484bc285a18e26a78539c Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 10 Mar 2025 17:01:25 +0800 Subject: [PATCH 076/127] =?UTF-8?q?chore:=20=E4=BF=AE=E6=94=B9=20table=20c?= =?UTF-8?q?ell=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ExperimentInstance/index.tsx | 8 ++-- .../components/ExperimentInstance/index.tsx | 8 ++-- react-ui/src/utils/table.tsx | 43 ++++++++++++------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx index ab713f48..9c8ea687 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -8,7 +8,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { DoubleRightOutlined } from '@ant-design/icons'; -import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo } from 'react'; import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; @@ -159,9 +159,9 @@ function ExperimentInstanceComponent({ {elapsedTime(item.create_time, item.finish_time)} </div> <div className={styles.startTime}> - <Tooltip title={formatDate(item.create_time)}> - <span>{formatDate(item.create_time)}</span> - </Tooltip> + <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> + {formatDate(item.create_time)} + </Typography.Text> </div> <div className={styles.statusBox}> <img diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 9b37dba8..2c80df24 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -13,7 +13,7 @@ import { elapsedTime, formatDate } from '@/utils/date'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { DoubleRightOutlined } from '@ant-design/icons'; -import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; +import { App, Button, Checkbox, ConfigProvider, Typography } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo } from 'react'; import TensorBoardStatusCell from '../TensorBoardStatus'; @@ -186,9 +186,9 @@ function ExperimentInstanceComponent({ <div className={styles.description}> <div style={{ width: '50%' }}>{elapsedTime(item.create_time, item.finish_time)}</div> <div style={{ width: '50%' }} className={styles.startTime}> - <Tooltip title={formatDate(item.create_time)}> - <span>{formatDate(item.create_time)}</span> - </Tooltip> + <Typography.Text ellipsis={{ tooltip: formatDate(item.create_time) }}> + {formatDate(item.create_time)} + </Typography.Text> </div> </div> <div className={styles.statusBox}> diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index d3ec10d6..dedc0d6d 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -6,7 +6,7 @@ import { isEmpty } from '@/utils'; import { formatDate } from '@/utils/date'; -import { Tooltip } from 'antd'; +import { Typography } from 'antd'; import dayjs from 'dayjs'; export enum TableCellValueType { @@ -92,39 +92,52 @@ function tableCellRender<T>( break; } - if (ellipsis && text) { - return ( - <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> - {renderCell(text, type === TableCellValueType.Link, record, options?.onClick)} - </Tooltip> - ); - } else { - return renderCell(text, type === TableCellValueType.Link, record, options?.onClick); - } + return renderCell(text, ellipsis, type, record, options?.onClick); }; } function renderCell<T>( text: any | undefined | null, - isLink: boolean, + ellipsis: boolean, + type: TableCellValueType = TableCellValueType.Text, record: T, onClick?: (record: T, e: React.MouseEvent) => void, ) { - return isLink ? renderLink(text, record, onClick) : renderText(text); + return type === TableCellValueType.Link + ? renderLink(text, ellipsis, record, onClick) + : renderText(text, ellipsis); } -function renderText(text: any | undefined | null) { - return <span>{!isEmpty(text) ? text : '--'}</span>; +function renderText(text: any | undefined | null, ellipsis: boolean) { + return ( + <Typography.Paragraph + style={{ marginBottom: 0 }} + ellipsis={ + ellipsis && !isEmpty(text) + ? { + tooltip: { + title: text, + destroyTooltipOnHide: true, + overlayStyle: { maxWidth: 400 }, + }, + } + : false + } + > + {!isEmpty(text) ? text : '--'} + </Typography.Paragraph> + ); } function renderLink<T>( text: any | undefined | null, + ellipsis: boolean, record: T, onClick?: (record: T, e: React.MouseEvent) => void, ) { return ( <a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}> - {text} + {renderText(text, ellipsis)} </a> ); } From 7ac9b7b01649f1afd60f24f1b4732b57314b5576 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 11 Mar 2025 10:33:15 +0800 Subject: [PATCH 077/127] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=20ellipsis:?= =?UTF-8?q?=20{=20showTitle:=20false=20}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ExperimentHistory/index.tsx | 5 -- .../components/ExperimentList/index.tsx | 4 +- .../src/pages/Experiment/Comparison/index.tsx | 3 - react-ui/src/pages/Experiment/index.jsx | 3 +- .../components/ExperimentHistory/index.tsx | 20 ++--- .../components/ParameterInfo/index.tsx | 8 +- react-ui/src/pages/Mirror/List/index.tsx | 1 - .../Model/components/ModelMetrics/index.tsx | 2 - .../ModelDeployment/ServiceInfo/index.tsx | 3 - react-ui/src/pages/Pipeline/index.jsx | 1 - react-ui/src/utils/table.tsx | 83 +++++++++++++------ 11 files changed, 69 insertions(+), 64 deletions(-) diff --git a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx index 223f768d..fa694b27 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx @@ -69,35 +69,30 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps dataIndex: 'accuracy', key: 'accuracy', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '耗时', dataIndex: 'duration', key: 'duration', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '训练损失', dataIndex: 'train_loss', key: 'train_loss', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '特征处理', dataIndex: 'feature', key: 'feature', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '算法', dataIndex: 'althorithm', key: 'althorithm', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '状态', diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index 01f6b899..4028d32c 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -261,7 +261,6 @@ function ExperimentList({ type }: ExperimentListProps) { dataIndex: config.descProperty, key: 'ml_description', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { @@ -269,8 +268,7 @@ function ExperimentList({ type }: ExperimentListProps) { dataIndex: 'update_time', key: 'update_time', width: '20%', - render: tableCellRender(true, TableCellValueType.Date), - ellipsis: { showTitle: false }, + render: tableCellRender(false, TableCellValueType.Date), }, { title: '最近五次运行状态', diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 2a75467c..095cdd15 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -154,7 +154,6 @@ function ExperimentComparison() { fixed: 'left', align: 'center', render: tableCellRender(true, TableCellValueType.Array), - ellipsis: { showTitle: false }, }, ], }, @@ -172,7 +171,6 @@ function ExperimentComparison() { width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), showSorterTooltip: false, })), @@ -191,7 +189,6 @@ function ExperimentComparison() { width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), showSorterTooltip: false, })), diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index efd18344..51c905bb 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -383,7 +383,7 @@ function Experiment() { title: '实验名称', dataIndex: 'name', key: 'name', - render: tableCellRender(), + render: tableCellRender(false), width: '16%', }, { @@ -400,7 +400,6 @@ function Experiment() { dataIndex: 'description', key: 'description', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '最近五次运行状态', diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 0e79687b..42b2e644 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -8,7 +8,7 @@ import { to } from '@/utils/promise'; import tableCellRender, { TableCellValueType } from '@/utils/table'; import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; import classNames from 'classnames'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import styles from './index.less'; const { DirectoryTree } = Tree; @@ -22,13 +22,16 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const { message } = App.useApp(); const [tableData, setTableData] = useState<HyperParameterTrial[]>([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); // 防止 Tabs 卡顿 - setTimeout(() => { - setTableData(trialList); - setLoading(false); - }, 100); + useEffect(() => { + setLoading(true); + setTimeout(() => { + setTableData(trialList); + setLoading(false); + }, 500); + }, []); // 计算 column const first: HyperParameterTrial | undefined = trialList[0]; @@ -67,7 +70,6 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { render: tableCellRender(false, TableCellValueType.Custom, { format: (value = 0) => Number(value).toFixed(2), }), - ellipsis: { showTitle: false }, }, { title: '状态', @@ -95,8 +97,6 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, - showSorterTooltip: false, })), }); } @@ -118,8 +118,6 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, - showSorterTooltip: false, })), }); } diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index 740010cc..b223988a 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -43,16 +43,14 @@ function ParameterInfo({ info }: ParameterInfoProps) { dataIndex: 'name', key: 'type', width: '40%', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + render: tableCellRender('auto'), }, { title: '参数类型', dataIndex: 'type', key: 'type', width: '20%', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + render: tableCellRender(false), }, { title: '取值范围', @@ -64,7 +62,6 @@ function ParameterInfo({ info }: ParameterInfoProps) { return JSON.stringify(value); }, }), - ellipsis: { showTitle: false }, }, ]; @@ -81,7 +78,6 @@ function ParameterInfo({ info }: ParameterInfoProps) { key: name, width: 150, render: tableCellRender(true), - ellipsis: { showTitle: false }, }; }) : []; diff --git a/react-ui/src/pages/Mirror/List/index.tsx b/react-ui/src/pages/Mirror/List/index.tsx index 629563d4..cbb8d014 100644 --- a/react-ui/src/pages/Mirror/List/index.tsx +++ b/react-ui/src/pages/Mirror/List/index.tsx @@ -192,7 +192,6 @@ function MirrorList() { key: 'description', width: '35%', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '创建时间', diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx index d01e36f8..11ef999c 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -184,7 +184,6 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), showSorterTooltip: false, })), @@ -223,7 +222,6 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr width: 120, align: 'center', render: tableCellRender(true), - ellipsis: { showTitle: false }, sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), showSorterTooltip: false, })), diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index b4cfb84a..143315a1 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -285,7 +285,6 @@ function ServiceInfo() { key: 'model', width: '20%', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '状态', @@ -300,7 +299,6 @@ function ServiceInfo() { key: 'image', width: '20%', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '副本数量', @@ -317,7 +315,6 @@ function ServiceInfo() { render: tableCellRender(true, TableCellValueType.Custom, { format: getResourceDescription, }), - ellipsis: { showTitle: false }, }, { title: '操作', diff --git a/react-ui/src/pages/Pipeline/index.jsx b/react-ui/src/pages/Pipeline/index.jsx index 2ba1b963..0c6b50ee 100644 --- a/react-ui/src/pages/Pipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/index.jsx @@ -161,7 +161,6 @@ const Pipeline = () => { dataIndex: 'description', key: 'description', render: tableCellRender(true), - ellipsis: { showTitle: false }, }, { title: '创建时间', diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index dedc0d6d..30cdedc9 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -6,7 +6,7 @@ import { isEmpty } from '@/utils'; import { formatDate } from '@/utils/date'; -import { Typography } from 'antd'; +import { Tooltip, TooltipProps, Typography } from 'antd'; import dayjs from 'dayjs'; export enum TableCellValueType { @@ -65,7 +65,7 @@ function formatArray(property?: string): TableCellFormatter { } function tableCellRender<T>( - ellipsis: boolean = false, + ellipsis: boolean | TooltipProps | 'auto' = false, type: TableCellValueType = TableCellValueType.Text, options?: TableCellValueOptions<T>, ) { @@ -92,14 +92,26 @@ function tableCellRender<T>( break; } - return renderCell(text, ellipsis, type, record, options?.onClick); + if (ellipsis === 'auto' && text) { + return renderCell(type, text, 'auto', record, options?.onClick); + } else if (ellipsis && text) { + const tooltipProps = typeof ellipsis === 'object' ? ellipsis : {}; + const { overlayStyle, ...rest } = tooltipProps; + return ( + <Tooltip {...rest} overlayStyle={{ maxWidth: 400, ...overlayStyle }} title={text}> + {renderCell(type, text, true, record, options?.onClick)} + </Tooltip> + ); + } else { + return renderCell(type, text, false, record, options?.onClick); + } }; } function renderCell<T>( + type: TableCellValueType, text: any | undefined | null, - ellipsis: boolean, - type: TableCellValueType = TableCellValueType.Text, + ellipsis: boolean | 'auto', record: T, onClick?: (record: T, e: React.MouseEvent) => void, ) { @@ -108,30 +120,9 @@ function renderCell<T>( : renderText(text, ellipsis); } -function renderText(text: any | undefined | null, ellipsis: boolean) { - return ( - <Typography.Paragraph - style={{ marginBottom: 0 }} - ellipsis={ - ellipsis && !isEmpty(text) - ? { - tooltip: { - title: text, - destroyTooltipOnHide: true, - overlayStyle: { maxWidth: 400 }, - }, - } - : false - } - > - {!isEmpty(text) ? text : '--'} - </Typography.Paragraph> - ); -} - function renderLink<T>( text: any | undefined | null, - ellipsis: boolean, + ellipsis: boolean | 'auto', record: T, onClick?: (record: T, e: React.MouseEvent) => void, ) { @@ -142,4 +133,42 @@ function renderLink<T>( ); } +function renderText(text: any | undefined | null, ellipsis: boolean | 'auto') { + if (ellipsis === 'auto') { + return ( + <Typography.Paragraph + style={{ marginBottom: 0 }} + ellipsis={{ + tooltip: { + title: text, + destroyTooltipOnHide: true, + overlayStyle: { maxWidth: 400 }, + }, + }} + > + {!isEmpty(text) ? text : '--'} + </Typography.Paragraph> + ); + } + + return ( + <span + style={ + ellipsis + ? { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-all', + display: 'inline-block', + maxWidth: '100%', + } + : undefined + } + > + {!isEmpty(text) ? text : '--'} + </span> + ); +} + export default tableCellRender; From eb391c2d47743b31702c92e9a3188f157770decf Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 11 Mar 2025 14:20:01 +0800 Subject: [PATCH 078/127] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BATableColTitle?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E5=8F=AF=E5=8F=98=E7=9A=84=E8=A1=A8=E6=A0=BC=E8=B6=85=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TableColTitle/index.less | 3 + .../src/components/TableColTitle/index.tsx | 32 +++++++++ .../components/ResourceVersion/index.tsx | 10 ++- .../pages/Experiment/Comparison/index.less | 6 +- .../src/pages/Experiment/Comparison/index.tsx | 19 ++---- .../components/ExperimentHistory/index.tsx | 67 ++++++++++--------- .../components/ParameterInfo/index.tsx | 19 ++++-- .../Model/components/ModelMetrics/index.less | 3 + .../Model/components/ModelMetrics/index.tsx | 29 ++++---- .../src/stories/BasicTableInfo.stories.tsx | 2 +- react-ui/src/stories/docs/Configure.mdx | 2 +- 11 files changed, 117 insertions(+), 75 deletions(-) create mode 100644 react-ui/src/components/TableColTitle/index.less create mode 100644 react-ui/src/components/TableColTitle/index.tsx diff --git a/react-ui/src/components/TableColTitle/index.less b/react-ui/src/components/TableColTitle/index.less new file mode 100644 index 00000000..51207953 --- /dev/null +++ b/react-ui/src/components/TableColTitle/index.less @@ -0,0 +1,3 @@ +.ant-table .ant-table-cell .kf-table-col-title { + margin-bottom: 0; +} diff --git a/react-ui/src/components/TableColTitle/index.tsx b/react-ui/src/components/TableColTitle/index.tsx new file mode 100644 index 00000000..0583f3ed --- /dev/null +++ b/react-ui/src/components/TableColTitle/index.tsx @@ -0,0 +1,32 @@ +/* + * @Author: 赵伟 + * @Date: 2025-03-11 10:52:23 + * @Description: 用于内容可变的表格类标题 + */ + +import { Typography } from 'antd'; +import classNames from 'classnames'; +import './index.less'; + +type TableColTitleProps = { + /** 标题 */ + title: string; + /** 自定义类名 */ + className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; +}; + +function TableColTitle({ title, className, style }: TableColTitleProps) { + return ( + <Typography.Paragraph + ellipsis={{ tooltip: title }} + className={classNames('kf-table-col-title', className)} + style={style} + > + {title} + </Typography.Paragraph> + ); +} + +export default TableColTitle; diff --git a/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx index 5e04ee17..3dbb3c40 100644 --- a/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx @@ -58,7 +58,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { title: '文件大小', dataIndex: 'file_size', key: 'file_size', - render: tableCellRender(), + render: tableCellRender(false), }, { title: '更新时间', @@ -99,7 +99,13 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { </Button> </Flex> </Flex> - <Table columns={columns} dataSource={fileList} pagination={false} rowKey="url" /> + <Table + columns={columns} + dataSource={fileList} + pagination={false} + rowKey="url" + tableLayout="fixed" + /> </div> ); } diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index b0984b91..e34f03ad 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -34,13 +34,13 @@ border-left: none !important; } } - .ant-table-tbody-virtual::after { - border-bottom: none !important; - } .ant-table-footer { padding: 0; border: none !important; } + .ant-table-column-title { + min-width: 0; + } } } } diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 095cdd15..3ed44cd1 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -4,6 +4,7 @@ * @Description: 实验对比 */ +import TableColTitle from '@/components/TableColTitle'; import { getExpEvaluateInfosReq, getExpMetricsReq, @@ -13,7 +14,7 @@ import { tableSorter } from '@/utils'; import { to } from '@/utils/promise'; import tableCellRender, { TableCellValueType } from '@/utils/table'; import { useSearchParams } from '@umijs/max'; -import { App, Button, Table, TablePaginationConfig, TableProps, Tooltip } from 'antd'; +import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; import ExperimentStatusCell from '../components/ExperimentStatusCell'; @@ -161,14 +162,10 @@ function ExperimentComparison() { title: `${config.title}参数`, align: 'center', children: paramsNames.map((name) => ({ - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: ['params', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), @@ -179,14 +176,10 @@ function ExperimentComparison() { title: `${config.title}指标`, align: 'center', children: metricsNames.map((name) => ({ - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: ['metrics', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 42b2e644..c4698464 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,12 +1,13 @@ import InfoGroup from '@/components/InfoGroup'; import KFIcon from '@/components/KFIcon'; +import TableColTitle from '@/components/TableColTitle'; import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types'; import { getExpMetricsReq } from '@/services/hyperParameter'; import { downLoadZip } from '@/utils/downloadfile'; import { to } from '@/utils/promise'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; +import { App, Button, Table, Tree, type TableProps, type TreeDataNode } from 'antd'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; import styles from './index.less'; @@ -46,6 +47,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { dataIndex: 'index', key: 'index', width: 100, + fixed: 'left', render: (_text, record, index: number) => { return ( <div className={styles['cell-index']}> @@ -56,27 +58,36 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }, }, { - title: '运行次数', - dataIndex: 'training_iteration', - key: 'training_iteration', - width: 120, - render: tableCellRender(false), - }, - { - title: '平均时长(秒)', - dataIndex: 'time_avg', - key: 'time_avg', - width: 150, - render: tableCellRender(false, TableCellValueType.Custom, { - format: (value = 0) => Number(value).toFixed(2), - }), - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 120, - render: TrialStatusCell, + title: '基本信息', + align: 'center', + children: [ + { + title: '运行次数', + dataIndex: 'training_iteration', + key: 'training_iteration', + width: 120, + fixed: 'left', + render: tableCellRender(false), + }, + { + title: '平均时长(秒)', + dataIndex: 'time_avg', + key: 'time_avg', + width: 150, + fixed: 'left', + render: tableCellRender(false, TableCellValueType.Custom, { + format: (value = 0) => Number(value).toFixed(2), + }), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 120, + fixed: 'left', + render: TrialStatusCell, + }, + ], }, ]; @@ -87,11 +98,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { key: 'config', align: 'center', children: paramsNames.map((name) => ({ - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: ['config', name], key: name, width: 120, @@ -108,11 +115,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { key: 'metrics', align: 'center', children: metricNames.map((name) => ({ - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: ['metric_analysis', name], key: name, width: 120, diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index b223988a..d946a080 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -1,10 +1,11 @@ +import TableColTitle from '@/components/TableColTitle'; import { getReqParamName, type FormParameter, } from '@/pages/HyperParameter/components/CreateForm/utils'; import { HyperParameterData } from '@/pages/HyperParameter/types'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Table, Tooltip, type TableProps } from 'antd'; +import { Table, type TableProps } from 'antd'; import { useMemo } from 'react'; import styles from './index.less'; @@ -69,11 +70,7 @@ function ParameterInfo({ info }: ParameterInfoProps) { runParameters.length > 0 ? parameters.map(({ name }) => { return { - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: name, key: name, width: 150, @@ -85,7 +82,14 @@ function ParameterInfo({ info }: ParameterInfoProps) { return ( <div className={styles['parameter-info']}> <div className={styles['parameter-info__title']}>超参数</div> - <Table dataSource={parameters} columns={columns} rowKey="name" bordered pagination={false} /> + <Table + dataSource={parameters} + columns={columns} + rowKey="name" + bordered + pagination={false} + tableLayout="fixed" + /> <div className={styles['parameter-info__title']}>手动运行超参数</div> <Table dataSource={runParameters} @@ -94,6 +98,7 @@ function ParameterInfo({ info }: ParameterInfoProps) { bordered pagination={false} scroll={{ x: '100%' }} + tableLayout="fixed" /> </div> ); diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.less b/react-ui/src/pages/Model/components/ModelMetrics/index.less index 03123746..bca3516c 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.less +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.less @@ -21,6 +21,9 @@ border-left: none !important; } } + .ant-table-column-title { + min-width: 0; + } } } diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx index 11ef999c..110d28ce 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -1,10 +1,11 @@ import SubAreaTitle from '@/components/SubAreaTitle'; +import TableColTitle from '@/components/TableColTitle'; import { useCheck } from '@/hooks'; import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset'; import { tableSorter } from '@/utils'; import { to } from '@/utils/promise'; import tableCellRender from '@/utils/table'; -import { Checkbox, Table, Tooltip, type TablePaginationConfig, type TableProps } from 'antd'; +import { Checkbox, Flex, Table, type TablePaginationConfig, type TableProps } from 'antd'; import { useEffect, useMemo, useState } from 'react'; import MetricsChart, { MetricsChatData } from '../MetricsChart'; import styles from './index.less'; @@ -174,14 +175,10 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr title: `训练参数`, align: 'center', children: paramsNames.map((name) => ({ - title: ( - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - ), + title: <TableColTitle title={name} />, dataIndex: ['params', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), @@ -196,14 +193,14 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr indeterminate={metricsIndeterminate} onChange={checkAllMetrics} disabled={metricsNames.length === 0} - ></Checkbox>{' '} - <span>训练指标</span> + ></Checkbox> + <span style={{ marginLeft: 4 }}>训练指标</span> </div> ), align: 'center', children: metricsNames.map((name) => ({ title: ( - <div> + <Flex align="center"> <Checkbox checked={isSingleMetricsChecked(name)} onChange={(e) => { @@ -211,15 +208,13 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr checkSingleMetrics(name); }} onClick={(e) => e.stopPropagation()} - ></Checkbox>{' '} - <Tooltip title={name}> - <span>{name}</span> - </Tooltip> - </div> + ></Checkbox> + <TableColTitle style={{ marginLeft: 4 }} title={name} /> + </Flex> ), dataIndex: ['metrics', name], key: name, - width: 120, + width: 150, align: 'center', render: tableCellRender(true), sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), @@ -251,6 +246,8 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr }} onChange={handleTableChange} rowKey="name" + tableLayout="fixed" + scroll={{ x: '100%' }} /> </div> <div className={styles['model-metrics__chart']}> diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index 3d261d03..7f9e9e84 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -4,7 +4,7 @@ import * as BasicInfoStories from './BasicInfo.stories'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { - title: 'Components/BasicTableInfo 表格基本信息', + title: 'Components/BasicTableInfo 基本信息表格版', component: BasicTableInfo, parameters: { // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout diff --git a/react-ui/src/stories/docs/Configure.mdx b/react-ui/src/stories/docs/Configure.mdx index 6a537304..7651e269 100644 --- a/react-ui/src/stories/docs/Configure.mdx +++ b/react-ui/src/stories/docs/Configure.mdx @@ -31,7 +31,7 @@ export const RightArrow = () => <svg <path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" /> </svg> -<Meta title="Configure your project" /> +<Meta title="Documentation/Storybook" /> <div className="sb-container"> <div className='sb-section-title'> From 7235e23e95edb1e97d6f2ad556e6f97af10f2e54 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 12 Mar 2025 08:50:50 +0800 Subject: [PATCH 079/127] =?UTF-8?q?styles:=20=E4=BF=AE=E6=94=B9=E6=B5=81?= =?UTF-8?q?=E6=B0=B4=E7=BA=BF=E8=A1=A8=E6=A0=BC=E5=88=97=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/AutoML/components/ExperimentInstance/index.less | 2 +- react-ui/src/pages/AutoML/components/ExperimentList/index.tsx | 3 +-- react-ui/src/pages/Pipeline/index.jsx | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less index 3e7d2eec..6cd9ef98 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.less @@ -26,7 +26,7 @@ .startTime { .singleLine(); - width: calc(20% + 10px); + width: 200px; } .status { diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index 4028d32c..bcc85a2f 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -262,12 +262,11 @@ function ExperimentList({ type }: ExperimentListProps) { key: 'ml_description', render: tableCellRender(true), }, - { title: '创建时间', dataIndex: 'update_time', key: 'update_time', - width: '20%', + width: 200, render: tableCellRender(false, TableCellValueType.Date), }, { diff --git a/react-ui/src/pages/Pipeline/index.jsx b/react-ui/src/pages/Pipeline/index.jsx index 0c6b50ee..08d1ccf6 100644 --- a/react-ui/src/pages/Pipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/index.jsx @@ -152,6 +152,7 @@ const Pipeline = () => { title: '流水线名称', dataIndex: 'name', key: 'name', + width: '50%', render: tableCellRender(false, TableCellValueType.Link, { onClick: gotoDetail, }), @@ -160,18 +161,21 @@ const Pipeline = () => { title: '流水线描述', dataIndex: 'description', key: 'description', + width: '50%', render: tableCellRender(true), }, { title: '创建时间', dataIndex: 'create_time', key: 'create_time', + width: 180, render: tableCellRender(false, TableCellValueType.Date), }, { title: '修改时间', dataIndex: 'update_time', key: 'update_time', + width: 180, render: tableCellRender(false, TableCellValueType.Date), }, { From 423409f6ef24a5a30ba553b452d74644f10c220c Mon Sep 17 00:00:00 2001 From: fanshuai <1141904845@qq.com> Date: Wed, 12 Mar 2025 16:25:28 +0800 Subject: [PATCH 080/127] =?UTF-8?q?label-studio=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataset/DatasetVersionController.java | 14 ++-- .../dataset/NewDatasetFromGitController.java | 14 ++++ .../service/DatasetVersionService.java | 3 +- .../platform/service/NewDatasetService.java | 3 + .../impl/DatasetVersionServiceImpl.java | 54 ++++++------- .../service/impl/NewDatasetServiceImpl.java | 51 ++++++++++-- .../com/ruoyi/platform/utils/FileUtil.java | 77 ++++++++++++++++++- .../platform/vo/LabelDatasetVersionVo.java | 59 +++----------- 8 files changed, 181 insertions(+), 94 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetVersionController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetVersionController.java index 87715d5d..f6e7c6e5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetVersionController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetVersionController.java @@ -136,12 +136,12 @@ public class DatasetVersionController extends BaseController { return genericsSuccess(this.datasetVersionService.deleteDatasetVersion(datasetId, version)); } - @PostMapping("/addDatasetVersionsFromLabel") - @ApiOperation("从数据标注添加数据集版本") - public GenericsAjaxResult<?> addDatasetVersionsFromLabel(@RequestBody LabelDatasetVersionVo labelDatasetVersionVo) throws Exception { - datasetVersionService.addDatasetVersionsFromLabel(labelDatasetVersionVo); - - return GenericsAjaxResult.success(); - } +// @PostMapping("/addDatasetVersionsFromLabel") +// @ApiOperation("从数据标注添加数据集版本") +// public GenericsAjaxResult<?> addDatasetVersionsFromLabel(@RequestBody LabelDatasetVersionVo labelDatasetVersionVo) throws Exception { +// datasetVersionService.addDatasetVersionsFromLabel(labelDatasetVersionVo); +// +// return GenericsAjaxResult.success(); +// } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java index 3e28b850..f2ba22d1 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java @@ -3,6 +3,7 @@ package com.ruoyi.platform.controller.dataset; import com.ruoyi.common.core.web.domain.AjaxResult; import com.ruoyi.platform.domain.Dataset; import com.ruoyi.platform.service.NewDatasetService; +import com.ruoyi.platform.vo.LabelDatasetVersionVo; import com.ruoyi.platform.vo.NewDatasetVo; import com.ruoyi.platform.vo.QueryModelMetricsVo; import io.swagger.annotations.ApiOperation; @@ -54,6 +55,19 @@ public class NewDatasetFromGitController { } + /** + * 新增数据集与版本新 + * + * @param datasetVo 实体 + * @return 新增结果 + */ + @PostMapping("/addVersionFromLabelStudio") + @ApiOperation("从labelsudio添加版本") + public AjaxResult addVersionFromLabelStudio(@RequestBody LabelDatasetVersionVo datasetVo) throws Exception { + return AjaxResult.success(this.newDatasetService.newCreateVersionFromLabelStudio(datasetVo)); + + } + @GetMapping("/queryDatasets") @ApiOperation("数据集广场公开数据集分页查询,根据data_type,data_tag筛选,true公开false私有") public AjaxResult queryDatasets(@RequestParam("page") int page, diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetVersionService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetVersionService.java index 3ae08ca3..31ec8ba2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetVersionService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetVersionService.java @@ -72,6 +72,5 @@ public interface DatasetVersionService { String addDatasetVersions(List<DatasetVersion> datasetVersions) throws Exception; - void - addDatasetVersionsFromLabel(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception; +// void addDatasetVersionsFromLabel(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/NewDatasetService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/NewDatasetService.java index c4967dba..a1116105 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/NewDatasetService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/NewDatasetService.java @@ -1,6 +1,7 @@ package com.ruoyi.platform.service; import com.ruoyi.platform.domain.Dataset; +import com.ruoyi.platform.vo.LabelDatasetVersionVo; import com.ruoyi.platform.vo.NewDatasetVo; import com.ruoyi.platform.vo.QueryModelMetricsVo; import org.springframework.core.io.InputStreamResource; @@ -39,4 +40,6 @@ public interface NewDatasetService { void deleteDatasetVersionNew(Integer repoId, String repo, String owner, String version, String relativePath) throws Exception; Map<String, Object> getVersionsCompare(QueryModelMetricsVo querydatasetVo) throws Exception; + + String newCreateVersionFromLabelStudio(LabelDatasetVersionVo datasetVo) throws Exception; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java index 4a00ddb4..61d61031 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetVersionServiceImpl.java @@ -217,33 +217,33 @@ public class DatasetVersionServiceImpl implements DatasetVersionService { } } - @Override - public void addDatasetVersionsFromLabel(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception{ - Dataset dataset = datasetDao.queryById(labelDatasetVersionVo.getDataset_id()); - if (dataset == null){ - throw new Exception("数据集不存在"); - } - // 获取label-studio数据流 - Map<String, String> headers = new HashMap<String, String>(); - headers.put("Authorization","Token "+labelDatasetVersionVo.getToken()); - InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl+"/api/projects/"+labelDatasetVersionVo.getProject_id()+"/export?exportType="+labelDatasetVersionVo.getExportType(), headers); - // 构建objectName - String username = SecurityUtils.getLoginUser().getUsername(); - String url = username + "/" + DateUtils.dateTimeNow() + "/" + dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType(); - String objectName = "datasets/" + url; - String formattedSize = FileUtil.formatFileSize(inputStream.available()); - minioService.uploaInputStream(bucketName,objectName,inputStream); - //保存DatasetVersion - DatasetVersion datasetVersion = new DatasetVersion(); - datasetVersion.setVersion(labelDatasetVersionVo.getVersion()); - datasetVersion.setDatasetId(labelDatasetVersionVo.getDataset_id()); - datasetVersion.setFileName(dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType()); - - datasetVersion.setFileSize(formattedSize); - datasetVersion.setUrl(objectName); - datasetVersion.setDescription(labelDatasetVersionVo.getDesc()); - this.insert(datasetVersion); - } +// @Override +// public void addDatasetVersionsFromLabel(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception{ +// Dataset dataset = datasetDao.queryById(labelDatasetVersionVo.getDataset_id()); +// if (dataset == null){ +// throw new Exception("数据集不存在"); +// } +// // 获取label-studio数据流 +// Map<String, String> headers = new HashMap<String, String>(); +// headers.put("Authorization","Token "+labelDatasetVersionVo.getToken()); +// InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl+"/api/projects/"+labelDatasetVersionVo.getProject_id()+"/export?exportType="+labelDatasetVersionVo.getExportType(), headers); +// // 构建objectName +// String username = SecurityUtils.getLoginUser().getUsername(); +// String url = username + "/" + DateUtils.dateTimeNow() + "/" + dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType(); +// String objectName = "datasets/" + url; +// String formattedSize = FileUtil.formatFileSize(inputStream.available()); +// minioService.uploaInputStream(bucketName,objectName,inputStream); +// //保存DatasetVersion +// DatasetVersion datasetVersion = new DatasetVersion(); +// datasetVersion.setVersion(labelDatasetVersionVo.getVersion()); +// datasetVersion.setDatasetId(labelDatasetVersionVo.getDataset_id()); +// datasetVersion.setFileName(dataset.getName()+"_"+labelDatasetVersionVo.getVersion()+"."+labelDatasetVersionVo.getExportType()); +// +// datasetVersion.setFileSize(formattedSize); +// datasetVersion.setUrl(objectName); +// datasetVersion.setDescription(labelDatasetVersionVo.getDesc()); +// this.insert(datasetVersion); +// } private void insertPrepare(DatasetVersion datasetVersion) throws Exception { checkDeclaredVersion(datasetVersion); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java index 9e0fa753..917e6057 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java @@ -14,10 +14,7 @@ import com.ruoyi.platform.service.DatasetTempStorageService; import com.ruoyi.platform.service.GitService; import com.ruoyi.platform.service.NewDatasetService; import com.ruoyi.platform.utils.*; -import com.ruoyi.platform.vo.GitProjectVo; -import com.ruoyi.platform.vo.NewDatasetVo; -import com.ruoyi.platform.vo.QueryModelMetricsVo; -import com.ruoyi.platform.vo.VersionVo; +import com.ruoyi.platform.vo.*; import com.ruoyi.system.api.model.LoginUser; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -40,10 +37,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -79,6 +73,9 @@ public class NewDatasetServiceImpl implements NewDatasetService { private String bucketName; @Value("${git.localPath}") String localPathlocal; + + @Value("${labelStudio.loaclUrl}") + private String labelloaclUrl; @Resource private NewHttpUtils httpUtils; @Resource @@ -472,6 +469,44 @@ public class NewDatasetServiceImpl implements NewDatasetService { return result; } + // 在你的方法中调用 + public String newCreateVersionFromLabelStudio(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception { + // 1. 下载数据集文件 + // 获取label-studio数据流 + Map<String, String> headers = new HashMap<String, String>(); + headers.put("Authorization", "Token " + labelDatasetVersionVo.getToken()); + InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl + "/api/projects/" + labelDatasetVersionVo.getProject_id() + "/export?exportType=" + labelDatasetVersionVo.getExportType(), headers); + + // 2. 打包成zip包 + String zipFileName = labelDatasetVersionVo.getProject_id() + ".zip"; + MultipartFile[] files = FileUtil.toMultipartFiles(inputStream, zipFileName); + + // 3. 上传到minio + List<Map<String, String>> maps = uploadDatasetlocal(files, UUID.randomUUID().toString()); + + // 4. 组装DatasetVersionVo + NewDatasetVo newDatasetVo = new NewDatasetVo(); + newDatasetVo.setId(labelDatasetVersionVo.getId()); + newDatasetVo.setIdentifier(labelDatasetVersionVo.getIdentifier()); + newDatasetVo.setIsPublic(labelDatasetVersionVo.getIsPublic()); + newDatasetVo.setOwner(labelDatasetVersionVo.getOwner()); + newDatasetVo.setName(labelDatasetVersionVo.getName()); + newDatasetVo.setVersion(labelDatasetVersionVo.getVersion()); + newDatasetVo.setDatasetSource(labelDatasetVersionVo.getDatasetSource()); + newDatasetVo.setVersionDesc(labelDatasetVersionVo.getVersionDesc()); + List<VersionVo> datasetVersionVos = new ArrayList<>(); + for (Map<String, String> map : maps) { + VersionVo versionVo = new VersionVo(); + versionVo.setUrl(map.get("url")); + versionVo.setFileName(map.get("fileName")); + versionVo.setFileSize(map.get("fileSize")); + datasetVersionVos.add(versionVo); + } + newDatasetVo.setDatasetVersionVos(datasetVersionVos); + // 调用新增版本方法 + return newCreateVersion(newDatasetVo); + } + @Override public List<Map<String, String>> uploadDatasetlocal(MultipartFile[] files, String uuid) throws Exception { List<Map<String, String>> results = new ArrayList<>(); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/FileUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/FileUtil.java index e31eed5a..25c3cfe5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/FileUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/FileUtil.java @@ -2,8 +2,10 @@ package com.ruoyi.platform.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.web.multipart.MultipartFile; -import java.io.File; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -11,6 +13,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; public class FileUtil { @@ -111,4 +115,75 @@ public class FileUtil { // 重命名文件夹 boolean renamed = oldFolder.renameTo(newFolder); } + + public static MultipartFile[] toMultipartFiles(InputStream inputStream, String fileName) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(baos); + ZipEntry zipEntry = new ZipEntry(fileName); + zos.putNextEntry(zipEntry); + + byte[] bytes = new byte[1024]; + int length; + while ((length = inputStream.read(bytes)) >= 0) { + zos.write(bytes, 0, length); + } + + zos.closeEntry(); + zos.close(); + inputStream.close(); + + ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); + return new MultipartFile[]{new CustomMultipartFile(resource, fileName)}; + } + + private static class CustomMultipartFile implements MultipartFile { + + private final ByteArrayResource resource; + private final String fileName; + + CustomMultipartFile(ByteArrayResource resource, String fileName) { + this.resource = resource; + this.fileName = fileName; + } + + @Override + public String getName() { + return fileName; + } + + @Override + public String getOriginalFilename() { + return fileName; + } + + @Override + public String getContentType() { + return "application/zip"; + } + + @Override + public boolean isEmpty() { + return resource.contentLength() == 0; + } + + @Override + public long getSize() { + return resource.contentLength(); + } + + @Override + public byte[] getBytes() throws IOException { + return resource.getByteArray(); + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(resource.getByteArray()); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + resource.getByteArray(); + } + } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java index 11a2e718..ef5697b6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java @@ -2,62 +2,23 @@ package com.ruoyi.platform.vo; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; import java.io.Serializable; @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@Data public class LabelDatasetVersionVo implements Serializable { private String token; private String project_id; - private Integer dataset_id; - private String version; - private String desc; private String exportType; - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getProject_id() { - return project_id; - } - - public void setProject_id(String project_id) { - this.project_id = project_id; - } - - public Integer getDataset_id() { - return dataset_id; - } - - public void setDataset_id(Integer dataset_id) { - this.dataset_id = dataset_id; - } - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getDesc() { - return desc; - } - - public void setDesc(String desc) { - this.desc = desc; - } - - public String getExportType() { - return exportType; - } - - public void setExportType(String exportType) { - this.exportType = exportType; - } + private Integer id; + private String identifier; + private Boolean isPublic; + private String owner; + private String name; + private String version; + private String versionDesc; + private String datasetSource; } From 4d85e5c1ef0f2fd10130ff7a81eb742200ba714d Mon Sep 17 00:00:00 2001 From: fanshuai <1141904845@qq.com> Date: Wed, 12 Mar 2025 16:30:24 +0800 Subject: [PATCH 081/127] =?UTF-8?q?label-studio=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/service/impl/NewDatasetServiceImpl.java | 4 ++-- .../java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java index 917e6057..705a9668 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java @@ -475,10 +475,10 @@ public class NewDatasetServiceImpl implements NewDatasetService { // 获取label-studio数据流 Map<String, String> headers = new HashMap<String, String>(); headers.put("Authorization", "Token " + labelDatasetVersionVo.getToken()); - InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl + "/api/projects/" + labelDatasetVersionVo.getProject_id() + "/export?exportType=" + labelDatasetVersionVo.getExportType(), headers); + InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl + "/api/projects/" + labelDatasetVersionVo.getProjectId() + "/export?exportType=" + labelDatasetVersionVo.getExportType(), headers); // 2. 打包成zip包 - String zipFileName = labelDatasetVersionVo.getProject_id() + ".zip"; + String zipFileName = labelDatasetVersionVo.getName()+"_"+labelDatasetVersionVo.getVersion() + ".zip"; MultipartFile[] files = FileUtil.toMultipartFiles(inputStream, zipFileName); // 3. 上传到minio diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java index ef5697b6..1511d043 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/LabelDatasetVersionVo.java @@ -10,7 +10,7 @@ import java.io.Serializable; @Data public class LabelDatasetVersionVo implements Serializable { private String token; - private String project_id; + private String projectId; private String exportType; private Integer id; From 7022688c22cd69f5a50c4ebbd49ce8333c025fba Mon Sep 17 00:00:00 2001 From: fanshuai <1141904845@qq.com> Date: Wed, 12 Mar 2025 17:00:54 +0800 Subject: [PATCH 082/127] =?UTF-8?q?label-studio=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dataset/NewDatasetFromGitController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java index f2ba22d1..8f1564d8 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java @@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import javax.annotation.Nullable; import javax.annotation.Resource; import java.util.List; import java.util.Map; @@ -72,10 +73,13 @@ public class NewDatasetFromGitController { @ApiOperation("数据集广场公开数据集分页查询,根据data_type,data_tag筛选,true公开false私有") public AjaxResult queryDatasets(@RequestParam("page") int page, @RequestParam("size") int size, - @RequestParam(value = "is_public") Boolean isPublic, + @RequestParam(value = "is_public", required = false) @Nullable Boolean isPublic, @RequestParam(value = "data_type", required = false) String dataType, @RequestParam(value = "data_tag", required = false) String dataTag, @RequestParam(value = "name", required = false) String name) throws Exception { + if (isPublic == null) { + isPublic = false; + } PageRequest pageRequest = PageRequest.of(page, size); Dataset dataset = new Dataset(); dataset.setDataTag(dataTag); From ffc856e67a03def89983fc35d157e5f2ec8f5c92 Mon Sep 17 00:00:00 2001 From: fanshuai <1141904845@qq.com> Date: Wed, 12 Mar 2025 20:28:50 +0800 Subject: [PATCH 083/127] =?UTF-8?q?label-studio=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataset/NewDatasetFromGitController.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java index 8f1564d8..2c60c3e6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/NewDatasetFromGitController.java @@ -17,6 +17,7 @@ import javax.annotation.Nullable; import javax.annotation.Resource; import java.util.List; import java.util.Map; +import java.util.Optional; @RestController @RequestMapping("newdataset") @@ -71,15 +72,15 @@ public class NewDatasetFromGitController { @GetMapping("/queryDatasets") @ApiOperation("数据集广场公开数据集分页查询,根据data_type,data_tag筛选,true公开false私有") - public AjaxResult queryDatasets(@RequestParam("page") int page, - @RequestParam("size") int size, + public AjaxResult queryDatasets(@RequestParam(value = "page", required = false) @Nullable Integer page, + @RequestParam(value = "size", required = false) @Nullable Integer size, @RequestParam(value = "is_public", required = false) @Nullable Boolean isPublic, @RequestParam(value = "data_type", required = false) String dataType, @RequestParam(value = "data_tag", required = false) String dataTag, @RequestParam(value = "name", required = false) String name) throws Exception { - if (isPublic == null) { - isPublic = false; - } + page = Optional.ofNullable(page).orElse(0); // 默认 page 为 0 + size = Optional.ofNullable(size).orElse(10000); // 默认 size 为 10000 + isPublic = Optional.ofNullable(isPublic).orElse(false); PageRequest pageRequest = PageRequest.of(page, size); Dataset dataset = new Dataset(); dataset.setDataTag(dataTag); From 90204e3210f2e24d30b6611daf3b3c57aacc720c Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 13 Mar 2025 09:41:32 +0800 Subject: [PATCH 084/127] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=9A=84=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/config/routes.ts | 77 ++++++++++--------- .../ModelDeployment/CreateVersion/index.tsx | 2 +- .../Workspace/components/QuickStart/index.tsx | 2 +- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index e9363f91..b2c41f14 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -291,60 +291,61 @@ export default [ }, ], }, - ], - }, - { - name: '模型部署', - path: '/modelDeployment', - routes: [ { name: '模型部署', - path: '', - component: './ModelDeployment/List', - }, - { - name: '创建推理服务', - path: 'createService', - component: './ModelDeployment/CreateService', - }, - { - name: '编辑推理服务', - path: 'editService/:serviceId', - component: './ModelDeployment/CreateService', - }, - { - name: '服务详情', - path: 'serviceInfo/:serviceId', + path: 'modelDeployment', routes: [ { - name: '服务详情', + name: '模型部署', path: '', - component: './ModelDeployment/ServiceInfo', + component: './ModelDeployment/List', }, { - name: '新增服务版本', - path: 'createVersion', - component: './ModelDeployment/CreateVersion', + name: '创建推理服务', + path: 'createService', + component: './ModelDeployment/CreateService', }, { - name: '更新服务版本', - path: 'updateVersion', - component: './ModelDeployment/CreateVersion', + name: '编辑推理服务', + path: 'editService/:serviceId', + component: './ModelDeployment/CreateService', }, { - name: '重启服务版本', - path: 'restartVersion', - component: './ModelDeployment/CreateVersion', - }, - { - name: '服务版本详情', - path: 'versionInfo/:id', - component: './ModelDeployment/VersionInfo', + name: '服务详情', + path: 'serviceInfo/:serviceId', + routes: [ + { + name: '服务详情', + path: '', + component: './ModelDeployment/ServiceInfo', + }, + { + name: '新增服务版本', + path: 'createVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '更新服务版本', + path: 'updateVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '重启服务版本', + path: 'restartVersion', + component: './ModelDeployment/CreateVersion', + }, + { + name: '服务版本详情', + path: 'versionInfo/:id', + component: './ModelDeployment/VersionInfo', + }, + ], }, ], }, ], }, + { name: '应用开发', path: '/appsDeployment', diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index 430eae87..86f21f4e 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -168,7 +168,7 @@ function CreateServiceVersion() { if (lastPage === CreateServiceVersionFrom.ServiceInfo) { navigate(-1); } else { - navigate(`/modelDeployment/serviceInfo/${serviceId}`, { replace: true }); + navigate(`/dataset/modelDeployment/serviceInfo/${serviceId}`, { replace: true }); } } }; diff --git a/react-ui/src/pages/Workspace/components/QuickStart/index.tsx b/react-ui/src/pages/Workspace/components/QuickStart/index.tsx index 621efead..d155dae9 100644 --- a/react-ui/src/pages/Workspace/components/QuickStart/index.tsx +++ b/react-ui/src/pages/Workspace/components/QuickStart/index.tsx @@ -92,7 +92,7 @@ function QuickStart() { buttonTop={20} x={left + 4 * (192 + space) + 60 + space} y={263} - onClick={() => navigate('/modelDeployment')} + onClick={() => navigate('/dataset/modelDeployment')} /> <div className={styles['quick-start__content__canvas__model']} From 50eeb44f0b56086ebc47eab366604045d5ffbf6d Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 13 Mar 2025 09:43:16 +0800 Subject: [PATCH 085/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E6=8A=BD=E5=B1=89=E6=99=83=E5=8A=A8=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/app.tsx | 2 +- react-ui/src/pages/Experiment/Info/index.less | 6 ------ .../pages/Experiment/components/ExperimentDrawer/index.less | 3 ++- .../pages/Experiment/components/ExperimentDrawer/index.tsx | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 857d2650..26dfa334 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -250,7 +250,7 @@ export const antd: RuntimeAntdConfig = (memo) => { }; memo.theme.cssVar = true; - // memo.theme.hashed = false; + memo.theme.hashed = false; memo.appConfig = { message: { diff --git a/react-ui/src/pages/Experiment/Info/index.less b/react-ui/src/pages/Experiment/Info/index.less index f6df0cb6..70b27284 100644 --- a/react-ui/src/pages/Experiment/Info/index.less +++ b/react-ui/src/pages/Experiment/Info/index.less @@ -30,10 +30,4 @@ background-image: url(@/assets/img/pipeline-canvas-bg.png); background-size: 100% 100%; } - - :global { - .ant-drawer-mask { - background: transparent !important; - } - } } diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less index c1f1b7ef..e524a987 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less @@ -1,4 +1,5 @@ .experiment-drawer { + line-height: var(--ant-line-height); :global { .ant-drawer-body { overflow-y: hidden; @@ -12,7 +13,7 @@ } &__tabs { - height: calc(100% - 170px); + height: calc(100% - 169px); :global { .ant-tabs-nav { padding-left: 24px; diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index cdcaea19..58077267 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -95,10 +95,9 @@ const ExperimentDrawer = ({ return ( <Drawer - rootStyle={{ marginTop: '55px' }} + rootStyle={{ marginTop: '111px' }} title="任务执行详情" placement="right" - getContainer={false} closeIcon={<CloseOutlined className={styles['experiment-drawer__close']} />} onClose={onClose} open={open} From 87d376d757f186c7da51e822a3f0c2b2733baa42 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 13 Mar 2025 09:43:43 +0800 Subject: [PATCH 086/127] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E9=9B=86?= =?UTF-8?q?=E6=9D=A5=E6=BA=90=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/Dataset/config.tsx | 3 ++- .../pages/HyperParameter/components/ParameterInfo/index.tsx | 2 +- react-ui/src/utils/format.ts | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx index d24586a6..61c63531 100644 --- a/react-ui/src/pages/Dataset/config.tsx +++ b/react-ui/src/pages/Dataset/config.tsx @@ -25,9 +25,10 @@ export enum ResourceType { } export enum DataSource { - AtuoExport = 'auto_export', // 自动导出 + AutoExport = 'auto_export', // 自动导出 HandExport = 'hand_export', // 手动导出 Create = 'add', // 新增 + LabelStudioExport = 'label_studio_export', // LabelStudio 导出 } type ResourceTypeInfo = { diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx index d946a080..337f43f7 100644 --- a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -34,7 +34,7 @@ function ParameterInfo({ info }: ParameterInfoProps) { } return info.points_to_evaluate.map((item, index) => ({ ...item, - id: index, + id: index, // 作为 key,这个数组不会变化 })); }, [info]); diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index 7d37fbf5..9c1d327d 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -98,8 +98,10 @@ export const formatSource = (source?: string) => { return '用户上传'; } else if (source === DataSource.HandExport) { return '手动导入'; - } else if (source === DataSource.AtuoExport) { + } else if (source === DataSource.AutoExport) { return '实验自动导入'; + } else if (source === DataSource.LabelStudioExport) { + return '数据标注导入'; } return source; }; From 0421f4d2a063281987b9007ce9483573c48bbadc Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Thu, 13 Mar 2025 11:01:13 +0800 Subject: [PATCH 087/127] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=96=B0=E5=BB=BA=E6=97=B6=E5=8F=AF=E4=BB=A5=E4=B8=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=89=8B=E5=8A=A8=E8=BF=90=E8=A1=8C=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/HyperParameter/Create/index.tsx | 4 +- .../components/CreateForm/ExecuteConfig.tsx | 15 ++++++- .../CreateForm/ParameterRange/index.tsx | 6 +-- .../components/CreateForm/index.less | 1 + .../components/ExperimentLog/index.tsx | 40 +++++++++---------- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index bd7aedb9..4cddd4a7 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -51,7 +51,7 @@ function CreateHyperParameter() { ...rest, name, parameters, - points_to_evaluate: points_to_evaluate ?? [undefined], + points_to_evaluate: points_to_evaluate ?? [], }; form.setFieldsValue(formData); @@ -138,7 +138,7 @@ function CreateHyperParameter() { name: '', }, ], - points_to_evaluate: [undefined], + points_to_evaluate: [], }} > <BasicConfig /> diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 307bc5dd..976b6467 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -513,7 +513,6 @@ function ExecuteConfig() { marginRight: '3px', }} shape="circle" - disabled={fields.length === 1} type="text" size="middle" icon={<MinusCircleOutlined />} @@ -538,6 +537,20 @@ function ExecuteConfig() { </div> </Flex> ))} + {fields.length === 0 && ( + <Form.Item className={styles['add-weight']}> + <Button + className={styles['add-weight__button']} + color="primary" + variant="dashed" + onClick={() => add()} + block + icon={<PlusCircleOutlined />} + > + 添加手动运行参数 + </Button> + </Form.Item> + )} <Form.ErrorList errors={errors} className={styles['run-parameter__error']} /> </div> </> diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx index e4b700c9..43cd25f2 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx @@ -1,5 +1,6 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Flex, Form, Input, InputNumber } from 'antd'; +import React from 'react'; import { ParameterType, getFormOptions, parameterTooltip } from '../utils'; import styles from './index.less'; @@ -115,9 +116,8 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { <Flex align="start" style={{ width: '100%', marginBottom: '20px' }}> {formOptions.map((item, index) => { return ( - <> + <React.Fragment key={item.name}> <Form.Item - key={item.name} name={item.name} style={{ flex: 1, marginInlineEnd: 0 }} rules={[ @@ -134,7 +134,7 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { {index === 0 ? '-' : ' '} </span> )} - </> + </React.Fragment> ); })} </Flex> diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less index 06bbd5b7..bd264d3c 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -11,6 +11,7 @@ // 增加样式权重 & &__button { + width: calc(100% - 126px); border-color: .addAlpha(@primary-color, 0.5) []; box-shadow: none !important; &:hover { diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index b27c20fe..a9c598b8 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -38,28 +38,28 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { }); const tabItems = [ - { - key: 'git-clone-framework', - label: '框架代码日志', - // icon: <KFIcon type="icon-rizhi1" />, - children: ( - <div className={styles['experiment-log__tabs__log']}> - {frameworkCloneNodeStatus && ( - <LogList - instanceName={instanceInfo.argo_ins_name} - instanceNamespace={instanceInfo.argo_ins_ns} - pipelineNodeId={frameworkCloneNodeStatus.displayName} - workflowId={frameworkCloneNodeStatus.id} - instanceNodeStartTime={frameworkCloneNodeStatus.startedAt} - instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus} - ></LogList> - )} - </div> - ), - }, + // { + // key: 'git-clone-framework', + // label: '框架代码日志', + // // icon: <KFIcon type="icon-rizhi1" />, + // children: ( + // <div className={styles['experiment-log__tabs__log']}> + // {frameworkCloneNodeStatus && ( + // <LogList + // instanceName={instanceInfo.argo_ins_name} + // instanceNamespace={instanceInfo.argo_ins_ns} + // pipelineNodeId={frameworkCloneNodeStatus.displayName} + // workflowId={frameworkCloneNodeStatus.id} + // instanceNodeStartTime={frameworkCloneNodeStatus.startedAt} + // instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus} + // ></LogList> + // )} + // </div> + // ), + // }, { key: 'git-clone-train', - label: '训练代码日志', + label: '系统日志', // icon: <KFIcon type="icon-rizhi1" />, children: ( <div className={styles['experiment-log__tabs__log']}> From 871c8d7b5a83c6f59010a776af67614f3f6b9510 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 14 Mar 2025 09:01:36 +0800 Subject: [PATCH 088/127] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E5=BD=93?= =?UTF-8?q?=E5=88=A0=E9=99=A4FormList=E7=9A=84Item=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E5=A6=82=E6=9E=9C=E6=B2=A1=E6=9C=89=E5=A1=AB=E5=86=99=E5=86=85?= =?UTF-8?q?=E5=AE=B9=EF=BC=8C=E5=88=99=E7=9B=B4=E6=8E=A5=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CreateForm/TrialConfig.tsx | 16 +++--- .../components/CreateForm/ExecuteConfig.tsx | 21 ++++---- .../CreateForm/ParameterRange/index.tsx | 2 +- .../ModelDeployment/CreateVersion/index.tsx | 23 ++++---- .../components/GlobalParamsDrawer/index.tsx | 3 +- react-ui/src/utils/ui.tsx | 53 +++++++++++++++---- 6 files changed, 78 insertions(+), 40 deletions(-) diff --git a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx index 0d8008fb..6a965e9a 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/TrialConfig.tsx @@ -1,6 +1,6 @@ import SubAreaTitle from '@/components/SubAreaTitle'; import { AutoMLTaskType } from '@/enums'; -import { modalConfirm } from '@/utils/ui'; +import { removeFormListItem } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; import { classificationMetrics, regressionMetrics } from './ExecuteConfig'; @@ -72,12 +72,14 @@ function TrialConfig() { type="text" icon={<MinusCircleOutlined />} onClick={() => { - modalConfirm({ - title: '确定要删除该指标权重吗?', - onOk: () => { - remove(name); - }, - }); + removeFormListItem( + form, + 'metrics', + name, + remove, + ['name', 'value'], + '删除后,该该指标权重将不可恢复', + ); }} ></Button> {index === fields.length - 1 && ( diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index 976b6467..cbd97448 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -8,7 +8,7 @@ import SubAreaTitle from '@/components/SubAreaTitle'; import { hyperParameterOptimizedModeOptions } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; import { isEmpty } from '@/utils'; -import { modalConfirm } from '@/utils/ui'; +import { modalConfirm, removeFormListItem } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { Button, @@ -396,12 +396,14 @@ function ExecuteConfig() { size="middle" icon={<MinusCircleOutlined />} onClick={() => { - modalConfirm({ - title: '确定要删除该参数吗?', - onOk: () => { - remove(name); - }, - }); + removeFormListItem( + form, + 'parameters', + name, + remove, + ['name', 'type'], + '删除后,该参数将不可恢复', + ); }} ></Button> {index === fields.length - 1 && ( @@ -460,7 +462,7 @@ function ExecuteConfig() { ); if (arr.length > 0 && arr.length < runParameters.length) { return Promise.reject( - new Error(`手动运行超参数 ${name} 必须全部填写或者都不填写`), + new Error(`手动运行超参数 "${name}" 必须全部填写或者都不填写`), ); } } @@ -518,7 +520,8 @@ function ExecuteConfig() { icon={<MinusCircleOutlined />} onClick={() => { modalConfirm({ - title: '确定要删除该运行参数吗?', + title: '删除后,该运行参数将不可恢复', + content: '是否确认删除?', onOk: () => { remove(name); }, diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx index 43cd25f2..d33fcf13 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ParameterRange/index.tsx @@ -68,7 +68,7 @@ function ParameterRange({ type, value, onConfirm }: ParameterRangeProps) { <Flex style={{ marginLeft: '10px', - marginBottom: '20px', + marginBottom: '24px', flex: 'none', width: '66px', }} diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index 86f21f4e..7adb7dc8 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -20,7 +20,7 @@ import { import { changePropertyName } from '@/utils'; import { to } from '@/utils/promise'; import SessionStorage from '@/utils/sessionStorage'; -import { modalConfirm } from '@/utils/ui'; +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, Select } from 'antd'; @@ -434,7 +434,6 @@ function CreateServiceVersion() { {fields.map(({ key, name, ...restField }, index) => ( <Flex key={key} - align="center" gap="0 8px" style={{ position: 'relative', @@ -453,16 +452,16 @@ function CreateServiceVersion() { }, ]} > - <Input placeholder="请输入变量名" disabled={disabled} /> + <Input placeholder="请输入变量名" disabled={disabled} allowClear /> </Form.Item> - <span style={{ marginBottom: '24px' }}>=</span> + <span style={{ lineHeight: '46px' }}>=</span> <Form.Item {...restField} name={[name, 'value']} style={{ flex: 1 }} rules={[{ required: true, message: '请输入变量值' }]} > - <Input placeholder="请输入变量值" disabled={disabled} /> + <Input placeholder="请输入变量值" disabled={disabled} allowClear /> </Form.Item> <Flex style={{ @@ -484,12 +483,14 @@ function CreateServiceVersion() { icon={<MinusCircleOutlined />} disabled={disabled} onClick={() => { - modalConfirm({ - title: '确定要删除该环境变量吗?', - onOk: () => { - remove(name); - }, - }); + removeFormListItem( + form, + 'env_variables', + name, + remove, + ['key', 'value'], + '删除后,该环境变量将不可恢复', + ); }} ></Button> {index === fields.length - 1 && ( diff --git a/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx index e54894ea..f1aa57c9 100644 --- a/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx @@ -40,7 +40,8 @@ const GlobalParamsDrawer = forwardRef( const removeParameter = (name: number, remove: (param: number) => void) => { modalConfirm({ - title: '确认删除该参数吗?', + title: '删除后,该全局参数将不可恢复', + content: '是否确认删除?', onOk: () => { remove(name); }, diff --git a/react-ui/src/utils/ui.tsx b/react-ui/src/utils/ui.tsx index a3a214ec..d9953a3e 100644 --- a/react-ui/src/utils/ui.tsx +++ b/react-ui/src/utils/ui.tsx @@ -8,7 +8,16 @@ import { removeAllPageCacheState } from '@/hooks/pageCacheState'; import themes from '@/styles/theme.less'; import { type ClientInfo } from '@/types'; import { history } from '@umijs/max'; -import { Modal, Upload, message, type ModalFuncProps, type UploadFile } from 'antd'; +import { + Modal, + Upload, + message, + type FormInstance, + type ModalFuncProps, + type UploadFile, +} from 'antd'; +import { NamePath } from 'antd/es/form/interface'; +import { isEmpty } from './index'; import { closeAllModals } from './modal'; import SessionStorage from './sessionStorage'; @@ -143,16 +152,38 @@ export const limitUploadFileType = (type: string) => { }; /** - * 滚动到底部 - * - * @param {boolean} smooth - Determines if the scroll should be smooth + * 删除 FormList 表单项,如果表单项没有值,则直接删除,否则弹出确认框 + * @param form From实例 + * @param listName FormList 的 name + * @param name FormList 的其中一项 + * @param remove FormList 的删除方法 + * @param fieldNames FormList 的子项名称数组 + * @param confirmTitle 弹出确认框的标题 */ -export const scrollToBottom = (element: HTMLElement | null, smooth: boolean = true) => { - if (element) { - const optons: ScrollToOptions = { - top: element.scrollHeight, - behavior: smooth ? 'smooth' : 'instant', - }; - element.scrollTo(optons); +export const removeFormListItem = ( + form: FormInstance, + listName: NamePath, + name: number, + remove: (name: number) => void, + fieldNames: NamePath[], + confirmTitle: string, +) => { + const fields = fieldNames.map((item) => [listName, name, item].flat()); + const isEmptyField = fields.every((item) => { + const value = form.getFieldValue(item); + return isEmpty(value); + }); + + if (isEmptyField) { + remove(name); + return; } + + modalConfirm({ + title: confirmTitle, + content: '是否确认删除?', + onOk: () => { + remove(name); + }, + }); }; From 83d5ec3b4c1992c210c2e16515afff2b31937137 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 14 Mar 2025 09:02:27 +0800 Subject: [PATCH 089/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=BC=94=E5=8C=96tooltip=E6=8E=A7=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/components/ModelEvolution/index.tsx | 23 ++++++++++++++----- .../Model/components/ModelMetrics/index.tsx | 9 +++++--- .../Model/components/NodeTooltips/index.less | 2 -- .../Model/components/NodeTooltips/index.tsx | 7 +++++- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index 63d116ad..3557c12e 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -45,6 +45,7 @@ function ModelEvolution({ const [enterTooltip, setEnterTooltip] = useState(false); const [nodeTooltipX, setNodeToolTipX] = useState(0); const [nodeTooltipY, setNodeToolTipY] = useState(0); + const [isNodeTooltipLeft, setIsNodeTooltipLeft] = useState(true); const [hoverNodeData, setHoverNodeData] = useState< ModelDepsData | ProjectDependency | TrainDataset | undefined >(undefined); @@ -141,19 +142,28 @@ function ModelEvolution({ const { x, y } = model; const point = graph.getCanvasByPoint(x!, y!); const zoom = graph.getZoom(); - // 更加缩放,调整 tooltip 位置 + + // 根据缩放,调整 tooltip 位置 const offsetX = (nodeWidth * zoom) / 4; const offsetY = (nodeHeight * zoom) / 4; - point.x += offsetX; + // 25 是 `.model-evolution` 的 `padding-left` 值 + const tooltipX = point.x + offsetX + 25; + // 20 是 `.model-evolution` 的 `padding-bottom` 值 + const tooltipY = graphRef.current!.clientHeight - point.y + offsetY + 20; + setNodeToolTipY(tooltipY); + // 如果右边显示不下 const canvasWidth = graphRef.current!.clientWidth; - if (point.x + 300 > canvasWidth + 30) { - point.x = canvasWidth + 30 - 300; + // 300 是 NodeTool 的宽度,canvasWidth + 50 是 `.model-evolution` 的宽度 + if (tooltipX + 300 > canvasWidth + 50) { + setIsNodeTooltipLeft(false); + setNodeToolTipX(canvasWidth + 50 - (point.x - offsetX + 25)); + } else { + setNodeToolTipX(tooltipX); + setIsNodeTooltipLeft(true); } setHoverNodeData(model); - setNodeToolTipX(point.x); - setNodeToolTipY(graphRef.current!.clientHeight - point.y + offsetY); setShowNodeTooltip(true); }); @@ -254,6 +264,7 @@ function ModelEvolution({ resourceId={resourceId} x={nodeTooltipX} y={nodeTooltipY} + isLeft={isNodeTooltipLeft} data={hoverNodeData!} onVersionChange={onVersionChange} onMouseEnter={handleTooltipsMouseEnter} diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx index 110d28ce..730afaac 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -150,11 +150,14 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr // 表头 const columns: TableProps<TableData>['columns'] = useMemo(() => { - const first: TableData | undefined = tableData.find( + const firstMetrics: TableData | undefined = tableData.find( (item) => item.metrics_names && item.metrics_names.length > 0, ); - const metricsNames = first?.metrics_names ?? []; - const paramsNames = first?.params_names ?? []; + const firstParams: TableData | undefined = tableData.find( + (item) => item.params_names && item.params_names.length > 0, + ); + const metricsNames = firstMetrics?.metrics_names ?? []; + const paramsNames = firstParams?.params_names ?? []; return [ { title: '基本信息', diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.less b/react-ui/src/pages/Model/components/NodeTooltips/index.less index eabb979a..420fbacc 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.less +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.less @@ -1,7 +1,5 @@ .node-tooltips { position: absolute; - bottom: -100px; - left: -300px; z-index: 10; width: 300px; padding: 10px; diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx index 8a0f055e..1e95a5c1 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx @@ -180,6 +180,7 @@ type NodeTooltipsProps = { data: ModelDepsData | ProjectDependency | TrainDataset; x: number; y: number; + isLeft: boolean; onMouseEnter?: () => void; onMouseLeave?: () => void; onVersionChange: (version: string) => void; @@ -190,6 +191,7 @@ function NodeTooltips({ data, x, y, + isLeft, onMouseEnter, onMouseLeave, onVersionChange, @@ -208,10 +210,13 @@ function NodeTooltips({ ) { Component = <ModelInfo resourceId={resourceId} data={data} onVersionChange={onVersionChange} />; } + const style = isLeft + ? { left: `${x}px`, bottom: `${y}px` } + : { right: `${x}px`, bottom: `${y}px` }; return ( <div className={styles['node-tooltips']} - style={{ left: `${x}px`, bottom: `${y}px` }} + style={style} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > From 011eab8822c15390cea4c4a8dee6d57a623518b6 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Fri, 14 Mar 2025 10:38:50 +0800 Subject: [PATCH 090/127] =?UTF-8?q?chore:=20=E6=A8=A1=E5=9E=8B=E6=BC=94?= =?UTF-8?q?=E5=8C=96tooltip=E6=B7=BB=E5=8A=A0=E7=AE=AD=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/components/ModelEvolution/index.tsx | 37 ++++++++++--- .../Model/components/ModelEvolution/utils.tsx | 2 +- .../Model/components/NodeTooltips/index.less | 52 +++++++++++++++++++ .../Model/components/NodeTooltips/index.tsx | 3 +- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index 3557c12e..3bc40b40 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -51,6 +51,8 @@ function ModelEvolution({ >(undefined); const apiData = useRef<ModelDepsData | undefined>(undefined); // 接口返回的树形结构 const hierarchyNodes = useRef<ModelDepsData[]>([]); // 层级迭代树形结构,得到的节点列表 + const leaveNodeTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); + const leaveTooltipTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); useEffect(() => { initGraph(); @@ -135,6 +137,12 @@ function ModelEvolution({ // 绑定事件 const bindEvents = () => { graph.on('node:mouseenter', (e: G6GraphEvent) => { + // 清除延时关闭tooltip的定时器 + if (leaveNodeTimeout.current) { + clearTimeout(leaveNodeTimeout.current); + leaveNodeTimeout.current = null; + } + const nodeItem = e.item; graph.setItemState(nodeItem, 'hover', true); @@ -144,12 +152,12 @@ function ModelEvolution({ const zoom = graph.getZoom(); // 根据缩放,调整 tooltip 位置 - const offsetX = (nodeWidth * zoom) / 4; - const offsetY = (nodeHeight * zoom) / 4; + // const offsetX = (nodeWidth * zoom) / 4; + const offsetY = (nodeHeight * zoom) / 2; // 25 是 `.model-evolution` 的 `padding-left` 值 - const tooltipX = point.x + offsetX + 25; + const tooltipX = point.x + 25 - 20; // 20 是 `.model-evolution` 的 `padding-bottom` 值 - const tooltipY = graphRef.current!.clientHeight - point.y + offsetY + 20; + const tooltipY = graphRef.current!.clientHeight - point.y + offsetY + 20 + 10; setNodeToolTipY(tooltipY); // 如果右边显示不下 @@ -157,7 +165,7 @@ function ModelEvolution({ // 300 是 NodeTool 的宽度,canvasWidth + 50 是 `.model-evolution` 的宽度 if (tooltipX + 300 > canvasWidth + 50) { setIsNodeTooltipLeft(false); - setNodeToolTipX(canvasWidth + 50 - (point.x - offsetX + 25)); + setNodeToolTipX(canvasWidth + 50 - (point.x + 25) - 20); } else { setNodeToolTipX(tooltipX); setIsNodeTooltipLeft(true); @@ -170,7 +178,9 @@ function ModelEvolution({ graph.on('node:mouseleave', (e: G6GraphEvent) => { const nodeItem = e.item; graph.setItemState(nodeItem, 'hover', false); - setShowNodeTooltip(false); + leaveNodeTimeout.current = setTimeout(() => { + setShowNodeTooltip(false); + }, 100); }); graph.on('node:click', (e: G6GraphEvent) => { @@ -201,6 +211,12 @@ function ModelEvolution({ setShowNodeTooltip(false); setEnterTooltip(false); }); + + // 开始拖拽画布时触发 + graph.on('DRAG_START', () => { + setShowNodeTooltip(false); + setEnterTooltip(false); + }); }; // toggle 展开 @@ -215,11 +231,18 @@ function ModelEvolution({ }; const handleTooltipsMouseEnter = () => { + // 清除延时关闭tooltip的定时器 + if (leaveTooltipTimeout.current) { + clearTimeout(leaveTooltipTimeout.current); + leaveTooltipTimeout.current = null; + } setEnterTooltip(true); }; const handleTooltipsMouseLeave = () => { - setEnterTooltip(false); + leaveTooltipTimeout.current = setTimeout(() => { + setEnterTooltip(false); + }, 100); }; // 获取模型依赖 diff --git a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx index c4511e17..dcb4c964 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx @@ -33,7 +33,7 @@ export type Rect = { }; export interface TrainDataset extends NodeConfig { - repo_id: number; + repo_id: string; name: string; version: string; identifier: string; diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.less b/react-ui/src/pages/Model/components/NodeTooltips/index.less index 420fbacc..245cbe5f 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.less +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.less @@ -8,6 +8,30 @@ border-radius: 4px; box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); + &::after { + position: absolute; + bottom: -8px; /* 让三角形紧贴 div 底部 */ + left: 12px; /* 控制三角形相对 div 的位置 */ + width: 0; + height: 0; + border-top: 8px solid white; /* 主要颜色 */ + border-right: 8px solid transparent; + border-left: 8px solid transparent; + content: ''; + } + + &::before { + position: absolute; + bottom: -10px; /* 边框略大,形成描边效果 */ + left: 10px; /* 调整边框的偏移量,使其覆盖白色三角形 */ + width: 0; + height: 0; + border-top: 10px solid #eaeaea; /* 这是边框颜色 */ + border-right: 10px solid transparent; + border-left: 10px solid transparent; + content: ''; + } + &__title { margin: 10px 0; color: @text-color; @@ -57,3 +81,31 @@ } } } + +.node-tooltips.node-tooltips--right { + &::after { + position: absolute; + right: 12px; /* 控制三角形相对 div 的位置 */ + bottom: -8px; /* 让三角形紧贴 div 底部 */ + left: auto; + width: 0; + height: 0; + border-top: 8px solid white; /* 主要颜色 */ + border-right: 8px solid transparent; + border-left: 8px solid transparent; + content: ''; + } + + &::before { + position: absolute; + right: 10px; /* 调整边框的偏移量,使其覆盖白色三角形 */ + bottom: -10px; /* 边框略大,形成描边效果 */ + left: auto; + width: 0; + height: 0; + border-top: 10px solid #eaeaea; /* 这是边框颜色 */ + border-right: 10px solid transparent; + border-left: 10px solid transparent; + content: ''; + } +} diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx index 1e95a5c1..574a5065 100644 --- a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx +++ b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx @@ -2,6 +2,7 @@ import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { getGitUrl } from '@/utils'; import { formatDate } from '@/utils/date'; import { useNavigate } from '@umijs/max'; +import classNames from 'classnames'; import { ModelDepsData, NodeType, ProjectDependency, TrainDataset } from '../ModelEvolution/utils'; import styles from './index.less'; @@ -215,7 +216,7 @@ function NodeTooltips({ : { right: `${x}px`, bottom: `${y}px` }; return ( <div - className={styles['node-tooltips']} + className={classNames(styles['node-tooltips'], { [styles['node-tooltips--right']]: !isLeft })} style={style} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} From 07fa9d00c7b492104772c90b86586384a928034a Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 14 Mar 2025 14:20:44 +0800 Subject: [PATCH 091/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/service/impl/ServiceServiceImpl.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 2a30da66..0244bba6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -44,6 +44,7 @@ public class ServiceServiceImpl implements ServiceService { private ServiceDao serviceDao; @Resource private AssetWorkflowDao assetWorkflowDao; + @Override public Page<com.ruoyi.platform.domain.Service> queryByPageService(com.ruoyi.platform.domain.Service service, PageRequest pageRequest) { long total = serviceDao.countService(service); @@ -225,9 +226,9 @@ public class ServiceServiceImpl implements ServiceService { String req = HttpUtils.sendPost(argoUrl + modelService + "/delete", JSON.toJSONString(paramMap)); if (StringUtils.isNotEmpty(req)) { Map<String, Object> reqMap = JacksonUtil.parseJSONStr2Map(req); - if ((Integer) reqMap.get("code") == 200) { - return serviceDao.updateServiceVersion(serviceVersion) > 0 ? "删除成功" : "删除失败"; - } +// if ((Integer) reqMap.get("code") == 200) { + return serviceDao.updateServiceVersion(serviceVersion) > 0 ? "删除成功" : "删除失败"; +// } } return "删除失败"; } From e78ad71de5f956c0e50c17cdfa86f758203a4a78 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Sat, 15 Mar 2025 11:09:09 +0800 Subject: [PATCH 092/127] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/ruoyi/platform/utils/K8sClientUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index a6eaed5f..17d36177 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -39,6 +39,8 @@ public class K8sClientUtil { @Value("${jupyter.hostPath}") private String hostPath; + @Value("${dockerpush.proxyUrl}") + private String proxyUrl; private String http; private String token; @@ -360,11 +362,11 @@ public class K8sClientUtil { .withNewSecurityContext().withNewPrivileged(true).endSecurityContext() .addNewEnv() .withName("HTTP_PROXY") - .withValue("http://172.20.32.253:3128") + .withValue(proxyUrl) .endEnv() .addNewEnv() .withName("HTTPS_PROXY") - .withValue("http://172.20.32.253:3128") + .withValue(proxyUrl) .endEnv() .addNewEnv() .withName("NO_PROXY") From 224d4969991ec3731c8e21344f6c5ac874d07bc0 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Sat, 15 Mar 2025 11:30:27 +0800 Subject: [PATCH 093/127] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=8Elabel=20stud?= =?UTF-8?q?io=E5=AF=BC=E5=87=BA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/service/impl/NewDatasetServiceImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java index 705a9668..d6dbe494 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/NewDatasetServiceImpl.java @@ -473,12 +473,13 @@ public class NewDatasetServiceImpl implements NewDatasetService { public String newCreateVersionFromLabelStudio(LabelDatasetVersionVo labelDatasetVersionVo) throws Exception { // 1. 下载数据集文件 // 获取label-studio数据流 - Map<String, String> headers = new HashMap<String, String>(); + Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Token " + labelDatasetVersionVo.getToken()); InputStream inputStream = HttpUtils.getIntputStream(labelloaclUrl + "/api/projects/" + labelDatasetVersionVo.getProjectId() + "/export?exportType=" + labelDatasetVersionVo.getExportType(), headers); // 2. 打包成zip包 - String zipFileName = labelDatasetVersionVo.getName()+"_"+labelDatasetVersionVo.getVersion() + ".zip"; +// String zipFileName = labelDatasetVersionVo.getName() + "_" + labelDatasetVersionVo.getVersion() + ".zip"; + String zipFileName = labelDatasetVersionVo.getName() + "_" + labelDatasetVersionVo.getVersion(); MultipartFile[] files = FileUtil.toMultipartFiles(inputStream, zipFileName); // 3. 上传到minio From 4d8704aabc032aa172fa1811c153947e3ea32490 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Mon, 17 Mar 2025 11:11:22 +0800 Subject: [PATCH 094/127] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.storybook/mock/websocket.mock.js | 92 +++++++++++++++++++ react-ui/.storybook/preview.tsx | 5 + .../Experiment/components/LogGroup/index.tsx | 23 +++-- .../Experiment/components/LogList/index.tsx | 36 +++++--- .../pages/HyperParameter/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.tsx | 2 +- react-ui/src/stories/mockData.ts | 53 +++++++++++ .../src/stories/pages/LogList.stories.tsx | 90 ++++++++++++++++++ react-ui/src/utils/index.ts | 10 +- 9 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 react-ui/.storybook/mock/websocket.mock.js create mode 100644 react-ui/src/stories/pages/LogList.stories.tsx diff --git a/react-ui/.storybook/mock/websocket.mock.js b/react-ui/.storybook/mock/websocket.mock.js new file mode 100644 index 00000000..3661b99b --- /dev/null +++ b/react-ui/.storybook/mock/websocket.mock.js @@ -0,0 +1,92 @@ +export const createWebSocketMock = () => { + class WebSocketMock { + constructor(url) { + this.url = url; + this.readyState = WebSocket.OPEN; + this.listeners = {}; + this.count = 0; + + console.log("Mock WebSocket connected to:", url); + + // 模拟服务器推送消息 + this.intervalId = setInterval(() => { + this.count += 1; + if (this.count > 5) { + this.count = 0; + clearInterval(this.intervalId); + return; + } + this.sendMessage(JSON.stringify(logStreamData)); + }, 3000); + } + + sendMessage(data) { + if (this.listeners["message"]) { + this.listeners["message"].forEach((callback) => callback({ data })); + } + } + + addEventListener(event, callback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + removeEventListener(event, callback) { + if (this.listeners[event]) { + this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); + } + } + + close() { + this.readyState = WebSocket.CLOSED; + console.log("Mock WebSocket closed"); + } + } + + return WebSocketMock; +}; + +export const logStreamData = { + streams: [ + { + stream: { + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + }, + values: [ + [ + '1742179591969785990', + 'time="2025-03-17T02:46:31.969Z" level=info msg="Starting Workflow Executor" version=v3.5.10\n', + ], + ], + }, + { + stream: { + filename: + '/var/log/pods/argo_workflow-p2ddj-git-clone-f33abcda-3988047653_e31cf6be-e013-4885-9eb6-ec84f83b9ba9/init/0.log', + job: 'argo/workflow-p2ddj-git-clone-f33abcda-3988047653', + namespace: 'argo', + pod: 'workflow-p2ddj-git-clone-f33abcda-3988047653', + stream: 'stderr', + workflows_argoproj_io_completed: 'false', + workflows_argoproj_io_workflow: 'workflow-p2ddj', + container: 'init', + }, + values: [ + [ + '1742179591973414064', + 'time="2025-03-17T02:46:31.973Z" level=info msg="Using executor retry strategy" Duration=1s Factor=1.6 Jitter=0.5 Steps=5\n', + ], + ], + }, + ], +}; \ No newline at end of file diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx index 61e82aaa..0ec22de0 100644 --- a/react-ui/.storybook/preview.tsx +++ b/react-ui/.storybook/preview.tsx @@ -5,6 +5,7 @@ import type { Preview } from '@storybook/react'; import { App, ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import { initialize, mswLoader } from 'msw-storybook-addon'; +import { createWebSocketMock } from './mock/websocket.mock'; import './storybook.css'; /* @@ -14,6 +15,10 @@ import './storybook.css'; */ initialize(); +// 替换全局 WebSocket 为 Mock 版本 +// @ts-ignore +global.WebSocket = createWebSocketMock(); + const preview: Preview = { parameters: { controls: { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 5123fae1..11d51aca 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -37,19 +37,20 @@ function LogGroup({ const [completed, setCompleted] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); - const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); const socketRef = useRef<WebSocket | undefined>(undefined); const retryRef = useRef(2); // 等待 2 秒,重试 3 次 - const elementRef = useRef<HTMLDivElement | null>(null); + const logElementRef = useRef<HTMLDivElement | null>(null); + // 如果是【运行中】状态,设置 hasRun 为 true,【运行中】或者从【运行中】切换到别的状态时,不显示【更多】按钮 + const [hasRun, setHasRun] = useState(false); + if (status === ExperimentStatus.Running && !hasRun) { + setHasRun(true); + } useEffect(() => { - scrollToBottom(false); if (status === ExperimentStatus.Running) { setupSockect(); - } else if (preStatusRef.current === ExperimentStatus.Running) { - setCompleted(true); } - preStatusRef.current = status; + scrollToBottom(false); }, [status]); // 鼠标拖到中不滚动到底部 @@ -208,15 +209,17 @@ function LogGroup({ // }; // element.scrollTo(optons); // } - elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' }); + logElementRef?.current?.scrollIntoView({ + block: 'end', + behavior: smooth ? 'smooth' : 'instant', + }); }; const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); - const showMoreBtn = - status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; + const showMoreBtn = !hasRun && !completed && showLog && logText !== ''; return ( - <div className={styles['log-group']} ref={elementRef}> + <div className={styles['log-group']} ref={logElementRef}> {log_type === 'resource' && ( <div className={styles['log-group__pod']} onClick={handleCollapse}> <div className={styles['log-group__pod__name']}>{pod_name}</div> diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 86c97d15..15ce0def 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -15,13 +15,21 @@ export type ExperimentLog = { }; type LogListProps = { - instanceName: string; // 实验实例 name - instanceNamespace: string; // 实验实例 namespace - pipelineNodeId: string; // 流水线节点 id - workflowId?: string; // 实验实例工作流 id - instanceNodeStartTime?: string; // 实验实例节点开始运行时间 + /** 实验实例 name */ + instanceName: string; + /** 实验实例 namespace */ + instanceNamespace: string; + /** 流水线节点 id */ + pipelineNodeId: string; + /** 实验实例工作流 id */ + workflowId?: string; + /** 实验实例节点开始运行时间 */ + instanceNodeStartTime?: string; + /** 实验实例节点运行状态 */ instanceNodeStatus?: ExperimentStatus; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; @@ -35,23 +43,21 @@ function LogList({ className, style, }: LogListProps) { - const [logList, setLogList] = useState<ExperimentLog[]>([]); - const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); + const [logGroups, setLogGroups] = useState<ExperimentLog[]>([]); const retryRef = useRef(3); // 等待 2 秒,重试 3 次 - // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 + // 当实例节点运行状态不是 Pending,获取实验日志组 useEffect(() => { if ( instanceNodeStatus && instanceNodeStatus !== ExperimentStatus.Pending && - (!preStatusRef.current || preStatusRef.current === ExperimentStatus.Pending) + logGroups.length === 0 ) { getExperimentLog(); } - preStatusRef.current = instanceNodeStatus; }, [instanceNodeStatus]); - // 获取实验日志 + // 获取实验 Pods 组 const getExperimentLog = async () => { const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; const params = { @@ -71,7 +77,7 @@ function LogList({ log_type, }, ]; - setLogList(list); + setLogGroups(list); } else if (log_type === 'resource') { const list = pods.map((v: string) => ({ log_type, @@ -79,7 +85,7 @@ function LogList({ log_content: '', start_time, })); - setLogList(list); + setLogGroups(list); } } else { if (retryRef.current > 0) { @@ -93,8 +99,8 @@ function LogList({ return ( <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> - {logList.length > 0 ? ( - logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) + {logGroups.length > 0 ? ( + logGroups.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />) ) : ( <div className={styles['log-list__empty']}>暂无日志</div> )} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index df25cc18..aa206059 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -192,7 +192,7 @@ function HyperParameterInstance() { key: TabKeys.History, label: '寻优列表', icon: <KFIcon type="icon-Trialliebiao" />, - children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, + children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />, }, ]; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index c4698464..d9091d26 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -35,7 +35,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }, []); // 计算 column - const first: HyperParameterTrial | undefined = trialList[0]; + const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined; const config: Record<string, any> = first?.config ?? {}; const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; const paramsNames = Object.keys(config); diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts index 11b5ffa2..e5ea5bd1 100644 --- a/react-ui/src/stories/mockData.ts +++ b/react-ui/src/stories/mockData.ts @@ -1,3 +1,4 @@ +// 数据集列表 export const datasetListData = { msg: '操作成功', code: 200, @@ -76,6 +77,7 @@ export const datasetListData = { }, }; +// 数据集版本列表 export const datasetVersionData = { msg: '操作成功', code: 200, @@ -107,6 +109,7 @@ export const datasetVersionData = { ], }; +// 数据集详情 export const datasetDetailData = { msg: '操作成功', code: 200, @@ -137,6 +140,7 @@ export const datasetDetailData = { }, }; +// 模型列表 export const modelListData = { msg: '操作成功', code: 200, @@ -211,6 +215,7 @@ export const modelListData = { }, }; +// 模型版本列表 export const modelVersionData = { msg: '操作成功', code: 200, @@ -234,6 +239,7 @@ export const modelVersionData = { ], }; +// 模型详情 export const modelDetailData = { msg: '操作成功', code: 200, @@ -266,6 +272,7 @@ export const modelDetailData = { }, }; +// 镜像列表 export const mirrorListData = { code: 200, msg: '操作成功', @@ -384,6 +391,7 @@ export const mirrorListData = { }, }; +// 镜像版本列表 export const mirrorVerionData = { code: 200, msg: '操作成功', @@ -433,6 +441,7 @@ export const mirrorVerionData = { }, }; +// 代码配置列表 export const codeListData = { code: 200, msg: '操作成功', @@ -547,6 +556,7 @@ export const codeListData = { }, }; +// 服务列表 export const serviceListData = { code: 200, msg: '操作成功', @@ -620,6 +630,7 @@ export const serviceListData = { }, }; +// 计算资源列表 export const computeResourceData = { code: 200, msg: '操作成功', @@ -793,3 +804,45 @@ export const computeResourceData = { empty: false, }, }; + +// 日志组 +export const logGroupData = { + code: 200, + msg: '操作成功', + data: { + log_type: 'normal', + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: + '[2025-03-06 14:02:23] time="2025-03-06T14:02:23.068Z" level=info msg="capturing logs" argo=true\n[2025-03-06 14:02:23] Cloning into \'/tmp/traincode\'...\n[2025-03-06 14:02:23] Cloning public repository without authentication.\n[2025-03-06 14:02:23] Repository cloned successfully.\n[2025-03-06 14:02:24] time="2025-03-06T14:02:24.069Z" level=info msg="sub-process exited" argo=true error="<nil>"\n', + start_time: '1741240944069759628', + }, + pods: ['workflow-txpb5-git-clone-05955a53-2484323670'], + }, +}; + +// 日志 +export const logData = { + code: 200, + msg: '操作成功', + data: { + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: + '[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Main container completed" error="<nil>"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No Script output reference in workflow. Capturing script output ignored"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output parameters"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="No output artifacts"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="S3 Save path: /tmp/argo/outputs/logs/main.log, key: workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Creating minio client using static credentials" endpoint="minio.argo.svc.cluster.local:9000"\n[2025-03-06 14:02:24] time="2025-03-06T06:02:24.315Z" level=info msg="Saving file to s3" bucket=my-bucket endpoint="minio.argo.svc.cluster.local:9000" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log path=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Save artifact" artifactName=main-logs duration=1.092064185s error="<nil>" key=workflow-txpb5/workflow-txpb5-git-clone-05955a53-2484323670/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="not deleting local artifact" localArtPath=/tmp/argo/outputs/logs/main.log\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.407Z" level=info msg="Successfully saved file: /tmp/argo/outputs/logs/main.log"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.408Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot create resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.418Z" level=info msg="Alloc=8553 TotalAlloc=14914 Sys=24421 NumGC=4 Goroutines=10"\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.419Z" level=warning msg="failed to patch task result, falling back to legacy/insecure pod patch, see https://argo-workflows.readthedocs.io/en/release-3.5/workflow-rbac/" error="workflowtaskresults.argoproj.io \\"workflow-txpb5-2484323670\\" is forbidden: User \\"system:serviceaccount:argo:argo\\" cannot patch resource \\"workflowtaskresults\\" in API group \\"argoproj.io\\" in the namespace \\"argo\\""\n[2025-03-06 14:02:25] time="2025-03-06T06:02:25.427Z" level=info msg="Deadline monitor stopped"\n', + start_time: '1741240945427542429', + }, + }, +}; + +export const logEmptyData = { + code: 200, + msg: '操作成功', + data: { + log_detail: { + pod_name: 'workflow-txpb5-git-clone-05955a53-2484323670', + log_content: '', + start_time: '1741240945427542429', + }, + }, +}; diff --git a/react-ui/src/stories/pages/LogList.stories.tsx b/react-ui/src/stories/pages/LogList.stories.tsx new file mode 100644 index 00000000..5685b8fa --- /dev/null +++ b/react-ui/src/stories/pages/LogList.stories.tsx @@ -0,0 +1,90 @@ +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import type { Meta, StoryObj } from '@storybook/react'; +import { http, HttpResponse } from 'msw'; +import { logData, logEmptyData, logGroupData } from '../mockData'; + +let count = 0; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Pages/LogList 日志组', + component: LogList, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.post('/api/mmp/experimentIns/realTimeLog', () => { + return HttpResponse.json(logGroupData); + }), + http.get('/api/mmp/experimentIns/pods/log', () => { + if (count > 0) { + count = 0; + return HttpResponse.json(logEmptyData); + } + count += 1; + return HttpResponse.json(logData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + instanceNodeStatus: { control: 'select', options: Object.values(ExperimentStatus) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta<typeof LogList>; + +export default meta; +type Story = StoryObj<typeof meta>; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 运行完成时 */ +export const Succeeded: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Succeeded, + }, +}; + +/** 无状态,空数据 */ +export const NoStatus: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + }, +}; + +/** Pending */ +export const Pending: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Pending, + }, +}; + +export const Running: Story = { + args: { + instanceName: 'workflow-txpb5', + instanceNamespace: 'argo', + pipelineNodeId: 'git-clone-05955a53', + workflowId: 'workflow-txpb5-2484323670', + instanceNodeStartTime: '2025-03-06 14:02:14', + instanceNodeStatus: ExperimentStatus.Running, + }, +}; diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 3deb9832..4df48c1c 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -88,7 +88,10 @@ export function camelCaseToUnderscore(obj: Record<string, any>) { } // null to undefined -export function nullToUndefined(obj: Record<string, any>) { +export function nullToUndefined(obj: Record<string, any> | null) { + if (obj === null) { + return undefined; + } if (!isPlainObject(obj)) { return obj; } @@ -111,7 +114,10 @@ export function nullToUndefined(obj: Record<string, any>) { } // undefined to null -export function undefinedToNull(obj: Record<string, any>) { +export function undefinedToNull(obj?: Record<string, any>) { + if (obj === undefined) { + return null; + } if (!isPlainObject(obj)) { return obj; } From f17aeb904e90ffe6a415edfd3898a5f675e5ba80 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 17 Mar 2025 11:29:08 +0800 Subject: [PATCH 095/127] =?UTF-8?q?label=20studio=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 57088d5d..b87c1b10 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -23,6 +23,10 @@ spec: value: Asia/Shanghai - name: JAVA_TOOL_OPTIONS value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" + - name: HTTP_PROXY + value: "http://172.20.32.253:3128" + - name: HTTPS_PROXY + value: "http://172.20.32.253:3128" ports: - containerPort: 9213 volumeMounts: From d0500f1f00e9e87d334346ddcab3c4e5061b68af Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Mon, 17 Mar 2025 13:42:02 +0800 Subject: [PATCH 096/127] =?UTF-8?q?label=20studio=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/template-yaml/k8s-7management.yaml | 4 ---- .../src/main/java/com/ruoyi/platform/utils/HttpUtils.java | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index b87c1b10..57088d5d 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -23,10 +23,6 @@ spec: value: Asia/Shanghai - name: JAVA_TOOL_OPTIONS value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" - - name: HTTP_PROXY - value: "http://172.20.32.253:3128" - - name: HTTPS_PROXY - value: "http://172.20.32.253:3128" ports: - containerPort: 9213 volumeMounts: diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/HttpUtils.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/HttpUtils.java index 0b6db3df..708a9500 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/HttpUtils.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/HttpUtils.java @@ -555,6 +555,7 @@ public class HttpUtils { * @throws IOException 如果请求失败或发生其他 I/O 错误。 */ public static InputStream getIntputStream(String url, Map<String, String> headers) throws IOException { + clearProxySettings(); URL requestUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); From 32883beac69bbbc90d16471d6f8958153e4deed1 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 18 Mar 2025 11:10:44 +0800 Subject: [PATCH 097/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/system/api/domain/SysUser.java | 10 ++ .../controller/jupyter/JupyterController.java | 9 +- .../platform/domain/ComputingResource.java | 123 ++++-------------- .../com/ruoyi/platform/domain/Resource.java | 25 ++++ .../ruoyi/platform/domain/ResourceOccupy.java | 39 ++++++ .../platform/mapper/ResourceOccupyDao.java | 15 +++ .../service/ResourceOccupyService.java | 13 ++ .../impl/ResourceOccupyServiceImpl.java | 71 ++++++++++ .../ComputingResourceDaoMapper.xml | 24 ++-- .../managementPlatform/ResourceOccupy.xml | 31 +++++ 10 files changed, 242 insertions(+), 118 deletions(-) create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Resource.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java create mode 100644 ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java index fc1aa78c..67457175 100644 --- a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java +++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java @@ -131,6 +131,8 @@ public class SysUser extends BaseEntity { private String gitLinkPassword; + private Float credit; + public SysUser() { } @@ -315,6 +317,14 @@ public class SysUser extends BaseEntity { return gitLinkPassword; } + public void setCredit(Float credit) { + this.credit = credit; + } + + public Float getCredit() { + return credit; + } + @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java index 6234820b..2772cd5b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/jupyter/JupyterController.java @@ -6,9 +6,7 @@ import com.ruoyi.common.core.web.domain.GenericsAjaxResult; import com.ruoyi.platform.domain.DevEnvironment; import com.ruoyi.platform.service.JupyterService; import com.ruoyi.platform.service.NewDatasetService; -import com.ruoyi.platform.vo.NewDatasetVo; import com.ruoyi.platform.vo.PodStatusVo; -import com.ruoyi.platform.vo.VersionVo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -19,8 +17,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; @RestController @RequestMapping("/jupyter") @@ -30,6 +26,7 @@ public class JupyterController extends BaseController { private JupyterService jupyterService; @Resource private NewDatasetService newDatasetService; + @GetMapping(value = "/getURL") @ApiOperation("得到访问地址") public GenericsAjaxResult<String> getURL() throws IOException { @@ -47,7 +44,7 @@ public class JupyterController extends BaseController { @ApiOperation("根据开发环境id启动jupyter pod") @ApiResponse public GenericsAjaxResult<String> runJupyter(@PathVariable("id") Integer id) throws Exception { - return genericsSuccess(this.jupyterService.runJupyterService(id)); + return genericsSuccess(this.jupyterService.runJupyterService(id)); } @@ -68,7 +65,7 @@ public class JupyterController extends BaseController { @ApiOperation("查询jupyter pod状态") @ApiResponse public GenericsAjaxResult<PodStatusVo> getStatus(DevEnvironment devEnvironment) throws Exception { - return genericsSuccess(this.jupyterService.getJupyterStatus(devEnvironment)); + return genericsSuccess(this.jupyterService.getJupyterStatus(devEnvironment)); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java index d0551fa8..9a52bd6b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java @@ -3,9 +3,10 @@ package com.ruoyi.platform.domain; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import io.swagger.annotations.ApiModelProperty; +import lombok.Data; -import java.util.Date; import java.io.Serializable; +import java.util.Date; /** * (ComputingResource)实体类 @@ -14,13 +15,17 @@ import java.io.Serializable; * @since 2023-11-29 14:23:01 */ @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@Data public class ComputingResource implements Serializable { private static final long serialVersionUID = -42500298368776666L; -/** + /** * 主键 */ -@ApiModelProperty(value = "编号", notes = "唯一标识符") -private Integer id; + @ApiModelProperty(value = "编号", notes = "唯一标识符") + private Integer id; + + @ApiModelProperty("资源id") + private Integer resourceId; @ApiModelProperty("计算资源的描述") private String computingResource; @@ -31,6 +36,24 @@ private Integer id; @ApiModelProperty("资源的详细描述") private String description; + @ApiModelProperty("cpu核数") + private Integer cpuCores; + + @ApiModelProperty("内存大小GB") + private Integer memoryGb; + + @ApiModelProperty("显存大小GB") + private Integer gpuMemoryGb; + + @ApiModelProperty("GPU个数") + private Integer gpuNums; + + @ApiModelProperty("积分/小时") + private Float creditPerHour; + + @ApiModelProperty("标签") + private String labels; + @ApiModelProperty(value = "创建者的用户名", example = "admin") private String createBy; @@ -46,100 +69,8 @@ private Integer id; @ApiModelProperty(value = "状态标识", notes = "0表示失效,1表示生效") private Integer state; - @ApiModelProperty(value = "占用情况(1-占用,0-未占用)") - private Integer usedState; - @ApiModelProperty(value = "节点") private String node; - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getComputingResource() { - return computingResource; - } - - public void setComputingResource(String computingResource) { - this.computingResource = computingResource; - } - - - - public String getStandard() { - return standard; - } - - public void setStandard(String standard) { - this.standard = standard; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getCreateBy() { - return createBy; - } - - public void setCreateBy(String createBy) { - this.createBy = createBy; - } - - public Date getCreateTime() { - return createTime; - } - - public void setCreateTime(Date createTime) { - this.createTime = createTime; - } - - public String getUpdateBy() { - return updateBy; - } - - public void setUpdateBy(String updateBy) { - this.updateBy = updateBy; - } - - public Date getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Date updateTime) { - this.updateTime = updateTime; - } - - public Integer getState() { - return state; - } - - public void setState(Integer state) { - this.state = state; - } - - public Integer getUsedState() { - return usedState; - } - - public void setUsedState(Integer usedState) { - this.usedState = usedState; - } - - public String getNode() { - return node; - } - - public void setNode(String node) { - this.node = node; - } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Resource.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Resource.java new file mode 100644 index 00000000..bdb92893 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Resource.java @@ -0,0 +1,25 @@ +package com.ruoyi.platform.domain; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@Data +public class Resource { + @ApiModelProperty(value = "编号", notes = "唯一标识符") + private Integer id; + + @ApiModelProperty("类型") + private String type; + + @ApiModelProperty("gpu") + private String gpu; + + @ApiModelProperty("总数") + private Integer total; + + @ApiModelProperty("已被占用的数量") + private Integer used; +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java new file mode 100644 index 00000000..80bf9b88 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -0,0 +1,39 @@ +package com.ruoyi.platform.domain; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@Data +public class ResourceOccupy { + @ApiModelProperty(value = "编号", notes = "唯一标识符") + private Integer id; + + @ApiModelProperty("用户") + private Long userId; + + @ApiModelProperty("计算资源") + private Integer computingResourceId; + + @ApiModelProperty("积分/小时") + private Float creditPerHour; + + @ApiModelProperty("上一次扣分时间") + private Date deduceLastTime; + + @ApiModelProperty("状态") + private Integer state; + + @ApiModelProperty("开始时间") + private Date startTime; + + @ApiModelProperty("任务类型") + private String taskType; + + @ApiModelProperty("类型id") + private Integer taskId; +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java new file mode 100644 index 00000000..e434f494 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -0,0 +1,15 @@ +package com.ruoyi.platform.mapper; + +import com.ruoyi.platform.domain.ResourceOccupy; +import org.apache.ibatis.annotations.Param; + +public interface ResourceOccupyDao { + + Boolean haveResource(@Param("id") Integer id, @Param("need") Integer need); + + int save(@Param("resourceOccupy") ResourceOccupy resourceOccupy); + + int edit(@Param("resourceOccupy") ResourceOccupy resourceOccupy); + + ResourceOccupy getResourceOccupyById(@Param("id") Integer id); +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java new file mode 100644 index 00000000..1f50706d --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -0,0 +1,13 @@ +package com.ruoyi.platform.service; + +public interface ResourceOccupyService { + + Boolean haveResource(Integer computingResourceId) throws Exception; + + void startDeduce(Integer computingResourceId); + + void endDeduce(Integer id); + + void deducing(); + +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java new file mode 100644 index 00000000..3cbbfe0b --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -0,0 +1,71 @@ +package com.ruoyi.platform.service.impl; + +import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.ComputingResource; +import com.ruoyi.platform.domain.ResourceOccupy; +import com.ruoyi.platform.mapper.ComputingResourceDao; +import com.ruoyi.platform.mapper.ResourceOccupyDao; +import com.ruoyi.platform.service.ResourceOccupyService; +import com.ruoyi.system.api.model.LoginUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; + +@Service("resourceOccupyService") +public class ResourceOccupyServiceImpl implements ResourceOccupyService { + @Resource + private ResourceOccupyDao resourceOccupyDao; + + @Resource + private ComputingResourceDao computingResourceDao; + + @Override + public Boolean haveResource(Integer computingResourceId) throws Exception { + ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); + + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser.getSysUser().getCredit() < computingResource.getCreditPerHour()) { + throw new Exception("积分不足"); + } + if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { + if (resourceOccupyDao.haveResource(computingResource.getResourceId(), computingResource.getGpuNums())) { + return true; + } else { + throw new Exception("资源不足,GPU资源已被占用"); + } + } else { + if (resourceOccupyDao.haveResource(computingResource.getResourceId(), computingResource.getCpuCores())) { + return true; + } else { + throw new Exception("资源不足,CPU资源已被占用完"); + } + } + } + + @Override + public void startDeduce(Integer computingResourceId) { + ResourceOccupy resourceOccupy = new ResourceOccupy(); + ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); + resourceOccupy.setComputingResourceId(computingResourceId); + LoginUser loginUser = SecurityUtils.getLoginUser(); + resourceOccupy.setUserId(loginUser.getUserid()); + resourceOccupy.setCreditPerHour(computingResource.getCreditPerHour()); + resourceOccupyDao.save(resourceOccupy); + } + + @Override + public void endDeduce(Integer id) { + ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyById(id); + resourceOccupy.setState(Constant.State_invalid); + deducing(); + resourceOccupy.setDeduceLastTime(new Date()); + resourceOccupyDao.edit(resourceOccupy); + } + + @Override + public void deducing() { + + } +} diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml index 162a12f7..25981868 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml @@ -2,30 +2,22 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.ComputingResourceDao"> - <resultMap type="com.ruoyi.platform.domain.ComputingResource" id="ComputingResourceMap"> - <result property="id" column="id" jdbcType="INTEGER"/> - <result property="computingResource" column="computing_resource" jdbcType="VARCHAR"/> - <result property="standard" column="standard" jdbcType="VARCHAR"/> - <result property="description" column="description" jdbcType="VARCHAR"/> - <result property="createBy" column="create_by" jdbcType="VARCHAR"/> - <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> - <result property="updateBy" column="update_by" jdbcType="VARCHAR"/> - <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/> - <result property="state" column="state" jdbcType="INTEGER"/> - </resultMap> - <!--查询单个--> - <select id="queryById" resultMap="ComputingResourceMap"> + <select id="queryById" resultType="com.ruoyi.platform.domain.ComputingResource"> select - id,computing_resource,standard,description,create_by,create_time,update_by,update_time,state + id,resource_id,computing_resource,standard,description, + cpu_cores,memory_gb,gpu_memory_gb,gpu_nums,credit_per_hour,labels, + create_by,create_time,update_by,update_time,state from computing_resource where id = #{id} and state = 1 </select> <!--查询指定行数据--> - <select id="queryAllByLimit" resultMap="ComputingResourceMap"> + <select id="queryAllByLimit" resultType="com.ruoyi.platform.domain.ComputingResource"> select - id,computing_resource,standard,description,create_by,create_time,update_by,update_time,state + id,resource_id,computing_resource,standard,description, + cpu_cores,memory_gb,gpu_memory_gb,gpu_nums,credit_per_hour,labels, + create_by,create_time,update_by,update_time,state from computing_resource <where> state = 1 diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml new file mode 100644 index 00000000..5a7d9cc6 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> +<mapper namespace="com.ruoyi.platform.mapper.ResourceOccupyDao"> + <insert id="save"> + insert into resource_occupy (user_id, computing_resource_id, credit_per_hour) + values (#{resourceOccupy.userId}, #{resourceOccupy.computingResourceId}, #{resourceOccupy.creditPerHour}) + </insert> + + <update id="edit"> + update resource_occupy + <set> + <if test="resourceOccupy.state != null"> + state = #{resourceOccupy.state}, + </if> + <if test="resourceOccupy.deduceLastTime != null"> + deduce_last_time = #{resourceOccupy.deduceLastTime}, + </if> + </set> + where id = #{resourceOccupy.id} + </update> + + <select id="haveResource" resultType="java.lang.Boolean"> + select case when used + #{need} <= total then TRUE else FALSE end + from resource + where id = #{id} + </select> + + <select id="getResourceOccupyById" resultType="com.ruoyi.platform.domain.ResourceOccupy"> + select * from resource_occupy where id = #{id} + </select> +</mapper> \ No newline at end of file From 0a432293783c36bb5fd3400ae209ba5f3721e379 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 18 Mar 2025 13:54:24 +0800 Subject: [PATCH 098/127] =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E5=AF=B9=E6=AF=94?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E6=9F=A5=E8=AF=A2bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/managementPlatform/ExperimentInsDaoMapper.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ExperimentInsDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ExperimentInsDaoMapper.xml index e46da649..6ecaafd9 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ExperimentInsDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ExperimentInsDaoMapper.xml @@ -277,6 +277,7 @@ <if test="! isTrain"> and not JSON_CONTAINS(metric_value, 'null', '$.evaluate') </if> + limit #{pageable.offset}, #{pageable.pageSize} </select> <!--新增所有列--> From 944d03423f8f0c75531893f96573a3abd3179b96 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 18 Mar 2025 14:17:35 +0800 Subject: [PATCH 099/127] =?UTF-8?q?refactor:=20=E6=B7=BB=E5=8A=A0eslint-pl?= =?UTF-8?q?ugin-react-hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.eslintignore | 3 +- react-ui/.eslintrc.js | 8 +- react-ui/package.json | 1 + react-ui/src/app.tsx | 4 +- .../components/CodeSelectorModal/index.tsx | 28 +-- react-ui/src/components/IFramePage/index.tsx | 25 ++- .../src/components/ParameterSelect/index.tsx | 27 +-- .../src/components/ResourceSelect/index.tsx | 2 +- .../ResourceSelectorModal/index.tsx | 104 +++++----- react-ui/src/hooks/index.ts | 8 +- react-ui/src/hooks/resource.ts | 37 ++-- react-ui/src/hooks/sessionStorage.ts | 25 --- react-ui/src/pages/AutoML/Create/index.tsx | 100 +++++----- react-ui/src/pages/AutoML/Info/index.tsx | 18 +- .../components/ExperimentHistory/index.tsx | 54 +++--- .../components/ExperimentInstance/index.tsx | 2 +- .../components/ExperimentList/index.tsx | 14 +- .../components/ExperimentResult/index.tsx | 16 +- react-ui/src/pages/CodeConfig/List/index.tsx | 14 +- .../Dataset/components/ResourceInfo/index.tsx | 101 +++++----- .../Dataset/components/ResourceList/index.tsx | 46 ++--- .../Dataset/components/ResourcePage/index.tsx | 45 ++--- .../components/VersionCompareModal/index.tsx | 36 ++-- .../DevelopmentEnvironment/List/index.tsx | 14 +- .../src/pages/Experiment/Comparison/index.tsx | 38 ++-- react-ui/src/pages/Experiment/Info/index.jsx | 4 - .../components/ExperimentDrawer/index.tsx | 3 + .../components/ExperimentInstance/index.tsx | 2 +- .../components/ExperimentResult/index.tsx | 23 ++- .../components/ExportModelModal/index.tsx | 28 +-- .../Experiment/components/LogGroup/index.tsx | 2 +- .../Experiment/components/LogList/index.tsx | 28 +-- react-ui/src/pages/Experiment/index.jsx | 40 ++-- .../src/pages/HyperParameter/Create/index.tsx | 54 +++--- .../src/pages/HyperParameter/Info/index.tsx | 18 +- .../components/ExperimentHistory/index.tsx | 2 +- .../components/ExperimentResult/index.tsx | 16 +- .../components/HyperParameterBasic/index.tsx | 13 +- react-ui/src/pages/Mirror/Create/index.tsx | 2 +- react-ui/src/pages/Mirror/Info/index.tsx | 42 +++-- react-ui/src/pages/Mirror/List/index.tsx | 36 ++-- .../Model/components/MetricsChart/index.tsx | 133 ++++++------- .../Model/components/ModelEvolution/index.tsx | 2 +- .../Model/components/ModelMetrics/index.tsx | 96 +++++----- .../ModelDeployment/CreateService/index.tsx | 34 ++-- .../ModelDeployment/CreateVersion/index.tsx | 27 ++- .../src/pages/ModelDeployment/List/index.tsx | 91 ++++----- .../ModelDeployment/ServiceInfo/index.tsx | 26 +-- .../ModelDeployment/VersionInfo/index.tsx | 18 +- .../components/ServerLog/index.tsx | 37 ++-- .../components/UserGuide/index.tsx | 20 +- .../components/VersionBasicInfo/index.tsx | 11 +- .../components/VersionCompareModal/index.tsx | 26 +-- react-ui/src/pages/Monitor/Job/edit.tsx | 2 +- react-ui/src/pages/Pipeline/Info/index.jsx | 4 +- .../components/GlobalParamsDrawer/index.tsx | 30 +-- .../Pipeline/components/ModelMenu/index.tsx | 76 ++++---- .../components/PipelineNodeDrawer/index.tsx | 98 +++++----- react-ui/src/pages/Pipeline/index.jsx | 15 +- react-ui/src/pages/System/Config/edit.tsx | 2 +- react-ui/src/pages/System/Dept/edit.tsx | 2 +- react-ui/src/pages/System/Dict/edit.tsx | 2 +- react-ui/src/pages/System/DictData/edit.tsx | 2 +- react-ui/src/pages/System/DictData/index.tsx | 2 +- react-ui/src/pages/System/Menu/edit.tsx | 2 +- react-ui/src/pages/System/Notice/edit.tsx | 2 +- react-ui/src/pages/System/Post/edit.tsx | 2 +- .../System/Role/components/DataScope.tsx | 2 +- react-ui/src/pages/System/Role/edit.tsx | 2 +- react-ui/src/pages/System/User/edit.tsx | 2 +- react-ui/src/pages/User/Login/login.tsx | 3 +- .../components/AssetsManagement/index.tsx | 76 ++++---- .../components/ExperimentChart/index.tsx | 178 +++++++++--------- react-ui/src/state/computingResourceStore.ts | 16 -- react-ui/src/utils/table.tsx | 1 + 75 files changed, 1054 insertions(+), 1071 deletions(-) delete mode 100644 react-ui/src/hooks/sessionStorage.ts delete mode 100644 react-ui/src/state/computingResourceStore.ts diff --git a/react-ui/.eslintignore b/react-ui/.eslintignore index 8336e935..3bc705a6 100644 --- a/react-ui/.eslintignore +++ b/react-ui/.eslintignore @@ -5,4 +5,5 @@ public dist .umi -mock \ No newline at end of file +mock +/src/iconfont/ \ No newline at end of file diff --git a/react-ui/.eslintrc.js b/react-ui/.eslintrc.js index 564a28d2..85537dd8 100644 --- a/react-ui/.eslintrc.js +++ b/react-ui/.eslintrc.js @@ -1,10 +1,16 @@ module.exports = { - extends: [require.resolve('@umijs/lint/dist/config/eslint')], + extends: [ + require.resolve('@umijs/lint/dist/config/eslint'), + 'plugin:react/recommended', + "plugin:react-hooks/recommended" + ], globals: { page: true, REACT_APP_ENV: true, }, rules: { '@typescript-eslint/no-use-before-define': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off' }, }; diff --git a/react-ui/package.json b/react-ui/package.json index 1a8d7ebc..fbc014d1 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -114,6 +114,7 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-react-hooks": "~5.2.0", "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 26dfa334..7c026d3d 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -168,7 +168,7 @@ export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { } }; -export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { +export const patchRoutes: RuntimeConfig['patchRoutes'] = () => { //console.log('patchRoutes', e); }; @@ -232,7 +232,7 @@ export const antd: RuntimeAntdConfig = (memo) => { memo.theme.components.Table = { headerBg: 'rgba(242, 244, 247, 0.36)', headerBorderRadius: 4, - rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + // rowSelectedBg: 'rgba(22, 100, 255, 0.05)', 固定列时,横向滑动导致重叠 }; memo.theme.components.Tabs = { titleFontSize: 16, diff --git a/react-ui/src/components/CodeSelectorModal/index.tsx b/react-ui/src/components/CodeSelectorModal/index.tsx index c983093e..430971e6 100644 --- a/react-ui/src/components/CodeSelectorModal/index.tsx +++ b/react-ui/src/components/CodeSelectorModal/index.tsx @@ -33,23 +33,23 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { const [inputText, setInputText] = useState<string | undefined>(undefined); useEffect(() => { + // 获取数据请求 + const getDataList = async () => { + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + code_repo_name: searchText || undefined, + }; + const [res] = await to(getCodeConfigListReq(params)); + if (res && res.data && res.data.content) { + setDataList(res.data.content); + setTotal(res.data.totalElements); + } + }; + getDataList(); }, [pagination, searchText]); - // 获取数据请求 - const getDataList = async () => { - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, - code_repo_name: searchText || undefined, - }; - const [res] = await to(getCodeConfigListReq(params)); - if (res && res.data && res.data.content) { - setDataList(res.data.content); - setTotal(res.data.totalElements); - } - }; - // 搜索 const handleSearch = (value: string) => { setSearchText(value); diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index ef96c914..aa292d47 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -45,23 +45,20 @@ type IframePageProps = { function IframePage({ type, className, style }: IframePageProps) { const [iframeUrl, setIframeUrl] = useState(''); const [loading, setLoading] = useState(false); + useEffect(() => { - requestIframeUrl(); - return () => { - if (type === IframePageType.DevEnv) { - SessionStorage.removeItem(SessionStorage.editorUrlKey); + const requestIframeUrl = async () => { + setLoading(true); + const [res] = await to(getRequestAPI(type)()); + if (res && res.data) { + setIframeUrl(res.data); + } else { + setLoading(false); } }; - }, []); - const requestIframeUrl = async () => { - setLoading(true); - const [res] = await to(getRequestAPI(type)()); - if (res && res.data) { - setIframeUrl(res.data); - } else { - setLoading(false); - } - }; + + requestIframeUrl(); + }, [type]); const hideLoading = () => { setLoading(false); diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 182db352..f1902e5c 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -39,8 +39,20 @@ function ParameterSelect({ const valueText = typeof value === 'object' && value !== null ? value.value : value; useEffect(() => { + // 获取下拉数据 + const getSelectOptions = async () => { + if (!propsConfig) { + return; + } + const getOptions = propsConfig.getOptions; + const [res] = await to(getOptions()); + if (res) { + setOptions(res); + } + }; + getSelectOptions(); - }, []); + }, [propsConfig]); const handleChange = (text: string) => { if (typeof value === 'object' && value !== null) { @@ -53,18 +65,7 @@ function ParameterSelect({ } }; - // 获取下拉数据 - const getSelectOptions = async () => { - if (!propsConfig) { - return; - } - const getOptions = propsConfig.getOptions; - const [res] = await to(getOptions()); - if (res) { - setOptions(res); - } - }; - + // 只用于展示,FormInfo 组件带有 Tooltip if (display) { return ( <FormInfo diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index c4bef4bf..29f01009 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -72,7 +72,7 @@ function ResourceSelect({ ]) as ResourceSelectorResponse; setSelectedResource(originResource); } - }, [value]); + }, [value, type]); const selectResource = () => { const resource = selectedResource; diff --git a/react-ui/src/components/ResourceSelectorModal/index.tsx b/react-ui/src/components/ResourceSelectorModal/index.tsx index 24e97a4d..bbc78db3 100644 --- a/react-ui/src/components/ResourceSelectorModal/index.tsx +++ b/react-ui/src/components/ResourceSelectorModal/index.tsx @@ -91,16 +91,7 @@ function ResourceSelectorModal({ const treeRef = useRef<TreeRef>(null); const config = selectorTypeConfig[type]; - useEffect(() => { - setExpandedKeys([]); - setCheckedKeys([]); - setLoadedKeys([]); - setFiles([]); - setVersionPath(''); - setSearchText(''); - getTreeData(); - }, [activeTab, type]); - + // 搜索 const treeData = useMemo( () => originTreeData.filter((v) => @@ -109,19 +100,45 @@ function ResourceSelectorModal({ [originTreeData, searchText], ); - // 获取数据集\模型\镜像列表 - const getTreeData = async () => { - const isPublic = activeTab === CommonTabKeys.Private ? false : true; - const [res] = await to(config.getList(isPublic)); - if (res) { - setOriginTreeData(res); + useEffect(() => { + // 获取数据集\模型\镜像列表 + const getTreeData = async () => { + const isPublic = activeTab === CommonTabKeys.Private ? false : true; + const [res] = await to(config.getList(isPublic)); + if (res) { + setOriginTreeData(res); - // 恢复上一次的 Expand 操作 - restoreLastExpand(); - } else { - setOriginTreeData([]); - } - }; + // 恢复上一次的 Expand 操作 + setFirstLoadList(true); + } else { + setOriginTreeData([]); + } + }; + + setExpandedKeys([]); + setCheckedKeys([]); + setLoadedKeys([]); + setFiles([]); + setVersionPath(''); + setSearchText(''); + getTreeData(); + }, [activeTab, config]); + + useEffect(() => { + // 恢复上一次的 Expand 操作 + // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys + // fisrtLoadList 标志位 + const restoreLastExpand = () => { + if (firstLoadList && Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length > 0) { + setExpandedKeys(defaultExpandedKeys); + // 延时滑动到 defaultExpandedKeys,不然不会加载 defaultExpandedKeys,不然不会加载版本 + setTimeout(() => { + treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); + }, 100); + } + }; + restoreLastExpand(); + }, [firstLoadList, defaultExpandedKeys]); // 获取数据集\模型\镜像版本列表 const getVersions = async (parentId: string, parentNode: any) => { @@ -136,10 +153,10 @@ function ResourceSelectorModal({ setLoadedKeys((prev) => prev.concat(parentId)); } - // 恢复上一次的 Check 操作 + // 恢复上一次的 Check 操作,需要延时以便 TreeData 更新完 setTimeout(() => { restoreLastCheck(parentId, res); - }, 300); + }, 100); } else { setExpandedKeys([]); return Promise.reject(error); @@ -158,7 +175,7 @@ function ResourceSelectorModal({ } }; - // 动态加载 tree children + // 展开时,动态加载 tree children const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { if (children) { return Promise.resolve(); @@ -187,42 +204,25 @@ function ResourceSelectorModal({ } }; - // 恢复上一次的 Expand 操作 - // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys - // fisrtLoadList 标志位 - const restoreLastExpand = () => { - if (!firstLoadList && defaultExpandedKeys.length > 0) { - setTimeout(() => { - setExpandedKeys(defaultExpandedKeys); - setFirstLoadList(true); - setTimeout(() => { - treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); - }, 100); - }, 0); - } - }; - // 恢复上一次的 Check 操作 // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 // fisrtLoadVersions 标志位 const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { - if (!firstLoadVersions && defaultCheckedKeys.length > 0) { + if (!firstLoadVersions && Array.isArray(defaultCheckedKeys) && defaultCheckedKeys.length > 0) { const last = defaultCheckedKeys[0] as string; const { id } = getIdAndVersion(last); // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 if (id === parentId) { + setCheckedKeys(defaultCheckedKeys); + const parentNode = versions.find((v) => v.key === last); + getFiles(last, parentNode); + setFirstLoadVersions(true); setTimeout(() => { - setCheckedKeys(defaultCheckedKeys); - const parentNode = versions.find((v) => v.key === last); - getFiles(last, parentNode); - setFirstLoadVersions(true); - setTimeout(() => { - treeRef?.current?.scrollTo({ - key: defaultCheckedKeys[0], - align: 'bottom', - }); - }, 100); - }, 0); + treeRef?.current?.scrollTo({ + key: defaultCheckedKeys[0], + align: 'bottom', + }); + }, 100); } } }; diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index f5ef64af..59ba1d6e 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -105,6 +105,7 @@ export function useDomSize<T extends HTMLElement>( return () => { window.removeEventListener('resize', debounceFunc); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [domRef, ...deps]); return [domRef, { width, height }] as const; @@ -136,10 +137,10 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => { * Executes the effect function when the specified condition is true. * * @param effect - The effect function to execute. - * @param deps - The dependencies for the effect. * @param when - The condition to trigger the effect. + * @param deps - The dependencies for the effect. */ -export const useEffectWhen = (effect: () => void, deps: React.DependencyList, when: boolean) => { +export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => { const requestFns = useRef<(() => void)[]>([]); useEffect(() => { if (when) { @@ -147,6 +148,7 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh } else { requestFns.current.splice(0, 1, effect); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); useEffect(() => { @@ -185,7 +187,7 @@ export const useCheck = <T>(list: T[]) => { } }); }, - [selected, isSingleChecked], + [isSingleChecked], ); return [ diff --git a/react-ui/src/hooks/resource.ts b/react-ui/src/hooks/resource.ts index 82a3ee78..cb4434ab 100644 --- a/react-ui/src/hooks/resource.ts +++ b/react-ui/src/hooks/resource.ts @@ -5,40 +5,39 @@ */ import { getComputingResourceReq } from '@/services/pipeline'; -import computingResourceState, { setComputingResource } from '@/state/computingResourceStore'; import { ComputingResource } from '@/types'; import { to } from '@/utils/promise'; import { type SelectProps } from 'antd'; import { useCallback, useEffect, useState } from 'react'; -import { useSnapshot } from 'umi'; + +const computingResource: ComputingResource[] = []; // 获取资源规格 export function useComputingResource() { const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); - const snap = useSnapshot(computingResourceState); useEffect(() => { - if (snap.computingResource.length > 0) { - setResourceStandardList(snap.computingResource as ComputingResource[]); + // 获取资源规格列表数据 + const getComputingResource = async () => { + const params = { + page: 0, + size: 1000, + resource_type: '', + }; + const [res] = await to(getComputingResourceReq(params)); + if (res && res.data && res.data.content) { + setResourceStandardList(res.data.content); + computingResource.splice(0, computingResource.length, ...res.data.content); + } + }; + + if (computingResource.length > 0) { + setResourceStandardList(computingResource); } else { getComputingResource(); } }, []); - // 获取资源规格列表数据 - const getComputingResource = useCallback(async () => { - const params = { - page: 0, - size: 1000, - resource_type: '', - }; - const [res] = await to(getComputingResourceReq(params)); - if (res && res.data && res.data.content) { - setResourceStandardList(res.data.content); - setComputingResource(res.data.content); - } - }, []); - // 过滤资源规格 const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = useCallback((input: string, option?: ComputingResource) => { diff --git a/react-ui/src/hooks/sessionStorage.ts b/react-ui/src/hooks/sessionStorage.ts deleted file mode 100644 index 8cf3c921..00000000 --- a/react-ui/src/hooks/sessionStorage.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * @Author: 赵伟 - * @Date: 2024-11-06 14:53:37 - * @Description: SessionStorage hook - */ - -import SessionStorage from '@/utils/sessionStorage'; -import { useEffect, useState } from 'react'; - -// 读取缓存数据,组件卸载时清除缓存 -export function useSessionStorage<T>(key: string, isObject: boolean, initialValue: T) { - const [storage, setStorage] = useState<T>(initialValue); - - useEffect(() => { - const res = SessionStorage.getItem(key, isObject); - if (res) { - setStorage(res); - } - return () => { - SessionStorage.removeItem(key); - }; - }, []); - - return [storage]; -} diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index ec016c3c..b5ceae7c 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -29,60 +29,60 @@ function CreateAutoML() { const isCopy = pathname.includes('copy'); useEffect(() => { + // 获取服务详情 + const getAutoMLInfo = async (id: number) => { + const [res] = await to(getAutoMLInfoReq({ id })); + if (res && res.data) { + const autoMLInfo: AutoMLData = res.data; + const { + include_classifier: include_classifier_str, + include_feature_preprocessor: include_feature_preprocessor_str, + include_regressor: include_regressor_str, + exclude_classifier: exclude_classifier_str, + exclude_feature_preprocessor: exclude_feature_preprocessor_str, + exclude_regressor: exclude_regressor_str, + metrics: metrics_str, + ml_name: ml_name_str, + ...rest + } = autoMLInfo; + const include_classifier = include_classifier_str?.split(',').filter(Boolean); + const include_feature_preprocessor = include_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const include_regressor = include_regressor_str?.split(',').filter(Boolean); + const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); + const exclude_feature_preprocessor = exclude_feature_preprocessor_str + ?.split(',') + .filter(Boolean); + const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); + const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; + const metrics = Object.entries(metricsObj).map(([key, value]) => ({ + name: key, + value, + })); + const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; + + const formData = { + ...rest, + include_classifier, + include_feature_preprocessor, + include_regressor, + exclude_classifier, + exclude_feature_preprocessor, + exclude_regressor, + metrics, + ml_name, + }; + + form.setFieldsValue(formData); + } + }; + // 编辑,复制 if (id && !Number.isNaN(id)) { getAutoMLInfo(id); } - }, [id]); - - // 获取服务详情 - const getAutoMLInfo = async (id: number) => { - const [res] = await to(getAutoMLInfoReq({ id })); - if (res && res.data) { - const autoMLInfo: AutoMLData = res.data; - const { - include_classifier: include_classifier_str, - include_feature_preprocessor: include_feature_preprocessor_str, - include_regressor: include_regressor_str, - exclude_classifier: exclude_classifier_str, - exclude_feature_preprocessor: exclude_feature_preprocessor_str, - exclude_regressor: exclude_regressor_str, - metrics: metrics_str, - ml_name: ml_name_str, - ...rest - } = autoMLInfo; - const include_classifier = include_classifier_str?.split(',').filter(Boolean); - const include_feature_preprocessor = include_feature_preprocessor_str - ?.split(',') - .filter(Boolean); - const include_regressor = include_regressor_str?.split(',').filter(Boolean); - const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); - const exclude_feature_preprocessor = exclude_feature_preprocessor_str - ?.split(',') - .filter(Boolean); - const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); - const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; - const metrics = Object.entries(metricsObj).map(([key, value]) => ({ - name: key, - value, - })); - const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; - - const formData = { - ...rest, - include_classifier, - include_feature_preprocessor, - include_regressor, - exclude_classifier, - exclude_feature_preprocessor, - exclude_regressor, - metrics, - ml_name, - }; - - form.setFieldsValue(formData); - } - }; + }, [id, form, isCopy]); // 创建、更新、复制实验 const createExperiment = async (formData: FormData) => { diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx index 0d0ec460..83f7dea0 100644 --- a/react-ui/src/pages/AutoML/Info/index.tsx +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -19,18 +19,18 @@ function AutoMLInfo() { const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); useEffect(() => { + // 获取自动机器学习详情 + const getAutoMLInfo = async () => { + const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); + if (res && res.data) { + setAutoMLInfo(res.data); + } + }; + if (autoMLId) { getAutoMLInfo(); } - }, []); - - // 获取自动机器学习详情 - const getAutoMLInfo = async () => { - const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); - if (res && res.data) { - setAutoMLInfo(res.data); - } - }; + }, [autoMLId]); return ( <div className={styles['auto-ml-info']}> diff --git a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx index fa694b27..09d0cd6e 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx @@ -25,36 +25,36 @@ type TableData = { function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { const [tableData, setTableData] = useState<TableData[]>([]); useEffect(() => { + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: item[1]?.[5]?.accuracy, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + if (fileUrl) { getHistoryFile(); } - }, [fileUrl]); - - // 获取实验运行历史记录 - const getHistoryFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - const data: any[] = res.data; - const list: TableData[] = data.map((item) => { - return { - id: item[0]?.[0], - accuracy: item[1]?.[5]?.accuracy, - duration: item[1]?.[5]?.duration, - train_loss: item[1]?.[5]?.train_loss, - status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], - }; - }); - list.forEach((item) => { - if (!item.id) return; - const config = (res as any).configs?.[item.id]; - item.feature = config?.['feature_preprocessor:__choice__']; - item.althorithm = isClassification - ? config?.['classifier:__choice__'] - : config?.['regressor:__choice__']; - }); - setTableData(list); - } - }; + }, [fileUrl, isClassification]); const columns: TableProps<TableData>['columns'] = [ { diff --git a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx index 9c8ea687..aded5f2a 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx @@ -53,7 +53,7 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInsList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { diff --git a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx index bcc85a2f..b4e7f24b 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentList/index.tsx @@ -28,7 +28,7 @@ import { } from 'antd'; import { type SearchProps } from 'antd/es/input'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import ExperimentInstance from '../ExperimentInstance'; import { ExperimentListType, experimentListConfig } from './config'; import styles from './index.less'; @@ -58,12 +58,8 @@ function ExperimentList({ type }: ExperimentListProps) { ); const config = experimentListConfig[type]; - useEffect(() => { - getAutoMLList(); - }, [pagination, searchText]); - // 获取自主机器学习或超参数自动优化列表 - const getAutoMLList = async () => { + const getAutoMLList = useCallback(async () => { const params: Record<string, any> = { page: pagination.current! - 1, size: pagination.pageSize, @@ -76,7 +72,11 @@ function ExperimentList({ type }: ExperimentListProps) { setTableData(content); setTotal(totalElements); } - }; + }, [pagination, searchText, config]); + + useEffect(() => { + getAutoMLList(); + }, [getAutoMLList]); // 搜索 const onSearch: SearchProps['onSearch'] = (value) => { diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx index a826155d..8680bf60 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.tsx @@ -22,19 +22,19 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp }, [imageUrl]); useEffect(() => { + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + if (fileUrl) { getResultFile(); } }, [fileUrl]); - // 获取实验运行历史记录 - const getResultFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - setResult(res as any as string); - } - }; - return ( <div className={styles['experiment-result']}> <InfoGroup title="实验结果" height={420} width="100%"> diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 0c484e54..3f567465 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -13,7 +13,7 @@ import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { modalConfirm } from '@/utils/ui'; import { App, Button, Input, Pagination, PaginationProps } from 'antd'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; import CodeConfigItem from '../components/CodeConfigItem'; import styles from './index.less'; @@ -50,12 +50,8 @@ function CodeConfigList() { const [inputText, setInputText] = useState<string | undefined>(undefined); const { message } = App.useApp(); - useEffect(() => { - getDataList(); - }, [pagination, searchText]); - // 获取数据请求 - const getDataList = async () => { + const getDataList = useCallback(async () => { const params = { page: pagination.current! - 1, size: pagination.pageSize, @@ -69,7 +65,11 @@ function CodeConfigList() { setDataList([]); setTotal(0); } - }; + }, [pagination, searchText]); + + useEffect(() => { + getDataList(); + }, [getDataList]); // 删除请求 const deleteRecord = async (id: number) => { diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx index c0bfdb33..c74a3cc8 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -18,7 +18,7 @@ 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 { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import AddVersionModal from '../AddVersionModal'; import ResourceIntro from '../ResourceIntro'; import ResourceVersion from '../ResourceVersion'; @@ -45,7 +45,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { // 模型演化传入的 tab const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction; // 模型演化传入的版本 - let versionParam = searchParams.get('version'); + const versionParam = searchParams.get('version'); const name = searchParams.get('name') || ''; const owner = searchParams.get('owner') || ''; const identifier = searchParams.get('identifier') || ''; @@ -57,63 +57,60 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const typeName = config.name; // 数据集/模型 const { message } = App.useApp(); - useEffect(() => { - getVersionList(); - }, [resourceId, owner, identifier]); - - useEffect(() => { - if (version) { - getResourceDetail({ - id: resourceId, - owner, - name, - identifier, - version, - is_public: is_public, - }); - } - }, [version]); - // 获取详情 - const getResourceDetail = async (params: { - owner: string; - name: string; - id: number; - identifier: string; - version?: string; - is_public: boolean; - }) => { + 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 getVersionList = async () => { - const request = config.getVersions; - const [res] = await to( - request({ - owner, - identifier, - }), - ); - if (res && res.data && res.data.length > 0) { - setVersionList(res.data); - if ( - versionParam && - res.data.find((item: ResourceVersionData) => item.name === versionParam) - ) { - setVersion(versionParam); - versionParam = null; + const getVersionList = useCallback( + async (refresh: boolean) => { + const request = config.getVersions; + const [res] = await to( + request({ + owner, + identifier, + }), + ); + if (res && res.data && res.data.length > 0) { + setVersionList(res.data); + if ( + !refresh && + versionParam && + res.data.find((item: ResourceVersionData) => item.name === versionParam) + ) { + setVersion(versionParam); + } else { + setVersion(res.data[0].name); + } } else { - setVersion(res.data[0].name); + setVersion(undefined); } - } else { - setVersion(undefined); + }, + [config, owner, identifier, versionParam], + ); + + useEffect(() => { + if (version) { + getResourceDetail(); } - }; + }, [version, getResourceDetail]); + + useEffect(() => { + getVersionList(false); + }, [getVersionList]); // 新建版本 const showModal = () => { @@ -125,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { identifier: info.identifier, is_public: is_public, onOk: () => { - getVersionList(); + getVersionList(true); close(); }, }); @@ -172,12 +169,12 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { const [res] = await to(request(params)); if (res) { message.success('删除成功'); - getVersionList(); + getVersionList(true); } }; // 处理删除 - const hanldeDelete = () => { + const handleDelete = () => { modalConfirm({ title: '删除后,该版本将不可恢复', content: '是否确认删除?', @@ -268,7 +265,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { <Button type="default" style={{ marginLeft: 'auto', marginRight: 0 }} - onClick={hanldeDelete} + onClick={handleDelete} icon={<KFIcon type="icon-shanchu" />} disabled={!version} danger diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 9577ad41..6549b9c1 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -8,7 +8,7 @@ import { modalConfirm } from '@/utils/ui'; import { useNavigate } from '@umijs/max'; import { App, Button, Input, Pagination, PaginationProps } from 'antd'; import { pick } from 'lodash'; -import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Ref, forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; import AddDatasetModal from '../AddDatasetModal'; import ResourceItem from '../ResourceItem'; @@ -58,9 +58,30 @@ function ResourceList( const { message } = App.useApp(); const config = resourceConfig[resourceType]; + // 获取数据请求 + const getDataList = useCallback(async () => { + const params: Record<string, any> = { + page: pagination.current! - 1, + size: pagination.pageSize, + is_public: isPublic, + [config.typeParamKey]: dataType, + [config.tagParamKey]: dataTag, + name: searchText || undefined, + }; + const request = config.getList; + const [res] = await to(request(params)); + if (res && res.data && res.data.content) { + setDataList(res.data.content); + setTotal(res.data.totalElements); + } else { + setDataList([]); + setTotal(0); + } + }, [dataType, dataTag, pagination, searchText, isPublic, config]); + useEffect(() => { getDataList(); - }, [resourceType, dataType, dataTag, pagination, searchText, isPublic]); + }, [getDataList]); useImperativeHandle( ref, @@ -81,27 +102,6 @@ function ResourceList( [], ); - // 获取数据请求 - const getDataList = async () => { - const params: Record<string, any> = { - page: pagination.current! - 1, - size: pagination.pageSize, - is_public: isPublic, - [config.typeParamKey]: dataType, - [config.tagParamKey]: dataTag, - name: searchText || undefined, - }; - const request = config.getList; - const [res] = await to(request(params)); - if (res && res.data && res.data.content) { - setDataList(res.data.content); - setTotal(res.data.totalElements); - } else { - setDataList([]); - setTotal(0); - } - }; - // 删除请求 const deleteRecord = async (params: { owner: string; identifier: string; repo_id?: number }) => { const request = config.deleteRecord; diff --git a/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx b/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx index 1dd72472..f8a22729 100644 --- a/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourcePage/index.tsx @@ -3,7 +3,7 @@ import { useCacheState } from '@/hooks/pageCacheState'; import { getAssetIcon } from '@/services/dataset/index.js'; import { to } from '@/utils/promise'; import { Flex, Tabs, type TabsProps } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { CategoryData, ResourceType, resourceConfig } from '../../config'; import CategoryList from '../CategoryList'; import ResourceList, { ResourceListRef } from '../ResourceList'; @@ -23,9 +23,31 @@ function ResourcePage({ resourceType }: ResourcePageProps) { const dataListRef = useRef<ResourceListRef>(null); const config = resourceConfig[resourceType]; + // 获取分类 + const getAssetIconList = useCallback( + async (name: string = '') => { + const params = { + name: name, + page: 0, + size: 10000, + }; + const [res] = await to(getAssetIcon(params)); + if (res && res.data && res.data.content) { + const { content } = res.data; + setTypeList( + content.filter((item: CategoryData) => Number(item.category_id) === config.typeValue), + ); + setTagList( + content.filter((item: CategoryData) => Number(item.category_id) === config.tagValue), + ); + } + }, + [config], + ); + useEffect(() => { getAssetIconList(); - }, []); + }, [getAssetIconList]); // 分类搜索 const handleCategorySearch = (value: string) => { @@ -42,25 +64,6 @@ function ResourcePage({ resourceType }: ResourcePageProps) { setActiveTag((prev) => (prev === record.name ? undefined : record.name)); }; - // 获取分类 - const getAssetIconList = async (name: string = '') => { - const params = { - name: name, - page: 0, - size: 10000, - }; - const [res] = await to(getAssetIcon(params)); - if (res && res.data && res.data.content) { - const { content } = res.data; - setTypeList( - content.filter((item: CategoryData) => Number(item.category_id) === config.typeValue), - ); - setTagList( - content.filter((item: CategoryData) => Number(item.category_id) === config.tagValue), - ); - } - }; - // 切换 Tab,重置数据 const hanleTabChange: TabsProps['onChange'] = (value) => { dataListRef.current?.reset(); diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx index b3b6b2f1..10660221 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx @@ -127,28 +127,28 @@ function VersionCompareModal({ text: '版本描述', }, ], - [], + [resourceType], ); useEffect(() => { - getServiceVersionCompare(); - }, []); - - // 获取对比数据 - const getServiceVersionCompare = async () => { - const params = { - versions, - identifier, - is_public, - owner, - repo_id, + // 获取对比数据 + const getServiceVersionCompare = async () => { + const params = { + versions, + identifier, + is_public, + owner, + repo_id, + }; + const request = config.compareVersion; + const [res] = await to(request(params)); + if (res && res.data) { + setCompareData(res.data); + } }; - const request = config.compareVersion; - const [res] = await to(request(params)); - if (res && res.data) { - setCompareData(res.data); - } - }; + + getServiceVersionCompare(); + }, [versions, identifier, is_public, owner, repo_id, config]); // 获取值 function getValue<T extends DatasetData | ModelData>( diff --git a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx index 71a7766f..4af043ba 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/List/index.tsx @@ -29,7 +29,7 @@ import { type TableProps, } from 'antd'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import CreateMirrorModal from '../components/CreateMirrorModal'; import EditorStatusCell from '../components/EditorStatusCell'; import styles from './index.less'; @@ -57,12 +57,8 @@ function EditorList() { }, ); - useEffect(() => { - getEditorList(); - }, [pagination]); - // 获取编辑器列表 - const getEditorList = async () => { + const getEditorList = useCallback(async () => { const params: Record<string, any> = { page: pagination.current! - 1, size: pagination.pageSize, @@ -73,7 +69,11 @@ function EditorList() { setTableData(content); setTotal(totalElements); } - }; + }, [pagination]); + + useEffect(() => { + getEditorList(); + }, [getEditorList]); // 删除编辑器 const deleteEditor = async (id: number) => { diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index 3ed44cd1..4b2d6d5b 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -46,27 +46,27 @@ function ExperimentComparison() { }); const { message } = App.useApp(); - const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); + const config = comparisonConfig[comparisonType]; useEffect(() => { - getComparisonData(); - }, [experimentId]); - - // 获取对比数据列表 - const getComparisonData = async () => { - const request = - comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, + // 获取对比数据列表 + const getComparisonData = async () => { + const request = + comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + }; + const [res] = await to(request(experimentId, params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } }; - const [res] = await to(request(experimentId, params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; + + getComparisonData(); + }, [experimentId, pagination, comparisonType]); // 获取对比 url const getExpMetrics = async () => { @@ -187,7 +187,7 @@ function ExperimentComparison() { })), }, ]; - }, [tableData]); + }, [tableData, config]); return ( <div className={styles['experiment-comparison']}> diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index c96d781e..e05480df 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -56,10 +56,6 @@ function ExperimentText() { }; }, []); - useEffect(() => { - propsDrawerOpenRef.current = propsDrawerOpen; - }, [propsDrawerOpen]); - // 获取流水线模版 const getWorkflow = async () => { const [res] = await to(getWorkflowById(locationParams.workflowId)); diff --git a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx index 58077267..c1a70141 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx @@ -90,6 +90,9 @@ const ExperimentDrawer = ({ instanceNodeStatus, workflowId, instanceNodeStartTime, + experimentName, + experimentId, + pipelineId, ], ); diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index 2c80df24..d184deee 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -57,7 +57,7 @@ function ExperimentInstanceComponent({ if (allIntanceIds.length === 0) { setSelectedIns([]); } - }, [experimentInsList]); + }, [allIntanceIds, setSelectedIns]); // 删除实验实例确认 const handleRemove = (instance: ExperimentInstance) => { diff --git a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx index 37e6a9df..1d7eedd9 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentResult/index.tsx @@ -43,19 +43,18 @@ function ExperimentResult({ : undefined; useEffect(() => { + // 获取实验结果 + const getExperimentResult = async (params: any) => { + const [res] = await to(getNodeResult(params)); + if (res && res.data && Array.isArray(res.data)) { + const data = res.data.filter((item: ExperimentResultData) => item.value.length > 0); + setExperimentResults(data); + } else { + setExperimentResults([]); + } + }; getExperimentResult({ id: `${experimentInsId}`, node_id: pipelineNodeId }); - }, []); - - // 获取实验结果 - const getExperimentResult = async (params: any) => { - const [res] = await to(getNodeResult(params)); - if (res && res.data && Array.isArray(res.data)) { - const data = res.data.filter((item: ExperimentResultData) => item.value.length > 0); - setExperimentResults(data); - } else { - setExperimentResults([]); - } - }; + }, [experimentInsId, pipelineNodeId]); // 下载 const download = (path: string) => { diff --git a/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx index 1f2e18c0..e4fde742 100644 --- a/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExportModelModal/index.tsx @@ -53,8 +53,21 @@ function ExportModelModal({ }; useEffect(() => { + // 获取数据集、模型列表 + const requestResourceList = async () => { + const params = { + page: 0, + size: 1000, + is_public: false, // 个人 + }; + const [res] = await to(config.getList(params)); + if (res && res.data) { + setResources(res.data.content || []); + } + }; + requestResourceList(); - }, []); + }, [config]); // 获取选中的数据集、模型 const getSelectedResource = (id: number | undefined) => { @@ -84,19 +97,6 @@ function ExportModelModal({ } }; - // 获取数据集、模型列表 - const requestResourceList = async () => { - const params = { - page: 0, - size: 1000, - is_public: false, // 个人 - }; - const [res] = await to(config.getList(params)); - if (res && res.data) { - setResources(res.data.content || []); - } - }; - // 获取数据集、模型版本列表 const getRecourceVersions = async (id: number) => { const resource = getSelectedResource(id); diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 11d51aca..dfedac5c 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -68,7 +68,7 @@ function LogGroup({ document.removeEventListener('mouseup', mouseUp); closeSocket(); }; - }, []); + }, [setIsMouseDown]); // 请求日志 const requestExperimentPodsLog = async () => { diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 15ce0def..a06311d4 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -3,7 +3,7 @@ import { getQueryByExperimentLog } from '@/services/experiment/index.js'; import { to } from '@/utils/promise'; import classNames from 'classnames'; import dayjs from 'dayjs'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import LogGroup from '../LogGroup'; import styles from './index.less'; @@ -46,19 +46,8 @@ function LogList({ const [logGroups, setLogGroups] = useState<ExperimentLog[]>([]); const retryRef = useRef(3); // 等待 2 秒,重试 3 次 - // 当实例节点运行状态不是 Pending,获取实验日志组 - useEffect(() => { - if ( - instanceNodeStatus && - instanceNodeStatus !== ExperimentStatus.Pending && - logGroups.length === 0 - ) { - getExperimentLog(); - } - }, [instanceNodeStatus]); - // 获取实验 Pods 组 - const getExperimentLog = async () => { + const getExperimentLog = useCallback(async () => { const start_time = dayjs(instanceNodeStartTime).valueOf() * 1.0e6; const params = { task_id: pipelineNodeId, @@ -95,7 +84,18 @@ function LogList({ }, 2 * 1000); } } - }; + }, [pipelineNodeId, workflowId, instanceName, instanceNamespace, instanceNodeStartTime]); + + // 当实例节点运行状态不是 Pending,获取实验日志组 + useEffect(() => { + if ( + instanceNodeStatus && + instanceNodeStatus !== ExperimentStatus.Pending && + logGroups.length === 0 + ) { + getExperimentLog(); + } + }, [getExperimentLog, logGroups, instanceNodeStatus]); return ( <div className={classNames(styles['log-list'], className)} id="log-list" style={style}> diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 51c905bb..9ae30a1a 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -20,7 +20,7 @@ import tableCellRender, { TableCellValueType } from '@/utils/table'; import { modalConfirm } from '@/utils/ui'; import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ComparisonType } from './Comparison/config'; import AddExperimentModal from './components/AddExperimentModal'; @@ -35,12 +35,6 @@ function Experiment() { const navigate = useNavigate(); const [experimentList, setExperimentList] = useState([]); const [workflowList, setWorkflowList] = useState([]); - const [queryFlow, setQueryFlow] = useState({ - offset: 1, - page: 0, - size: 10000, - name: null, - }); const [experimentId, setExperimentId] = useState(null); const [experimentInList, setExperimentInList] = useState([]); const [expandedRowKeys, setExpandedRowKeys] = useState(null); @@ -61,18 +55,28 @@ function Experiment() { const { message } = App.useApp(); useEffect(() => { + // 获取流水线列表 + const getWorkflowList = async () => { + const queryFlow = { + offset: 1, + page: 0, + size: 10000, + name: null, + }; + const [res] = await to(getWorkflow(queryFlow)); + if (res && res.data && res.data.content) { + setWorkflowList(res.data.content); + } + }; + getWorkflowList(); return () => { clearExperimentInTimers(); }; }, []); - useEffect(() => { - getExperimentList(); - }, [pagination, searchText]); - // 获取实验列表 - const getExperimentList = async () => { + const getExperimentList = useCallback(async () => { const params = { page: pagination.current - 1, size: pagination.pageSize, @@ -88,15 +92,11 @@ function Experiment() { setTotal(res.data.totalElements); } - }; + }, [pagination, searchText]); - // 获取流水线列表 - const getWorkflowList = async () => { - const [res] = await to(getWorkflow(queryFlow)); - if (res && res.data && res.data.content) { - setWorkflowList(res.data.content); - } - }; + useEffect(() => { + getExperimentList(); + }, [getExperimentList]); // 搜索 const onSearch = (value) => { diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index 4cddd4a7..3276a94e 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -26,37 +26,37 @@ function CreateHyperParameter() { const isCopy = pathname.includes('copy'); useEffect(() => { + // 获取服务详情 + const getHyperParameterInfo = async (id: number) => { + const [res] = await to(getRayInfoReq({ id })); + if (res && res.data) { + const info: HyperParameterData = res.data; + const { name: name_str, parameters, points_to_evaluate, ...rest } = info; + const name = isCopy ? `${name_str}-copy` : name_str; + if (parameters && Array.isArray(parameters)) { + parameters.forEach((item) => { + const paramName = getReqParamName(item.type); + item.range = item[paramName]; + item[paramName] = undefined; + }); + } + + const formData = { + ...rest, + name, + parameters, + points_to_evaluate: points_to_evaluate ?? [], + }; + + form.setFieldsValue(formData); + } + }; + // 编辑,复制 if (id && !Number.isNaN(id)) { getHyperParameterInfo(id); } - }, [id]); - - // 获取服务详情 - const getHyperParameterInfo = async (id: number) => { - const [res] = await to(getRayInfoReq({ id })); - if (res && res.data) { - const info: HyperParameterData = res.data; - const { name: name_str, parameters, points_to_evaluate, ...rest } = info; - const name = isCopy ? `${name_str}-copy` : name_str; - if (parameters && Array.isArray(parameters)) { - parameters.forEach((item) => { - const paramName = getReqParamName(item.type); - item.range = item[paramName]; - item[paramName] = undefined; - }); - } - - const formData = { - ...rest, - name, - parameters, - points_to_evaluate: points_to_evaluate ?? [], - }; - - form.setFieldsValue(formData); - } - }; + }, [id, form, isCopy]); // 创建、更新、复制实验 const createExperiment = async (formData: FormData) => { diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx index 2a175d8d..cf5d9bc1 100644 --- a/react-ui/src/pages/HyperParameter/Info/index.tsx +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -8,7 +8,7 @@ import { getRayInfoReq } from '@/services/hyperParameter'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import { useParams } from '@umijs/max'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import HyperParameterBasic from '../components/HyperParameterBasic'; import { HyperParameterData } from '../types'; import styles from './index.less'; @@ -20,19 +20,19 @@ function HyperparameterInfo() { undefined, ); - useEffect(() => { - if (hyperparameterId) { - getHyperparameterInfo(); - } - }, []); - // 获取自动机器学习详情 - const getHyperparameterInfo = async () => { + const getHyperparameterInfo = useCallback(async () => { const [res] = await to(getRayInfoReq({ id: hyperparameterId })); if (res && res.data) { setHyperparameterInfo(res.data); } - }; + }, [hyperparameterId]); + + useEffect(() => { + if (hyperparameterId) { + getHyperparameterInfo(); + } + }, [hyperparameterId, getHyperparameterInfo]); return ( <div className={styles['hyper-parameter-info']}> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index d9091d26..0e5fd66f 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -32,7 +32,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { setTableData(trialList); setLoading(false); }, 500); - }, []); + }, [trialList]); // 计算 column const first: HyperParameterTrial | undefined = trialList ? trialList[0] : undefined; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index dfb60b04..4a7d3dcc 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -12,19 +12,19 @@ function ExperimentResult({ fileUrl }: ExperimentResultProps) { const [result, setResult] = useState<string | undefined>(''); useEffect(() => { + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + if (fileUrl) { getResultFile(); } }, [fileUrl]); - // 获取实验运行历史记录 - const getResultFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - setResult(res as any as string); - } - }; - return ( <div className={styles['experiment-result']}> <InfoGroup title="最佳实验结果" width="100%"> diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index df2ca0ad..2f419f66 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -43,15 +43,6 @@ function HyperParameterBasic({ }: HyperParameterBasicProps) { const getResourceDescription = useComputingResource()[2]; - // 格式化资源规格 - const formatResource = (resource?: string) => { - if (!resource) { - return undefined; - } - - return getResourceDescription(resource); - }; - const basicDatas: BasicInfoData[] = useMemo(() => { if (!info) { return []; @@ -146,10 +137,10 @@ function HyperParameterBasic({ { label: '资源规格', value: info.resource, - format: formatResource, + format: getResourceDescription, }, ]; - }, [info]); + }, [info, getResourceDescription]); const instanceDatas = useMemo(() => { if (!runStatus) { diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx index 681103bf..7db2f4d0 100644 --- a/react-ui/src/pages/Mirror/Create/index.tsx +++ b/react-ui/src/pages/Mirror/Create/index.tsx @@ -65,7 +65,7 @@ function MirrorCreate() { return () => { SessionStorage.removeItem(SessionStorage.mirrorNameKey); }; - }, []); + }, [form]); // 创建公网、本地镜像 const createPublicMirror = async (formData: FormData) => { diff --git a/react-ui/src/pages/Mirror/Info/index.tsx b/react-ui/src/pages/Mirror/Info/index.tsx index 96425f81..8215db0c 100644 --- a/react-ui/src/pages/Mirror/Info/index.tsx +++ b/react-ui/src/pages/Mirror/Info/index.tsx @@ -16,6 +16,7 @@ import { } from '@/services/mirror'; import themes from '@/styles/theme.less'; import { formatDate } from '@/utils/date'; +import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import SessionStorage from '@/utils/sessionStorage'; import tableCellRender, { TableCellValueType } from '@/utils/table'; @@ -32,7 +33,7 @@ import { type TablePaginationConfig, type TableProps, } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import MirrorStatusCell from '../components/MirrorStatusCell'; import styles from './index.less'; @@ -70,31 +71,28 @@ function MirrorInfo() { ); const { message } = App.useApp(); const isPublic = useMemo(() => mirrorInfo.image_type === 1, [mirrorInfo]); - - useEffect(() => { - getMirrorInfo(); - }, []); - - useEffect(() => { - getMirrorVersionList(); - }, [pagination]); + const mirrorId = safeInvoke(Number)(urlParams.id); // 获取镜像详情 - const getMirrorInfo = async () => { - const id = Number(urlParams.id); - const [res] = await to(getMirrorInfoReq(id)); + const getMirrorInfo = useCallback(async () => { + if (!mirrorId) { + return; + } + const [res] = await to(getMirrorInfoReq(mirrorId)); if (res && res.data) { setMirrorInfo(res.data); } - }; + }, [mirrorId]); // 获取镜像版本列表 - const getMirrorVersionList = async () => { - const id = Number(urlParams.id); + const getMirrorVersionList = useCallback(async () => { + if (!mirrorId) { + return; + } const params = { page: pagination.current! - 1, size: pagination.pageSize, - image_id: id, + image_id: mirrorId, }; const [res] = await to(getMirrorVersionListReq(params)); if (res && res.data) { @@ -102,7 +100,17 @@ function MirrorInfo() { setTableData(content); setTotal(totalElements); } - }; + }, [mirrorId, pagination]); + + // 获取镜像详情 + useEffect(() => { + getMirrorInfo(); + }, [getMirrorInfo]); + + // 获取镜像版本列表 + useEffect(() => { + getMirrorVersionList(); + }, [getMirrorVersionList]); // 删除镜像版本 const deleteMirrorVersion = async (id: number) => { diff --git a/react-ui/src/pages/Mirror/List/index.tsx b/react-ui/src/pages/Mirror/List/index.tsx index cbb8d014..feae1971 100644 --- a/react-ui/src/pages/Mirror/List/index.tsx +++ b/react-ui/src/pages/Mirror/List/index.tsx @@ -26,7 +26,7 @@ import { } from 'antd'; import { type SearchProps } from 'antd/es/input'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import styles from './index.less'; const mirrorTabItems = [ @@ -65,9 +65,25 @@ function MirrorList() { ); const { message } = App.useApp(); + // 获取镜像列表 + const getMirrorList = useCallback(async () => { + const params: Record<string, any> = { + page: pagination.current! - 1, + size: pagination.pageSize, + name: searchText || undefined, + image_type: activeTab === CommonTabKeys.Public ? 1 : 0, + }; + const [res] = await to(getMirrorListReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } + }, [activeTab, pagination, searchText]); + useEffect(() => { getMirrorList(); - }, [activeTab, pagination, searchText]); + }, [getMirrorList]); // 切换 Tab,重置数据 const hanleTabChange: TabsProps['onChange'] = (value) => { @@ -82,22 +98,6 @@ function MirrorList() { setActiveTab(value); }; - // 获取镜像列表 - const getMirrorList = async () => { - const params: Record<string, any> = { - page: pagination.current! - 1, - size: pagination.pageSize, - name: searchText || undefined, - image_type: activeTab === CommonTabKeys.Public ? 1 : 0, - }; - const [res] = await to(getMirrorListReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; - // 删除镜像 const deleteMirror = async (id: number) => { const [res] = await to(deleteMirrorReq(id)); diff --git a/react-ui/src/pages/Model/components/MetricsChart/index.tsx b/react-ui/src/pages/Model/components/MetricsChart/index.tsx index bab4e6bb..1a8ec631 100644 --- a/react-ui/src/pages/Model/components/MetricsChart/index.tsx +++ b/react-ui/src/pages/Model/components/MetricsChart/index.tsx @@ -1,5 +1,5 @@ import * as echarts from 'echarts'; -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import styles from './index.less'; import './tooltip.css'; @@ -74,77 +74,80 @@ function MetricsChart({ name, chartData }: MetricsChartProps) { }; }); - const options: echarts.EChartsOption = { - backgroundColor: backgroundColor, - title: { - show: false, - }, - tooltip: { - trigger: 'item', - padding: 10, - formatter: (params: any) => { - const { name: xTitle, data } = params; - return getTooltip('step', xTitle, name, data); - }, - }, - legend: { - bottom: 10, - icon: 'rect', - itemWidth: 10, - itemHeight: 10, - itemGap: 20, - textStyle: { - color: 'rgba(29, 29, 32, 0.75)', - fontSize: 12, - }, - }, - color: colors, - grid: { - left: '15', - right: '15', - top: '20', - bottom: '60', - containLabel: true, - }, - xAxis: { - type: 'category', - boundaryGap: true, - offset: 10, - data: xAxisData, - axisLabel: { - color: 'rgba(29, 29, 32, 0.75)', - fontSize: 12, - }, - axisTick: { + const options: echarts.EChartsOption = useMemo( + () => ({ + backgroundColor: backgroundColor, + title: { show: false, }, - axisLine: { - lineStyle: { - color: '#eaeaea', - width: 1, + tooltip: { + trigger: 'item', + padding: 10, + formatter: (params: any) => { + const { name: xTitle, data } = params; + return getTooltip('step', xTitle, name, data); }, }, - }, - yAxis: { - type: 'value', - axisLabel: { - color: 'rgba(29, 29, 32, 0.75)', - fontSize: 12, - margin: 15, + legend: { + bottom: 10, + icon: 'rect', + itemWidth: 10, + itemHeight: 10, + itemGap: 20, + textStyle: { + color: 'rgba(29, 29, 32, 0.75)', + fontSize: 12, + }, }, - axisLine: { - show: false, + color: colors, + grid: { + left: '15', + right: '15', + top: '20', + bottom: '60', + containLabel: true, + }, + xAxis: { + type: 'category', + boundaryGap: true, + offset: 10, + data: xAxisData, + axisLabel: { + color: 'rgba(29, 29, 32, 0.75)', + fontSize: 12, + }, + axisTick: { + show: false, + }, + axisLine: { + lineStyle: { + color: '#eaeaea', + width: 1, + }, + }, }, - splitLine: { - lineStyle: { - color: '#e4e4e4', - width: 1, - type: 'dashed', + yAxis: { + type: 'value', + axisLabel: { + color: 'rgba(29, 29, 32, 0.75)', + fontSize: 12, + margin: 15, + }, + axisLine: { + show: false, + }, + splitLine: { + lineStyle: { + color: '#e4e4e4', + width: 1, + type: 'dashed', + }, }, }, - }, - series: seriesData, - }; + series: seriesData, + }), + [name, seriesData, xAxisData], + ); useEffect(() => { // 创建一个echarts实例,返回echarts实例 @@ -158,7 +161,7 @@ function MetricsChart({ name, chartData }: MetricsChartProps) { // myChart.dispose() 销毁实例 chart.dispose(); }; - }, []); + }, [options]); return ( <div className={styles['metrics-chart']}> diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index 3bc40b40..0f02ead6 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -77,8 +77,8 @@ function ModelEvolution({ clearGraphData(); } }, - [resourceId, version], isActive, + [resourceId, version], ); // 初始化图 diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx index 730afaac..0b8c2652 100644 --- a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -60,60 +60,61 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr ] = useCheck(allMetricsNames); useEffect(() => { - if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) { - getModelVersionsMetrics(); - } else { - setChartData(undefined); - } - }, [selectedMetrics, selectedRowKeys, identifier, resourceId]); + // 获取模型版本列表,带有参数和指标数据 + const getModelPageVersions = async () => { + const params = { + page: pagination.current! - 1, + size: pagination.pageSize, + identifier: identifier, + owner: owner, + type: MetricsType.Train, + }; + const [res] = await to(getModelPageVersionsReq(params)); + if (res && res.data) { + const { content = [], totalElements = 0 } = res.data; + setTableData(content); + setTotal(totalElements); + } + }; + + getModelPageVersions(); + }, [pagination, identifier, owner]); // 版本切换,自动勾选当前版本 useEffect(() => { const curRow = tableData.find((item) => item.name === version); - if ( - curRow && - curRow.metrics_names && - curRow.metrics_names.length > 0 && - !selectedRowKeys.includes(version) - ) { - setSelectedRowKeys([version, ...selectedRowKeys]); + if (curRow && curRow.metrics_names && curRow.metrics_names.length > 0) { + setSelectedRowKeys((prev) => { + if (!prev.includes(version)) { + return [version, ...prev]; + } + return prev; + }); } }, [tableData, version]); useEffect(() => { - getModelPageVersions(); - }, [pagination, identifier, owner]); - - // 获取模型版本列表,带有参数和指标数据 - const getModelPageVersions = async () => { - const params = { - page: pagination.current! - 1, - size: pagination.pageSize, - identifier: identifier, - owner: owner, - type: MetricsType.Train, + // 获取模型版本指标的图表数据 + const getModelVersionsMetrics = async () => { + const params = { + versions: selectedRowKeys, + metrics: selectedMetrics, + type: MetricsType.Train, + identifier: identifier, + repo_id: resourceId, + }; + const [res] = await to(getModelVersionsMetricsReq(params)); + if (res && res.data) { + setChartData(res.data); + } }; - const [res] = await to(getModelPageVersionsReq(params)); - if (res && res.data) { - const { content = [], totalElements = 0 } = res.data; - setTableData(content); - setTotal(totalElements); - } - }; - const getModelVersionsMetrics = async () => { - const params = { - versions: selectedRowKeys, - metrics: selectedMetrics, - type: MetricsType.Train, - identifier: identifier, - repo_id: resourceId, - }; - const [res] = await to(getModelVersionsMetricsReq(params)); - if (res && res.data) { - setChartData(res.data); + if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) { + getModelVersionsMetrics(); + } else { + setChartData(undefined); } - }; + }, [selectedMetrics, selectedRowKeys, identifier, resourceId]); // 分页切换 const handleTableChange: TableProps<TableData>['onChange'] = ( @@ -225,7 +226,14 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr })), }, ]; - }, [tableData, selectedMetrics]); + }, [ + tableData, + checkAllMetrics, + checkSingleMetrics, + isSingleMetricsChecked, + metricsChecked, + metricsIndeterminate, + ]); return ( <div className={styles['model-metrics']}> diff --git a/react-ui/src/pages/ModelDeployment/CreateService/index.tsx b/react-ui/src/pages/ModelDeployment/CreateService/index.tsx index 8701c07b..ae7b1ad8 100644 --- a/react-ui/src/pages/ModelDeployment/CreateService/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateService/index.tsx @@ -11,8 +11,8 @@ import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/service import { to } from '@/utils/promise'; import { useNavigate, useParams } from '@umijs/max'; import { App, Button, Col, Form, Input, Row, Select } from 'antd'; -import { useEffect, useState } from 'react'; -import { ServiceData, createServiceVersionMessage } from '../types'; +import { useEffect } from 'react'; +import { createServiceVersionMessage } from '../types'; import styles from './index.less'; // 表单数据 @@ -25,30 +25,28 @@ export type FormData = { function CreateService() { const navigate = useNavigate(); const [form] = Form.useForm(); - const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); const { message } = App.useApp(); const params = useParams(); const serviceId = params.serviceId; useEffect(() => { + // 获取服务详情 + const getServiceInfo = async () => { + const [res] = await to(getServiceInfoReq(serviceId)); + if (res && res.data) { + const { service_type, service_name, description } = res.data; + form.setFieldsValue({ + service_type, + service_name, + description, + }); + } + }; + if (serviceId) { getServiceInfo(); } - }, []); - - // 获取服务详情 - const getServiceInfo = async () => { - const [res] = await to(getServiceInfoReq(serviceId)); - if (res && res.data) { - setServiceInfo(res.data); - const { service_type, service_name, description } = res.data; - form.setFieldsValue({ - service_type, - service_name, - description, - }); - } - }; + }, [serviceId, form]); // 创建、更新服务 const createService = async (formData: FormData) => { diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index 7adb7dc8..a4d35444 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -55,11 +55,11 @@ function CreateServiceVersion() { const [operationType, setOperationType] = useState(ServiceOperationType.Create); const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo); const { message } = App.useApp(); - // const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined); const params = useParams(); const serviceId = params.serviceId; + // 因为没有服务版本详情接口,需要从缓存中获取 useEffect(() => { const res: ServiceVersionCache | undefined = SessionStorage.getItem( SessionStorage.serviceVersionInfoKey, @@ -98,22 +98,21 @@ function CreateServiceVersion() { return () => { SessionStorage.removeItem(SessionStorage.serviceVersionInfoKey); }; - }, []); + }, [form]); useEffect(() => { - getServiceInfo(); - }, []); + // 获取服务详情,设置服务名称 + const getServiceInfo = async () => { + const [res] = await to(getServiceInfoReq(serviceId)); + if (res && res.data) { + form.setFieldsValue({ + service_name: res.data.service_name, + }); + } + }; - // 获取服务详情 - const getServiceInfo = async () => { - const [res] = await to(getServiceInfoReq(serviceId)); - if (res && res.data) { - // setServiceInfo(res.data); - form.setFieldsValue({ - service_name: res.data.service_name, - }); - } - }; + getServiceInfo(); + }, [serviceId, form]); // 创建版本 const createServiceVersion = async (formData: FormData) => { diff --git a/react-ui/src/pages/ModelDeployment/List/index.tsx b/react-ui/src/pages/ModelDeployment/List/index.tsx index 0632e547..50fe9b84 100644 --- a/react-ui/src/pages/ModelDeployment/List/index.tsx +++ b/react-ui/src/pages/ModelDeployment/List/index.tsx @@ -26,7 +26,7 @@ import { } from 'antd'; import { type SearchProps } from 'antd/es/input'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { CreateServiceVersionFrom, ServiceData, @@ -53,19 +53,8 @@ function ModelDeployment() { }, ); - useEffect(() => { - window.addEventListener('message', handleMessage); - return () => { - window.removeEventListener('message', handleMessage); - }; - }, []); - - useEffect(() => { - getServiceList(); - }, [pagination, searchText, serviceType]); - // 获取模型部署服务列表 - const getServiceList = async () => { + const getServiceList = useCallback(async () => { const params: Record<string, any> = { page: pagination.current! - 1, size: pagination.pageSize, @@ -78,7 +67,52 @@ function ModelDeployment() { setTableData(content); setTotal(totalElements); } - }; + }, [pagination, searchText, serviceType]); + + // 去创建服务版本 + const gotoCreateServiceVersion = useCallback( + (serviceId: number) => { + SessionStorage.setItem( + SessionStorage.serviceVersionInfoKey, + { + operationType: ServiceOperationType.Create, + lastPage: CreateServiceVersionFrom.CreateService, + }, + true, + ); + + navigate(`serviceInfo/${serviceId}/createVersion`); + }, + [navigate], + ); + + // 获取模型部署服务列表 + useEffect(() => { + getServiceList(); + }, [getServiceList]); + + // 接收创建服务成功的消息 + useEffect(() => { + const handleMessage = (e: MessageEvent) => { + const { type, payload } = e.data; + if (type === createServiceVersionMessage) { + modalConfirm({ + title: '创建服务成功', + content: '是否创建服务版本?', + isDelete: false, + cancelText: '稍后创建', + onOk: () => { + gotoCreateServiceVersion(payload); + }, + }); + } + }; + + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [gotoCreateServiceVersion]); // 删除模型部署 const deleteService = async (record: ServiceData) => { @@ -145,35 +179,6 @@ function ModelDeployment() { navigate(`serviceInfo/${record.id}`); }; - const handleMessage = (e: MessageEvent) => { - const { type, payload } = e.data; - if (type === createServiceVersionMessage) { - modalConfirm({ - title: '创建服务成功', - content: '是否创建服务版本?', - isDelete: false, - cancelText: '稍后创建', - onOk: () => { - gotoCreateServiceVersion(payload); - }, - }); - } - }; - - // 去创建服务版本 - const gotoCreateServiceVersion = (serviceId: number) => { - SessionStorage.setItem( - SessionStorage.serviceVersionInfoKey, - { - operationType: ServiceOperationType.Create, - lastPage: CreateServiceVersionFrom.CreateService, - }, - true, - ); - - navigate(`serviceInfo/${serviceId}/createVersion`); - }; - // 分页切换 const handleTableChange: TableProps<ServiceData>['onChange'] = ( pagination, diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 143315a1..8d7a6a1b 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -36,7 +36,7 @@ import { } from 'antd'; import { type SearchProps } from 'antd/es/input'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import ServiceRunStatusCell from '../components/ModelDeployStatusCell'; import VersionCompareModal from '../components/VersionCompareModal'; import { @@ -89,24 +89,16 @@ function ServiceInfo() { ]; const getResourceDescription = useComputingResource()[2]; - useEffect(() => { - getServiceInfo(); - }, []); - - useEffect(() => { - getServiceVersions(); - }, [pagination, searchText, serviceStatus]); - // 获取服务详情 - const getServiceInfo = async () => { + const getServiceInfo = useCallback(async () => { const [res] = await to(getServiceInfoReq(serviceId)); if (res && res.data) { setServiceInfo(res.data); } - }; + }, [serviceId]); // 获取服务版本列表 - const getServiceVersions = async () => { + const getServiceVersions = useCallback(async () => { const params: Record<string, any> = { page: pagination.current! - 1, size: pagination.pageSize, @@ -125,7 +117,15 @@ function ServiceInfo() { setTableData(content); setTotal(totalElements); } - }; + }, [pagination, serviceStatus, searchText, serviceId]); + + useEffect(() => { + getServiceInfo(); + }, [getServiceInfo]); + + useEffect(() => { + getServiceVersions(); + }, [getServiceVersions]); // 删除模型部署 const deleteServiceVersion = async (record: ServiceVersionData) => { diff --git a/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx b/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx index ec6092ba..2aa46722 100644 --- a/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx @@ -29,16 +29,18 @@ function ServiceVersionInfo() { const id = params.id; useEffect(() => { - getServiceVersionInfo(); - }, []); + // 获取服务版本详情 + const getServiceVersionInfo = async () => { + const [res] = await to(getServiceVersionInfoReq(id)); + if (res && res.data) { + setVersionInfo(res.data); + } + }; - // 获取服务版本详情 - const getServiceVersionInfo = async () => { - const [res] = await to(getServiceVersionInfoReq(id)); - if (res && res.data) { - setVersionInfo(res.data); + if (id) { + getServiceVersionInfo(); } - }; + }, [id]); const tabItems = [ { diff --git a/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx index 2f6d0368..08f3ed7a 100644 --- a/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx @@ -54,28 +54,27 @@ function ServerLog({ info }: ServerLogProps) { ]; useEffect(() => { + // 获取服务日志 + const getModelDeploymentLog = async () => { + if (info && logTime && logTime.length === 2) { + const params = { + start_time: logTime[0], + end_time: logTime[1], + id: info.id, + }; + const [res] = await to(getServiceVersionLogReq(params)); + if (res && res.data) { + setLogData((prev) => [...prev, res.data]); + setHasMore(!!res.data.log_content); + // setTimeout(() => { + // scrollToBottom(); + // }, 100); + } + } + }; getModelDeploymentLog(); }, [info, logTime]); - // 获取服务日志 - const getModelDeploymentLog = async () => { - if (info && logTime && logTime.length === 2) { - const params = { - start_time: logTime[0], - end_time: logTime[1], - id: info.id, - }; - const [res] = await to(getServiceVersionLogReq(params)); - if (res && res.data) { - setLogData((prev) => [...prev, res.data]); - setHasMore(!!res.data.log_content); - // setTimeout(() => { - // scrollToBottom(); - // }, 100); - } - } - }; - // 搜索 const handleSearch = () => { setLogData([]); diff --git a/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx index 8abd15be..49eca067 100644 --- a/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx @@ -10,20 +10,20 @@ type UserGuideProps = { function UserGuide({ info }: UserGuideProps) { const [docs, setDocs] = useState(''); + useEffect(() => { + // 获取服务文档 + const getModelDeploymentDocs = async () => { + if (info) { + const [res] = await to(getServiceVersionDocsReq(info.id)); + if (res && res.data && res.data.docs) { + setDocs(JSON.stringify(res.data.docs, null, 2)); + } + } + }; getModelDeploymentDocs(); }, [info]); - // 获取服务文档 - const getModelDeploymentDocs = async () => { - if (info) { - const [res] = await to(getServiceVersionDocsReq(info.id)); - if (res && res.data && res.data.docs) { - setDocs(JSON.stringify(res.data.docs, null, 2)); - } - } - }; - return <div className={styles['user-guide']}>{docs}</div>; } diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index 4eacca04..5faa3ddc 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -38,15 +38,6 @@ const formatEnvText = (env?: Record<string, string>) => { function VersionBasicInfo({ info }: BasicInfoProps) { const getResourceDescription = useComputingResource()[2]; - // 格式化资源规格 - const formatResource = (resource?: string) => { - if (!resource) { - return undefined; - } - - return getResourceDescription(resource); - }; - const datas: BasicInfoData[] = [ { label: '服务名称', @@ -78,7 +69,7 @@ function VersionBasicInfo({ info }: BasicInfoProps) { { label: '资源规格', value: info?.resource, - format: formatResource, + format: getResourceDescription, }, { label: '挂载路径', diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx index b6562237..5fcd472a 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -106,20 +106,20 @@ function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModa ); useEffect(() => { - getServiceVersionCompare(); - }, []); - - // 获取对比数据 - const getServiceVersionCompare = async () => { - const params = { - id1: version1, - id2: version2, + // 获取对比数据 + const getServiceVersionCompare = async () => { + const params = { + id1: version1, + id2: version2, + }; + const [res] = await to(getServiceVersionCompareReq(params)); + if (res && res.data) { + setCompareData(res.data); + } }; - const [res] = await to(getServiceVersionCompareReq(params)); - if (res && res.data) { - setCompareData(res.data); - } - }; + + getServiceVersionCompare(); + }, [version1, version2]); const { version1: v1 = {} as ServiceVersionData, diff --git a/react-ui/src/pages/Monitor/Job/edit.tsx b/react-ui/src/pages/Monitor/Job/edit.tsx index 218b985e..88b1b2e9 100644 --- a/react-ui/src/pages/Monitor/Job/edit.tsx +++ b/react-ui/src/pages/Monitor/Job/edit.tsx @@ -55,7 +55,7 @@ const JobForm: React.FC<JobFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/Pipeline/Info/index.jsx b/react-ui/src/pages/Pipeline/Info/index.jsx index 1e489082..74760f4d 100644 --- a/react-ui/src/pages/Pipeline/Info/index.jsx +++ b/react-ui/src/pages/Pipeline/Info/index.jsx @@ -85,7 +85,7 @@ const EditPipeline = () => { }; // 保存 - const savePipeline = async (val) => { + const savePipeline = async (isBack) => { const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); if (globalParamError) { message.error('全局参数配置有误'); @@ -122,7 +122,7 @@ const EditPipeline = () => { saveWorkflow(params).then((ret) => { message.success('保存成功'); setTimeout(() => { - if (val) { + if (isBack) { navigate({ pathname: `/pipeline/template` }); } }, 500); diff --git a/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx index f1aa57c9..65833bf7 100644 --- a/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx @@ -19,19 +19,23 @@ const GlobalParamsDrawer = forwardRef( ({ open = false, onClose, globalParam = [] }: GlobalParamsDrawerProps, ref) => { const [form] = Form.useForm(); - useImperativeHandle(ref, () => ({ - validateFields: async () => { - const [values, error] = await to(form.validateFields()); - if (!error && values) { - return values; - } else { - return Promise.reject(error); - } - }, - getFieldsValue: () => { - return form.getFieldsValue(); - }, - })); + useImperativeHandle( + ref, + () => ({ + validateFields: async () => { + const [values, error] = await to(form.validateFields()); + if (!error && values) { + return values; + } else { + return Promise.reject(error); + } + }, + getFieldsValue: () => { + return form.getFieldsValue(); + }, + }), + [form], + ); // 处理参数类型变化 const handleTypeChange = (name: NamePath) => { diff --git a/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx b/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx index 1418b4f8..2224192c 100644 --- a/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx @@ -21,47 +21,47 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]); useEffect(() => { + // 获取所有组件 + const getAllComponents = async () => { + const [res] = await to(getComponentAll()); + if (res && res.data) { + const menus = res.data as ModelMenuData[]; + setModelMenusList(menus); + const items = menus.map((item) => { + return { + key: item.key, + label: item.name, + children: item.value.map((ele) => { + return ( + <div + key={ele.id} + draggable="true" + onDragEnd={(e) => { + dragEnd(e, ele); + }} + className={Styles.collapseItem} + > + {ele.icon_path && ( + <img + style={{ height: '16px', marginRight: '15px' }} + src={`/assets/images/${ele.icon_path}.png`} + draggable={false} + alt="" + /> + )} + {ele.component_label} + </div> + ); + }), + }; + }); + setCollapseItems(items); + } + }; + getAllComponents(); }, []); - // 获取所有组件 - const getAllComponents = async () => { - const [res] = await to(getComponentAll()); - if (res && res.data) { - const menus = res.data as ModelMenuData[]; - setModelMenusList(menus); - const items = menus.map((item) => { - return { - key: item.key, - label: item.name, - children: item.value.map((ele) => { - return ( - <div - key={ele.id} - draggable="true" - onDragEnd={(e) => { - dragEnd(e, ele); - }} - className={Styles.collapseItem} - > - {ele.icon_path && ( - <img - style={{ height: '16px', marginRight: '15px' }} - src={`/assets/images/${ele.icon_path}.png`} - draggable={false} - alt="" - /> - )} - {ele.component_label} - </div> - ); - }), - }; - }); - setCollapseItems(items); - } - }; - const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => { onComponentDragEnd({ ...data, diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index 6314ea76..e1d0efc1 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -78,55 +78,59 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete setOpen(false); }; - useImperativeHandle(ref, () => ({ - showDrawer( - model: PipelineNodeModel, - params: PipelineGlobalParam[], - parentNodes: INode[], - validate: boolean = false, - ) { - try { - const nodeData: PipelineNodeModelSerialize = { - ...model, - in_parameters: JSON.parse(model.in_parameters), - out_parameters: JSON.parse(model.out_parameters), - control_strategy: JSON.parse(model.control_strategy), - }; - // console.log('model', nodeData); - setStagingItem({ - ...nodeData, - }); - form.resetFields(); - form.setFieldsValue({ - ...nodeData, - }); - if (validate) { - form.validateFields(); + useImperativeHandle( + ref, + () => ({ + showDrawer( + model: PipelineNodeModel, + params: PipelineGlobalParam[], + parentNodes: INode[], + validate: boolean = false, + ) { + try { + const nodeData: PipelineNodeModelSerialize = { + ...model, + in_parameters: JSON.parse(model.in_parameters), + out_parameters: JSON.parse(model.out_parameters), + control_strategy: JSON.parse(model.control_strategy), + }; + // console.log('model', nodeData); + setStagingItem({ + ...nodeData, + }); + form.resetFields(); + form.setFieldsValue({ + ...nodeData, + }); + if (validate) { + form.validateFields(); + } + } catch (error) { + console.error('JSON.parse error: ', error); } - } catch (error) { - console.error('JSON.parse error: ', error); - } - setOpen(true); + setOpen(true); - // 参数下拉菜单 - setMenuItems(createMenuItems(params, parentNodes)); - }, - close: () => { - onClose(); - }, - validateFields: async () => { - if (!open) { - return; - } - const [values, error] = await to(form.validateFields()); - if (!error && values) { - return values; - } else { - form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); - return Promise.reject(error); - } - }, - })); + // 参数下拉菜单 + setMenuItems(createMenuItems(params, parentNodes)); + }, + close: () => { + onClose(); + }, + validateFields: async () => { + if (!open) { + return; + } + const [values, error] = await to(form.validateFields()); + if (!error && values) { + return values; + } else { + form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); + return Promise.reject(error); + } + }, + }), + [form, open], + ); // ref 类型选择 const selectRefData = ( diff --git a/react-ui/src/pages/Pipeline/index.jsx b/react-ui/src/pages/Pipeline/index.jsx index 08d1ccf6..eb2db981 100644 --- a/react-ui/src/pages/Pipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/index.jsx @@ -15,7 +15,7 @@ import tableCellRender, { TableCellValueType } from '@/utils/table'; import { modalConfirm } from '@/utils/ui'; import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd'; import classNames from 'classnames'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styles from './index.less'; @@ -39,12 +39,8 @@ const Pipeline = () => { ); const { message } = App.useApp(); - useEffect(() => { - getList(); - }, [pagination, searchText]); - // 获取流水线模板列表 - const getList = () => { + const getList = useCallback(() => { const params = { page: pagination.current - 1, size: pagination.pageSize, @@ -56,7 +52,11 @@ const Pipeline = () => { setTotal(res.data.totalElements); } }); - }; + }, [pagination, searchText]); + + useEffect(() => { + getList(); + }, [getList]); // 搜索 const onSearch = (value) => { @@ -133,7 +133,6 @@ const Pipeline = () => { current, pageSize, }); - getList(); }; const columns = [ diff --git a/react-ui/src/pages/System/Config/edit.tsx b/react-ui/src/pages/System/Config/edit.tsx index daa6e24e..c40641ad 100644 --- a/react-ui/src/pages/System/Config/edit.tsx +++ b/react-ui/src/pages/System/Config/edit.tsx @@ -42,7 +42,7 @@ const ConfigForm: React.FC<ConfigFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, configTypeOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Dept/edit.tsx b/react-ui/src/pages/System/Dept/edit.tsx index 90310d86..3a9695c5 100644 --- a/react-ui/src/pages/System/Dept/edit.tsx +++ b/react-ui/src/pages/System/Dept/edit.tsx @@ -53,7 +53,7 @@ const DeptForm: React.FC<DeptFormProps> = (props) => { updateBy: props.values.updateBy, updateTime: props.values.updateTime, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Dict/edit.tsx b/react-ui/src/pages/System/Dict/edit.tsx index decb5628..a737693b 100644 --- a/react-ui/src/pages/System/Dict/edit.tsx +++ b/react-ui/src/pages/System/Dict/edit.tsx @@ -41,7 +41,7 @@ const DictTypeForm: React.FC<DictTypeFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/DictData/edit.tsx b/react-ui/src/pages/System/DictData/edit.tsx index deef59d6..ca093819 100644 --- a/react-ui/src/pages/System/DictData/edit.tsx +++ b/react-ui/src/pages/System/DictData/edit.tsx @@ -47,7 +47,7 @@ const DictDataForm: React.FC<DataFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/DictData/index.tsx b/react-ui/src/pages/System/DictData/index.tsx index 28005cc8..7a3e3c34 100644 --- a/react-ui/src/pages/System/DictData/index.tsx +++ b/react-ui/src/pages/System/DictData/index.tsx @@ -187,7 +187,7 @@ const DictDataTableList: React.FC = () => { } }); } - }, [dictId, dictType, params]); + }, [dictId, dictType, id]); const columns: ProColumns<API.System.DictData>[] = [ { diff --git a/react-ui/src/pages/System/Menu/edit.tsx b/react-ui/src/pages/System/Menu/edit.tsx index e8ac269a..845f1480 100644 --- a/react-ui/src/pages/System/Menu/edit.tsx +++ b/react-ui/src/pages/System/Menu/edit.tsx @@ -68,7 +68,7 @@ const MenuForm: React.FC<MenuFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions, visibleOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Notice/edit.tsx b/react-ui/src/pages/System/Notice/edit.tsx index 647d485b..629325a7 100644 --- a/react-ui/src/pages/System/Notice/edit.tsx +++ b/react-ui/src/pages/System/Notice/edit.tsx @@ -44,7 +44,7 @@ const NoticeForm: React.FC<NoticeFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Post/edit.tsx b/react-ui/src/pages/System/Post/edit.tsx index b89dc737..2704a446 100644 --- a/react-ui/src/pages/System/Post/edit.tsx +++ b/react-ui/src/pages/System/Post/edit.tsx @@ -42,7 +42,7 @@ const PostForm: React.FC<PostFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Role/components/DataScope.tsx b/react-ui/src/pages/System/Role/components/DataScope.tsx index e4754c72..8bc3cc7a 100644 --- a/react-ui/src/pages/System/Role/components/DataScope.tsx +++ b/react-ui/src/pages/System/Role/components/DataScope.tsx @@ -48,7 +48,7 @@ const DataScopeForm: React.FC<DataScopeFormProps> = (props) => { dataScope: props.values.dataScope, }); setDataScopeType(props.values.dataScope); - }, [props.values]); + }, [props.values, deptCheckedKeys, form]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/Role/edit.tsx b/react-ui/src/pages/System/Role/edit.tsx index ce4a7f15..e44147b9 100644 --- a/react-ui/src/pages/System/Role/edit.tsx +++ b/react-ui/src/pages/System/Role/edit.tsx @@ -52,7 +52,7 @@ const RoleForm: React.FC<RoleFormProps> = (props) => { updateTime: props.values.updateTime, remark: props.values.remark, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/System/User/edit.tsx b/react-ui/src/pages/System/User/edit.tsx index 71252d51..dd23b6e0 100644 --- a/react-ui/src/pages/System/User/edit.tsx +++ b/react-ui/src/pages/System/User/edit.tsx @@ -65,7 +65,7 @@ const UserForm: React.FC<UserFormProps> = (props) => { gitLinkUsername: props.values.gitLinkUsername, gitLinkPassword: props.values.gitLinkPassword, }); - }, [form, props]); + }, [form, props, statusOptions]); const intl = useIntl(); const handleOk = () => { diff --git a/react-ui/src/pages/User/Login/login.tsx b/react-ui/src/pages/User/Login/login.tsx index 9ef18148..a7cdcfba 100644 --- a/react-ui/src/pages/User/Login/login.tsx +++ b/react-ui/src/pages/User/Login/login.tsx @@ -49,7 +49,8 @@ const Login = () => { } else { form.setFieldsValue({ username: '', password: '', autoLogin: false }); } - }, []); + }, [form]); + const getCaptchaCode = async () => { const [res] = await to(getCaptchaImg()); if (res) { diff --git a/react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx b/react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx index 74227506..7c911318 100644 --- a/react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx +++ b/react-ui/src/pages/Workspace/components/AssetsManagement/index.tsx @@ -7,46 +7,48 @@ import styles from './index.less'; function AssetsManagement() { const [type, setType] = useState(CommonTabKeys.Public); const [assetCounts, setAssetCounts] = useState<{ title: string; value: number }[]>([]); + useEffect(() => { + // 获取工作空间资产数量 + const getWorkspacAssetCount = async () => { + const params = { + isPublic: type === CommonTabKeys.Public, + }; + const [res] = await to(getWorkspaceAssetCountReq(params)); + if (res && res.data) { + const { component, dataset, image, model, workflow } = res.data; + const items = [ + { + title: '数据集', + value: dataset, + }, + { + title: '模型', + value: model, + }, + { + title: '镜像', + value: image, + }, + { + title: '组件', + value: component, + }, + // { + // title: '代码配置', + // value: 0, + // }, + { + title: '流水线模版', + value: workflow, + }, + ]; + setAssetCounts(items); + } + }; + getWorkspacAssetCount(); }, [type]); - // 获取工作空间资产数量 - const getWorkspacAssetCount = async () => { - const params = { - isPublic: type === CommonTabKeys.Public, - }; - const [res] = await to(getWorkspaceAssetCountReq(params)); - if (res && res.data) { - const { component, dataset, image, model, workflow } = res.data; - const items = [ - { - title: '数据集', - value: dataset, - }, - { - title: '模型', - value: model, - }, - { - title: '镜像', - value: image, - }, - { - title: '组件', - value: component, - }, - // { - // title: '代码配置', - // value: 0, - // }, - { - title: '流水线模版', - value: workflow, - }, - ]; - setAssetCounts(items); - } - }; return ( <div className={styles['assets-management']}> diff --git a/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx b/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx index 3cede1ef..235683ee 100644 --- a/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx +++ b/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx @@ -1,6 +1,6 @@ import themes from '@/styles/theme.less'; import * as echarts from 'echarts'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import styles from './index.less'; const color1 = new echarts.graphic.LinearGradient( @@ -94,102 +94,106 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { chartData.Running + chartData.Succeeded + chartData.Terminated; - const options: echarts.EChartsOption = { - title: { - show: true, - left: '29%', - top: 'center', - textAlign: 'center', - text: [`{a|${total}}`, '{b|实验状态}'].join('\n'), - textStyle: { - rich: { - a: { - color: themes['textColor'], - fontSize: 20, - fontWeight: 700, - lineHeight: 28, - }, - b: { - color: themes['textColorSecondary'], - fontSize: 10, - fontWeight: 'normal', + + const options: echarts.EChartsOption = useMemo( + () => ({ + title: { + show: true, + left: '29%', + top: 'center', + textAlign: 'center', + text: [`{a|${total}}`, '{b|实验状态}'].join('\n'), + textStyle: { + rich: { + a: { + color: themes['textColor'], + fontSize: 20, + fontWeight: 700, + lineHeight: 28, + }, + b: { + color: themes['textColorSecondary'], + fontSize: 10, + fontWeight: 'normal', + }, }, }, }, - }, - tooltip: { - trigger: 'item', - }, - legend: { - top: 'center', - right: '5%', - orient: 'vertical', - icon: 'circle', - itemWidth: 6, - itemGap: 20, - height: 100, - }, - color: [color1, color2, color3, color4, color5], - series: [ - { - type: 'pie', - radius: ['70%', '80%'], - center: ['30%', '50%'], - avoidLabelOverlap: false, - padAngle: 3, - itemStyle: { - borderRadius: 3, - }, - minAngle: 5, - label: { - show: false, - }, - emphasis: { + tooltip: { + trigger: 'item', + }, + legend: { + top: 'center', + right: '5%', + orient: 'vertical', + icon: 'circle', + itemWidth: 6, + itemGap: 20, + height: 100, + }, + color: [color1, color2, color3, color4, color5], + series: [ + { + type: 'pie', + radius: ['70%', '80%'], + center: ['30%', '50%'], + avoidLabelOverlap: false, + padAngle: 3, + itemStyle: { + borderRadius: 3, + }, + minAngle: 5, label: { show: false, }, + emphasis: { + label: { + show: false, + }, + }, + labelLine: { + show: false, + }, + data: [ + { value: chartData.Failed > 0 ? chartData.Failed : undefined, name: '失败' }, + { value: chartData.Succeeded > 0 ? chartData.Succeeded : undefined, name: '成功' }, + { value: chartData.Terminated > 0 ? chartData.Terminated : undefined, name: '中止' }, + { value: chartData.Pending > 0 ? chartData.Pending : undefined, name: '等待' }, + { value: chartData.Running > 0 ? chartData.Running : undefined, name: '运行中' }, + ], }, - labelLine: { - show: false, - }, - data: [ - { value: chartData.Failed > 0 ? chartData.Failed : undefined, name: '失败' }, - { value: chartData.Succeeded > 0 ? chartData.Succeeded : undefined, name: '成功' }, - { value: chartData.Terminated > 0 ? chartData.Terminated : undefined, name: '中止' }, - { value: chartData.Pending > 0 ? chartData.Pending : undefined, name: '等待' }, - { value: chartData.Running > 0 ? chartData.Running : undefined, name: '运行中' }, - ], - }, - { - type: 'pie', - radius: '60%', - center: ['30%', '50%'], - avoidLabelOverlap: false, - label: { - show: false, - }, - tooltip: { - show: false, - }, - emphasis: { + { + type: 'pie', + radius: '60%', + center: ['30%', '50%'], + avoidLabelOverlap: false, label: { show: false, }, - disabled: true, - }, - animation: false, - labelLine: { - show: false, - }, - data: [ - { - value: 100, - itemStyle: { color: color6, borderColor: 'rgba(22, 100, 255, 0.08)', borderWidth: 1 }, + tooltip: { + show: false, }, - ], - }, - ], - }; + emphasis: { + label: { + show: false, + }, + disabled: true, + }, + animation: false, + labelLine: { + show: false, + }, + data: [ + { + value: 100, + itemStyle: { color: color6, borderColor: 'rgba(22, 100, 255, 0.08)', borderWidth: 1 }, + }, + ], + }, + ], + }), + [chartData, total], + ); useEffect(() => { // 创建一个echarts实例,返回echarts实例 @@ -203,7 +207,7 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { // myChart.dispose() 销毁实例 chart.dispose(); }; - }, []); + }, [options]); return ( <div className={styles['experiment-chart']} style={style}> diff --git a/react-ui/src/state/computingResourceStore.ts b/react-ui/src/state/computingResourceStore.ts deleted file mode 100644 index 23d1455b..00000000 --- a/react-ui/src/state/computingResourceStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ComputingResource } from '@/types'; -import { proxy } from 'umi'; - -type ComputingResourceStore = { - computingResource: ComputingResource[]; -}; - -const state = proxy<ComputingResourceStore>({ - computingResource: [], -}); - -export const setComputingResource = (computingResource: ComputingResource[]) => { - state.computingResource = computingResource; -}; - -export default state; diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index 30cdedc9..1a1e84c4 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -162,6 +162,7 @@ function renderText(text: any | undefined | null, ellipsis: boolean | 'auto') { wordBreak: 'break-all', display: 'inline-block', maxWidth: '100%', + verticalAlign: 'middle', } : undefined } From f36a29baec9dff59f6f5c89dadbad5d31f22eebc Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 18 Mar 2025 15:18:57 +0800 Subject: [PATCH 100/127] =?UTF-8?q?refactor:=20=E6=B3=A8=E9=87=8A=20react-?= =?UTF-8?q?hooks/exhaustive-deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/hooks/resource.ts | 5 +- react-ui/src/pages/Authorize/index.tsx | 35 ++-- react-ui/src/pages/AutoML/Instance/index.tsx | 4 +- react-ui/src/pages/Experiment/Info/index.jsx | 18 +- .../Experiment/components/LogGroup/index.tsx | 160 ++++++++++-------- .../pages/HyperParameter/Instance/index.tsx | 10 +- .../Model/components/ModelEvolution/index.tsx | 4 + react-ui/src/pages/Monitor/JobLog/index.tsx | 2 +- react-ui/src/pages/Pipeline/Info/index.jsx | 3 + .../Pipeline/components/ModelMenu/index.tsx | 63 ++++--- .../pages/System/User/components/DeptTree.tsx | 46 ++--- react-ui/src/pages/Tool/Gen/edit.tsx | 3 +- 12 files changed, 190 insertions(+), 163 deletions(-) diff --git a/react-ui/src/hooks/resource.ts b/react-ui/src/hooks/resource.ts index cb4434ab..6331edab 100644 --- a/react-ui/src/hooks/resource.ts +++ b/react-ui/src/hooks/resource.ts @@ -49,7 +49,10 @@ export function useComputingResource() { // 根据 standard 获取 description const getDescription = useCallback( - (standard: string) => { + (standard?: string) => { + if (!standard) { + return undefined; + } return resourceStandardList.find((item) => item.standard === standard)?.description; }, [resourceStandardList], diff --git a/react-ui/src/pages/Authorize/index.tsx b/react-ui/src/pages/Authorize/index.tsx index e42a0f1b..2caf3195 100644 --- a/react-ui/src/pages/Authorize/index.tsx +++ b/react-ui/src/pages/Authorize/index.tsx @@ -3,7 +3,7 @@ import { loginByOauth2Req } from '@/services/auth'; import { to } from '@/utils/promise'; import { history, useModel, useSearchParams } from '@umijs/max'; import { message } from 'antd'; -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { flushSync } from 'react-dom'; import styles from './index.less'; @@ -12,12 +12,21 @@ function Authorize() { const [searchParams] = useSearchParams(); const code = searchParams.get('code'); const redirect = searchParams.get('redirect'); - useEffect(() => { - loginByOauth2(); - }, []); + + const fetchUserInfo = useCallback(async () => { + const userInfo = await initialState?.fetchUserInfo?.(); + if (userInfo) { + flushSync(() => { + setInitialState((s) => ({ + ...s, + currentUser: userInfo, + })); + }); + } + }, [initialState, setInitialState]); // 登录 - const loginByOauth2 = async () => { + const loginByOauth2 = useCallback(async () => { const params = { code, }; @@ -29,19 +38,11 @@ function Authorize() { await fetchUserInfo(); history.push(redirect || '/'); } - }; + }, [fetchUserInfo, redirect, code]); - const fetchUserInfo = async () => { - const userInfo = await initialState?.fetchUserInfo?.(); - if (userInfo) { - flushSync(() => { - setInitialState((s) => ({ - ...s, - currentUser: userInfo, - })); - }); - } - }; + useEffect(() => { + loginByOauth2(); + }, [loginByOauth2]); return <div className={styles.container}></div>; } diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 8b525bdd..2dead784 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -39,6 +39,7 @@ function AutoMLInstance() { return () => { closeSSE(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 获取实验实例详情 @@ -83,9 +84,6 @@ function AutoMLInstance() { const setupSSE = (name: string, namespace: string) => { const { origin } = location; - // if (process.env.NODE_ENV === 'development') { - // origin = 'http://172.20.32.197:31213'; - // } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index e05480df..f0cf9ae6 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -21,7 +21,6 @@ function ExperimentText() { const [experimentIns, setExperimentIns] = useState(undefined); const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); const graphRef = useRef(); - const timerRef = useRef(); const workflowRef = useRef(); const locationParams = useParams(); // 新版本获取路由参数接口 const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); @@ -36,6 +35,16 @@ function ExperimentText() { initGraph(); getWorkflow(); + return () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { const changeSize = () => { if (!graph || graph.get('destroyed')) return; if (!graphRef.current) return; @@ -46,13 +55,6 @@ function ExperimentText() { window.addEventListener('resize', changeSize); return () => { window.removeEventListener('resize', changeSize); - if (timerRef.current) { - clearTimeout(timerRef.current); - } - if (evtSourceRef.current) { - evtSourceRef.current.close(); - evtSourceRef.current = null; - } }; }, []); diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index dfedac5c..e69b4773 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -46,12 +46,94 @@ function LogGroup({ setHasRun(true); } + // 进入页面时,滚动到底部 useEffect(() => { + scrollToBottom(false); + }, []); + + useEffect(() => { + // 建立 socket 连接 + const setupSockect = () => { + let { host } = location; + if (process.env.NODE_ENV === 'development') { + host = '172.20.32.197:31213'; + } + const socket = new WebSocket( + `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, + ); + + socket.addEventListener('open', () => { + console.log('WebSocket is open now.'); + }); + + socket.addEventListener('close', (event) => { + console.log('WebSocket is closed:', event); + // 有时候会出现连接失败,重试 3 次 + if (event.code !== 1000 && retryRef.current > 0) { + retryRef.current -= 1; + setTimeout(() => { + setupSockect(); + }, 2 * 1000); + } + }); + + socket.addEventListener('error', (event) => { + console.error('WebSocket error observed:', event); + }); + + socket.addEventListener('message', (event) => { + // console.log('message received.', event); + if (!event.data) { + return; + } + try { + const data = JSON.parse(event.data); + const streams = data.streams; + if (!streams || !Array.isArray(streams)) { + return; + } + let startTime = start_time; + const logContent = streams.reduce((result, item) => { + const values = item.values; + return ( + result + + values.reduce((prev: string, cur: [string, string]) => { + const [time, value] = cur; + startTime = time; + const str = `[${dayjs(Number(time) / 1.0e6).format( + 'YYYY-MM-DD HH:mm:ss', + )}] ${value}`; + return prev + str; + }, '') + ); + }, ''); + const logDetail: Log = { + start_time: startTime!, + log_content: logContent, + pod_name: pod_name, + }; + setLogList((oldList) => oldList.concat(logDetail)); + if (!isMouseDownRef.current && logContent) { + setTimeout(() => { + scrollToBottom(); + }, 100); + } + } catch (error) { + console.error('JSON parse error: ', error); + } + }); + + socketRef.current = socket; + }; + if (status === ExperimentStatus.Running) { setupSockect(); } - scrollToBottom(false); - }, [status]); + + return () => { + closeSocket(); + }; + }, [status, start_time, pod_name, isMouseDownRef, setLogList]); // 鼠标拖到中不滚动到底部 useEffect(() => { @@ -66,7 +148,6 @@ function LogGroup({ return () => { document.removeEventListener('mousedown', mouseDown); document.removeEventListener('mouseup', mouseUp); - closeSocket(); }; }, [setIsMouseDown]); @@ -120,78 +201,7 @@ function LogGroup({ requestExperimentPodsLog(); }; - // 建立 socket 连接 - const setupSockect = () => { - let { host } = location; - if (process.env.NODE_ENV === 'development') { - host = '172.20.32.197:31213'; - } - const socket = new WebSocket( - `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, - ); - - socket.addEventListener('open', () => { - console.log('WebSocket is open now.'); - }); - - socket.addEventListener('close', (event) => { - console.log('WebSocket is closed:', event); - // 有时候会出现连接失败,重试 3 次 - if (event.code !== 1000 && retryRef.current > 0) { - retryRef.current -= 1; - setTimeout(() => { - setupSockect(); - }, 2 * 1000); - } - }); - - socket.addEventListener('error', (event) => { - console.error('WebSocket error observed:', event); - }); - - socket.addEventListener('message', (event) => { - // console.log('message received.', event); - if (!event.data) { - return; - } - try { - const data = JSON.parse(event.data); - const streams = data.streams; - if (!streams || !Array.isArray(streams)) { - return; - } - let startTime = start_time; - const logContent = streams.reduce((result, item) => { - const values = item.values; - return ( - result + - values.reduce((prev: string, cur: [string, string]) => { - const [time, value] = cur; - startTime = time; - const str = `[${dayjs(Number(time) / 1.0e6).format('YYYY-MM-DD HH:mm:ss')}] ${value}`; - return prev + str; - }, '') - ); - }, ''); - const logDetail: Log = { - start_time: startTime!, - log_content: logContent, - pod_name: pod_name, - }; - setLogList((oldList) => oldList.concat(logDetail)); - if (!isMouseDownRef.current && logContent) { - setTimeout(() => { - scrollToBottom(); - }, 100); - } - } catch (error) { - console.error('JSON parse error: ', error); - } - }); - - socketRef.current = socket; - }; - + // 关闭 socket const closeSocket = () => { if (socketRef.current) { socketRef.current.close(1000, 'completed'); diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index aa206059..5e4c5ab7 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -22,6 +22,8 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'workflow'; + function HyperParameterInstance() { const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( @@ -41,6 +43,7 @@ function HyperParameterInstance() { return () => { closeSSE(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 获取实验实例详情 @@ -83,7 +86,7 @@ function HyperParameterInstance() { if (nodeStatusJson) { setNodes(nodeStatusJson); Object.keys(nodeStatusJson).some((key) => { - if (key.startsWith('workflow')) { + if (key.startsWith(NodePrefix)) { const workflowStatus = nodeStatusJson[key]; setWorkflowStatus(workflowStatus); return true; @@ -101,9 +104,6 @@ function HyperParameterInstance() { const setupSSE = (name: string, namespace: string) => { const { origin } = location; - // if (process.env.NODE_ENV === 'development') { - // origin = 'http://172.20.32.197:31213'; - // } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, @@ -119,7 +119,7 @@ function HyperParameterInstance() { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { const workflowStatus = Object.values(nodes).find((node: any) => - node.displayName.startsWith('workflow'), + node.displayName.startsWith(NodePrefix), ) as NodeStatus; // 节点 diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx index 0f02ead6..42b85f21 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx @@ -56,6 +56,10 @@ function ModelEvolution({ useEffect(() => { initGraph(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { const changeSize = () => { if (!graph || graph.get('destroyed')) return; if (!graphRef.current) return; diff --git a/react-ui/src/pages/Monitor/JobLog/index.tsx b/react-ui/src/pages/Monitor/JobLog/index.tsx index 4c69330a..9af85b9c 100644 --- a/react-ui/src/pages/Monitor/JobLog/index.tsx +++ b/react-ui/src/pages/Monitor/JobLog/index.tsx @@ -124,7 +124,7 @@ const JobLogTableList: React.FC = () => { getDictValueEnum('sys_job_group').then((data) => { setJobGroupOptions(data); }); - }, []); + }, [jobId]); const columns: ProColumns<API.Monitor.JobLog>[] = [ { diff --git a/react-ui/src/pages/Pipeline/Info/index.jsx b/react-ui/src/pages/Pipeline/Info/index.jsx index 74760f4d..353ef7e0 100644 --- a/react-ui/src/pages/Pipeline/Info/index.jsx +++ b/react-ui/src/pages/Pipeline/Info/index.jsx @@ -32,7 +32,10 @@ const EditPipeline = () => { useEffect(() => { initGraph(); getFirstWorkflow(locationParams.id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { const changeSize = () => { if (!graph || graph.get('destroyed')) return; if (!graphRef.current) return; diff --git a/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx b/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx index 2224192c..8b420fff 100644 --- a/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx +++ b/react-ui/src/pages/Pipeline/components/ModelMenu/index.tsx @@ -1,7 +1,7 @@ import { getComponentAll } from '@/services/pipeline/index.js'; import { PipelineNodeModel } from '@/types'; import { to } from '@/utils/promise'; -import { Collapse, type CollapseProps } from 'antd'; +import { Collapse } from 'antd'; import { useEffect, useState } from 'react'; import Styles from './index.less'; @@ -18,7 +18,6 @@ type ModelMenuProps = { }; const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]); - const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]); useEffect(() => { // 获取所有组件 @@ -27,35 +26,6 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { if (res && res.data) { const menus = res.data as ModelMenuData[]; setModelMenusList(menus); - const items = menus.map((item) => { - return { - key: item.key, - label: item.name, - children: item.value.map((ele) => { - return ( - <div - key={ele.id} - draggable="true" - onDragEnd={(e) => { - dragEnd(e, ele); - }} - className={Styles.collapseItem} - > - {ele.icon_path && ( - <img - style={{ height: '16px', marginRight: '15px' }} - src={`/assets/images/${ele.icon_path}.png`} - draggable={false} - alt="" - /> - )} - {ele.component_label} - </div> - ); - }), - }; - }); - setCollapseItems(items); } }; @@ -73,6 +43,35 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { }; const defaultActiveKey = modelMenusList.map((item) => item.key + ''); + const items = modelMenusList.map((item) => { + return { + key: item.key, + label: item.name, + children: item.value.map((ele) => { + return ( + <div + key={ele.id} + draggable="true" + onDragEnd={(e) => { + dragEnd(e, ele); + }} + className={Styles.collapseItem} + > + {ele.icon_path && ( + <img + style={{ height: '16px', marginRight: '15px' }} + src={`/assets/images/${ele.icon_path}.png`} + draggable={false} + alt="" + /> + )} + {ele.component_label} + </div> + ); + }), + }; + }); + return ( <div className={Styles.collapse}> <div className={Styles.modelMenusTitle}>组件库</div> @@ -82,7 +81,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { collapsible="header" expandIconPosition="end" defaultActiveKey={defaultActiveKey} - items={collapseItems} + items={items} ></Collapse> ) : null} </div> diff --git a/react-ui/src/pages/System/User/components/DeptTree.tsx b/react-ui/src/pages/System/User/components/DeptTree.tsx index f839651c..395cf7a0 100644 --- a/react-ui/src/pages/System/User/components/DeptTree.tsx +++ b/react-ui/src/pages/System/User/components/DeptTree.tsx @@ -19,31 +19,37 @@ const DeptTree: React.FC<TreeProps> = (props) => { const [treeData, setTreeData] = useState<any>([]); const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]); - - const fetchDeptList = async () => { - const hide = message.loading('正在查询'); - try { - const res = await getDeptTree({}); - const treeData = res.map((item: any) => ({ ...item, key: item.id })); - setTreeData(treeData); - props.onSelect(treeData[0]); - setExpandedKeys([treeData[0].key]); - setSelectedKeys([treeData[0].key]); - hide(); - return true; - } catch (error) { - hide(); - return false; - } - }; + const { onSelect } = props; useEffect(() => { + const fetchDeptList = async () => { + const hide = message.loading('正在查询'); + try { + const res = await getDeptTree({}); + const treeData = res.map((item: any) => ({ ...item, key: item.id })); + setTreeData(treeData); + setExpandedKeys([treeData[0].key]); + setSelectedKeys([treeData[0].key]); + hide(); + return true; + } catch (error) { + hide(); + return false; + } + }; + fetchDeptList(); }, []); - const onSelect = (keys: React.Key[], info: any) => { + useEffect(() => { + if (treeData.length > 0) { + onSelect(treeData[0]); + } + }, [treeData, onSelect]); + + const handleSelect = (keys: React.Key[], info: any) => { setSelectedKeys(keys); - props.onSelect(info.node); + onSelect(info.node); }; const onExpand = (keys: React.Key[]) => { @@ -57,7 +63,7 @@ const DeptTree: React.FC<TreeProps> = (props) => { onExpand={onExpand} expandedKeys={expandedKeys} selectedKeys={selectedKeys} - onSelect={onSelect} + onSelect={handleSelect} treeData={treeData} /> ); diff --git a/react-ui/src/pages/Tool/Gen/edit.tsx b/react-ui/src/pages/Tool/Gen/edit.tsx index be4d7111..c38bac3b 100644 --- a/react-ui/src/pages/Tool/Gen/edit.tsx +++ b/react-ui/src/pages/Tool/Gen/edit.tsx @@ -114,6 +114,7 @@ const TableList: React.FC = () => { }; useEffect(() => { setStepComponent(getCurrentStepAndComponent(stepKey)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [stepKey]); useEffect(() => { @@ -150,7 +151,7 @@ const TableList: React.FC = () => { message.error(res.msg); } }); - }, []); + }, [tableId]); // const onFinish = (values: any) => { // console.log('Success:', values); From 91cadb5c7c83310f301662953baa7f932c587c25 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Tue, 18 Mar 2025 15:48:11 +0800 Subject: [PATCH 101/127] =?UTF-8?q?refactor:=20=E6=B3=A8=E9=87=8A=20react-?= =?UTF-8?q?hooks/exhaustive-deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- react-ui/src/pages/HyperParameter/Instance/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 2dead784..60cc2142 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -40,7 +40,7 @@ function AutoMLInstance() { closeSSE(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [instanceId]); // 获取实验实例详情 const getExperimentInsInfo = async (isStatusDetermined: boolean) => { diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 5e4c5ab7..93b108db 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -44,7 +44,7 @@ function HyperParameterInstance() { closeSSE(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [instanceId]); // 获取实验实例详情 const getExperimentInsInfo = async (isStatusDetermined: boolean) => { From 64b62830af89648afdd4bcc6e953897f48484eff Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 18 Mar 2025 16:55:10 +0800 Subject: [PATCH 102/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/platform/constant/Constant.java | 8 ++ .../controller/service/ServiceController.java | 2 +- .../ruoyi/platform/domain/DevEnvironment.java | 134 +----------------- .../java/com/ruoyi/platform/domain/Ray.java | 2 + .../ruoyi/platform/domain/ResourceOccupy.java | 2 +- .../ruoyi/platform/domain/ServiceVersion.java | 2 + .../platform/mapper/DevEnvironmentDao.java | 19 +-- .../platform/mapper/ResourceOccupyDao.java | 4 +- .../com/ruoyi/platform/mapper/ServiceDao.java | 2 + .../platform/scheduling/RayInsStatusTask.java | 7 + .../scheduling/ResourceOccupyTask.java | 44 ++++++ .../service/ResourceOccupyService.java | 6 +- .../platform/service/ServiceService.java | 4 +- .../impl/DevEnvironmentServiceImpl.java | 18 +-- .../service/impl/JupyterServiceImpl.java | 16 +-- .../service/impl/RayInsServiceImpl.java | 6 + .../platform/service/impl/RayServiceImpl.java | 111 ++++++++------- .../impl/ResourceOccupyServiceImpl.java | 17 ++- .../service/impl/ServiceServiceImpl.java | 42 ++++-- .../ruoyi/platform/utils/K8sClientUtil.java | 16 +-- .../ruoyi/platform/vo/DevEnvironmentVo.java | 3 + .../vo/serviceVos/ServiceVersionVo.java | 2 + .../DevEnvironmentDaoMapper.xml | 56 ++------ .../managementPlatform/RayDaoMapper.xml | 13 +- .../managementPlatform/RayInsDaoMapper.xml | 2 +- .../managementPlatform/ResourceOccupy.xml | 13 +- .../managementPlatform/ServiceDaoMapper.xml | 5 + 27 files changed, 254 insertions(+), 302 deletions(-) create mode 100644 ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java index fd86febd..80480fca 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java @@ -49,4 +49,12 @@ public class Constant { public final static String Asset_Type_Image = "image"; public final static String Asset_Type_Code = "code"; public final static String Asset_Type_Service = "service"; + + // 任务类型 + public final static String TaskType_Dev = "dev_environment"; + public final static String TaskType_Workflow = "workflow"; + public final static String TaskType_AutoMl = "auto_ml"; + public final static String TaskType_Ray = "ray"; + public final static String TaskType_ActiveLearn = "active_learn"; + public final static String TaskType_Service = "service"; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java index de372f21..11d8f07d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java @@ -61,7 +61,7 @@ public class ServiceController extends BaseController { @PostMapping("/serviceVersion") @ApiOperation("新增服务版本") - public GenericsAjaxResult<ServiceVersion> addServiceVersion(@RequestBody ServiceVersionVo serviceVersionVo) { + public GenericsAjaxResult<ServiceVersion> addServiceVersion(@RequestBody ServiceVersionVo serviceVersionVo) throws Exception { return genericsSuccess(serviceService.addServiceVersion(serviceVersionVo)); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java index cb4ebf3c..90a67719 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/DevEnvironment.java @@ -2,6 +2,7 @@ package com.ruoyi.platform.domain; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; import java.util.Date; import java.io.Serializable; @@ -13,6 +14,7 @@ import java.io.Serializable; * @since 2024-06-03 15:17:37 */ @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@Data public class DevEnvironment implements Serializable { private static final long serialVersionUID = 936999018935545992L; /** @@ -31,6 +33,9 @@ public class DevEnvironment implements Serializable { * 计算资源 */ private String computingResource; + + private Integer computingResourceId; + /** * 资源规格 */ @@ -80,134 +85,5 @@ public class DevEnvironment implements Serializable { */ private Integer state; - - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } - - public String getComputingResource() { - return computingResource; - } - - public void setComputingResource(String computingResource) { - this.computingResource = computingResource; - } - - public String getStandard() { - return standard; - } - - public void setStandard(String standard) { - this.standard = standard; - } - - public String getEnvVariable() { - return envVariable; - } - - public void setEnvVariable(String envVariable) { - this.envVariable = envVariable; - } - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - public String getDataset() { - return dataset; - } - - public void setDataset(String dataset) { - this.dataset = dataset; - } - - public String getModel() { - return model; - } - - public void setModel(String model) { - this.model = model; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getAltField2() { - return altField2; - } - - public void setAltField2(String altField2) { - this.altField2 = altField2; - } - - public String getCreateBy() { - return createBy; - } - - public void setCreateBy(String createBy) { - this.createBy = createBy; - } - - public Date getCreateTime() { - return createTime; - } - - public void setCreateTime(Date createTime) { - this.createTime = createTime; - } - - public String getUpdateBy() { - return updateBy; - } - - public void setUpdateBy(String updateBy) { - this.updateBy = updateBy; - } - - public Date getUpdateTime() { - return updateTime; - } - - public void setUpdateTime(Date updateTime) { - this.updateTime = updateTime; - } - - public Integer getState() { - return state; - } - - public void setState(Integer state) { - this.state = state; - } - } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java index 674c6e34..ebf06de2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/Ray.java @@ -67,6 +67,8 @@ public class Ray { private String resource; + private Integer computingResourceId; + private Integer state; private String createBy; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java index 80bf9b88..4a030c5c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -35,5 +35,5 @@ public class ResourceOccupy { private String taskType; @ApiModelProperty("类型id") - private Integer taskId; + private Long taskId; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ServiceVersion.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ServiceVersion.java index 0ca0b8ed..0afa77cc 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ServiceVersion.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ServiceVersion.java @@ -26,6 +26,8 @@ public class ServiceVersion implements Serializable { private String resource; + private Integer computingResourceId; + private Integer replicas; private String mountPath; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java index e4f4bbe7..0c1df5fe 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DevEnvironmentDao.java @@ -46,24 +46,6 @@ public interface DevEnvironmentDao { */ int insert(@Param("devEnvironment") DevEnvironment devEnvironment); - /** - * 批量新增数据(MyBatis原生foreach方法) - * - * @param entities List<DevEnvironment> 实例对象列表 - * @return 影响行数 - */ - int insertBatch(@Param("entities") List<DevEnvironment> entities); - - /** - * 批量新增或按主键更新数据(MyBatis原生foreach方法) - * - * @param entities List<DevEnvironment> 实例对象列表 - * - * @return 影响行数 - * @throws org.springframework.jdbc.BadSqlGrammarException 入参是空List的时候会抛SQL语句错误的异常,请自行校验入参 - */ - int insertOrUpdateBatch(@Param("entities") List<DevEnvironment> entities); - /** * 修改数据 * @@ -80,5 +62,6 @@ public interface DevEnvironmentDao { */ int deleteById(Integer id); + List<DevEnvironment> getRunning(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index e434f494..7f3cf4a4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -11,5 +11,7 @@ public interface ResourceOccupyDao { int edit(@Param("resourceOccupy") ResourceOccupy resourceOccupy); - ResourceOccupy getResourceOccupyById(@Param("id") Integer id); + ResourceOccupy getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId); + + int deduceCredit(@Param("credit") Float credit, @Param("userId") Long userId); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ServiceDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ServiceDao.java index b359812c..6b65a33d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ServiceDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ServiceDao.java @@ -36,4 +36,6 @@ public interface ServiceDao { Service getServiceByName(@Param("serviceName") String serviceName); ServiceVersion getSvByVersion(@Param("version") String version, @Param("serviceId") Long serviceId); + + List<ServiceVersion> getRunning(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java index 23d03164..e0327431 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; +import com.ruoyi.platform.service.ResourceOccupyService; import org.apache.commons.lang3.StringUtils; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -25,6 +26,9 @@ public class RayInsStatusTask { @Resource private RayDao rayDao; + @Resource + private ResourceOccupyService resourceOccupyService; + private List<Long> rayIds = new ArrayList<>(); @Scheduled(cron = "0/30 * * * * ?") // 每30S执行一次 @@ -38,6 +42,9 @@ public class RayInsStatusTask { //当原本状态为null或非终止态时才调用argo接口 try { rayIns = rayInsService.queryStatusFromArgo(rayIns); + if (Constant.Running.equals(rayIns.getStatus())) { + resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId()); + } } catch (Exception e) { rayIns.setStatus(Constant.Failed); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java new file mode 100644 index 00000000..1ae0d2f1 --- /dev/null +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -0,0 +1,44 @@ +package com.ruoyi.platform.scheduling; + +import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.DevEnvironment; +import com.ruoyi.platform.domain.ServiceVersion; +import com.ruoyi.platform.mapper.DevEnvironmentDao; +import com.ruoyi.platform.mapper.ServiceDao; +import com.ruoyi.platform.service.ResourceOccupyService; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +@Component() +public class ResourceOccupyTask { + + @Resource + private ResourceOccupyService resourceOccupyService; + + @Resource + private DevEnvironmentDao devEnvironmentDao; + + @Resource + private ServiceDao serviceDao; + + // 开发环境功能扣除积分 + @Scheduled(cron = "0 0/10 * * * ?") // 每10分钟执行一次 + public void devDeduceCredit() { + List<DevEnvironment> devEnvironments = devEnvironmentDao.getRunning(); + for (DevEnvironment devEnvironment : devEnvironments) { + resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId())); + } + } + + // 服务功能扣除积分 + @Scheduled(cron = "0 0/10 * * * ?") // 每10分钟执行一次 + public void serviceDeduceCredit() { + List<ServiceVersion> serviceVersions = serviceDao.getRunning(); + for (ServiceVersion serviceVersion : serviceVersions) { + resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId()); + } + } +} diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 1f50706d..f8aed04b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -4,10 +4,10 @@ public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; - void startDeduce(Integer computingResourceId); + void startDeduce(Integer computingResourceId, String taskType, Long taskId); - void endDeduce(Integer id); + void endDeduce(String taskType, Long taskId); - void deducing(); + void deducing(String taskType, Long taskId); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java index 9e3dd48d..947a4a6d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java @@ -18,7 +18,7 @@ public interface ServiceService { Service addService(Service service); - ServiceVersion addServiceVersion(ServiceVersionVo serviceVersionVo); + ServiceVersion addServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception; Service editService(Service service); @@ -34,7 +34,7 @@ public interface ServiceService { String deleteServiceVersion(Long id); - String runServiceVersion(Long id); + String runServiceVersion(Long id) throws Exception; String stopServiceVersion(Long id); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java index 606d35aa..0180d88d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java @@ -10,18 +10,16 @@ import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.vo.DevEnvironmentVo; import com.ruoyi.platform.vo.PodStatusVo; import com.ruoyi.system.api.model.LoginUser; -import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; import java.util.List; -import java.util.Map; /** * (DevEnvironment)表服务实现类 @@ -54,7 +52,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { * 分页查询 * * @param devEnvironment 筛选条件 - * @param pageRequest 分页对象 + * @param pageRequest 分页对象 * @return 查询结果 */ @Override @@ -64,12 +62,15 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { //查询每个开发环境的pod状态,注意:只有pod为非终止态时才去调状态接口 devEnvironmentList.forEach(devEnv -> { - try{ + try { if (!devEnv.getStatus().equals(PodStatus.Terminated.getName()) && !devEnv.getStatus().equals(PodStatus.Failed.getName())) { PodStatusVo podStatusVo = this.jupyterService.getJupyterStatus(devEnv); devEnv.setStatus(podStatusVo.getStatus()); devEnv.setUrl(podStatusVo.getUrl()); + if(!devEnv.getStatus().equals(podStatusVo.getStatus())){ + this.devEnvironmentDao.update(devEnv); + } } } catch (Exception e) { devEnv.setStatus(PodStatus.Unknown.getName()); @@ -94,6 +95,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { //状态先设为未知 devEnvironment.setStatus("Unknown"); devEnvironment.setComputingResource(devEnvironmentVo.getComputingResource()); + devEnvironment.setComputingResourceId(devEnvironmentVo.getComputingResourceId()); devEnvironment.setStandard(devEnvironmentVo.getStandard()); devEnvironment.setEnvVariable(devEnvironmentVo.getEnvVariable()); devEnvironment.setImage(devEnvironmentVo.getImage()); @@ -140,7 +142,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { @Override public String removeById(Integer id) throws Exception { DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); - if (devEnvironment == null){ + if (devEnvironment == null) { return "开发环境信息不存在"; } @@ -148,13 +150,13 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { LoginUser loginUser = SecurityUtils.getLoginUser(); String username = loginUser.getUsername(); String createdBy = devEnvironment.getCreateBy(); - if (!(StringUtils.equals(username,"admin") || StringUtils.equals(username,createdBy))){ + if (!(StringUtils.equals(username, "admin") || StringUtils.equals(username, createdBy))) { return "无权限删除该开发环境"; } jupyterService.stopJupyterService(id); devEnvironment.setState(0); - return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; + return this.devEnvironmentDao.update(devEnvironment) > 0 ? "删除成功" : "删除失败"; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 02cb59a2..c37a920f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -9,6 +9,7 @@ import com.ruoyi.platform.mapper.ComputingResourceDao; import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.service.DevEnvironmentService; import com.ruoyi.platform.service.JupyterService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.utils.K8sClientUtil; import com.ruoyi.platform.utils.MinioUtil; @@ -56,9 +57,6 @@ public class JupyterServiceImpl implements JupyterService { @Resource private DevEnvironmentDao devEnvironmentDao; - @Resource - private ComputingResourceDao computingResourceDao; - @Resource @Lazy private DevEnvironmentService devEnvironmentService; @@ -66,6 +64,9 @@ public class JupyterServiceImpl implements JupyterService { @Resource private RedisService redisService; + @Resource + private ResourceOccupyService resourceOccupyService; + public JupyterServiceImpl(MinioUtil minioUtil) { this.minioUtil = minioUtil; } @@ -109,7 +110,7 @@ public class JupyterServiceImpl implements JupyterService { Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, null, devEnvironment, minioPvcName, datasetPath, modelPath); String url = masterIp + ":" + podPort; redisService.setCacheObject(podName, masterIp + ":" + podPort); - devEnvironment.setStatus("Pending"); + devEnvironment.setStatus(Constant.Pending); devEnvironment.setUrl(url); this.devEnvironmentService.update(devEnvironment); return url; @@ -133,16 +134,15 @@ public class JupyterServiceImpl implements JupyterService { return "pod不存在!"; } - if (Constant.Computing_Resource_GPU.equals(devEnvironment.getComputingResource())) { - computingResourceDao.updateUsedStateByNode(pod.getSpec().getNodeName(), Constant.Used_State_unused); - } + // 结束扣积分 + resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id)); // 使用 Kubernetes API 删除 Pod String deleteResult = k8sClientUtil.deletePod(podName, namespace); // 删除service k8sClientUtil.deleteService(svcName, namespace); - devEnvironment.setStatus("Terminated"); + devEnvironment.setStatus(Constant.Terminated); this.devEnvironmentService.update(devEnvironment); return deleteResult + ",编辑器已停止"; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 0f6b7412..2ae6a5de 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.RayIns; import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.*; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -48,6 +49,9 @@ public class RayInsServiceImpl implements RayInsService { @Resource private RayDao rayDao; + @Resource + private ResourceOccupyService resourceOccupyService; + @Resource private MinioUtil minioUtil; @@ -163,6 +167,8 @@ public class RayInsServiceImpl implements RayInsService { ins.setUpdateTime(new Date()); rayInsDao.update(ins); updateRayStatus(rayIns.getRayId()); + // 结束扣积分 + resourceOccupyService.endDeduce(Constant.TaskType_Ray, id); return true; } else { return false; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 67f30d29..1050499f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -10,6 +10,7 @@ import com.ruoyi.platform.mapper.RayDao; import com.ruoyi.platform.mapper.RayInsDao; import com.ruoyi.platform.service.RayInsService; import com.ruoyi.platform.service.RayService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.HttpUtils; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.utils.JsonUtils; @@ -42,8 +43,8 @@ public class RayServiceImpl implements RayService { @Value("${argo.workflowRun}") private String argoWorkflowRun; - @Value("${minio.endpoint}") - private String minioEndpoint; + @Resource + private ResourceOccupyService resourceOccupyService; @Resource private RayDao rayDao; @@ -151,58 +152,62 @@ public class RayServiceImpl implements RayService { throw new Exception("自动超参数寻优配置不存在"); } - RayParamVo rayParamVo = new RayParamVo(); - BeanUtils.copyProperties(ray, rayParamVo); - rayParamVo.setCodeConfig(JsonUtils.jsonToMap(ray.getCodeConfig())); - rayParamVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); - rayParamVo.setModel(JsonUtils.jsonToMap(ray.getModel())); - rayParamVo.setImage(JsonUtils.jsonToMap(ray.getImage())); - String param = JsonUtils.objectToJson(rayParamVo); - - // 调argo转换接口 - try { - String convertRes = HttpUtils.sendPost(argoUrl + convertRay, param); - if (convertRes == null || StringUtils.isEmpty(convertRes)) { - throw new RuntimeException("转换流水线失败"); - } - Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes); - // 组装运行接口json - Map<String, Object> output = (Map<String, Object>) converMap.get("output"); - Map<String, Object> runReqMap = new HashMap<>(); - runReqMap.put("data", converMap.get("data")); - // 调argo运行接口 - String runRes = HttpUtils.sendPost(argoUrl + argoWorkflowRun, JsonUtils.mapToJson(runReqMap)); - if (runRes == null || StringUtils.isEmpty(runRes)) { - throw new RuntimeException("Failed to run workflow."); - } - Map<String, Object> runResMap = JsonUtils.jsonToMap(runRes); - Map<String, Object> data = (Map<String, Object>) runResMap.get("data"); - //判断data为空 - if (data == null || MapUtils.isEmpty(data)) { - throw new RuntimeException("Failed to run workflow."); + // 记录开始扣积分 + if (resourceOccupyService.haveResource(ray.getComputingResourceId())) { + RayParamVo rayParamVo = new RayParamVo(); + BeanUtils.copyProperties(ray, rayParamVo); + rayParamVo.setCodeConfig(JsonUtils.jsonToMap(ray.getCodeConfig())); + rayParamVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); + rayParamVo.setModel(JsonUtils.jsonToMap(ray.getModel())); + rayParamVo.setImage(JsonUtils.jsonToMap(ray.getImage())); + String param = JsonUtils.objectToJson(rayParamVo); + + // 调argo转换接口 + try { + String convertRes = HttpUtils.sendPost(argoUrl + convertRay, param); + if (convertRes == null || StringUtils.isEmpty(convertRes)) { + throw new RuntimeException("转换流水线失败"); + } + Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes); + // 组装运行接口json + Map<String, Object> output = (Map<String, Object>) converMap.get("output"); + Map<String, Object> runReqMap = new HashMap<>(); + runReqMap.put("data", converMap.get("data")); + // 调argo运行接口 + String runRes = HttpUtils.sendPost(argoUrl + argoWorkflowRun, JsonUtils.mapToJson(runReqMap)); + if (runRes == null || StringUtils.isEmpty(runRes)) { + throw new RuntimeException("运行失败"); + } + Map<String, Object> runResMap = JsonUtils.jsonToMap(runRes); + Map<String, Object> data = (Map<String, Object>) runResMap.get("data"); + //判断data为空 + if (data == null || MapUtils.isEmpty(data)) { + throw new RuntimeException("运行失败"); + } + Map<String, Object> metadata = (Map<String, Object>) data.get("metadata"); + + // 插入记录到实验实例表 + RayIns rayIns = new RayIns(); + rayIns.setRayId(ray.getId()); + rayIns.setArgoInsNs((String) metadata.get("namespace")); + rayIns.setArgoInsName((String) metadata.get("name")); + rayIns.setParam(param); + rayIns.setStatus(Constant.Pending); + //替换argoInsName + String outputString = JsonUtils.mapToJson(output); + rayIns.setNodeResult(outputString.replace("{{workflow.name}}", (String) metadata.get("name"))); + + Map<String, Object> param_output = (Map<String, Object>) output.get("param_output"); + List output1 = (ArrayList) param_output.values().toArray()[0]; + Map<String, String> output2 = (Map<String, String>) output1.get(0); + String outputPath = output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/hpo"; + rayIns.setResultPath(outputPath); + rayInsDao.insert(rayIns); + rayInsService.updateRayStatus(id); + resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, rayIns.getId()); + } catch (Exception e) { + throw new RuntimeException(e); } - Map<String, Object> metadata = (Map<String, Object>) data.get("metadata"); - - // 插入记录到实验实例表 - RayIns rayIns = new RayIns(); - rayIns.setRayId(ray.getId()); - rayIns.setArgoInsNs((String) metadata.get("namespace")); - rayIns.setArgoInsName((String) metadata.get("name")); - rayIns.setParam(param); - rayIns.setStatus(Constant.Pending); - //替换argoInsName - String outputString = JsonUtils.mapToJson(output); - rayIns.setNodeResult(outputString.replace("{{workflow.name}}", (String) metadata.get("name"))); - - Map<String, Object> param_output = (Map<String, Object>) output.get("param_output"); - List output1 = (ArrayList) param_output.values().toArray()[0]; - Map<String, String> output2 = (Map<String, String>) output1.get(0); - String outputPath = output2.get("path").replace("{{workflow.name}}", (String) metadata.get("name")) + "/hpo"; - rayIns.setResultPath(outputPath); - rayInsDao.insert(rayIns); - rayInsService.updateRayStatus(id); - } catch (Exception e) { - throw new RuntimeException(e); } return "执行成功"; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 3cbbfe0b..3809629f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -45,27 +45,34 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public void startDeduce(Integer computingResourceId) { + public void startDeduce(Integer computingResourceId, String taskType, Long taskId) { ResourceOccupy resourceOccupy = new ResourceOccupy(); ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); resourceOccupy.setComputingResourceId(computingResourceId); LoginUser loginUser = SecurityUtils.getLoginUser(); resourceOccupy.setUserId(loginUser.getUserid()); resourceOccupy.setCreditPerHour(computingResource.getCreditPerHour()); + resourceOccupy.setTaskType(taskType); + resourceOccupy.setTaskId(taskId); resourceOccupyDao.save(resourceOccupy); } @Override - public void endDeduce(Integer id) { - ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyById(id); + public void endDeduce(String taskType, Long taskId) { + ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId); + deducing(taskType, taskId); resourceOccupy.setState(Constant.State_invalid); - deducing(); resourceOccupy.setDeduceLastTime(new Date()); resourceOccupyDao.edit(resourceOccupy); } @Override - public void deducing() { + public void deducing(String taskType, Long taskId) { + ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId); + long timeDifferenceMillis = new Date().getTime() - resourceOccupy.getDeduceLastTime().getTime(); + Float hours = (float) (timeDifferenceMillis / (1000 * 60 * 60)); + float deduceCredit = resourceOccupy.getCreditPerHour() * hours; + resourceOccupyDao.deduceCredit(deduceCredit, resourceOccupy.getUserId()); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 0244bba6..8e613098 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -8,6 +8,7 @@ import com.ruoyi.platform.domain.AssetWorkflow; import com.ruoyi.platform.domain.ServiceVersion; import com.ruoyi.platform.mapper.AssetWorkflowDao; import com.ruoyi.platform.mapper.ServiceDao; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.service.ServiceService; import com.ruoyi.platform.utils.ConvertUtil; import com.ruoyi.platform.utils.HttpUtils; @@ -45,6 +46,9 @@ public class ServiceServiceImpl implements ServiceService { @Resource private AssetWorkflowDao assetWorkflowDao; + @Resource + private ResourceOccupyService resourceOccupyService; + @Override public Page<com.ruoyi.platform.domain.Service> queryByPageService(com.ruoyi.platform.domain.Service service, PageRequest pageRequest) { long total = serviceDao.countService(service); @@ -110,7 +114,7 @@ public class ServiceServiceImpl implements ServiceService { } @Override - public ServiceVersion addServiceVersion(ServiceVersionVo serviceVersionVo) { + public ServiceVersion addServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception { ServiceVersion svByVersion = serviceDao.getSvByVersion(serviceVersionVo.getVersion(), serviceVersionVo.getServiceId()); if (svByVersion != null) { throw new RuntimeException("服务版本已存在,无法新增"); @@ -234,7 +238,7 @@ public class ServiceServiceImpl implements ServiceService { } @Override - public String runServiceVersion(Long id) { + public String runServiceVersion(Long id) throws Exception { ServiceVersion serviceVersion = serviceDao.getServiceVersionById(id); com.ruoyi.platform.domain.Service service = serviceDao.getServiceById(serviceVersion.getServiceId()); HashMap<String, Object> paramMap = new HashMap<>(); @@ -249,23 +253,29 @@ public class ServiceServiceImpl implements ServiceService { paramMap.put("model", JSONObject.parseObject(serviceVersion.getModel())); paramMap.put("service_type", service.getServiceType()); paramMap.put("deploy_type", serviceVersion.getDeployType()); - String req = HttpUtils.sendPost(argoUrl + modelService + "/create", JSON.toJSONString(paramMap)); - if (StringUtils.isNotEmpty(req)) { - Map<String, Object> reqMap = JacksonUtil.parseJSONStr2Map(req); - if ((Integer) reqMap.get("code") == 200) { - Map<String, String> data = (Map<String, String>) reqMap.get("data"); - serviceVersion.setUrl(data.get("url")); - serviceVersion.setDeploymentName(data.get("deployment_name")); - serviceVersion.setSvcName(data.get("svc_name")); - serviceVersion.setRunState(Constant.Pending); - serviceDao.updateServiceVersion(serviceVersion); - return "启动成功"; + + // 记录开始扣积分 + if (resourceOccupyService.haveResource(serviceVersion.getComputingResourceId())) { + String req = HttpUtils.sendPost(argoUrl + modelService + "/create", JSON.toJSONString(paramMap)); + if (StringUtils.isNotEmpty(req)) { + Map<String, Object> reqMap = JacksonUtil.parseJSONStr2Map(req); + if ((Integer) reqMap.get("code") == 200) { + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId()); + Map<String, String> data = (Map<String, String>) reqMap.get("data"); + serviceVersion.setUrl(data.get("url")); + serviceVersion.setDeploymentName(data.get("deployment_name")); + serviceVersion.setSvcName(data.get("svc_name")); + serviceVersion.setRunState(Constant.Pending); + serviceDao.updateServiceVersion(serviceVersion); + return "启动成功"; + } else { + throw new RuntimeException("启动失败"); + } } else { throw new RuntimeException("启动失败"); } - } else { - throw new RuntimeException("启动失败"); } + return "启动失败"; } @Override @@ -277,6 +287,8 @@ public class ServiceServiceImpl implements ServiceService { if (StringUtils.isNotEmpty(req)) { serviceVersion.setRunState(Constant.Stopped); serviceDao.updateServiceVersion(serviceVersion); + // 结束扣积分 + resourceOccupyService.endDeduce(Constant.TaskType_Service, id); return "停止成功"; } else { throw new RuntimeException("停止失败"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index 17d36177..a3b73160 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -3,7 +3,7 @@ package com.ruoyi.platform.utils; import com.alibaba.fastjson2.JSON; import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.DevEnvironment; -import com.ruoyi.platform.mapper.ComputingResourceDao; +import com.ruoyi.platform.service.ResourceOccupyService; import io.kubernetes.client.Exec; import io.kubernetes.client.custom.IntOrString; import io.kubernetes.client.custom.Quantity; @@ -12,9 +12,7 @@ import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.AppsV1Api; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.*; -import io.kubernetes.client.util.ClientBuilder; import io.kubernetes.client.util.Config; -import io.kubernetes.client.util.credentials.AccessTokenAuthentication; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.json.JSONObject; @@ -50,7 +48,7 @@ public class K8sClientUtil { private static ApiClient apiClient; @Resource - private ComputingResourceDao computingResourceDao; + private ResourceOccupyService resourceOccupyService; /** * 构建集群POD内通过SA访问的客户端 @@ -509,10 +507,12 @@ public class K8sClientUtil { .build(); try { - pod = api.createNamespacedPod(namespace, pod, null, null, null); - String nodeName = getNodeName(podName, namespace); - if (Constant.Computing_Resource_GPU.equals(devEnvironment.getComputingResource())) { - computingResourceDao.updateUsedStateByNode(nodeName, Constant.Used_State_used); + // 记录开始扣积分 + if (resourceOccupyService.haveResource(devEnvironment.getComputingResourceId())) { + pod = api.createNamespacedPod(namespace, pod, null, null, null); + String nodeName = getNodeName(podName, namespace); + + resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId())); } } catch (ApiException e) { throw new RuntimeException("创建pod异常:" + e.getResponseBody()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/DevEnvironmentVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/DevEnvironmentVo.java index 14089f14..0d019417 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/DevEnvironmentVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/DevEnvironmentVo.java @@ -19,6 +19,9 @@ public class DevEnvironmentVo implements Serializable { * 计算资源 */ private String computingResource; + + private Integer computingResourceId; + /** * 资源规格 */ diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/serviceVos/ServiceVersionVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/serviceVos/ServiceVersionVo.java index 2cf03f47..d1cb79cd 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/serviceVos/ServiceVersionVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/serviceVos/ServiceVersionVo.java @@ -28,6 +28,8 @@ public class ServiceVersionVo { private String resource; + private Integer computingResourceId; + private Integer replicas; private String mountPath; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml index cfab27a7..82e92a56 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml @@ -1,38 +1,18 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.DevEnvironmentDao"> - - <resultMap type="com.ruoyi.platform.domain.DevEnvironment" id="DevEnvironmentMap"> - <result property="id" column="id" jdbcType="INTEGER"/> - <result property="name" column="name" jdbcType="VARCHAR"/> - <result property="status" column="status" jdbcType="VARCHAR"/> - <result property="computingResource" column="computing_resource" jdbcType="VARCHAR"/> - <result property="standard" column="standard" jdbcType="VARCHAR"/> - <result property="envVariable" column="env_variable" jdbcType="VARCHAR"/> - <result property="image" column="image" jdbcType="VARCHAR"/> - <result property="dataset" column="dataset" jdbcType="VARCHAR"/> - <result property="model" column="model" jdbcType="VARCHAR"/> - <result property="url" column="url" jdbcType="VARCHAR"/> - <result property="altField2" column="alt_field2" jdbcType="VARCHAR"/> - <result property="createBy" column="create_by" jdbcType="VARCHAR"/> - <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> - <result property="updateBy" column="update_by" jdbcType="VARCHAR"/> - <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/> - <result property="state" column="state" jdbcType="INTEGER"/> - </resultMap> - <!--查询单个--> - <select id="queryById" resultMap="DevEnvironmentMap"> + <select id="queryById" resultType="com.ruoyi.platform.domain.DevEnvironment"> select - id,name,status,computing_resource,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state + id,name,status,computing_resource,computing_resource_id, standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state from dev_environment where id = #{id} and state = 1 </select> <!--查询指定行数据--> - <select id="queryAllByLimit" resultMap="DevEnvironmentMap"> + <select id="queryAllByLimit" resultType="com.ruoyi.platform.domain.DevEnvironment"> select - id,name,status,computing_resource,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state + id,name,status,computing_resource,computing_resource_id,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state from dev_environment <where> state = 1 @@ -146,12 +126,17 @@ </where> </select> + <select id="getRunning" resultType="com.ruoyi.platform.domain.DevEnvironment"> + select * from dev_environment where state = 1 and status = 'Running' + </select> + <!--新增所有列--> <insert id="insert" keyProperty="id" useGeneratedKeys="true"> - insert into dev_environment(name,status,computing_resource,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state) + insert into dev_environment(name,status,computing_resource,computing_resource_id,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state) values (#{devEnvironment.name}, #{devEnvironment.status}, #{devEnvironment.computingResource}, + #{devEnvironment.computingResourceId}, #{devEnvironment.standard}, #{devEnvironment.envVariable}, #{devEnvironment.image}, @@ -167,24 +152,6 @@ ) </insert> - <insert id="insertBatch" keyProperty="id" useGeneratedKeys="true"> - insert into dev_environment(name,status,computing_resource,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state ) - values - <foreach collection="entities" item="entity" separator=","> - (#{entity.name},#{entity.status},#{entity.computingResource},#{entity.standard},#{entity.envVariable},#{entity.image},#{entity.dataset},#{entity.model},#{entity.url},#{entity.altField2},#{entity.createBy},#{entity.createTime},#{entity.updateBy},#{entity.updateTime},#{entity.state}) - </foreach> - </insert> - - <insert id="insertOrUpdateBatch" keyProperty="id" useGeneratedKeys="true"> - insert into dev_environment(name,status,computing_resource,standard,env_variable,image,dataset,model,url,alt_field2,create_by,create_time,update_by,update_time,state) - values - <foreach collection="entities" item="entity" separator=","> - (#{entity.name}#{entity.status}#{entity.computingResource}#{entity.standard}#{entity.envVariable}#{entity.image}#{entity.dataset}#{entity.model}#{entity.url}#{entity.altField2}#{entity.createBy}#{entity.createTime}#{entity.updateBy}#{entity.updateTime}#{entity.state}) - </foreach> - on duplicate key update -name = values(name)status = values(status)computing_resource = values(computing_resource)standard = values(standard)env_variable = values(env_variable)image = values(image)dataset = values(dataset)model = values(model)url = values(url)alt_field2 = values(alt_field2)create_by = values(create_by)create_time = values(create_time)update_by = values(update_by)update_time = values(update_time)state = values(state) - </insert> - <!--通过主键修改数据--> <update id="update"> update dev_environment @@ -198,6 +165,9 @@ name = values(name)status = values(status)computing_resource = values(computing_ <if test="devEnvironment.computingResource != null and devEnvironment.computingResource != ''"> computing_resource = #{devEnvironment.computingResource}, </if> + <if test="devEnvironment.computingResourceId != null and devEnvironment.computingResourceId != ''"> + computing_resource_id = #{devEnvironment.computingResourceId}, + </if> <if test="devEnvironment.standard != null and devEnvironment.standard != ''"> standard = #{devEnvironment.standard}, </if> diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml index 236141c4..72aebb6e 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayDaoMapper.xml @@ -2,13 +2,15 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.RayDao"> <insert id="save"> - insert into ray(name, description, dataset, model, code_config, main_py, num_samples, parameters, points_to_evaluate, storage_path, + insert into ray(name, description, dataset, model, code_config, main_py, num_samples, parameters, + points_to_evaluate, storage_path, search_alg, scheduler, metric, mode, max_t, - min_samples_required, resource, image, create_by, update_by) - values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.model}, #{ray.codeConfig}, #{ray.mainPy}, #{ray.numSamples}, #{ray.parameters}, + min_samples_required, resource, computing_resource_id, image, create_by, update_by) + values (#{ray.name}, #{ray.description}, #{ray.dataset}, #{ray.model}, #{ray.codeConfig}, #{ray.mainPy}, + #{ray.numSamples}, #{ray.parameters}, #{ray.pointsToEvaluate}, #{ray.storagePath}, #{ray.searchAlg}, #{ray.scheduler}, #{ray.metric}, #{ray.mode}, #{ray.maxT}, #{ray.minSamplesRequired}, - #{ray.resource}, #{ray.image}, #{ray.createBy}, #{ray.updateBy}) + #{ray.resource}, #{ray.computingResourceId}, #{ray.image}, #{ray.createBy}, #{ray.updateBy}) </insert> <update id="edit"> @@ -68,6 +70,9 @@ <if test="ray.resource != null"> resource = #{ray.resource}, </if> + <if test="ray.computingResourceId != null"> + computing_resource_id = #{ray.computingResourceId}, + </if> <if test="ray.updateBy != null and ray.updateBy !=''"> update_by = #{ray.updateBy}, </if> diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml index 2b9c12ac..2a5a3c20 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/RayInsDaoMapper.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.RayInsDao"> - <insert id="insert"> + <insert id="insert" keyProperty="id" useGeneratedKeys="true"> insert into ray_ins(ray_id, result_path, argo_ins_name, argo_ins_ns, node_status, node_result, param, source, status) values (#{rayIns.rayId}, #{rayIns.resultPath}, #{rayIns.argoInsName}, #{rayIns.argoInsNs}, diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 5a7d9cc6..6b87f09c 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -19,13 +19,22 @@ where id = #{resourceOccupy.id} </update> + <update id="deduceCredit"> + update sys_user + set credit = credit - #{credit} + where user_id = #{userId} + </update> + <select id="haveResource" resultType="java.lang.Boolean"> select case when used + #{need} <= total then TRUE else FALSE end from resource where id = #{id} </select> - <select id="getResourceOccupyById" resultType="com.ruoyi.platform.domain.ResourceOccupy"> - select * from resource_occupy where id = #{id} + <select id="getResourceOccupyByTask" resultType="com.ruoyi.platform.domain.ResourceOccupy"> + select * + from resource_occupy + where task_type = #{task_type} + and task_id = #{task_id} </select> </mapper> \ No newline at end of file diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml index cfa98aa1..bec944af 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml @@ -84,6 +84,11 @@ where service_id = #{serviceId} and version = #{version} and state = 1 </select> + <select id="getRunning" resultType="com.ruoyi.platform.domain.ServiceVersion"> + select * + from service_version where state = 1 and status = 'Running' + </select> + <insert id="insertService" keyProperty="id" useGeneratedKeys="true"> insert into service(service_name, service_type, description, create_by, update_by) values (#{service.serviceName}, #{service.serviceType}, #{service.description}, #{service.createBy}, #{service.updateBy}) From 7bbe6835749d7da54d381c65bb1db890fbea1dc9 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Tue, 18 Mar 2025 17:56:07 +0800 Subject: [PATCH 103/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevEnvironmentController.java | 4 ++-- .../controller/service/ServiceController.java | 2 +- .../platform/mapper/ResourceOccupyDao.java | 4 ++++ .../platform/service/DevEnvironmentService.java | 4 ++-- .../ruoyi/platform/service/ServiceService.java | 2 +- .../service/impl/DevEnvironmentServiceImpl.java | 13 +++++++++++-- .../platform/service/impl/RayServiceImpl.java | 8 ++++++++ .../service/impl/ResourceOccupyServiceImpl.java | 17 ++++++++++++++++- .../service/impl/ServiceServiceImpl.java | 8 +++++++- .../main/java/com/ruoyi/platform/vo/RayVo.java | 2 ++ .../managementPlatform/ResourceOccupy.xml | 12 ++++++++++++ 11 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java index 0a3b608d..f7451a23 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/devEnvironment/DevEnvironmentController.java @@ -61,7 +61,7 @@ public class DevEnvironmentController extends BaseController { * @return 新增结果 */ @PostMapping - public GenericsAjaxResult<DevEnvironment> add(@RequestBody DevEnvironmentVo devEnvironmentVo) { + public GenericsAjaxResult<DevEnvironment> add(@RequestBody DevEnvironmentVo devEnvironmentVo) throws Exception { return genericsSuccess(this.devEnvironmentService.insert(devEnvironmentVo)); } @@ -72,7 +72,7 @@ public class DevEnvironmentController extends BaseController { * @return 编辑结果 */ @PutMapping - public GenericsAjaxResult<DevEnvironment> edit(@RequestBody DevEnvironment devEnvironment) { + public GenericsAjaxResult<DevEnvironment> edit(@RequestBody DevEnvironment devEnvironment) throws Exception { return genericsSuccess(this.devEnvironmentService.update(devEnvironment)); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java index 11d8f07d..e18d0899 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/service/ServiceController.java @@ -73,7 +73,7 @@ public class ServiceController extends BaseController { @PutMapping("/serviceVersion") @ApiOperation("编辑服务版本") - public GenericsAjaxResult<String> editServiceVersion(@RequestBody ServiceVersionVo serviceVersionVo) { + public GenericsAjaxResult<String> editServiceVersion(@RequestBody ServiceVersionVo serviceVersionVo) throws Exception { return genericsSuccess(serviceService.editServiceVersion(serviceVersionVo)); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index 7f3cf4a4..8a92d4b2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -14,4 +14,8 @@ public interface ResourceOccupyDao { ResourceOccupy getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId); int deduceCredit(@Param("credit") Float credit, @Param("userId") Long userId); + + int updateUsed(@Param("id") Integer id, @Param("used") Integer used); + + int updateUnUsed(@Param("id") Integer id, @Param("used") Integer used); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java index 4562c7d4..4b6723ab 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DevEnvironmentService.java @@ -36,7 +36,7 @@ public interface DevEnvironmentService { * @param devEnvironment 实例对象 * @return 实例对象 */ - DevEnvironment insert(DevEnvironmentVo devEnvironmentVo); + DevEnvironment insert(DevEnvironmentVo devEnvironmentVo) throws Exception; /** * 修改数据 @@ -44,7 +44,7 @@ public interface DevEnvironmentService { * @param devEnvironment 实例对象 * @return 实例对象 */ - DevEnvironment update(DevEnvironment devEnvironment); + DevEnvironment update(DevEnvironment devEnvironment) throws Exception; /** * 通过主键删除数据 diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java index 947a4a6d..12b34188 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java @@ -22,7 +22,7 @@ public interface ServiceService { Service editService(Service service); - String editServiceVersion(ServiceVersionVo serviceVersionVo); + String editServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception; Service getService(Long id); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java index 0180d88d..6488b967 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java @@ -6,6 +6,7 @@ import com.ruoyi.platform.domain.PodStatus; import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.service.DevEnvironmentService; import com.ruoyi.platform.service.JupyterService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.vo.DevEnvironmentVo; import com.ruoyi.platform.vo.PodStatusVo; @@ -36,6 +37,8 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { @Lazy private JupyterService jupyterService; + @Resource + private ResourceOccupyService resourceOccupyService; /** * 通过ID查询单条数据 @@ -87,7 +90,10 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { * @return 实例对象 */ @Override - public DevEnvironment insert(DevEnvironmentVo devEnvironmentVo) { + public DevEnvironment insert(DevEnvironmentVo devEnvironmentVo) throws Exception { + // 判断是否有资源 + resourceOccupyService.haveResource(devEnvironmentVo.getComputingResourceId()); + //插入预备,此时不需要判断版本重复 DevEnvironment devEnvironment = new DevEnvironment(); LoginUser loginUser = SecurityUtils.getLoginUser(); @@ -120,7 +126,10 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { * @return 实例对象 */ @Override - public DevEnvironment update(DevEnvironment devEnvironment) { + public DevEnvironment update(DevEnvironment devEnvironment) throws Exception { + // 判断是否有资源 + resourceOccupyService.haveResource(devEnvironment.getComputingResourceId()); + LoginUser loginUser = SecurityUtils.getLoginUser(); devEnvironment.setUpdateBy(loginUser.getUsername()); devEnvironment.setUpdateTime(new Date()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 1050499f..8ea5d473 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -66,6 +66,10 @@ public class RayServiceImpl implements RayService { if (rayByName != null) { throw new RuntimeException("实验名称已存在"); } + + // 判断是否有资源 + resourceOccupyService.haveResource(rayVo.getComputingResourceId()); + Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); String username = SecurityUtils.getLoginUser().getUsername(); @@ -87,6 +91,10 @@ public class RayServiceImpl implements RayService { if (oldRay != null && !oldRay.getId().equals(rayVo.getId())) { throw new RuntimeException("实验名称已存在"); } + + // 判断是否有资源 + resourceOccupyService.haveResource(rayVo.getComputingResourceId()); + Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); ray.setUpdateBy(SecurityUtils.getLoginUser().getUsername()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 3809629f..ea0f12c7 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -55,6 +55,12 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { resourceOccupy.setTaskType(taskType); resourceOccupy.setTaskId(taskId); resourceOccupyDao.save(resourceOccupy); + + if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { + resourceOccupyDao.updateUsed(computingResource.getResourceId(), computingResource.getGpuNums()); + } else { + resourceOccupyDao.updateUsed(computingResource.getResourceId(), computingResource.getCpuCores()); + } } @Override @@ -62,8 +68,14 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId); deducing(taskType, taskId); resourceOccupy.setState(Constant.State_invalid); - resourceOccupy.setDeduceLastTime(new Date()); resourceOccupyDao.edit(resourceOccupy); + + ComputingResource computingResource = computingResourceDao.queryById(resourceOccupy.getComputingResourceId()); + if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { + resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getGpuNums()); + } else { + resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getCpuCores()); + } } @Override @@ -74,5 +86,8 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { float deduceCredit = resourceOccupy.getCreditPerHour() * hours; resourceOccupyDao.deduceCredit(deduceCredit, resourceOccupy.getUserId()); + + resourceOccupy.setDeduceLastTime(new Date()); + resourceOccupyDao.edit(resourceOccupy); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 8e613098..d046fe25 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -120,6 +120,9 @@ public class ServiceServiceImpl implements ServiceService { throw new RuntimeException("服务版本已存在,无法新增"); } + // 判断是否有资源 + resourceOccupyService.haveResource(serviceVersionVo.getComputingResourceId()); + ServiceVersion serviceVersion = getServiceVersion(serviceVersionVo); LoginUser loginUser = SecurityUtils.getLoginUser(); serviceVersion.setCreateBy(loginUser.getUsername()); @@ -139,7 +142,10 @@ public class ServiceServiceImpl implements ServiceService { } @Override - public String editServiceVersion(ServiceVersionVo serviceVersionVo) { + public String editServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception { + // 判断是否有资源 + resourceOccupyService.haveResource(serviceVersionVo.getComputingResourceId()); + ServiceVersion serviceVersion = getServiceVersion(serviceVersionVo); ServiceVersion oldServiceVersion = serviceDao.getServiceVersionById(serviceVersionVo.getId()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java index 4cf000e5..fe3cc30b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayVo.java @@ -57,6 +57,8 @@ public class RayVo { private String resource; + private Integer computingResourceId; + private String createBy; private Date createTime; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 6b87f09c..b20b9215 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -25,6 +25,18 @@ where user_id = #{userId} </update> + <update id="updateUsed"> + update resource + set used = used + #{used} + where id = #{id} + </update> + + <update id="updateUnUsed"> + update resource + set used = used - #{used} + where id = #{id} + </update> + <select id="haveResource" resultType="java.lang.Boolean"> select case when used + #{need} <= total then TRUE else FALSE end from resource From 52db9156e8a741d0d4f165e46b3613e52c6b2086 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 19 Mar 2025 11:32:56 +0800 Subject: [PATCH 104/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComputingResourceController.java | 28 ++++-- .../platform/domain/ComputingResource.java | 4 - .../platform/mapper/ComputingResourceDao.java | 2 - .../platform/mapper/ResourceOccupyDao.java | 7 ++ .../service/ResourceOccupyService.java | 5 + .../service/impl/ExperimentServiceImpl.java | 5 - .../platform/service/impl/RayServiceImpl.java | 1 + .../impl/ResourceOccupyServiceImpl.java | 9 ++ .../service/impl/ServiceServiceImpl.java | 2 +- .../ruoyi/platform/utils/K8sClientUtil.java | 91 +++---------------- .../com/ruoyi/platform/vo/RayParamVo.java | 2 +- .../ComputingResourceDaoMapper.xml | 4 - .../managementPlatform/ResourceOccupy.xml | 10 ++ .../managementPlatform/ServiceDaoMapper.xml | 2 +- .../resources/mapper/system/SysUserMapper.xml | 11 ++- 15 files changed, 77 insertions(+), 106 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java index aac53576..741dd69a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java @@ -3,7 +3,9 @@ package com.ruoyi.platform.controller.resources; import com.ruoyi.common.core.web.controller.BaseController; import com.ruoyi.common.core.web.domain.GenericsAjaxResult; import com.ruoyi.platform.domain.ComputingResource; +import com.ruoyi.platform.domain.ResourceOccupy; import com.ruoyi.platform.service.ComputingResourceService; +import com.ruoyi.platform.service.ResourceOccupyService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.data.domain.Page; @@ -28,6 +30,9 @@ public class ComputingResourceController extends BaseController { @Resource private ComputingResourceService computingResourceService; + @Resource + private ResourceOccupyService resourceOccupyService; + /** * 分页查询 * @@ -36,12 +41,12 @@ public class ComputingResourceController extends BaseController { */ @GetMapping @ApiOperation("分页查询") - public GenericsAjaxResult<Page<ComputingResource>> queryByPage(ComputingResource computingResource, @RequestParam("page") int page, + public GenericsAjaxResult<Page<ComputingResource>> queryByPage(ComputingResource computingResource, @RequestParam("page") int page, @RequestParam("size") int size, - @RequestParam(value = "resource_type") String resourceType ) { + @RequestParam(value = "resource_type") String resourceType) { computingResource.setComputingResource(resourceType); - PageRequest pageRequest = PageRequest.of(page,size); - return genericsSuccess(this.computingResourceService.queryByPage(computingResource, pageRequest)); + PageRequest pageRequest = PageRequest.of(page, size); + return genericsSuccess(this.computingResourceService.queryByPage(computingResource, pageRequest)); } /** @@ -53,7 +58,7 @@ public class ComputingResourceController extends BaseController { @GetMapping("{id}") @ApiOperation("根据id查询") public GenericsAjaxResult<ComputingResource> queryById(@PathVariable("id") Integer id) { - return genericsSuccess(this.computingResourceService.queryById(id)); + return genericsSuccess(this.computingResourceService.queryById(id)); } /** @@ -65,7 +70,7 @@ public class ComputingResourceController extends BaseController { @PostMapping @ApiOperation("新增计算资源") public GenericsAjaxResult<ComputingResource> add(@RequestBody ComputingResource computingResource) { - return genericsSuccess(this.computingResourceService.insert(computingResource)); + return genericsSuccess(this.computingResourceService.insert(computingResource)); } /** @@ -77,7 +82,7 @@ public class ComputingResourceController extends BaseController { @PutMapping @ApiOperation("编辑计算资源") public GenericsAjaxResult<ComputingResource> edit(@RequestBody ComputingResource computingResource) { - return genericsSuccess(this.computingResourceService.update(computingResource)); + return genericsSuccess(this.computingResourceService.update(computingResource)); } /** @@ -89,8 +94,15 @@ public class ComputingResourceController extends BaseController { @DeleteMapping("{id}") @ApiOperation("删除计算资源") public GenericsAjaxResult<String> deleteById(@PathVariable("id") Integer id) { - return genericsSuccess(this.computingResourceService.removeById(id)); + return genericsSuccess(this.computingResourceService.removeById(id)); } + @GetMapping("/resouceOccupy") + @ApiOperation("分页查询用户资源使用情况") + public GenericsAjaxResult<Page<ResourceOccupy>> queryResourceOccupyByPage(@RequestParam("page") int page, + @RequestParam("size") int size) { + PageRequest pageRequest = PageRequest.of(page, size); + return genericsSuccess(resourceOccupyService.queryByPage(pageRequest)); + } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java index 9a52bd6b..5e7b83b1 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java @@ -68,9 +68,5 @@ public class ComputingResource implements Serializable { @ApiModelProperty(value = "状态标识", notes = "0表示失效,1表示生效") private Integer state; - - @ApiModelProperty(value = "节点") - private String node; - } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ComputingResourceDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ComputingResourceDao.java index 93cdaaf7..b9a5de6d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ComputingResourceDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ComputingResourceDao.java @@ -73,8 +73,6 @@ public interface ComputingResourceDao { */ int update(@Param("computingResource") ComputingResource computingResource); - int updateUsedStateByNode(@Param("node")String node, @Param("usedState") Integer usedState); - /** * 通过主键删除数据 * diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index 8a92d4b2..cb03d638 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -2,6 +2,9 @@ package com.ruoyi.platform.mapper; import com.ruoyi.platform.domain.ResourceOccupy; import org.apache.ibatis.annotations.Param; +import org.springframework.data.domain.Pageable; + +import java.util.List; public interface ResourceOccupyDao { @@ -18,4 +21,8 @@ public interface ResourceOccupyDao { int updateUsed(@Param("id") Integer id, @Param("used") Integer used); int updateUnUsed(@Param("id") Integer id, @Param("used") Integer used); + + long count(); + + List<ResourceOccupy> queryByPage(@Param("pageable") Pageable pageable); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index f8aed04b..8395bc75 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -1,5 +1,9 @@ package com.ruoyi.platform.service; +import com.ruoyi.platform.domain.ResourceOccupy; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; @@ -10,4 +14,5 @@ public interface ResourceOccupyService { void deducing(String taskType, Long taskId); + Page<ResourceOccupy> queryByPage(PageRequest pageRequest); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java index 49a233c2..dcdb3b7c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java @@ -46,9 +46,6 @@ import java.util.*; public class ExperimentServiceImpl implements ExperimentService { @Resource private ExperimentDao experimentDao; - - @Resource - private ExperimentInsDao experimentInsDao; @Resource private ModelsService modelsService; @Resource @@ -74,8 +71,6 @@ public class ExperimentServiceImpl implements ExperimentService { private String argoConvert; @Value("${argo.workflowRun}") private String argoWorkflowRun; - @Value("${argo.workflowStatus}") - private String argoWorkflowStatus; @Value("${git.localPath}") String localPath; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 8ea5d473..3afb6ddd 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -164,6 +164,7 @@ public class RayServiceImpl implements RayService { if (resourceOccupyService.haveResource(ray.getComputingResourceId())) { RayParamVo rayParamVo = new RayParamVo(); BeanUtils.copyProperties(ray, rayParamVo); + rayParamVo.setResource(ray.getComputingResourceId()); rayParamVo.setCodeConfig(JsonUtils.jsonToMap(ray.getCodeConfig())); rayParamVo.setDataset(JsonUtils.jsonToMap(ray.getDataset())); rayParamVo.setModel(JsonUtils.jsonToMap(ray.getModel())); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index ea0f12c7..12f79edf 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -8,6 +8,9 @@ import com.ruoyi.platform.mapper.ComputingResourceDao; import com.ruoyi.platform.mapper.ResourceOccupyDao; import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.system.api.model.LoginUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -90,4 +93,10 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { resourceOccupy.setDeduceLastTime(new Date()); resourceOccupyDao.edit(resourceOccupy); } + + @Override + public Page<ResourceOccupy> queryByPage(PageRequest pageRequest) { + long total = resourceOccupyDao.count(); + return new PageImpl<>(resourceOccupyDao.queryByPage(pageRequest), pageRequest, total); + } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index d046fe25..c43be025 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -250,7 +250,7 @@ public class ServiceServiceImpl implements ServiceService { HashMap<String, Object> paramMap = new HashMap<>(); paramMap.put("service_name", service.getServiceName()); paramMap.put("description", serviceVersion.getDescription()); - paramMap.put("resource", serviceVersion.getResource()); + paramMap.put("resource", serviceVersion.getComputingResourceId()); paramMap.put("mount_path", serviceVersion.getMountPath()); paramMap.put("replicas", serviceVersion.getReplicas()); paramMap.put("env", JSONObject.parseObject(serviceVersion.getEnvVariables())); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index a3b73160..31ea6338 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -2,7 +2,9 @@ package com.ruoyi.platform.utils; import com.alibaba.fastjson2.JSON; import com.ruoyi.platform.constant.Constant; +import com.ruoyi.platform.domain.ComputingResource; import com.ruoyi.platform.domain.DevEnvironment; +import com.ruoyi.platform.mapper.ComputingResourceDao; import com.ruoyi.platform.service.ResourceOccupyService; import io.kubernetes.client.Exec; import io.kubernetes.client.custom.IntOrString; @@ -12,7 +14,9 @@ import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.apis.AppsV1Api; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.*; +import io.kubernetes.client.util.ClientBuilder; import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.credentials.AccessTokenAuthentication; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.json.JSONObject; @@ -50,6 +54,9 @@ public class K8sClientUtil { @Resource private ResourceOccupyService resourceOccupyService; + @Resource + private ComputingResourceDao computingResourceDao; + /** * 构建集群POD内通过SA访问的客户端 * loading the in-cluster config, including: @@ -485,7 +492,7 @@ public class K8sClientUtil { //配置资源 - V1ResourceRequirements v1ResourceRequirements = setPodResource(devEnvironment.getStandard()); + V1ResourceRequirements v1ResourceRequirements = setPodResource(devEnvironment.getComputingResourceId()); V1Pod pod = new V1PodBuilder() .withNewMetadata() @@ -688,87 +695,17 @@ public class K8sClientUtil { } - public Integer createDeployment(String dpName, String namespace, Integer replicas, String model, String image, Integer port, String resource, String mountPath - , String envVariables, String codeConfig) { - AppsV1Api api = new AppsV1Api(apiClient); - - //配置标签选择 - HashMap<String, String> selector = new HashMap<>(); - selector.put("app", dpName); - - //配置资源 - V1ResourceRequirements v1ResourceRequirements = setPodResource(resource); - - //配置环境变量 - List<V1EnvVar> env = new ArrayList<>(); - if (StringUtils.isNotEmpty(envVariables)) { - HashMap<String, String> envMap = JSON.parseObject(envVariables, HashMap.class); - for (String key : envMap.keySet()) { - V1EnvVar envVar = new V1EnvVar().name(key).value(envMap.get(key)); - env.add(envVar); - } - } - - // 配置卷和卷挂载 -// List<V1VolumeMount> volumeMounts = new ArrayList<>(); -// volumeMounts.add(new V1VolumeMount().name("workspace").mountPath(mountPath)); -// volumeMounts.add(new V1VolumeMount().name("minio-pvc").mountPath("/opt/code").subPath(codeConfig).readOnly(true)); -// volumeMounts.add(new V1VolumeMount().name("minio-pvc").mountPath("/opt/model").subPath(model).readOnly(true)); -// -// List<V1Volume> volumes = new ArrayList<>(); -// volumes.add(new V1Volume().name("workspace").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); -// volumes.add(new V1Volume().name("minio-pvc").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(dataPvcName))); - + V1ResourceRequirements setPodResource(Integer computingResourceId) { + ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); - //创建deployment - V1Deployment deployment = new V1DeploymentBuilder().withNewMetadata() - .withName(dpName) - .endMetadata() - .withNewSpec() - .withReplicas(replicas) - .withSelector(new V1LabelSelector().matchLabels(selector)) - .withNewTemplate() - .withNewMetadata() - .addToLabels("app", dpName) - .endMetadata() - .withNewSpec() - .addNewContainer() - .withName(dpName) - .withImage(image) - .withEnv(env) - .withPorts(new V1ContainerPort().containerPort(port).protocol("TCP")) - .withResources(v1ResourceRequirements) - .endContainer() - .endSpec() - .endTemplate() - .endSpec() - .build(); - - try { - api.createNamespacedDeployment(namespace, deployment, null, null, null); - } catch (ApiException e) { - throw new RuntimeException("创建deployment异常:" + e.getResponseBody()); - } - - V1Service service = createService(namespace, dpName + "-svc", port, selector); - return service.getSpec().getPorts().get(0).getNodePort(); - } - - - V1ResourceRequirements setPodResource(String resource) { //配置pod资源 - JSONObject standardJson = new JSONObject(resource); - JSONObject valueJson = (JSONObject) standardJson.get("value"); - int cpu = (int) valueJson.get("cpu"); - String memory = (String) valueJson.get("memory"); + String memory = computingResource.getMemoryGb().toString(); memory = memory.substring(0, memory.length() - 1).concat("i"); - Integer gpu = (Integer) valueJson.get("gpu"); - HashMap<String, Quantity> limitMap = new HashMap<>(); - if (gpu != null && gpu != 0) { - limitMap.put("nvidia.com/gpu", new Quantity(String.valueOf(gpu))); + if (computingResource.getGpuNums() != null && computingResource.getGpuNums() != 0) { + limitMap.put("nvidia.com/gpu", new Quantity(String.valueOf(computingResource.getGpuNums()))); } - limitMap.put("cpu", new Quantity(String.valueOf(cpu))); + limitMap.put("cpu", new Quantity(String.valueOf(computingResource.getCpuCores()))); limitMap.put("memory", new Quantity(memory)); limitMap.put("ephemeral-storage", new Quantity("100Gi")); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java index 86c77018..67e768b4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/vo/RayParamVo.java @@ -44,5 +44,5 @@ public class RayParamVo { private Integer minSamplesRequired; - private String resource; + private Integer resource; } diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml index 25981868..f02849fd 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ComputingResourceDaoMapper.xml @@ -148,10 +148,6 @@ computing_resource = values(computing_resource)standard = values(standard)descri where id = #{computingResource.id} </update> - <update id="updateUsedStateByNode"> - update computing_resource set used_state = #{usedState} where node = #{node} - </update> - <!--通过主键删除--> <delete id="deleteById"> delete from computing_resource where id = #{id} diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index b20b9215..ad8a782c 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -49,4 +49,14 @@ where task_type = #{task_type} and task_id = #{task_id} </select> + + <select id="count" resultType="java.lang.Long"> + select count(1) resource_occupy + </select> + + <select id="queryByPage" resultType="com.ruoyi.platform.domain.ResourceOccupy"> + select * + from resource_occupy + order by start_time desc limit #{pageable.offset}, #{pageable.pageSize} + </select> </mapper> \ No newline at end of file diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml index bec944af..5ece071a 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml @@ -86,7 +86,7 @@ <select id="getRunning" resultType="com.ruoyi.platform.domain.ServiceVersion"> select * - from service_version where state = 1 and status = 'Running' + from service_version where state = 1 and run_state = 'Running' </select> <insert id="insertService" keyProperty="id" useGeneratedKeys="true"> diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index 085ab4f4..c205566f 100644 --- a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -25,6 +25,7 @@ <result property="remark" column="remark"/> <result property="gitLinkUsername" column="git_link_username"/> <result property="gitLinkPassword" column="git_link_password"/> + <result property="credit" column="credit"/> <association property="dept" javaType="SysDept" resultMap="deptResult"/> <collection property="roles" javaType="java.util.List" resultMap="RoleResult"/> </resultMap> @@ -59,6 +60,7 @@ u.password, u.git_link_username, u.git_link_password, + u.credit, u.sex, u.status, u.del_flag, @@ -88,7 +90,7 @@ <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult"> select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, - u.git_link_username, + u.git_link_username, u.credit, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id @@ -123,7 +125,7 @@ </select> <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult"> - select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time, u.git_link_username + select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time, u.credit, u.git_link_username from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id @@ -140,7 +142,7 @@ </select> <select id="selectUnallocatedList" parameterType="SysUser" resultMap="SysUserResult"> - select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time ,u.git_link_username + select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time , u.credit, u.git_link_username from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id @@ -212,6 +214,7 @@ <if test="remark != null and remark != ''">remark,</if> <if test="gitLinkUsername != null and gitLinkUsername != ''">git_link_username,</if> <if test="gitLinkPassword != null and gitLinkPassword != ''">git_link_password,</if> + <if test="credit != null">credit,</if> create_time )values( <if test="userId != null and userId != ''">#{userId},</if> @@ -228,6 +231,7 @@ <if test="remark != null and remark != ''">#{remark},</if> <if test="gitLinkUsername != null and gitLinkUsername != ''">#{gitLinkUsername},</if> <if test="gitLinkPassword != null and gitLinkPassword != ''">#{gitLinkPassword},</if> + <if test="credit != null">#{credit},</if> sysdate() ) </insert> @@ -250,6 +254,7 @@ <if test="remark != null">remark = #{remark},</if> <if test="gitLinkUsername != null and gitLinkUsername != ''">git_link_username = #{gitLinkUsername},</if> <if test="gitLinkPassword != null and gitLinkPassword != ''">git_link_password = #{gitLinkPassword},</if> + <if test="credit != null">credit = #{credit},</if> update_time = sysdate() </set> where user_id = #{userId} From c79fcf8c542ace63392f85c79bf06e10ac8eef49 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 19 Mar 2025 14:09:39 +0800 Subject: [PATCH 105/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/scheduling/RayInsStatusTask.java | 2 ++ .../service/impl/DevEnvironmentServiceImpl.java | 11 ----------- .../platform/service/impl/JupyterServiceImpl.java | 1 - .../ruoyi/platform/service/impl/RayServiceImpl.java | 8 -------- .../platform/service/impl/ServiceServiceImpl.java | 8 -------- 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java index e0327431..86f21d65 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -44,6 +44,8 @@ public class RayInsStatusTask { rayIns = rayInsService.queryStatusFromArgo(rayIns); if (Constant.Running.equals(rayIns.getStatus())) { resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId()); + } else { + resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId()); } } catch (Exception e) { rayIns.setStatus(Constant.Failed); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java index 6488b967..67861b16 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java @@ -6,7 +6,6 @@ import com.ruoyi.platform.domain.PodStatus; import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.service.DevEnvironmentService; import com.ruoyi.platform.service.JupyterService; -import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.vo.DevEnvironmentVo; import com.ruoyi.platform.vo.PodStatusVo; @@ -36,10 +35,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { @Resource @Lazy private JupyterService jupyterService; - - @Resource - private ResourceOccupyService resourceOccupyService; - /** * 通过ID查询单条数据 * @@ -91,9 +86,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { */ @Override public DevEnvironment insert(DevEnvironmentVo devEnvironmentVo) throws Exception { - // 判断是否有资源 - resourceOccupyService.haveResource(devEnvironmentVo.getComputingResourceId()); - //插入预备,此时不需要判断版本重复 DevEnvironment devEnvironment = new DevEnvironment(); LoginUser loginUser = SecurityUtils.getLoginUser(); @@ -127,9 +119,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { */ @Override public DevEnvironment update(DevEnvironment devEnvironment) throws Exception { - // 判断是否有资源 - resourceOccupyService.haveResource(devEnvironment.getComputingResourceId()); - LoginUser loginUser = SecurityUtils.getLoginUser(); devEnvironment.setUpdateBy(loginUser.getUsername()); devEnvironment.setUpdateTime(new Date()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index c37a920f..3a6048eb 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -5,7 +5,6 @@ import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.DevEnvironment; import com.ruoyi.platform.domain.PodStatus; -import com.ruoyi.platform.mapper.ComputingResourceDao; import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.service.DevEnvironmentService; import com.ruoyi.platform.service.JupyterService; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 3afb6ddd..105025a6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -66,10 +66,6 @@ public class RayServiceImpl implements RayService { if (rayByName != null) { throw new RuntimeException("实验名称已存在"); } - - // 判断是否有资源 - resourceOccupyService.haveResource(rayVo.getComputingResourceId()); - Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); String username = SecurityUtils.getLoginUser().getUsername(); @@ -91,10 +87,6 @@ public class RayServiceImpl implements RayService { if (oldRay != null && !oldRay.getId().equals(rayVo.getId())) { throw new RuntimeException("实验名称已存在"); } - - // 判断是否有资源 - resourceOccupyService.haveResource(rayVo.getComputingResourceId()); - Ray ray = new Ray(); BeanUtils.copyProperties(rayVo, ray); ray.setUpdateBy(SecurityUtils.getLoginUser().getUsername()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index c43be025..8160c457 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -119,10 +119,6 @@ public class ServiceServiceImpl implements ServiceService { if (svByVersion != null) { throw new RuntimeException("服务版本已存在,无法新增"); } - - // 判断是否有资源 - resourceOccupyService.haveResource(serviceVersionVo.getComputingResourceId()); - ServiceVersion serviceVersion = getServiceVersion(serviceVersionVo); LoginUser loginUser = SecurityUtils.getLoginUser(); serviceVersion.setCreateBy(loginUser.getUsername()); @@ -143,11 +139,7 @@ public class ServiceServiceImpl implements ServiceService { @Override public String editServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception { - // 判断是否有资源 - resourceOccupyService.haveResource(serviceVersionVo.getComputingResourceId()); - ServiceVersion serviceVersion = getServiceVersion(serviceVersionVo); - ServiceVersion oldServiceVersion = serviceDao.getServiceVersionById(serviceVersionVo.getId()); if (!oldServiceVersion.getReplicas().equals(serviceVersionVo.getReplicas()) || !oldServiceVersion.getResource().equals(serviceVersionVo.getResource()) || serviceVersionVo.getRerun()) { From 0fd5af7ee381984d99b948d60acbe6ed585c1ec4 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 19 Mar 2025 16:57:52 +0800 Subject: [PATCH 106/127] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/hooks/resource.ts | 35 +++++++++++-------- .../DevelopmentEnvironment/Create/index.tsx | 23 +++--------- .../components/ExperimentParameter/index.tsx | 12 +------ react-ui/src/pages/Experiment/index.jsx | 1 - .../components/CreateForm/ExecuteConfig.tsx | 19 ++-------- .../components/HyperParameterBasic/index.tsx | 4 +-- react-ui/src/pages/HyperParameter/types.ts | 2 +- .../ModelDeployment/CreateVersion/index.tsx | 17 +++------ .../ModelDeployment/ServiceInfo/index.tsx | 2 +- .../components/VersionBasicInfo/index.tsx | 2 +- .../components/VersionCompareModal/index.tsx | 2 +- .../components/PipelineNodeDrawer/index.tsx | 15 ++------ 12 files changed, 42 insertions(+), 92 deletions(-) diff --git a/react-ui/src/hooks/resource.ts b/react-ui/src/hooks/resource.ts index 6331edab..e1e27506 100644 --- a/react-ui/src/hooks/resource.ts +++ b/react-ui/src/hooks/resource.ts @@ -12,6 +12,22 @@ import { useCallback, useEffect, useState } from 'react'; 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 resourceFieldNames = { + label: 'description', + value: 'id', +}; + // 获取资源规格 export function useComputingResource() { const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); @@ -25,7 +41,7 @@ export function useComputingResource() { resource_type: '', }; const [res] = await to(getComputingResourceReq(params)); - if (res && res.data && res.data.content) { + if (res && res.data && Array.isArray(res.data.content)) { setResourceStandardList(res.data.content); computingResource.splice(0, computingResource.length, ...res.data.content); } @@ -38,25 +54,16 @@ export function useComputingResource() { } }, []); - // 过滤资源规格 - const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = - useCallback((input: string, option?: ComputingResource) => { - return ( - option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? - false - ); - }, []); - // 根据 standard 获取 description const getDescription = useCallback( - (standard?: string) => { - if (!standard) { + (id?: string | number) => { + if (!id) { return undefined; } - return resourceStandardList.find((item) => item.standard === standard)?.description; + return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; }, [resourceStandardList], ); - return [resourceStandardList, filterResourceStandard, getDescription] as const; + return [resourceStandardList, getDescription] as const; } diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 1a0b9a18..6469c5af 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -6,17 +6,17 @@ import KFIcon from '@/components/KFIcon'; import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import PageTitle from '@/components/PageTitle'; +import ParameterSelect from '@/components/ParameterSelect'; import ResourceSelect, { requiredValidator, ResourceSelectorType, type ParameterInputObject, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; -import { useComputingResource } from '@/hooks/resource'; import { createEditorReq } from '@/services/developmentEnvironment'; import { to } from '@/utils/promise'; import { useNavigate } from '@umijs/max'; -import { App, Button, Col, Form, Input, Row, Select } from 'antd'; +import { App, Button, Col, Form, Input, Row } from 'antd'; import { omit, pick } from 'lodash'; import styles from './index.less'; @@ -51,7 +51,6 @@ function EditorCreate() { const navigate = useNavigate(); const [form] = Form.useForm(); const { message } = App.useApp(); - const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 创建编辑器 const createEditor = async (formData: FormData) => { @@ -62,8 +61,8 @@ function EditorCreate() { const params = { ...omit(formData, ['image', 'model', 'dataset']), image: image.value, - model: pick(model, ['id', 'version', 'path', 'showValue']), - dataset: pick(dataset, ['id', 'version', 'path', 'showValue']), + model: model && pick(model, ['id', 'version', 'path', 'showValue']), + dataset: dataset && pick(dataset, ['id', 'version', 'path', 'showValue']), }; const [res] = await to(createEditorReq(params)); if (res) { @@ -146,16 +145,7 @@ function EditorCreate() { }, ]} > - <Select - showSearch - placeholder="请选择资源规格" - filterOption={filterResourceStandard} - options={resourceStandardList} - fieldNames={{ - label: 'description', - value: 'standard', - }} - /> + <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> </Form.Item> </Col> </Row> @@ -181,7 +171,6 @@ function EditorCreate() { type={ResourceSelectorType.Mirror} placeholder="请选择镜像" canInput={false} - size="large" /> </Form.Item> </Col> @@ -193,7 +182,6 @@ function EditorCreate() { type={ResourceSelectorType.Model} placeholder="请选择模型" canInput={false} - size="large" /> </Form.Item> </Col> @@ -205,7 +193,6 @@ function EditorCreate() { type={ResourceSelectorType.Dataset} placeholder="请选择数据集" canInput={false} - size="large" /> </Form.Item> </Col> diff --git a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx index a81cf106..edb66534 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx @@ -1,7 +1,6 @@ import FormInfo from '@/components/FormInfo'; import ParameterSelect from '@/components/ParameterSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; -import { useComputingResource } from '@/hooks/resource'; import { PipelineNodeModelSerialize } from '@/types'; import { Form } from 'antd'; import styles from './index.less'; @@ -11,8 +10,6 @@ type ExperimentParameterProps = { }; function ExperimentParameter({ nodeData }: ExperimentParameterProps) { - const [resourceStandardList] = useComputingResource(); // 资源规模 - // 控制策略 const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( ([key, value]) => ({ key, value }), @@ -112,14 +109,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { }, ]} > - <FormInfo - select - options={resourceStandardList} - fieldNames={{ - label: 'description', - value: 'standard', - }} - /> + <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> </Form.Item> <Form.Item label="挂载路径" name="mount_path"> <FormInfo /> diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index 9ae30a1a..9b7cab84 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -277,7 +277,6 @@ function Experiment() { current, pageSize, }); - getExperimentList(); }; // 运行实验 const runExperiment = async (id) => { diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx index cbd97448..01bc4001 100644 --- a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -1,12 +1,12 @@ import CodeSelect from '@/components/CodeSelect'; import KFIcon from '@/components/KFIcon'; +import ParameterSelect from '@/components/ParameterSelect'; import ResourceSelect, { ResourceSelectorType, requiredValidator, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; import { hyperParameterOptimizedModeOptions } from '@/enums'; -import { useComputingResource } from '@/hooks/resource'; import { isEmpty } from '@/utils'; import { modalConfirm, removeFormListItem } from '@/utils/ui'; import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; @@ -86,7 +86,6 @@ function ExecuteConfig() { const searchAlgorithm = Form.useWatch('search_alg', form); const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; const paramsTypeTooltip = searchAlgorithm === 'Ax' ? axParameterTooltip : parameterTooltip; - const [resourceStandardList, filterResourceStandard] = useComputingResource(); const handleSearchAlgorithmChange = (value: string) => { if ( @@ -157,7 +156,6 @@ function ExecuteConfig() { type={ResourceSelectorType.Mirror} placeholder="请选择镜像" canInput={false} - size="large" /> </Form.Item> </Col> @@ -180,7 +178,6 @@ function ExecuteConfig() { type={ResourceSelectorType.Dataset} placeholder="请选择数据集" canInput={false} - size="large" /> </Form.Item> </Col> @@ -193,7 +190,6 @@ function ExecuteConfig() { type={ResourceSelectorType.Model} placeholder="请选择模型" canInput={false} - size="large" /> </Form.Item> </Col> @@ -596,7 +592,7 @@ function ExecuteConfig() { <Col span={10}> <Form.Item label="资源规格" - name="resource" + name="computing_resource_id" rules={[ { required: true, @@ -604,16 +600,7 @@ function ExecuteConfig() { }, ]} > - <Select - showSearch - placeholder="请选择资源规格" - filterOption={filterResourceStandard} - options={resourceStandardList} - fieldNames={{ - label: 'description', - value: 'standard', - }} - /> + <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> </Form.Item> </Col> </Row> diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 2f419f66..feb72d48 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -41,7 +41,7 @@ function HyperParameterBasic({ runStatus, isInstance = false, }: HyperParameterBasicProps) { - const getResourceDescription = useComputingResource()[2]; + const getResourceDescription = useComputingResource()[1]; const basicDatas: BasicInfoData[] = useMemo(() => { if (!info) { @@ -136,7 +136,7 @@ function HyperParameterBasic({ }, { label: '资源规格', - value: info.resource, + value: info.computing_resource_id, format: getResourceDescription, }, ]; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index e0c02957..6787f15a 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -24,7 +24,7 @@ export type FormData = { num_samples: number; // 总试验次数 max_t: number; // 单次试验最大时间 min_samples_required: number; // 计算中位数的最小试验数 - resource: string; // 资源规格 + computing_resource_id: number; // 资源规格 parameters: FormParameter[]; points_to_evaluate: { [key: string]: any }[]; }; diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index a4d35444..6b47ce30 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -5,13 +5,13 @@ */ import CodeSelect from '@/components/CodeSelect'; import PageTitle from '@/components/PageTitle'; +import ParameterSelect from '@/components/ParameterSelect'; import ResourceSelect, { ResourceSelectorType, requiredValidator, type ParameterInputObject, } from '@/components/ResourceSelect'; import SubAreaTitle from '@/components/SubAreaTitle'; -import { useComputingResource } from '@/hooks/resource'; import { createServiceVersionReq, getServiceInfoReq, @@ -23,7 +23,7 @@ import SessionStorage from '@/utils/sessionStorage'; 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, Select } from 'antd'; +import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd'; import { omit, pick } from 'lodash'; import { useEffect, useState } from 'react'; import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; @@ -51,7 +51,7 @@ type ServiceVersionCache = ServiceVersionData & { function CreateServiceVersion() { const navigate = useNavigate(); const [form] = Form.useForm(); - const [resourceStandardList, filterResourceStandard] = useComputingResource(); + const [operationType, setOperationType] = useState(ServiceOperationType.Create); const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo); const { message } = App.useApp(); @@ -357,16 +357,7 @@ function CreateServiceVersion() { }, ]} > - <Select - showSearch - placeholder="请选择资源规格" - filterOption={filterResourceStandard} - options={resourceStandardList} - fieldNames={{ - label: 'description', - value: 'standard', - }} - /> + <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> </Form.Item> </Col> </Row> diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 8d7a6a1b..12811d86 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -87,7 +87,7 @@ function ServiceInfo() { format: formatDate, }, ]; - const getResourceDescription = useComputingResource()[2]; + const getResourceDescription = useComputingResource()[1]; // 获取服务详情 const getServiceInfo = useCallback(async () => { diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx index 5faa3ddc..7785a179 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -36,7 +36,7 @@ const formatEnvText = (env?: Record<string, string>) => { }; function VersionBasicInfo({ info }: BasicInfoProps) { - const getResourceDescription = useComputingResource()[2]; + const getResourceDescription = useComputingResource()[1]; const datas: BasicInfoData[] = [ { diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx index 5fcd472a..9d021e9b 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -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()[2]; + const getResourceDescription = useComputingResource()[1]; const fields: FiledType[] = useMemo( () => [ diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index e1d0efc1..8159e646 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -8,7 +8,6 @@ import ResourceSelectorModal, { } from '@/components/ResourceSelectorModal'; import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys } from '@/enums'; -import { useComputingResource } from '@/hooks/resource'; import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; import { PipelineGlobalParam, @@ -19,7 +18,7 @@ import { import { openAntdModal } from '@/utils/modal'; import { to } from '@/utils/promise'; import { INode } from '@antv/g6'; -import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; +import { Button, Drawer, Form, Input, MenuProps } from 'antd'; import { NamePath } from 'antd/es/form/interface'; import { forwardRef, useImperativeHandle, useState } from 'react'; import PropsLabel from '../PropsLabel'; @@ -36,7 +35,6 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete {} as PipelineNodeModelSerialize, ); const [open, setOpen] = useState(false); - const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); const afterOpenChange = async () => { @@ -458,16 +456,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete }, ]} > - <Select - placeholder="请选择资源规格" - filterOption={filterResourceStandard} - options={resourceStandardList} - fieldNames={{ - label: 'description', - value: 'standard', - }} - showSearch - /> + <ParameterSelect dataType="resource" placeholder="请选择资源规格" /> </Form.Item> <Form.Item name="mount_path" From 572efdbc1da6ed932cf64db289d8977295fd2ce9 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 19 Mar 2025 17:04:26 +0800 Subject: [PATCH 107/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E6=95=B0=E6=8D=AE=E9=9B=86=E4=B9=8B=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=88=A0=E9=99=A4=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/.nvmrc | 2 +- react-ui/src/components/CodeSelect/index.tsx | 7 ++ .../src/components/ParameterInput/index.tsx | 18 ++-- .../src/components/ParameterSelect/config.tsx | 26 +----- .../src/components/ParameterSelect/index.tsx | 12 ++- .../src/components/ResourceSelect/index.tsx | 82 +++++++++---------- .../components/CreateForm/DatasetConfig.tsx | 1 - .../components/CreateForm/ExecuteConfig.tsx | 7 +- .../Experiment/components/LogGroup/index.tsx | 24 +++--- .../ModelDeployment/CreateVersion/index.tsx | 2 - react-ui/src/pages/Pipeline/Info/index.jsx | 2 +- react-ui/src/stories/CodeSelect.stories.tsx | 1 + .../src/stories/ParameterInput.stories.tsx | 70 ++++++---------- .../src/stories/ResourceSelect.stories.tsx | 4 +- 14 files changed, 119 insertions(+), 139 deletions(-) diff --git a/react-ui/.nvmrc b/react-ui/.nvmrc index 8ddbc0c6..216afccf 100644 --- a/react-ui/.nvmrc +++ b/react-ui/.nvmrc @@ -1 +1 @@ -v18.16.0 +v18.20.7 diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index d2afbb94..c6486d1e 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -30,6 +30,7 @@ function CodeSelect({ onChange, ...rest }: CodeSelectProps) { + // 选择代码配置 const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { @@ -59,6 +60,11 @@ function CodeSelect({ }); }; + // 删除 + const handleRemove = () => { + onChange?.(undefined); + }; + return ( <div className={classNames('kf-code-select', className)} style={style}> <ParameterInput @@ -68,6 +74,7 @@ function CodeSelect({ value={value} onChange={onChange} onClick={selectResource} + onRemove={handleRemove} ></ParameterInput> <Button className="kf-code-select__button" diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 47ef5d0e..32672b98 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -6,7 +6,7 @@ import { CommonTabKeys } from '@/enums'; import { CloseOutlined } from '@ant-design/icons'; -import { Form, Input } from 'antd'; +import { ConfigProvider, Form, Input } from 'antd'; import { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; @@ -67,7 +67,7 @@ function ParameterInput({ allowClear, className, style, - size = 'middle', + size, disabled = false, id, ...rest @@ -81,10 +81,17 @@ function ParameterInput({ const placeholder = valueObj?.placeholder || rest?.placeholder; const InputComponent = textArea ? Input.TextArea : Input; const { status } = Form.Item.useStatus(); + const { componentSize } = ConfigProvider.useConfig(); + const mySize = size || componentSize; // 删除 const handleRemove = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => { e.stopPropagation(); + if (onRemove) { + onRemove(); + return; + } + onChange?.({ ...valueObj, value: undefined, @@ -94,7 +101,6 @@ function ParameterInput({ expandedKeys: [], checkedKeys: [], }); - onRemove?.(); }; return ( @@ -104,8 +110,8 @@ function ParameterInput({ id={id} className={classNames( 'parameter-input', - { 'parameter-input--large': size === 'large' }, - { 'parameter-input--small': size === 'small' }, + { 'parameter-input--large': mySize === 'large' }, + { 'parameter-input--small': mySize === 'small' }, { [`parameter-input--${status}`]: status }, className, )} @@ -128,7 +134,7 @@ function ParameterInput({ <InputComponent {...rest} id={id} - size={size} + size={mySize} className={className} style={style} placeholder={placeholder} diff --git a/react-ui/src/components/ParameterSelect/config.tsx b/react-ui/src/components/ParameterSelect/config.tsx index 2548f44c..f9ac72d2 100644 --- a/react-ui/src/components/ParameterSelect/config.tsx +++ b/react-ui/src/components/ParameterSelect/config.tsx @@ -1,21 +1,10 @@ +import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource'; import { ServiceData } from '@/pages/ModelDeployment/types'; import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getServiceListReq } from '@/services/modelDeployment'; -import { getComputingResourceReq } from '@/services/pipeline'; -import { ComputingResource } from '@/types'; import { type SelectProps } from 'antd'; import { pick } from 'lodash'; -// 过滤资源规格 -const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( - input: string, - option?: ComputingResource, -) => { - return ( - option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false - ); -}; - // id 从 number 转换为 string const convertId = (item: any) => ({ ...item, @@ -86,17 +75,10 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { }, resource: { getOptions: async () => { - const res = await getComputingResourceReq({ - page: 0, - size: 1000, - resource_type: '', - }); - return res?.data?.content ?? []; - }, - fieldNames: { - label: 'description', - value: 'standard', + // 不需要这个函数 + return []; }, + fieldNames: resourceFieldNames, filterOption: filterResourceStandard as SelectProps['filterOption'], }, }; diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index f1902e5c..b765ea61 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -4,6 +4,7 @@ * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ +import { useComputingResource } from '@/hooks/resource'; import { to } from '@/utils/promise'; import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; @@ -20,7 +21,7 @@ export interface ParameterSelectProps extends SelectProps { dataType: 'dataset' | 'model' | 'service' | 'resource'; /** 是否只是展示信息 */ display?: boolean; - /** 值 */ + /** 值,支持对象,对象必须包含 value */ value?: string | ParameterSelectObject; /** 修改后回调 */ onChange?: (value: string | ParameterSelectObject) => void; @@ -34,9 +35,10 @@ function ParameterSelect({ onChange, ...rest }: ParameterSelectProps) { - const [options, setOptions] = useState([]); + const [options, setOptions] = useState<SelectProps['options']>([]); const propsConfig = paramSelectConfig[dataType]; const valueText = typeof value === 'object' && value !== null ? value.value : value; + const [resourceStandardList] = useComputingResource(); useEffect(() => { // 获取下拉数据 @@ -54,6 +56,8 @@ function ParameterSelect({ getSelectOptions(); }, [propsConfig]); + const selectOptions = dataType === 'resource' ? resourceStandardList : options; + const handleChange = (text: string) => { if (typeof value === 'object' && value !== null) { onChange?.({ @@ -71,7 +75,7 @@ function ParameterSelect({ <FormInfo select value={valueText} - options={options} + options={selectOptions} fieldNames={propsConfig?.fieldNames} ></FormInfo> ); @@ -81,7 +85,7 @@ function ParameterSelect({ <Select {...rest} filterOption={propsConfig?.filterOption} - options={options} + options={selectOptions} fieldNames={propsConfig?.fieldNames} optionFilterProp={propsConfig?.optionFilterProp} value={valueText} diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 29f01009..a7e02ce3 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -11,10 +11,9 @@ import ResourceSelectorModal, { selectorTypeConfig, } from '@/components/ResourceSelectorModal'; import { openAntdModal } from '@/utils/modal'; -import { Button } from 'antd'; +import { Button, ConfigProvider } from 'antd'; import classNames from 'classnames'; import { pick } from 'lodash'; -import { useEffect, useState } from 'react'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; @@ -46,43 +45,40 @@ function ResourceSelect({ onChange, ...rest }: ResourceSelectProps) { - const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( - undefined, - ); - - useEffect(() => { - if ( - value && - typeof value === 'object' && - value.activeTab && - value.id && - value.name && - value.version && - value.path && - (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) - ) { - const originResource = pick(value, [ - 'activeTab', - 'id', - 'identifier', - 'name', - 'owner', - 'version', - 'path', - ]) as ResourceSelectorResponse; - setSelectedResource(originResource); - } - }, [value, type]); + const { componentSize } = ConfigProvider.useConfig(); + const mySize = size || componentSize; + let selectedResource: ResourceSelectorResponse | undefined = undefined; + if ( + value && + typeof value === 'object' && + value.activeTab && + value.id && + value.name && + value.version && + value.path && + (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) + ) { + selectedResource = pick(value, [ + 'activeTab', + 'id', + 'identifier', + 'name', + 'owner', + 'version', + 'path', + ]) as ResourceSelectorResponse; + } + // 选择数据集、模型、镜像 const selectResource = () => { - const resource = selectedResource; const { close } = openAntdModal(ResourceSelectorModal, { type, - defaultExpandedKeys: resource ? [resource.id] : [], - defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [], - defaultActiveTab: resource?.activeTab, + defaultExpandedKeys: selectedResource ? [selectedResource.id] : [], + defaultCheckedKeys: selectedResource + ? [`${selectedResource.id}-${selectedResource.version}`] + : [], + defaultActiveTab: selectedResource?.activeTab, onOk: (res) => { - setSelectedResource(res); if (res) { const { activeTab, id, name, version, path, identifier, owner } = res; if (type === ResourceSelectorType.Mirror) { @@ -116,32 +112,32 @@ function ResourceSelect({ }); } } else { - onChange?.({ - value: undefined, - showValue: undefined, - fromSelect: false, - activeTab: undefined, - }); + onChange?.(undefined); } close(); }, }); }; + // 删除 + const handleRemove = () => { + onChange?.(undefined); + }; + return ( <div className={classNames('kf-resource-select', className)} style={style}> <ParameterInput {...rest} disabled={disabled} value={value} - size={size} + size={mySize} onChange={onChange} - onRemove={() => setSelectedResource(undefined)} + onRemove={handleRemove} onClick={selectResource} ></ParameterInput> <Button className="kf-resource-select__button" - size={size} + size={mySize} type="link" icon={getSelectBtnIcon(type)} disabled={disabled} diff --git a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx index b3b3f2dd..ae70e806 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx @@ -30,7 +30,6 @@ function DatasetConfig() { type={ResourceSelectorType.Dataset} placeholder="请选择数据集" canInput={false} - size="large" /> </Form.Item> </Col> diff --git a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx index d03445f3..689deb1b 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx @@ -431,7 +431,12 @@ function ExecuteConfig() { <Row gutter={8}> <Col span={10}> - <Form.Item label="是否打乱" name="shuffle" tooltip="拆分数据前是否打乱顺序"> + <Form.Item + label="是否打乱" + name="shuffle" + tooltip="拆分数据前是否打乱顺序" + valuePropName="checked" + > <Switch /> </Form.Item> </Col> diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index e69b4773..3bc1b566 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -33,7 +33,7 @@ function LogGroup({ status, }: LogGroupProps) { const [collapse, setCollapse] = useState(true); - const [logList, setLogList, logListRef] = useStateRef<Log[]>([]); + const [logList, setLogList] = useState<Log[]>([]); const [completed, setCompleted] = useState(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); @@ -126,6 +126,14 @@ function LogGroup({ socketRef.current = socket; }; + // 关闭 socket + const closeSocket = () => { + if (socketRef.current) { + socketRef.current.close(1000, 'completed'); + socketRef.current = undefined; + } + }; + if (status === ExperimentStatus.Running) { setupSockect(); } @@ -133,7 +141,7 @@ function LogGroup({ return () => { closeSocket(); }; - }, [status, start_time, pod_name, isMouseDownRef, setLogList]); + }, [status, start_time, pod_name, isMouseDownRef]); // 鼠标拖到中不滚动到底部 useEffect(() => { @@ -153,8 +161,8 @@ function LogGroup({ // 请求日志 const requestExperimentPodsLog = async () => { - const list = logListRef.current; - const startTime = list.length > 0 ? list[list.length - 1].start_time : start_time; + const last = logList[logList.length - 1]; + const startTime = last ? last.start_time : start_time; const params = { pod_name, start_time: startTime, @@ -201,14 +209,6 @@ function LogGroup({ requestExperimentPodsLog(); }; - // 关闭 socket - const closeSocket = () => { - if (socketRef.current) { - socketRef.current.close(1000, 'completed'); - socketRef.current = undefined; - } - }; - // 滚动到底部 const scrollToBottom = (smooth: boolean = true) => { // const element = document.getElementById(listId); diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index 6b47ce30..e0a2c074 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -305,7 +305,6 @@ function CreateServiceVersion() { placeholder="请选择模型" disabled={disabled} canInput={false} - size="large" /> </Form.Item> </Col> @@ -327,7 +326,6 @@ function CreateServiceVersion() { type={ResourceSelectorType.Mirror} placeholder="请选择镜像" canInput={false} - size="large" disabled={disabled} /> </Form.Item> diff --git a/react-ui/src/pages/Pipeline/Info/index.jsx b/react-ui/src/pages/Pipeline/Info/index.jsx index 353ef7e0..3cc69a4c 100644 --- a/react-ui/src/pages/Pipeline/Info/index.jsx +++ b/react-ui/src/pages/Pipeline/Info/index.jsx @@ -24,7 +24,7 @@ const EditPipeline = () => { const propsRef = useRef(); const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false); const [globalParam, setGlobalParam, globalParamRef] = useStateRef([]); - const [workflowInfo, setWorkflowInfo] = useStateRef(undefined); + const [workflowInfo, setWorkflowInfo] = useState(undefined); const { message } = App.useApp(); let sourceAnchorIdx, targetAnchorIdx, dropAnchorIdx; let dragSourceNode; diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index 415d05da..b2246700 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -41,6 +41,7 @@ export const Primary: Story = { canInput: false, textArea: false, size: 'large', + placeholder: '请选择代码配置', style: { width: 400 }, }, render: function Render(args) { diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx index 914b6196..3ca116c8 100644 --- a/react-ui/src/stories/ParameterInput.stories.tsx +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -1,5 +1,7 @@ import ParameterInput, { ParameterInputValue } from '@/components/ParameterInput'; +import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; import { Button } from 'antd'; import { useState } from 'react'; @@ -18,7 +20,7 @@ const meta = { // backgroundColor: { control: 'color' }, }, // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args - // args: { onClick: fn() }, + args: { onChange: fn() }, } satisfies Meta<typeof ParameterInput>; export default meta; @@ -37,45 +39,6 @@ export const Input: Story = { }; export const Select: Story = { - args: { - placeholder: '请输入工作目录', - style: { width: 300 }, - value: 'storybook', - canInput: false, - size: 'large', - }, -}; - -export const SelectWithObjctValue: Story = { - args: { - placeholder: '请输入工作目录', - style: { width: 300 }, - value: { - value: 'storybook', - showValue: 'storybook', - fromSelect: true, - }, - canInput: true, - size: 'large', - }, -}; - -export const Disabled: Story = { - args: { - placeholder: '请输入工作目录', - style: { width: 300 }, - value: { - value: 'storybook', - showValue: 'storybook', - fromSelect: true, - }, - canInput: true, - size: 'large', - disabled: true, - }, -}; - -export const Application: Story = { args: { placeholder: '请输入工作目录', style: { width: 300 }, @@ -86,18 +49,24 @@ export const Application: Story = { const [value, setValue] = useState<ParameterInputValue | undefined>(''); const onClick = () => { - setValue({ + const value = { value: 'storybook', showValue: 'storybook', fromSelect: true, - }); + otherValue: 'others', + }; + setValue(value); + action('onChange')(value); }; return ( <> <ParameterInput {...args} value={value} - onChange={(value) => setValue(value)} + onChange={(value) => { + setValue(value); + action('onChange')(value); + }} ></ParameterInput> <Button type="primary" style={{ display: 'block', marginTop: 10 }} onClick={onClick}> 模拟从全局参数选择 @@ -106,3 +75,18 @@ export const Application: Story = { ); }, }; + +export const Disabled: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + disabled: true, + }, +}; diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx index 8b87f990..3eafc0f7 100644 --- a/react-ui/src/stories/ResourceSelect.stories.tsx +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -76,6 +76,7 @@ export const Primary: Story = { canInput: false, textArea: false, size: 'large', + placeholder: '请选择数据集', style: { width: 400 }, }, render: function Render(args) { @@ -120,7 +121,6 @@ export const InForm: Story = { type={ResourceSelectorType.Dataset} placeholder="请选择" canInput={false} - size="large" onChange={onChange} /> </Form.Item> @@ -133,7 +133,6 @@ export const InForm: Story = { type={ResourceSelectorType.Model} placeholder="请选择" canInput={false} - size="large" onChange={onChange} /> </Form.Item> @@ -146,7 +145,6 @@ export const InForm: Story = { type={ResourceSelectorType.Mirror} placeholder="请选择" canInput={false} - size="large" onChange={onChange} /> </Form.Item> From 53e07d871111d92e7318aada73f069e6586ba8ce Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Wed, 19 Mar 2025 17:18:54 +0800 Subject: [PATCH 108/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/system/api/domain/SysUser.java | 1 + .../ruoyi/platform/domain/ResourceOccupy.java | 6 +++ .../platform/mapper/ResourceOccupyDao.java | 4 +- .../ExperimentInstanceStatusTask.java | 17 +++++++ .../platform/scheduling/RayInsStatusTask.java | 8 ++-- .../scheduling/ResourceOccupyTask.java | 4 +- .../service/ResourceOccupyService.java | 6 +-- .../service/impl/ExperimentServiceImpl.java | 17 ++++++- .../service/impl/JupyterServiceImpl.java | 2 +- .../service/impl/RayInsServiceImpl.java | 2 +- .../platform/service/impl/RayServiceImpl.java | 2 +- .../impl/ResourceOccupyServiceImpl.java | 45 +++++++++++-------- .../service/impl/ServiceServiceImpl.java | 4 +- .../ruoyi/platform/utils/K8sClientUtil.java | 5 +-- .../managementPlatform/ResourceOccupy.xml | 15 +++++-- 15 files changed, 95 insertions(+), 43 deletions(-) diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java index 67457175..4d960370 100644 --- a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java +++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java @@ -349,6 +349,7 @@ public class SysUser extends BaseEntity { .append("dept", getDept()) .append("gitLinkUsername", getGitLinkUsername()) .append("gitLinkPassword", getGitLinkPassword()) + .append("credit", getCredit()) .toString(); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java index 4a030c5c..d570472c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -19,6 +19,9 @@ public class ResourceOccupy { @ApiModelProperty("计算资源") private Integer computingResourceId; + @ApiModelProperty("描述") + private String description; + @ApiModelProperty("积分/小时") private Float creditPerHour; @@ -36,4 +39,7 @@ public class ResourceOccupy { @ApiModelProperty("类型id") private Long taskId; + + @ApiModelProperty("流水线节点id") + private String nodeId; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index cb03d638..ca3b98d2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -14,9 +14,9 @@ public interface ResourceOccupyDao { int edit(@Param("resourceOccupy") ResourceOccupy resourceOccupy); - ResourceOccupy getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId); + List<ResourceOccupy> getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId, @Param("nodeId") String nodeId); - int deduceCredit(@Param("credit") Float credit, @Param("userId") Long userId); + int deduceCredit(@Param("credit") Double credit, @Param("userId") Long userId); int updateUsed(@Param("id") Integer id, @Param("used") Integer used); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java index 55ed5a45..44266a04 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java @@ -1,11 +1,13 @@ package com.ruoyi.platform.scheduling; +import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.Experiment; import com.ruoyi.platform.domain.ExperimentIns; import com.ruoyi.platform.mapper.ExperimentDao; import com.ruoyi.platform.mapper.ExperimentInsDao; import com.ruoyi.platform.service.AimService; import com.ruoyi.platform.service.ExperimentInsService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.JacksonUtil; import com.ruoyi.platform.utils.JsonUtils; import com.ruoyi.platform.vo.InsMetricInfoVo; @@ -28,6 +30,8 @@ public class ExperimentInstanceStatusTask { private ExperimentInsDao experimentInsDao; @Resource private AimService aimService; + @Resource + private ResourceOccupyService resourceOccupyService; private List<Integer> experimentIds = new ArrayList<>(); @@ -44,6 +48,19 @@ public class ExperimentInstanceStatusTask { experimentIns = experimentInsService.queryStatusFromArgo(experimentIns); } catch (Exception e) { experimentIns.setStatus("Failed"); + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null); + } + + // 扣除积分 + Map<String, Object> nodesStatusMap = JsonUtils.jsonToMap(experimentIns.getNodesStatus()); + for (String key : nodesStatusMap.keySet()) { + Map<String, Object> value = (Map<String, Object>) nodesStatusMap.get(key); + Date finishedAt = (Date) value.get("finishedAt"); + if (finishedAt == null) { + resourceOccupyService.deducing(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key); + } else { + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key); + } } //运行成功的实验实例记录指标数值 Map<String, Object> metricRecord = JacksonUtil.parseJSONStr2Map(experimentIns.getMetricRecord()); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java index 86f21d65..1ce1003f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -43,12 +43,14 @@ public class RayInsStatusTask { try { rayIns = rayInsService.queryStatusFromArgo(rayIns); if (Constant.Running.equals(rayIns.getStatus())) { - resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId()); - } else { - resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId()); + resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId(), null); + } else if (Constant.Failed.equals(rayIns.getStatus()) || Constant.Terminated.equals(rayIns.getStatus()) + || Constant.Succeeded.equals(rayIns.getStatus())) { + resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null); } } catch (Exception e) { rayIns.setStatus(Constant.Failed); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null); } // 线程安全的添加操作 synchronized (rayIds) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java index 1ae0d2f1..9ca83e85 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -29,7 +29,7 @@ public class ResourceOccupyTask { public void devDeduceCredit() { List<DevEnvironment> devEnvironments = devEnvironmentDao.getRunning(); for (DevEnvironment devEnvironment : devEnvironments) { - resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId())); + resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null); } } @@ -38,7 +38,7 @@ public class ResourceOccupyTask { public void serviceDeduceCredit() { List<ServiceVersion> serviceVersions = serviceDao.getRunning(); for (ServiceVersion serviceVersion : serviceVersions) { - resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId()); + resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId(), null); } } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 8395bc75..a925ef71 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -8,11 +8,11 @@ public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; - void startDeduce(Integer computingResourceId, String taskType, Long taskId); + void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId); - void endDeduce(String taskType, Long taskId); + void endDeduce(String taskType, Long taskId, String nodeId); - void deducing(String taskType, Long taskId); + void deducing(String taskType, Long taskId, String nodeId); Page<ResourceOccupy> queryByPage(PageRequest pageRequest); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java index dcdb3b7c..0fc2785a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java @@ -10,7 +10,6 @@ import com.ruoyi.platform.domain.*; import com.ruoyi.platform.domain.dependencydomain.ProjectDepency; import com.ruoyi.platform.domain.dependencydomain.TrainTaskDepency; import com.ruoyi.platform.mapper.ExperimentDao; -import com.ruoyi.platform.mapper.ExperimentInsDao; import com.ruoyi.platform.mapper.ModelDependency1Dao; import com.ruoyi.platform.service.*; import com.ruoyi.platform.utils.HttpUtils; @@ -62,6 +61,9 @@ public class ExperimentServiceImpl implements ExperimentService { @Lazy private ExperimentInsService experimentInsService; + @Resource + private ResourceOccupyService resourceOccupyService; + @Resource private ModelDependency1Dao modelDependency1Dao; @@ -233,6 +235,14 @@ public class ExperimentServiceImpl implements ExperimentService { } Map<String, Object> converMap = JsonUtils.jsonToMap(convertRes); + // 判断积分和资源是否足够 + Map<String, Map<String, Object>> resourceInfo = (Map<String, Map<String, Object>>) converMap.get("resource_info"); + for (Map.Entry<String, Map<String, Object>> entry : resourceInfo.entrySet()) { + Map<String, Object> node = entry.getValue(); + resourceOccupyService.haveResource((Integer) node.get("computing_resource_id")); + } + + // 组装运行接口json Map<String, Object> runReqMap = new HashMap<>(); runReqMap.put("data", converMap.get("data")); @@ -300,6 +310,11 @@ public class ExperimentServiceImpl implements ExperimentService { insertDatasetTempStorage(datasetDependendcy, trainInfo, experiment.getId(), insert.getId(), experiment.getName(), experiment.getWorkflowId()); } + // 记录开始扣积分 + for (Map.Entry<String, Map<String, Object>> entry : resourceInfo.entrySet()) { + Map<String, Object> node = entry.getValue(); + resourceOccupyService.startDeduce((Integer) node.get("computing_resource_id"), Constant.TaskType_Workflow, Long.valueOf(insert.getId()), entry.getKey()); + } } catch (Exception e) { throw new RuntimeException(e); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 3a6048eb..2a542b4c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -134,7 +134,7 @@ public class JupyterServiceImpl implements JupyterService { } // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id)); + resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id), null); // 使用 Kubernetes API 删除 Pod String deleteResult = k8sClientUtil.deletePod(podName, namespace); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 2ae6a5de..4d86a5ba 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -168,7 +168,7 @@ public class RayInsServiceImpl implements RayInsService { rayInsDao.update(ins); updateRayStatus(rayIns.getRayId()); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Ray, id); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, id, null); return true; } else { return false; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 105025a6..ac19a3d8 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -205,7 +205,7 @@ public class RayServiceImpl implements RayService { rayIns.setResultPath(outputPath); rayInsDao.insert(rayIns); rayInsService.updateRayStatus(id); - resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, rayIns.getId()); + resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, rayIns.getId(), null); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 12f79edf..91d29c1d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Date; +import java.util.List; @Service("resourceOccupyService") public class ResourceOccupyServiceImpl implements ResourceOccupyService { @@ -48,15 +49,17 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public void startDeduce(Integer computingResourceId, String taskType, Long taskId) { + public void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId) { ResourceOccupy resourceOccupy = new ResourceOccupy(); ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); resourceOccupy.setComputingResourceId(computingResourceId); LoginUser loginUser = SecurityUtils.getLoginUser(); resourceOccupy.setUserId(loginUser.getUserid()); resourceOccupy.setCreditPerHour(computingResource.getCreditPerHour()); + resourceOccupy.setDescription(computingResource.getDescription()); resourceOccupy.setTaskType(taskType); resourceOccupy.setTaskId(taskId); + resourceOccupy.setNodeId(nodeId); resourceOccupyDao.save(resourceOccupy); if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { @@ -67,31 +70,35 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public void endDeduce(String taskType, Long taskId) { - ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId); - deducing(taskType, taskId); - resourceOccupy.setState(Constant.State_invalid); - resourceOccupyDao.edit(resourceOccupy); + public void endDeduce(String taskType, Long taskId, String nodeId) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); + for (ResourceOccupy resourceOccupy : resourceOccupys) { + deducing(taskType, taskId, nodeId); + resourceOccupy.setState(Constant.State_invalid); + resourceOccupyDao.edit(resourceOccupy); - ComputingResource computingResource = computingResourceDao.queryById(resourceOccupy.getComputingResourceId()); - if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { - resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getGpuNums()); - } else { - resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getCpuCores()); + ComputingResource computingResource = computingResourceDao.queryById(resourceOccupy.getComputingResourceId()); + if (Constant.Computing_Resource_GPU.equals(computingResource.getComputingResource())) { + resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getGpuNums()); + } else { + resourceOccupyDao.updateUnUsed(computingResource.getResourceId(), computingResource.getCpuCores()); + } } } @Override - public void deducing(String taskType, Long taskId) { - ResourceOccupy resourceOccupy = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId); - long timeDifferenceMillis = new Date().getTime() - resourceOccupy.getDeduceLastTime().getTime(); - Float hours = (float) (timeDifferenceMillis / (1000 * 60 * 60)); + public void deducing(String taskType, Long taskId, String nodeId) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); + for (ResourceOccupy resourceOccupy : resourceOccupys) { + long timeDifferenceMillis = new Date().getTime() - resourceOccupy.getDeduceLastTime().getTime(); + Double hours = (double) timeDifferenceMillis / (1000 * 60 * 60); - float deduceCredit = resourceOccupy.getCreditPerHour() * hours; - resourceOccupyDao.deduceCredit(deduceCredit, resourceOccupy.getUserId()); + Double deduceCredit = resourceOccupy.getCreditPerHour() * hours; + resourceOccupyDao.deduceCredit(deduceCredit, resourceOccupy.getUserId()); - resourceOccupy.setDeduceLastTime(new Date()); - resourceOccupyDao.edit(resourceOccupy); + resourceOccupy.setDeduceLastTime(new Date()); + resourceOccupyDao.edit(resourceOccupy); + } } @Override diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 8160c457..ddf81f14 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -258,7 +258,7 @@ public class ServiceServiceImpl implements ServiceService { if (StringUtils.isNotEmpty(req)) { Map<String, Object> reqMap = JacksonUtil.parseJSONStr2Map(req); if ((Integer) reqMap.get("code") == 200) { - resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId()); + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId(), null); Map<String, String> data = (Map<String, String>) reqMap.get("data"); serviceVersion.setUrl(data.get("url")); serviceVersion.setDeploymentName(data.get("deployment_name")); @@ -286,7 +286,7 @@ public class ServiceServiceImpl implements ServiceService { serviceVersion.setRunState(Constant.Stopped); serviceDao.updateServiceVersion(serviceVersion); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Service, id); + resourceOccupyService.endDeduce(Constant.TaskType_Service, id, null); return "停止成功"; } else { throw new RuntimeException("停止失败"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index 31ea6338..03796fdb 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -1,6 +1,5 @@ package com.ruoyi.platform.utils; -import com.alibaba.fastjson2.JSON; import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.ComputingResource; import com.ruoyi.platform.domain.DevEnvironment; @@ -11,7 +10,6 @@ import io.kubernetes.client.custom.IntOrString; import io.kubernetes.client.custom.Quantity; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; -import io.kubernetes.client.openapi.apis.AppsV1Api; import io.kubernetes.client.openapi.apis.CoreV1Api; import io.kubernetes.client.openapi.models.*; import io.kubernetes.client.util.ClientBuilder; @@ -19,7 +17,6 @@ import io.kubernetes.client.util.Config; import io.kubernetes.client.util.credentials.AccessTokenAuthentication; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; -import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -519,7 +516,7 @@ public class K8sClientUtil { pod = api.createNamespacedPod(namespace, pod, null, null, null); String nodeName = getNodeName(podName, namespace); - resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId())); + resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null); } } catch (ApiException e) { throw new RuntimeException("创建pod异常:" + e.getResponseBody()); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index ad8a782c..9b905949 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -2,8 +2,11 @@ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.platform.mapper.ResourceOccupyDao"> <insert id="save"> - insert into resource_occupy (user_id, computing_resource_id, credit_per_hour) - values (#{resourceOccupy.userId}, #{resourceOccupy.computingResourceId}, #{resourceOccupy.creditPerHour}) + insert into resource_occupy (user_id, computing_resource_id, credit_per_hour, description, task_type, task_id, + node_id) + values (#{resourceOccupy.userId}, #{resourceOccupy.computingResourceId}, #{resourceOccupy.creditPerHour}, + #{resourceOccupy.description}, #{resourceOccupy.taskType}, #{resourceOccupy.taskId}, + #{resourceOccupy.nodeId}) </insert> <update id="edit"> @@ -46,8 +49,12 @@ <select id="getResourceOccupyByTask" resultType="com.ruoyi.platform.domain.ResourceOccupy"> select * from resource_occupy - where task_type = #{task_type} - and task_id = #{task_id} + where task_type = #{taskType} + and task_id = #{taskId} + <if test="nodeId != null and nodeId !=''"> + node_id = #{nodeId}, + </if> + and state = 1 </select> <select id="count" resultType="java.lang.Long"> From a181da66c0f77ddb0377d49f83064271dc3a6301 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 19 Mar 2025 17:21:44 +0800 Subject: [PATCH 109/127] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DuseStatebug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/Pipeline/Info/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-ui/src/pages/Pipeline/Info/index.jsx b/react-ui/src/pages/Pipeline/Info/index.jsx index 3cc69a4c..1e1e078b 100644 --- a/react-ui/src/pages/Pipeline/Info/index.jsx +++ b/react-ui/src/pages/Pipeline/Info/index.jsx @@ -7,7 +7,7 @@ import { to } from '@/utils/promise'; import G6 from '@antv/g6'; import { useNavigate, useParams } from '@umijs/max'; import { App, Button } from 'antd'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import GlobalParamsDrawer from '../components/GlobalParamsDrawer'; import ModelMenu from '../components/ModelMenu'; import Props from '../components/PipelineNodeDrawer'; From 872cb80abe1f875e961a7b175ab3b8403f819669 Mon Sep 17 00:00:00 2001 From: cp3hnu <cp3hnu@gmail.com> Date: Wed, 19 Mar 2025 17:37:08 +0800 Subject: [PATCH 110/127] =?UTF-8?q?feat:=20=E4=BB=A3=E7=A0=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=B7=BB=E5=8A=A0=E5=A4=9A=E4=B8=AA=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/CodeSelect/index.tsx | 17 +++++++++-------- .../components/PipelineNodeDrawer/index.tsx | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index c6486d1e..059d857f 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -35,25 +35,26 @@ function CodeSelect({ const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { if (res) { - const { git_url, git_branch, code_repo_name } = res; + const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = + res; const jsonObj = { + 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 showValue = code_repo_name; onChange?.({ value: jsonObjStr, - showValue, + showValue: code_repo_name, fromSelect: true, ...jsonObj, }); } else { - onChange?.({ - value: undefined, - showValue: undefined, - fromSelect: false, - }); + onChange?.(undefined); } close(); }, diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index 8159e646..8defc169 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -150,19 +150,21 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { if (res) { + const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = + res; const value = JSON.stringify({ - id: res.id, - name: res.code_repo_name, - code_path: res.git_url, - branch: res.git_branch, - username: res.git_user_name, - password: res.git_password, - ssh_private_key: res.ssh_key, + id, + name: code_repo_name, + code_path: git_url, + branch: git_branch, + username: git_user_name, + password: git_password, + ssh_private_key: ssh_key, }); form.setFieldValue(formItemName, { ...item, value, - showValue: res.code_repo_name, + showValue: code_repo_name, fromSelect: true, }); form.validateFields([formItemName]); From 0c5fd354048ea64619856d971013963dcaf03e20 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 20 Mar 2025 11:22:46 +0800 Subject: [PATCH 111/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/domain/ComputingResource.java | 2 +- .../ruoyi/platform/domain/ResourceOccupy.java | 5 +++- .../ExperimentInstanceStatusTask.java | 29 ++++++++++++------- .../platform/scheduling/RayInsStatusTask.java | 9 ++++-- .../scheduling/ResourceOccupyTask.java | 6 ++-- .../service/ResourceOccupyService.java | 6 ++-- .../impl/ExperimentInsServiceImpl.java | 7 +++++ .../service/impl/JupyterServiceImpl.java | 6 ++-- .../service/impl/RayInsServiceImpl.java | 2 +- .../platform/service/impl/RayServiceImpl.java | 1 + .../impl/ResourceOccupyServiceImpl.java | 22 ++++++++++---- .../service/impl/ServiceServiceImpl.java | 8 +++-- .../ruoyi/platform/utils/K8sClientUtil.java | 4 +-- .../managementPlatform/ResourceOccupy.xml | 7 +++-- 14 files changed, 77 insertions(+), 37 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java index 5e7b83b1..b5a13d7e 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ComputingResource.java @@ -49,7 +49,7 @@ public class ComputingResource implements Serializable { private Integer gpuNums; @ApiModelProperty("积分/小时") - private Float creditPerHour; + private Double creditPerHour; @ApiModelProperty("标签") private String labels; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java index d570472c..53eaba3b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -23,7 +23,10 @@ public class ResourceOccupy { private String description; @ApiModelProperty("积分/小时") - private Float creditPerHour; + private Double creditPerHour; + + @ApiModelProperty("扣除的积分") + private Double deduceCredit; @ApiModelProperty("上一次扣分时间") private Date deduceLastTime; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java index 44266a04..1c32edff 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java @@ -18,6 +18,8 @@ import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.*; @Component() @@ -47,19 +49,26 @@ public class ExperimentInstanceStatusTask { try { experimentIns = experimentInsService.queryStatusFromArgo(experimentIns); } catch (Exception e) { - experimentIns.setStatus("Failed"); - resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null); + experimentIns.setStatus(Constant.Failed); + // 结束扣除积分 + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null, null); } // 扣除积分 - Map<String, Object> nodesStatusMap = JsonUtils.jsonToMap(experimentIns.getNodesStatus()); - for (String key : nodesStatusMap.keySet()) { - Map<String, Object> value = (Map<String, Object>) nodesStatusMap.get(key); - Date finishedAt = (Date) value.get("finishedAt"); - if (finishedAt == null) { - resourceOccupyService.deducing(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key); - } else { - resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key); + if (StringUtils.isNotEmpty(experimentIns.getNodesStatus())) { + Map<String, Object> nodesStatusMap = JsonUtils.jsonToMap(experimentIns.getNodesStatus()); + for (String key : nodesStatusMap.keySet()) { + Map<String, Object> value = (Map<String, Object>) nodesStatusMap.get(key); + String startedAt = (String) value.get("startedAt"); + Instant instant = Instant.parse(startedAt); + Date startTime = Date.from(instant); + + String finishedAt = (String) value.get("finishedAt"); + if (StringUtils.isEmpty(finishedAt)) { + resourceOccupyService.deducing(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key, startTime); + } else { + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key, startTime); + } } } //运行成功的实验实例记录指标数值 diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java index 1ce1003f..083559ca 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -42,15 +42,18 @@ public class RayInsStatusTask { //当原本状态为null或非终止态时才调用argo接口 try { rayIns = rayInsService.queryStatusFromArgo(rayIns); + + // 扣除积分 if (Constant.Running.equals(rayIns.getStatus())) { - resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId(), null); + resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId(), null,null); } else if (Constant.Failed.equals(rayIns.getStatus()) || Constant.Terminated.equals(rayIns.getStatus()) || Constant.Succeeded.equals(rayIns.getStatus())) { - resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null,null); } } catch (Exception e) { rayIns.setStatus(Constant.Failed); - resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null); + // 结束扣除积分 + resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null, null); } // 线程安全的添加操作 synchronized (rayIds) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java index 9ca83e85..d3e0b0dd 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -25,11 +25,11 @@ public class ResourceOccupyTask { private ServiceDao serviceDao; // 开发环境功能扣除积分 - @Scheduled(cron = "0 0/10 * * * ?") // 每10分钟执行一次 + @Scheduled(cron = "0 0/1 * * * ?") // 每10分钟执行一次 public void devDeduceCredit() { List<DevEnvironment> devEnvironments = devEnvironmentDao.getRunning(); for (DevEnvironment devEnvironment : devEnvironments) { - resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null); + resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null, null); } } @@ -38,7 +38,7 @@ public class ResourceOccupyTask { public void serviceDeduceCredit() { List<ServiceVersion> serviceVersions = serviceDao.getRunning(); for (ServiceVersion serviceVersion : serviceVersions) { - resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId(), null); + resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId(), null, null); } } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index a925ef71..1c57ba1b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -4,15 +4,17 @@ import com.ruoyi.platform.domain.ResourceOccupy; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import java.util.Date; + public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId); - void endDeduce(String taskType, Long taskId, String nodeId); + void endDeduce(String taskType, Long taskId, String nodeId, Date nodeStartTime); - void deducing(String taskType, Long taskId, String nodeId); + void deducing(String taskType, Long taskId, String nodeId, Date nodeStartTime); Page<ResourceOccupy> queryByPage(PageRequest pageRequest); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java index 03599dd1..995a2191 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java @@ -3,6 +3,7 @@ package com.ruoyi.platform.service.impl; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.security.utils.SecurityUtils; +import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.DatasetTempStorage; import com.ruoyi.platform.domain.Experiment; import com.ruoyi.platform.domain.ExperimentIns; @@ -12,6 +13,7 @@ import com.ruoyi.platform.mapper.ExperimentDao; import com.ruoyi.platform.mapper.ExperimentInsDao; import com.ruoyi.platform.mapper.ModelDependency1Dao; import com.ruoyi.platform.service.ExperimentInsService; +import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.utils.*; import com.ruoyi.platform.vo.LogRequestVo; import com.ruoyi.platform.vo.PodLogVo; @@ -66,6 +68,9 @@ public class ExperimentInsServiceImpl implements ExperimentInsService { @Resource private DatasetTempStorageDao datasetTempStorageDao; + @Resource + private ResourceOccupyService resourceOccupyService; + private final MinioUtil minioUtil; public ExperimentInsServiceImpl(MinioUtil minioUtil) { @@ -410,6 +415,8 @@ public class ExperimentInsServiceImpl implements ExperimentInsService { //修改实验状态 updateExperimentStatus(experimentIns.getExperimentId()); + // 结束扣除积分 + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null, null); return true; } else { throw new Exception("终止错误"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 2a542b4c..76fe1d02 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -133,9 +133,6 @@ public class JupyterServiceImpl implements JupyterService { return "pod不存在!"; } - // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id), null); - // 使用 Kubernetes API 删除 Pod String deleteResult = k8sClientUtil.deletePod(podName, namespace); // 删除service @@ -143,6 +140,9 @@ public class JupyterServiceImpl implements JupyterService { devEnvironment.setStatus(Constant.Terminated); this.devEnvironmentService.update(devEnvironment); + + // 结束扣积分 + resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id), null, null); return deleteResult + ",编辑器已停止"; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 4d86a5ba..31c4ab91 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -168,7 +168,7 @@ public class RayInsServiceImpl implements RayInsService { rayInsDao.update(ins); updateRayStatus(rayIns.getRayId()); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Ray, id, null); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, id, null, null); return true; } else { return false; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index ac19a3d8..f758a58d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -205,6 +205,7 @@ public class RayServiceImpl implements RayService { rayIns.setResultPath(outputPath); rayInsDao.insert(rayIns); rayInsService.updateRayStatus(id); + // 记录开始扣除积分 resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, rayIns.getId(), null); } catch (Exception e) { throw new RuntimeException(e); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 91d29c1d..19283e6f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.Date; @@ -49,6 +50,7 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override + @Transactional public void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId) { ResourceOccupy resourceOccupy = new ResourceOccupy(); ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); @@ -70,10 +72,11 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public void endDeduce(String taskType, Long taskId, String nodeId) { + @Transactional + public void endDeduce(String taskType, Long taskId, String nodeId, Date nodeStartTime) { List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { - deducing(taskType, taskId, nodeId); + deducing(taskType, taskId, nodeId, nodeStartTime); resourceOccupy.setState(Constant.State_invalid); resourceOccupyDao.edit(resourceOccupy); @@ -87,16 +90,23 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public void deducing(String taskType, Long taskId, String nodeId) { + @Transactional + public void deducing(String taskType, Long taskId, String nodeId, Date nodeStartTime) { List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { - long timeDifferenceMillis = new Date().getTime() - resourceOccupy.getDeduceLastTime().getTime(); + Date now = new Date(); + long timeDifferenceMillis; + if (nodeStartTime != null && resourceOccupy.getDeduceLastTime().before(nodeStartTime)) { + timeDifferenceMillis = now.getTime() - nodeStartTime.getTime(); + } else { + timeDifferenceMillis = now.getTime() - resourceOccupy.getDeduceLastTime().getTime(); + } Double hours = (double) timeDifferenceMillis / (1000 * 60 * 60); - Double deduceCredit = resourceOccupy.getCreditPerHour() * hours; resourceOccupyDao.deduceCredit(deduceCredit, resourceOccupy.getUserId()); - resourceOccupy.setDeduceLastTime(new Date()); + resourceOccupy.setDeduceCredit(resourceOccupy.getDeduceCredit() + deduceCredit); + resourceOccupy.setDeduceLastTime(now); resourceOccupyDao.edit(resourceOccupy); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index ddf81f14..75cbe10a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -252,19 +252,21 @@ public class ServiceServiceImpl implements ServiceService { paramMap.put("service_type", service.getServiceType()); paramMap.put("deploy_type", serviceVersion.getDeployType()); - // 记录开始扣积分 + // 判断是否有资源 if (resourceOccupyService.haveResource(serviceVersion.getComputingResourceId())) { String req = HttpUtils.sendPost(argoUrl + modelService + "/create", JSON.toJSONString(paramMap)); if (StringUtils.isNotEmpty(req)) { Map<String, Object> reqMap = JacksonUtil.parseJSONStr2Map(req); if ((Integer) reqMap.get("code") == 200) { - resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId(), null); Map<String, String> data = (Map<String, String>) reqMap.get("data"); serviceVersion.setUrl(data.get("url")); serviceVersion.setDeploymentName(data.get("deployment_name")); serviceVersion.setSvcName(data.get("svc_name")); serviceVersion.setRunState(Constant.Pending); serviceDao.updateServiceVersion(serviceVersion); + + // 记录开始扣积分 + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId(), null); return "启动成功"; } else { throw new RuntimeException("启动失败"); @@ -286,7 +288,7 @@ public class ServiceServiceImpl implements ServiceService { serviceVersion.setRunState(Constant.Stopped); serviceDao.updateServiceVersion(serviceVersion); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Service, id, null); + resourceOccupyService.endDeduce(Constant.TaskType_Service, id, null, null); return "停止成功"; } else { throw new RuntimeException("停止失败"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index 03796fdb..a55ad632 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -516,6 +516,7 @@ public class K8sClientUtil { pod = api.createNamespacedPod(namespace, pod, null, null, null); String nodeName = getNodeName(podName, namespace); + // 记录开始扣除积分 resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null); } } catch (ApiException e) { @@ -696,8 +697,7 @@ public class K8sClientUtil { ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); //配置pod资源 - String memory = computingResource.getMemoryGb().toString(); - memory = memory.substring(0, memory.length() - 1).concat("i"); + String memory = computingResource.getMemoryGb().toString().concat("Gi"); HashMap<String, Quantity> limitMap = new HashMap<>(); if (computingResource.getGpuNums() != null && computingResource.getGpuNums() != 0) { limitMap.put("nvidia.com/gpu", new Quantity(String.valueOf(computingResource.getGpuNums()))); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 9b905949..10407dc2 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -18,6 +18,9 @@ <if test="resourceOccupy.deduceLastTime != null"> deduce_last_time = #{resourceOccupy.deduceLastTime}, </if> + <if test="resourceOccupy.deduceCredit != null"> + deduce_credit = #{resourceOccupy.deduceCredit}, + </if> </set> where id = #{resourceOccupy.id} </update> @@ -52,13 +55,13 @@ where task_type = #{taskType} and task_id = #{taskId} <if test="nodeId != null and nodeId !=''"> - node_id = #{nodeId}, + and node_id = #{nodeId} </if> and state = 1 </select> <select id="count" resultType="java.lang.Long"> - select count(1) resource_occupy + select count(1) from resource_occupy </select> <select id="queryByPage" resultType="com.ruoyi.platform.domain.ResourceOccupy"> From 7321458290ef951005d0d31670f0d6f7c8caaf2d Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 20 Mar 2025 13:59:37 +0800 Subject: [PATCH 112/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComputingResourceController.java | 7 +++++ .../platform/mapper/ResourceOccupyDao.java | 8 +++-- .../service/ResourceOccupyService.java | 3 ++ .../impl/ResourceOccupyServiceImpl.java | 16 ++++++++-- .../managementPlatform/ResourceOccupy.xml | 29 ++++++++++++++++--- .../resources/mapper/system/SysUserMapper.xml | 8 ++--- 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java index 741dd69a..fff00c5b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java @@ -13,6 +13,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import java.util.Map; /** * (ComputingResource)表控制层 @@ -104,5 +105,11 @@ public class ComputingResourceController extends BaseController { PageRequest pageRequest = PageRequest.of(page, size); return genericsSuccess(resourceOccupyService.queryByPage(pageRequest)); } + + @GetMapping("/credit") + @ApiOperation("查询用户积分使用情况") + public GenericsAjaxResult<Map<String, Integer>> queryCredit() { + return genericsSuccess(resourceOccupyService.queryCredit()); + } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index ca3b98d2..da008f3e 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -22,7 +22,11 @@ public interface ResourceOccupyDao { int updateUnUsed(@Param("id") Integer id, @Param("used") Integer used); - long count(); + long count(@Param("userId") Long userId); - List<ResourceOccupy> queryByPage(@Param("pageable") Pageable pageable); + List<ResourceOccupy> queryByPage(@Param("userId") Long userId, @Param("pageable") Pageable pageable); + + Integer getUserCredit(@Param("userId") Long userId); + + Integer getDeduceCredit(@Param("userId") Long userId); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 1c57ba1b..fcebe3c3 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import java.util.Date; +import java.util.Map; public interface ResourceOccupyService { @@ -17,4 +18,6 @@ public interface ResourceOccupyService { void deducing(String taskType, Long taskId, String nodeId, Date nodeStartTime); Page<ResourceOccupy> queryByPage(PageRequest pageRequest); + + Map<String, Integer> queryCredit(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 19283e6f..9b08d484 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -16,7 +16,9 @@ import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service("resourceOccupyService") public class ResourceOccupyServiceImpl implements ResourceOccupyService { @@ -113,7 +115,17 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override public Page<ResourceOccupy> queryByPage(PageRequest pageRequest) { - long total = resourceOccupyDao.count(); - return new PageImpl<>(resourceOccupyDao.queryByPage(pageRequest), pageRequest, total); + long total = resourceOccupyDao.count(SecurityUtils.getLoginUser().getUserid()); + return new PageImpl<>(resourceOccupyDao.queryByPage(SecurityUtils.getLoginUser().getUserid(), pageRequest), pageRequest, total); + } + + @Override + public Map<String, Integer> queryCredit() { + Integer userCredit = resourceOccupyDao.getUserCredit(SecurityUtils.getLoginUser().getUserid()); + Integer deduceCredit = resourceOccupyDao.getDeduceCredit(SecurityUtils.getLoginUser().getUserid()); + HashMap<String, Integer> result = new HashMap<>(); + result.put("userCredit", userCredit); + result.put("deduceCredit", deduceCredit); + return result; } } diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 10407dc2..48c62105 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -53,20 +53,41 @@ select * from resource_occupy where task_type = #{taskType} - and task_id = #{taskId} + and task_id = #{taskId} <if test="nodeId != null and nodeId !=''"> and node_id = #{nodeId} </if> - and state = 1 + and state = 1 </select> <select id="count" resultType="java.lang.Long"> - select count(1) from resource_occupy + select count(1) + from resource_occupy + where user_id = #{userId} </select> <select id="queryByPage" resultType="com.ruoyi.platform.domain.ResourceOccupy"> - select * + select id, + user_id, + description, + credit_per_hour, + TRUNCATE(deduce_credit, 1) as deduce_credit, + start_time, + state from resource_occupy + where user_id = #{userId} order by start_time desc limit #{pageable.offset}, #{pageable.pageSize} </select> + + <select id="getUserCredit" resultType="java.lang.Integer"> + select TRUNCATE(credit, 1) as credit + from sys_user + where user_id = #{userId} + </select> + + <select id="getDeduceCredit" resultType="java.lang.Integer"> + select TRUNCATE(sum(deduce_credit), 1) as deduce_credit + from resource_occupy + where user_id = #{userId} + </select> </mapper> \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index c205566f..79e79a01 100644 --- a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -60,7 +60,7 @@ u.password, u.git_link_username, u.git_link_password, - u.credit, + TRUNCATE(u.credit, 1) as credit, u.sex, u.status, u.del_flag, @@ -90,7 +90,7 @@ <select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult"> select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, - u.git_link_username, u.credit, + u.git_link_username, TRUNCATE(u.credit, 1) as credit, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id @@ -125,7 +125,7 @@ </select> <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult"> - select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time, u.credit, u.git_link_username + select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time, TRUNCATE(u.credit, 1) as credit, u.git_link_username from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id @@ -142,7 +142,7 @@ </select> <select id="selectUnallocatedList" parameterType="SysUser" resultMap="SysUserResult"> - select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time , u.credit, u.git_link_username + select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time , TRUNCATE(u.credit, 1) as credit, u.git_link_username from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id From 13c41ece1251ae72ca2486d7b40b84ca1bd9f893 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 20 Mar 2025 14:46:04 +0800 Subject: [PATCH 113/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/resources/ComputingResourceController.java | 2 +- .../java/com/ruoyi/platform/mapper/ResourceOccupyDao.java | 4 ++-- .../com/ruoyi/platform/service/ResourceOccupyService.java | 2 +- .../platform/service/impl/ResourceOccupyServiceImpl.java | 8 ++++---- .../mapper/managementPlatform/ResourceOccupy.xml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java index fff00c5b..d7fd42ca 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/resources/ComputingResourceController.java @@ -108,7 +108,7 @@ public class ComputingResourceController extends BaseController { @GetMapping("/credit") @ApiOperation("查询用户积分使用情况") - public GenericsAjaxResult<Map<String, Integer>> queryCredit() { + public GenericsAjaxResult<Map<String, Double>> queryCredit() { return genericsSuccess(resourceOccupyService.queryCredit()); } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index da008f3e..49d71704 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -26,7 +26,7 @@ public interface ResourceOccupyDao { List<ResourceOccupy> queryByPage(@Param("userId") Long userId, @Param("pageable") Pageable pageable); - Integer getUserCredit(@Param("userId") Long userId); + Double getUserCredit(@Param("userId") Long userId); - Integer getDeduceCredit(@Param("userId") Long userId); + Double getDeduceCredit(@Param("userId") Long userId); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index fcebe3c3..5828b110 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -19,5 +19,5 @@ public interface ResourceOccupyService { Page<ResourceOccupy> queryByPage(PageRequest pageRequest); - Map<String, Integer> queryCredit(); + Map<String, Double> queryCredit(); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 9b08d484..62caf886 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -120,10 +120,10 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { } @Override - public Map<String, Integer> queryCredit() { - Integer userCredit = resourceOccupyDao.getUserCredit(SecurityUtils.getLoginUser().getUserid()); - Integer deduceCredit = resourceOccupyDao.getDeduceCredit(SecurityUtils.getLoginUser().getUserid()); - HashMap<String, Integer> result = new HashMap<>(); + public Map<String, Double> queryCredit() { + Double userCredit = resourceOccupyDao.getUserCredit(SecurityUtils.getLoginUser().getUserid()); + Double deduceCredit = resourceOccupyDao.getDeduceCredit(SecurityUtils.getLoginUser().getUserid()); + HashMap<String, Double> result = new HashMap<>(); result.put("userCredit", userCredit); result.put("deduceCredit", deduceCredit); return result; diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 48c62105..2249e650 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -79,13 +79,13 @@ order by start_time desc limit #{pageable.offset}, #{pageable.pageSize} </select> - <select id="getUserCredit" resultType="java.lang.Integer"> + <select id="getUserCredit" resultType="java.lang.Double"> select TRUNCATE(credit, 1) as credit from sys_user where user_id = #{userId} </select> - <select id="getDeduceCredit" resultType="java.lang.Integer"> + <select id="getDeduceCredit" resultType="java.lang.Double"> select TRUNCATE(sum(deduce_credit), 1) as deduce_credit from resource_occupy where user_id = #{userId} From c41589159e1dbaff7206a846363d89d32c641a8c Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Thu, 20 Mar 2025 14:52:48 +0800 Subject: [PATCH 114/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/ruoyi/platform/constant/Constant.java | 1 - 1 file changed, 1 deletion(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java index 80480fca..f6b273b4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/constant/Constant.java @@ -53,7 +53,6 @@ public class Constant { // 任务类型 public final static String TaskType_Dev = "dev_environment"; public final static String TaskType_Workflow = "workflow"; - public final static String TaskType_AutoMl = "auto_ml"; public final static String TaskType_Ray = "ray"; public final static String TaskType_ActiveLearn = "active_learn"; public final static String TaskType_Service = "service"; From 4bd9223209ecf41865c8b1f6b6b809ec2a45affc Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 08:43:06 +0800 Subject: [PATCH 115/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/service/impl/DevEnvironmentServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java index 67861b16..abb8dbd0 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java @@ -64,11 +64,11 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { if (!devEnv.getStatus().equals(PodStatus.Terminated.getName()) && !devEnv.getStatus().equals(PodStatus.Failed.getName())) { PodStatusVo podStatusVo = this.jupyterService.getJupyterStatus(devEnv); - devEnv.setStatus(podStatusVo.getStatus()); - devEnv.setUrl(podStatusVo.getUrl()); if(!devEnv.getStatus().equals(podStatusVo.getStatus())){ this.devEnvironmentDao.update(devEnv); } + devEnv.setStatus(podStatusVo.getStatus()); + devEnv.setUrl(podStatusVo.getUrl()); } } catch (Exception e) { devEnv.setStatus(PodStatus.Unknown.getName()); From 0e3eb31cdaf5de040170e0fd192e94a6129c0e1b Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 09:08:23 +0800 Subject: [PATCH 116/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/service/impl/DevEnvironmentServiceImpl.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java index abb8dbd0..a35a6881 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DevEnvironmentServiceImpl.java @@ -35,6 +35,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { @Resource @Lazy private JupyterService jupyterService; + /** * 通过ID查询单条数据 * @@ -64,11 +65,13 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { if (!devEnv.getStatus().equals(PodStatus.Terminated.getName()) && !devEnv.getStatus().equals(PodStatus.Failed.getName())) { PodStatusVo podStatusVo = this.jupyterService.getJupyterStatus(devEnv); - if(!devEnv.getStatus().equals(podStatusVo.getStatus())){ + if (!devEnv.getStatus().equals(podStatusVo.getStatus())) { + devEnv.setStatus(podStatusVo.getStatus()); + devEnv.setUrl(podStatusVo.getUrl()); this.devEnvironmentDao.update(devEnv); + } else { + devEnv.setUrl(podStatusVo.getUrl()); } - devEnv.setStatus(podStatusVo.getStatus()); - devEnv.setUrl(podStatusVo.getUrl()); } } catch (Exception e) { devEnv.setStatus(PodStatus.Unknown.getName()); From a6a4bb006a49cb71b4a0a5fdc6c6223ec5b100e2 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 10:17:56 +0800 Subject: [PATCH 117/127] =?UTF-8?q?=E8=B7=A8=E5=9F=9F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E8=A7=A3=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- k8s/dockerfiles/conf/nginx.conf | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/k8s/dockerfiles/conf/nginx.conf b/k8s/dockerfiles/conf/nginx.conf index c23e02c3..0d04beef 100644 --- a/k8s/dockerfiles/conf/nginx.conf +++ b/k8s/dockerfiles/conf/nginx.conf @@ -29,11 +29,25 @@ http { location /label-studio/ { # rewrite ^/label-studio/(.*)$ /$1 break; - proxy_pass http://label-studio-service.argo.svc:8080/projects/; + proxy_pass http://label-studio-service.argo.svc:9000/projects/; proxy_hide_header X-Frame-Options; add_header X-Frame-Options ALLOWALL; } + location /minio/ { + # rewrite ^/label-studio/(.*)$ /$1 break; + proxy_pass http://juicefs-s3-gateway.juicefs.svc:9000/; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options ALLOWALL; + } + + location /neo4j/ { + # rewrite ^/label-studio/(.*)$ /$1 break; + proxy_pass http://172.20.20.88:7474/; + proxy_hide_header X-Frame-Options; + add_header X-Frame-Options ALLOWALL; + } + location / { rewrite ^/prod-api/(.*)$ /$1 break; root /home/ruoyi/projects/ruoyi-ui; From 52b069a6fc8fe4a4143e84d54ba047ae21c3aeea Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 11:05:57 +0800 Subject: [PATCH 118/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/service/impl/ServiceServiceImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 75cbe10a..cfb33079 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -5,8 +5,10 @@ import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; import com.ruoyi.platform.domain.AssetWorkflow; +import com.ruoyi.platform.domain.ComputingResource; import com.ruoyi.platform.domain.ServiceVersion; import com.ruoyi.platform.mapper.AssetWorkflowDao; +import com.ruoyi.platform.mapper.ComputingResourceDao; import com.ruoyi.platform.mapper.ServiceDao; import com.ruoyi.platform.service.ResourceOccupyService; import com.ruoyi.platform.service.ServiceService; @@ -49,6 +51,9 @@ public class ServiceServiceImpl implements ServiceService { @Resource private ResourceOccupyService resourceOccupyService; + @Resource + private ComputingResourceDao computingResourceDao; + @Override public Page<com.ruoyi.platform.domain.Service> queryByPageService(com.ruoyi.platform.domain.Service service, PageRequest pageRequest) { long total = serviceDao.countService(service); @@ -124,6 +129,8 @@ public class ServiceServiceImpl implements ServiceService { serviceVersion.setCreateBy(loginUser.getUsername()); serviceVersion.setUpdateBy(loginUser.getUsername()); serviceVersion.setRunState(Constant.Pending); + ComputingResource computingResource = computingResourceDao.queryById(serviceVersionVo.getComputingResourceId()); + serviceVersion.setResource(computingResource.getDescription()); serviceDao.insertServiceVersion(serviceVersion); runServiceVersion(serviceVersion.getId()); return serviceVersion; From bbe902ac507b1028b1185b2e33e8eabc2c97c632 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 11:15:12 +0800 Subject: [PATCH 119/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index cfb33079..2dce56b5 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -131,6 +131,7 @@ public class ServiceServiceImpl implements ServiceService { serviceVersion.setRunState(Constant.Pending); ComputingResource computingResource = computingResourceDao.queryById(serviceVersionVo.getComputingResourceId()); serviceVersion.setResource(computingResource.getDescription()); + serviceVersion.setComputingResourceId(serviceVersionVo.getComputingResourceId()); serviceDao.insertServiceVersion(serviceVersion); runServiceVersion(serviceVersion.getId()); return serviceVersion; From df17e143897a9bc208bdd6c64e03add80427f1c6 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 11:23:20 +0800 Subject: [PATCH 120/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/managementPlatform/ServiceDaoMapper.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml index 5ece071a..d9dceb2c 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml @@ -95,10 +95,10 @@ </insert> <insert id="insertServiceVersion" keyProperty="id" useGeneratedKeys="true"> - insert into service_version(service_id, version, model, description, image, resource, replicas, mount_path, env_variables, + insert into service_version(service_id, version, model, description, image, resource, computing_resource_id, replicas, mount_path, env_variables, code_config, command, create_by, update_by, deploy_type) values (#{serviceVersion.serviceId}, #{serviceVersion.version}, #{serviceVersion.model}, #{serviceVersion.description}, #{serviceVersion.image}, - #{serviceVersion.resource}, #{serviceVersion.replicas}, #{serviceVersion.mountPath}, #{serviceVersion.envVariables}, + #{serviceVersion.resource}, #{serviceVersion.computingResourceId}, #{serviceVersion.replicas}, #{serviceVersion.mountPath}, #{serviceVersion.envVariables}, #{serviceVersion.codeConfig}, #{serviceVersion.command}, #{serviceVersion.createBy}, #{serviceVersion.updateBy}, #{serviceVersion.deployType}) </insert> @@ -136,6 +136,9 @@ <if test="serviceVersion.resource != null and serviceVersion.resource !=''"> resource = #{serviceVersion.resource}, </if> + <if test="serviceVersion.computingResourceId != null"> + computing_resource_id = #{serviceVersion.computingResourceId}, + </if> <if test="serviceVersion.replicas != null"> replicas = #{serviceVersion.replicas}, </if> From 90e87af78461faf04f2cb8b43bec1f49f4e6bda4 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 11:36:52 +0800 Subject: [PATCH 121/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java index d3e0b0dd..32c99c78 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -34,7 +34,7 @@ public class ResourceOccupyTask { } // 服务功能扣除积分 - @Scheduled(cron = "0 0/10 * * * ?") // 每10分钟执行一次 + @Scheduled(cron = "0 0/1 * * * ?") // 每10分钟执行一次 public void serviceDeduceCredit() { List<ServiceVersion> serviceVersions = serviceDao.getRunning(); for (ServiceVersion serviceVersion : serviceVersions) { From bb965adbb0807c99d8cbbdc1c2a4f27a7e8ced2d Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 11:53:52 +0800 Subject: [PATCH 122/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ruoyi/platform/domain/ResourceOccupy.java | 8 +++++++- .../com/ruoyi/platform/service/ResourceOccupyService.java | 2 +- .../platform/service/impl/ExperimentServiceImpl.java | 2 +- .../com/ruoyi/platform/service/impl/RayServiceImpl.java | 2 +- .../platform/service/impl/ResourceOccupyServiceImpl.java | 4 +++- .../ruoyi/platform/service/impl/ServiceServiceImpl.java | 2 +- .../main/java/com/ruoyi/platform/utils/K8sClientUtil.java | 2 +- .../mapper/managementPlatform/ResourceOccupy.xml | 2 ++ 8 files changed, 17 insertions(+), 7 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java index 53eaba3b..c9c1b14c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -40,9 +40,15 @@ public class ResourceOccupy { @ApiModelProperty("任务类型") private String taskType; - @ApiModelProperty("类型id") + @ApiModelProperty("任务id") private Long taskId; + @ApiModelProperty("任务id") + private Long taskInsId; + + @ApiModelProperty("流水线id") + private Long workflowId; + @ApiModelProperty("流水线节点id") private String nodeId; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 5828b110..177d2dd6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -11,7 +11,7 @@ public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; - void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId); + void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String nodeId); void endDeduce(String taskType, Long taskId, String nodeId, Date nodeStartTime); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java index 0fc2785a..2adbb5bf 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java @@ -313,7 +313,7 @@ public class ExperimentServiceImpl implements ExperimentService { // 记录开始扣积分 for (Map.Entry<String, Map<String, Object>> entry : resourceInfo.entrySet()) { Map<String, Object> node = entry.getValue(); - resourceOccupyService.startDeduce((Integer) node.get("computing_resource_id"), Constant.TaskType_Workflow, Long.valueOf(insert.getId()), entry.getKey()); + resourceOccupyService.startDeduce((Integer) node.get("computing_resource_id"), Constant.TaskType_Workflow, Long.valueOf(id), Long.valueOf(insert.getId()), experiment.getWorkflowId(), entry.getKey()); } } catch (Exception e) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index f758a58d..3f499866 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -206,7 +206,7 @@ public class RayServiceImpl implements RayService { rayInsDao.insert(rayIns); rayInsService.updateRayStatus(id); // 记录开始扣除积分 - resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, rayIns.getId(), null); + resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, id, rayIns.getId(), null, null); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 62caf886..647ead62 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -53,7 +53,7 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void startDeduce(Integer computingResourceId, String taskType, Long taskId, String nodeId) { + public void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String nodeId) { ResourceOccupy resourceOccupy = new ResourceOccupy(); ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); resourceOccupy.setComputingResourceId(computingResourceId); @@ -63,6 +63,8 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { resourceOccupy.setDescription(computingResource.getDescription()); resourceOccupy.setTaskType(taskType); resourceOccupy.setTaskId(taskId); + resourceOccupy.setTaskInsId(taskInsId); + resourceOccupy.setWorkflowId(workflowId); resourceOccupy.setNodeId(nodeId); resourceOccupyDao.save(resourceOccupy); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 2dce56b5..8fee155a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -274,7 +274,7 @@ public class ServiceServiceImpl implements ServiceService { serviceDao.updateServiceVersion(serviceVersion); // 记录开始扣积分 - resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getId(), null); + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), null, null); return "启动成功"; } else { throw new RuntimeException("启动失败"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index a55ad632..1614ff4b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -517,7 +517,7 @@ public class K8sClientUtil { String nodeName = getNodeName(podName, namespace); // 记录开始扣除积分 - resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null); + resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null, null, null); } } catch (ApiException e) { throw new RuntimeException("创建pod异常:" + e.getResponseBody()); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 2249e650..c1c906cc 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -3,9 +3,11 @@ <mapper namespace="com.ruoyi.platform.mapper.ResourceOccupyDao"> <insert id="save"> insert into resource_occupy (user_id, computing_resource_id, credit_per_hour, description, task_type, task_id, + task_ins_id, workflow_id, node_id) values (#{resourceOccupy.userId}, #{resourceOccupy.computingResourceId}, #{resourceOccupy.creditPerHour}, #{resourceOccupy.description}, #{resourceOccupy.taskType}, #{resourceOccupy.taskId}, + #{resourceOccupy.taskInsId}, #{resourceOccupy.workflowId}, #{resourceOccupy.nodeId}) </insert> From 15af3929acdf9f2c3ed800d1170054b72155b5a5 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 14:20:36 +0800 Subject: [PATCH 123/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/domain/ResourceOccupy.java | 3 ++ .../platform/mapper/ResourceOccupyDao.java | 2 +- .../service/ResourceOccupyService.java | 6 ++-- .../platform/service/ServiceService.java | 2 +- .../service/impl/ExperimentServiceImpl.java | 2 +- .../platform/service/impl/RayServiceImpl.java | 2 +- .../impl/ResourceOccupyServiceImpl.java | 13 +++---- .../service/impl/ServiceServiceImpl.java | 36 +++++++++++-------- .../ruoyi/platform/utils/K8sClientUtil.java | 2 +- .../managementPlatform/ResourceOccupy.xml | 11 ++++-- 10 files changed, 48 insertions(+), 31 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java index c9c1b14c..fb7ceb03 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/domain/ResourceOccupy.java @@ -49,6 +49,9 @@ public class ResourceOccupy { @ApiModelProperty("流水线id") private Long workflowId; + @ApiModelProperty("任务名称") + private String taskName; + @ApiModelProperty("流水线节点id") private String nodeId; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index 49d71704..ba2bcd9d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -14,7 +14,7 @@ public interface ResourceOccupyDao { int edit(@Param("resourceOccupy") ResourceOccupy resourceOccupy); - List<ResourceOccupy> getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId, @Param("nodeId") String nodeId); + List<ResourceOccupy> getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskInsId") Long taskInsId, @Param("nodeId") String nodeId); int deduceCredit(@Param("credit") Double credit, @Param("userId") Long userId); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 177d2dd6..8670a290 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -11,11 +11,11 @@ public interface ResourceOccupyService { Boolean haveResource(Integer computingResourceId) throws Exception; - void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String nodeId); + void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String taskName, String nodeId); - void endDeduce(String taskType, Long taskId, String nodeId, Date nodeStartTime); + void endDeduce(String taskType, Long taskInsId, String nodeId, Date nodeStartTime); - void deducing(String taskType, Long taskId, String nodeId, Date nodeStartTime); + void deducing(String taskType, Long taskInsId, String nodeId, Date nodeStartTime); Page<ResourceOccupy> queryByPage(PageRequest pageRequest); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java index 12b34188..0459aea2 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java @@ -38,7 +38,7 @@ public interface ServiceService { String stopServiceVersion(Long id); - String updateServiceVersion(ServiceVersion serviceVersion); + String updateServiceVersion(ServiceVersion serviceVersion) throws Exception; HashMap<String, String> getServiceVersionLog(Long id, String startTime, String endTime); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java index 2adbb5bf..bf87969a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentServiceImpl.java @@ -313,7 +313,7 @@ public class ExperimentServiceImpl implements ExperimentService { // 记录开始扣积分 for (Map.Entry<String, Map<String, Object>> entry : resourceInfo.entrySet()) { Map<String, Object> node = entry.getValue(); - resourceOccupyService.startDeduce((Integer) node.get("computing_resource_id"), Constant.TaskType_Workflow, Long.valueOf(id), Long.valueOf(insert.getId()), experiment.getWorkflowId(), entry.getKey()); + resourceOccupyService.startDeduce((Integer) node.get("computing_resource_id"), Constant.TaskType_Workflow, Long.valueOf(id), Long.valueOf(insert.getId()), experiment.getWorkflowId(), experiment.getName(), entry.getKey()); } } catch (Exception e) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java index 3f499866..0cbf6ffb 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayServiceImpl.java @@ -206,7 +206,7 @@ public class RayServiceImpl implements RayService { rayInsDao.insert(rayIns); rayInsService.updateRayStatus(id); // 记录开始扣除积分 - resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, id, rayIns.getId(), null, null); + resourceOccupyService.startDeduce(ray.getComputingResourceId(), Constant.TaskType_Ray, id, rayIns.getId(), null, ray.getName(), null); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index 647ead62..bfc4760b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -53,7 +53,7 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String nodeId) { + public void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String taskName, String nodeId) { ResourceOccupy resourceOccupy = new ResourceOccupy(); ComputingResource computingResource = computingResourceDao.queryById(computingResourceId); resourceOccupy.setComputingResourceId(computingResourceId); @@ -65,6 +65,7 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { resourceOccupy.setTaskId(taskId); resourceOccupy.setTaskInsId(taskInsId); resourceOccupy.setWorkflowId(workflowId); + resourceOccupy.setTaskName(taskName); resourceOccupy.setNodeId(nodeId); resourceOccupyDao.save(resourceOccupy); @@ -77,10 +78,10 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void endDeduce(String taskType, Long taskId, String nodeId, Date nodeStartTime) { - List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); + public void endDeduce(String taskType, Long taskInsId, String nodeId, Date nodeStartTime) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskInsId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { - deducing(taskType, taskId, nodeId, nodeStartTime); + deducing(taskType, taskInsId, nodeId, nodeStartTime); resourceOccupy.setState(Constant.State_invalid); resourceOccupyDao.edit(resourceOccupy); @@ -95,8 +96,8 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void deducing(String taskType, Long taskId, String nodeId, Date nodeStartTime) { - List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, nodeId); + public void deducing(String taskType, Long taskInsId, String nodeId, Date nodeStartTime) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskInsId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { Date now = new Date(); long timeDifferenceMillis; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 8fee155a..825acc5f 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -149,13 +149,14 @@ public class ServiceServiceImpl implements ServiceService { public String editServiceVersion(ServiceVersionVo serviceVersionVo) throws Exception { ServiceVersion serviceVersion = getServiceVersion(serviceVersionVo); ServiceVersion oldServiceVersion = serviceDao.getServiceVersionById(serviceVersionVo.getId()); - if (!oldServiceVersion.getReplicas().equals(serviceVersionVo.getReplicas()) || !oldServiceVersion.getResource().equals(serviceVersionVo.getResource()) + if (!oldServiceVersion.getReplicas().equals(serviceVersionVo.getReplicas()) || !oldServiceVersion.getComputingResourceId().equals(serviceVersionVo.getComputingResourceId()) || serviceVersionVo.getRerun()) { updateServiceVersion(serviceVersion); } LoginUser loginUser = SecurityUtils.getLoginUser(); serviceVersion.setUpdateBy(loginUser.getUsername()); serviceDao.updateServiceVersion(serviceVersion); + return "修改成功"; } @@ -274,7 +275,7 @@ public class ServiceServiceImpl implements ServiceService { serviceDao.updateServiceVersion(serviceVersion); // 记录开始扣积分 - resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), null, null); + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), null, service.getServiceName(), null); return "启动成功"; } else { throw new RuntimeException("启动失败"); @@ -304,19 +305,26 @@ public class ServiceServiceImpl implements ServiceService { } @Override - public String updateServiceVersion(ServiceVersion serviceVersion) { - HashMap<String, Object> paramMap = new HashMap<>(); - paramMap.put("deployment_name", serviceVersion.getDeploymentName()); - HashMap<String, Object> updateMap = new HashMap<>(); - updateMap.put("replicas", serviceVersion.getReplicas()); - updateMap.put("resource", serviceVersion.getResource()); - paramMap.put("update_model", new JSONObject(updateMap)); - String req = HttpUtils.sendPost(argoUrl + modelService + "/update", JSON.toJSONString(paramMap)); - if (StringUtils.isNotEmpty(req)) { - return "修改成功"; - } else { - throw new RuntimeException("更新失败"); + public String updateServiceVersion(ServiceVersion serviceVersion) throws Exception { + // 判断是否有资源 + if (resourceOccupyService.haveResource(serviceVersion.getComputingResourceId())) { + HashMap<String, Object> paramMap = new HashMap<>(); + paramMap.put("deployment_name", serviceVersion.getDeploymentName()); + HashMap<String, Object> updateMap = new HashMap<>(); + updateMap.put("replicas", serviceVersion.getReplicas()); + updateMap.put("resource", serviceVersion.getResource()); + paramMap.put("update_model", new JSONObject(updateMap)); + String req = HttpUtils.sendPost(argoUrl + modelService + "/update", JSON.toJSONString(paramMap)); + if (StringUtils.isNotEmpty(req)) { + com.ruoyi.platform.domain.Service service = serviceDao.getServiceById(serviceVersion.getServiceId()); + // 记录开始扣积分 + resourceOccupyService.startDeduce(serviceVersion.getComputingResourceId(), Constant.TaskType_Service, serviceVersion.getServiceId(), serviceVersion.getId(), null, service.getServiceName(), null); + return "修改成功"; + } else { + throw new RuntimeException("更新失败"); + } } + throw new RuntimeException("更新失败"); } @Override diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java index 1614ff4b..c4608fd6 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/K8sClientUtil.java @@ -517,7 +517,7 @@ public class K8sClientUtil { String nodeName = getNodeName(podName, namespace); // 记录开始扣除积分 - resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null, null, null); + resourceOccupyService.startDeduce(devEnvironment.getComputingResourceId(), Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null, null, devEnvironment.getName(), null); } } catch (ApiException e) { throw new RuntimeException("创建pod异常:" + e.getResponseBody()); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index c1c906cc..527ea618 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -3,11 +3,11 @@ <mapper namespace="com.ruoyi.platform.mapper.ResourceOccupyDao"> <insert id="save"> insert into resource_occupy (user_id, computing_resource_id, credit_per_hour, description, task_type, task_id, - task_ins_id, workflow_id, + task_ins_id, workflow_id, task_name, node_id) values (#{resourceOccupy.userId}, #{resourceOccupy.computingResourceId}, #{resourceOccupy.creditPerHour}, #{resourceOccupy.description}, #{resourceOccupy.taskType}, #{resourceOccupy.taskId}, - #{resourceOccupy.taskInsId}, #{resourceOccupy.workflowId}, + #{resourceOccupy.taskInsId}, #{resourceOccupy.workflowId}, #{resourceOccupy.taskName}, #{resourceOccupy.nodeId}) </insert> @@ -55,7 +55,7 @@ select * from resource_occupy where task_type = #{taskType} - and task_id = #{taskId} + and task_ins_id = #{taskInsId} <if test="nodeId != null and nodeId !=''"> and node_id = #{nodeId} </if> @@ -75,6 +75,11 @@ credit_per_hour, TRUNCATE(deduce_credit, 1) as deduce_credit, start_time, + task_type, + task_id, + task_ins_id, + workflow_id, + task_name, state from resource_occupy where user_id = #{userId} From 288bb3d2dcc11af9a3cf0cf67cbc86c9eef32f30 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 15:34:59 +0800 Subject: [PATCH 124/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/mapper/ResourceOccupyDao.java | 2 +- .../ExperimentInstanceStatusTask.java | 6 ++--- .../platform/scheduling/RayInsStatusTask.java | 6 ++--- .../scheduling/ResourceOccupyTask.java | 25 ++++++++++++++----- .../service/ResourceOccupyService.java | 4 +-- .../impl/ExperimentInsServiceImpl.java | 3 +-- .../service/impl/JupyterServiceImpl.java | 2 +- .../service/impl/RayInsServiceImpl.java | 2 +- .../impl/ResourceOccupyServiceImpl.java | 10 ++++---- .../service/impl/ServiceServiceImpl.java | 2 +- .../DevEnvironmentDaoMapper.xml | 2 +- .../managementPlatform/ResourceOccupy.xml | 7 +++++- 12 files changed, 44 insertions(+), 27 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java index ba2bcd9d..5c0b469a 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/ResourceOccupyDao.java @@ -14,7 +14,7 @@ public interface ResourceOccupyDao { int edit(@Param("resourceOccupy") ResourceOccupy resourceOccupy); - List<ResourceOccupy> getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskInsId") Long taskInsId, @Param("nodeId") String nodeId); + List<ResourceOccupy> getResourceOccupyByTask(@Param("taskType") String taskType, @Param("taskId") Long taskId, @Param("taskInsId") Long taskInsId, @Param("nodeId") String nodeId); int deduceCredit(@Param("credit") Double credit, @Param("userId") Long userId); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java index 1c32edff..926b7e21 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ExperimentInstanceStatusTask.java @@ -51,7 +51,7 @@ public class ExperimentInstanceStatusTask { } catch (Exception e) { experimentIns.setStatus(Constant.Failed); // 结束扣除积分 - resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, null, Long.valueOf(experimentIns.getId()), null, null); } // 扣除积分 @@ -65,9 +65,9 @@ public class ExperimentInstanceStatusTask { String finishedAt = (String) value.get("finishedAt"); if (StringUtils.isEmpty(finishedAt)) { - resourceOccupyService.deducing(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key, startTime); + resourceOccupyService.deducing(Constant.TaskType_Workflow, null, Long.valueOf(experimentIns.getId()), key, startTime); } else { - resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), key, startTime); + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, null, Long.valueOf(experimentIns.getId()), key, startTime); } } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java index 083559ca..533431d0 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/RayInsStatusTask.java @@ -45,15 +45,15 @@ public class RayInsStatusTask { // 扣除积分 if (Constant.Running.equals(rayIns.getStatus())) { - resourceOccupyService.deducing(Constant.TaskType_Ray, rayIns.getId(), null,null); + resourceOccupyService.deducing(Constant.TaskType_Ray, null, rayIns.getId(), null, null); } else if (Constant.Failed.equals(rayIns.getStatus()) || Constant.Terminated.equals(rayIns.getStatus()) || Constant.Succeeded.equals(rayIns.getStatus())) { - resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null,null); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, null, rayIns.getId(), null, null); } } catch (Exception e) { rayIns.setStatus(Constant.Failed); // 结束扣除积分 - resourceOccupyService.endDeduce(Constant.TaskType_Ray, rayIns.getId(), null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, null, rayIns.getId(), null, null); } // 线程安全的添加操作 synchronized (rayIds) { diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java index 32c99c78..3bf0d25b 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -5,7 +5,9 @@ import com.ruoyi.platform.domain.DevEnvironment; import com.ruoyi.platform.domain.ServiceVersion; import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.mapper.ServiceDao; +import com.ruoyi.platform.service.JupyterService; import com.ruoyi.platform.service.ResourceOccupyService; +import com.ruoyi.platform.vo.PodStatusVo; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -24,21 +26,32 @@ public class ResourceOccupyTask { @Resource private ServiceDao serviceDao; + @Resource + private JupyterService jupyterService; + // 开发环境功能扣除积分 - @Scheduled(cron = "0 0/1 * * * ?") // 每10分钟执行一次 - public void devDeduceCredit() { + @Scheduled(cron = "0 0/1 * * * ?") // 每1分钟执行一次 + public void devDeduceCredit() throws Exception { List<DevEnvironment> devEnvironments = devEnvironmentDao.getRunning(); - for (DevEnvironment devEnvironment : devEnvironments) { - resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnvironment.getId()), null, null); + for (DevEnvironment devEnv : devEnvironments) { + PodStatusVo podStatusVo = this.jupyterService.getJupyterStatus(devEnv); + if (!devEnv.getStatus().equals(podStatusVo.getStatus())) { + devEnv.setStatus(podStatusVo.getStatus()); + devEnv.setUrl(podStatusVo.getUrl()); + this.devEnvironmentDao.update(devEnv); + } + if (Constant.Running.equals(devEnv.getStatus())) { + resourceOccupyService.deducing(Constant.TaskType_Dev, Long.valueOf(devEnv.getId()), null, null, null); + } } } // 服务功能扣除积分 - @Scheduled(cron = "0 0/1 * * * ?") // 每10分钟执行一次 + @Scheduled(cron = "0 0/1 * * * ?") // 每1分钟执行一次 public void serviceDeduceCredit() { List<ServiceVersion> serviceVersions = serviceDao.getRunning(); for (ServiceVersion serviceVersion : serviceVersions) { - resourceOccupyService.deducing(Constant.TaskType_Service, serviceVersion.getId(), null, null); + resourceOccupyService.deducing(Constant.TaskType_Service, null, serviceVersion.getId(), null, null); } } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java index 8670a290..46c60d06 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ResourceOccupyService.java @@ -13,9 +13,9 @@ public interface ResourceOccupyService { void startDeduce(Integer computingResourceId, String taskType, Long taskId, Long taskInsId, Long workflowId, String taskName, String nodeId); - void endDeduce(String taskType, Long taskInsId, String nodeId, Date nodeStartTime); + void endDeduce(String taskType, Long taskId, Long taskInsId, String nodeId, Date nodeStartTime); - void deducing(String taskType, Long taskInsId, String nodeId, Date nodeStartTime); + void deducing(String taskType, Long taskId, Long taskInsId, String nodeId, Date nodeStartTime); Page<ResourceOccupy> queryByPage(PageRequest pageRequest); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java index 995a2191..a47abee3 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ExperimentInsServiceImpl.java @@ -1,6 +1,5 @@ package com.ruoyi.platform.service.impl; -import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSON; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.platform.constant.Constant; @@ -416,7 +415,7 @@ public class ExperimentInsServiceImpl implements ExperimentInsService { updateExperimentStatus(experimentIns.getExperimentId()); // 结束扣除积分 - resourceOccupyService.endDeduce(Constant.TaskType_Workflow, Long.valueOf(experimentIns.getId()), null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Workflow, null, Long.valueOf(experimentIns.getId()), null, null); return true; } else { throw new Exception("终止错误"); diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 76fe1d02..08485dcf 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -142,7 +142,7 @@ public class JupyterServiceImpl implements JupyterService { this.devEnvironmentService.update(devEnvironment); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id), null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Dev, Long.valueOf(id), null, null, null); return deleteResult + ",编辑器已停止"; } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java index 31c4ab91..42701bc0 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/RayInsServiceImpl.java @@ -168,7 +168,7 @@ public class RayInsServiceImpl implements RayInsService { rayInsDao.update(ins); updateRayStatus(rayIns.getRayId()); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Ray, id, null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Ray, null, id, null, null); return true; } else { return false; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java index bfc4760b..bdcf7b58 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ResourceOccupyServiceImpl.java @@ -78,10 +78,10 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void endDeduce(String taskType, Long taskInsId, String nodeId, Date nodeStartTime) { - List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskInsId, nodeId); + public void endDeduce(String taskType, Long taskId, Long taskInsId, String nodeId, Date nodeStartTime) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, taskInsId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { - deducing(taskType, taskInsId, nodeId, nodeStartTime); + deducing(taskType, taskId, taskInsId, nodeId, nodeStartTime); resourceOccupy.setState(Constant.State_invalid); resourceOccupyDao.edit(resourceOccupy); @@ -96,8 +96,8 @@ public class ResourceOccupyServiceImpl implements ResourceOccupyService { @Override @Transactional - public void deducing(String taskType, Long taskInsId, String nodeId, Date nodeStartTime) { - List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskInsId, nodeId); + public void deducing(String taskType, Long taskId, Long taskInsId, String nodeId, Date nodeStartTime) { + List<ResourceOccupy> resourceOccupys = resourceOccupyDao.getResourceOccupyByTask(taskType, taskId, taskInsId, nodeId); for (ResourceOccupy resourceOccupy : resourceOccupys) { Date now = new Date(); long timeDifferenceMillis; diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index 825acc5f..fba918d1 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -297,7 +297,7 @@ public class ServiceServiceImpl implements ServiceService { serviceVersion.setRunState(Constant.Stopped); serviceDao.updateServiceVersion(serviceVersion); // 结束扣积分 - resourceOccupyService.endDeduce(Constant.TaskType_Service, id, null, null); + resourceOccupyService.endDeduce(Constant.TaskType_Service, null, id, null, null); return "停止成功"; } else { throw new RuntimeException("停止失败"); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml index 82e92a56..17dec6c4 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/DevEnvironmentDaoMapper.xml @@ -127,7 +127,7 @@ </select> <select id="getRunning" resultType="com.ruoyi.platform.domain.DevEnvironment"> - select * from dev_environment where state = 1 and status = 'Running' + select * from dev_environment where state = 1 and (status = 'Running' or status = 'Pending' or status = 'Init') </select> <!--新增所有列--> diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml index 527ea618..7f7544b0 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ResourceOccupy.xml @@ -55,7 +55,12 @@ select * from resource_occupy where task_type = #{taskType} - and task_ins_id = #{taskInsId} + <if test="taskId != null and taskId !=''"> + and task_id = #{taskId} + </if> + <if test="taskInsId != null and taskInsId !=''"> + and task_ins_id = #{taskInsId} + </if> <if test="nodeId != null and nodeId !=''"> and node_id = #{nodeId} </if> From 5546a4a6eb7311badc7ea55a2e56bb2b9a38e03b Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 16:35:55 +0800 Subject: [PATCH 125/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduling/ResourceOccupyTask.java | 13 +++++++- .../platform/service/ServiceService.java | 4 +++ .../service/impl/ServiceServiceImpl.java | 6 ++-- .../managementPlatform/ServiceDaoMapper.xml | 32 +++++++++++++------ 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java index 3bf0d25b..c13dce32 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/scheduling/ResourceOccupyTask.java @@ -7,12 +7,15 @@ import com.ruoyi.platform.mapper.DevEnvironmentDao; import com.ruoyi.platform.mapper.ServiceDao; import com.ruoyi.platform.service.JupyterService; import com.ruoyi.platform.service.ResourceOccupyService; +import com.ruoyi.platform.service.ServiceService; import com.ruoyi.platform.vo.PodStatusVo; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Component() public class ResourceOccupyTask { @@ -26,6 +29,9 @@ public class ResourceOccupyTask { @Resource private ServiceDao serviceDao; + @Resource + private ServiceService serviceService; + @Resource private JupyterService jupyterService; @@ -50,8 +56,13 @@ public class ResourceOccupyTask { @Scheduled(cron = "0 0/1 * * * ?") // 每1分钟执行一次 public void serviceDeduceCredit() { List<ServiceVersion> serviceVersions = serviceDao.getRunning(); + List<String> deploymentNames = serviceVersions.stream().map(ServiceVersion::getDeploymentName).collect(Collectors.toList()); + Map<String, String> runStates = serviceService.getRunState(deploymentNames); + serviceService.updateRunState(runStates, serviceVersions); for (ServiceVersion serviceVersion : serviceVersions) { - resourceOccupyService.deducing(Constant.TaskType_Service, null, serviceVersion.getId(), null, null); + if (Constant.Running.equals(serviceVersion.getRunState())) { + resourceOccupyService.deducing(Constant.TaskType_Service, null, serviceVersion.getId(), null, null); + } } } } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java index 0459aea2..11498f5c 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ServiceService.java @@ -45,4 +45,8 @@ public interface ServiceService { Map<String, Object> getServiceVersionDocs(Long id); List<ServiceVersion> serviceVersionList(Long id); + + Map<String, String> getRunState(List<String> deploymentNames); + + void updateRunState(Map<String, String> runStates, List<ServiceVersion> serviceVersionList); } diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java index fba918d1..41e1df5d 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ServiceServiceImpl.java @@ -401,7 +401,8 @@ public class ServiceServiceImpl implements ServiceService { return serviceVersionVo; } - Map<String, String> getRunState(List<String> deploymentNames) { + @Override + public Map<String, String> getRunState(List<String> deploymentNames) { HashMap<String, Object> paramMap = new HashMap<>(); paramMap.put("deployment_names", deploymentNames); String req = HttpUtils.sendPost(argoUrl + modelService + "/getStatus", JSON.toJSONString(paramMap)); @@ -417,7 +418,8 @@ public class ServiceServiceImpl implements ServiceService { } } - void updateRunState(Map<String, String> runStates, List<ServiceVersion> serviceVersionList) { + @Override + public void updateRunState(Map<String, String> runStates, List<ServiceVersion> serviceVersionList) { for (ServiceVersion sv : serviceVersionList) { String runState = runStates.get(sv.getDeploymentName()); sv.setRunState(runState); diff --git a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml index d9dceb2c..811a113b 100644 --- a/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml +++ b/ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ServiceDaoMapper.xml @@ -56,9 +56,9 @@ </sql> <select id="getServiceById" resultType="com.ruoyi.platform.domain.Service"> - select a.*,count(b.id) as version_count + select a.*, count(b.id) as version_count from service a - left join (select * from service_version where state = 1) b on a.id = b.service_id + left join (select * from service_version where state = 1) b on a.id = b.service_id where a.id = #{id} </select> @@ -75,31 +75,43 @@ </select> <select id="getServiceByName" resultType="com.ruoyi.platform.domain.Service"> - select * from service where service_name = #{serviceName} and state = 1 + select * + from service + where service_name = #{serviceName} + and state = 1 </select> <select id="getSvByVersion" resultType="com.ruoyi.platform.domain.ServiceVersion"> select * from service_version - where service_id = #{serviceId} and version = #{version} and state = 1 + where service_id = #{serviceId} + and version = #{version} + and state = 1 </select> <select id="getRunning" resultType="com.ruoyi.platform.domain.ServiceVersion"> select * - from service_version where state = 1 and run_state = 'Running' + from service_version + where state = 1 + and (run_state = 'Running' or run_state = 'Pending' or run_state = 'Init') </select> <insert id="insertService" keyProperty="id" useGeneratedKeys="true"> insert into service(service_name, service_type, description, create_by, update_by) - values (#{service.serviceName}, #{service.serviceType}, #{service.description}, #{service.createBy}, #{service.updateBy}) + values (#{service.serviceName}, #{service.serviceType}, #{service.description}, #{service.createBy}, + #{service.updateBy}) </insert> <insert id="insertServiceVersion" keyProperty="id" useGeneratedKeys="true"> - insert into service_version(service_id, version, model, description, image, resource, computing_resource_id, replicas, mount_path, env_variables, + insert into service_version(service_id, version, model, description, image, resource, computing_resource_id, + replicas, mount_path, env_variables, code_config, command, create_by, update_by, deploy_type) - values (#{serviceVersion.serviceId}, #{serviceVersion.version}, #{serviceVersion.model}, #{serviceVersion.description}, #{serviceVersion.image}, - #{serviceVersion.resource}, #{serviceVersion.computingResourceId}, #{serviceVersion.replicas}, #{serviceVersion.mountPath}, #{serviceVersion.envVariables}, - #{serviceVersion.codeConfig}, #{serviceVersion.command}, #{serviceVersion.createBy}, #{serviceVersion.updateBy}, #{serviceVersion.deployType}) + values (#{serviceVersion.serviceId}, #{serviceVersion.version}, #{serviceVersion.model}, + #{serviceVersion.description}, #{serviceVersion.image}, + #{serviceVersion.resource}, #{serviceVersion.computingResourceId}, #{serviceVersion.replicas}, + #{serviceVersion.mountPath}, #{serviceVersion.envVariables}, + #{serviceVersion.codeConfig}, #{serviceVersion.command}, #{serviceVersion.createBy}, + #{serviceVersion.updateBy}, #{serviceVersion.deployType}) </insert> <update id="updateService"> From 0ef63e3bebaf3075b2e8111e9ed27b0fa766c661 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 16:49:27 +0800 Subject: [PATCH 126/127] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/platform/service/impl/JupyterServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 08485dcf..93f38105 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -155,8 +155,7 @@ public class JupyterServiceImpl implements JupyterService { if (devEnvironment == null) { return JupyterStatusVo; } - LoginUser loginUser = SecurityUtils.getLoginUser(); - String podName = loginUser.getUsername().toLowerCase() + "-editor-pod" + "-" + devEnvironment.getId(); + String podName = devEnvironment.getCreateBy() + "-editor-pod" + "-" + devEnvironment.getId(); try { // 查询相应pod状态 From 8f38bb39f309638e6a96b2082d8dfee660a054b7 Mon Sep 17 00:00:00 2001 From: chenzhihang <709011834@qq.com> Date: Fri, 21 Mar 2025 16:58:09 +0800 Subject: [PATCH 127/127] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/platform/service/impl/JupyterServiceImpl.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java index 93f38105..747b2f60 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/JupyterServiceImpl.java @@ -97,9 +97,8 @@ public class JupyterServiceImpl implements JupyterService { // String modelPath = "argo-workflow" + "/" + model.get("path"); String modelPath = (String) model.get("path"); - LoginUser loginUser = SecurityUtils.getLoginUser(); //构造pod名称 - String podName = loginUser.getUsername().toLowerCase() + "-editor-pod" + "-" + id; + String podName = devEnvironment.getCreateBy() + "-editor-pod" + "-" + id; //新建编辑器的pvc // String pvcName = loginUser.getUsername().toLowerCase() + "-editor-pvc"; // V1PersistentVolumeClaim pvc = k8sClientUtil.createPvc(namespace, pvcName, storage, storageClassName); @@ -123,10 +122,9 @@ public class JupyterServiceImpl implements JupyterService { if (devEnvironment == null) { throw new Exception("开发环境配置不存在"); } - LoginUser loginUser = SecurityUtils.getLoginUser(); //构造pod和svc名称 - String podName = loginUser.getUsername().toLowerCase() + "-editor-pod" + "-" + id; - String svcName = loginUser.getUsername().toLowerCase() + "-editor-pod" + "-" + id + "-svc"; + String podName = devEnvironment.getCreateBy() + "-editor-pod" + "-" + id; + String svcName = devEnvironment.getCreateBy() + "-editor-pod" + "-" + id + "-svc"; //得到pod V1Pod pod = k8sClientUtil.getNSPodList(namespace, podName); if (pod == null) {