feat(系统设置): 任务中心增加测试计划列表

This commit is contained in:
wxg0103 2024-06-05 19:18:09 +08:00 committed by 刘瑞斌
parent a7356c2d98
commit 4e4266972a
17 changed files with 928 additions and 19 deletions

View File

@ -45,10 +45,9 @@ public class MessageListener {
@KafkaListener(id = MESSAGE_CONSUME_ID, topics = KafkaTopicConstants.API_REPORT_TASK_TOPIC, groupId = MESSAGE_CONSUME_ID) @KafkaListener(id = MESSAGE_CONSUME_ID, topics = KafkaTopicConstants.API_REPORT_TASK_TOPIC, groupId = MESSAGE_CONSUME_ID)
public void messageConsume(ConsumerRecord<?, String> record) { public void messageConsume(ConsumerRecord<?, String> record) {
try { try {
LogUtils.info("接收到发送通知信息:{}", record.key());
if (ObjectUtils.isNotEmpty(record.value())) { if (ObjectUtils.isNotEmpty(record.value())) {
ApiNoticeDTO dto = JSON.parseObject(record.value(), ApiNoticeDTO.class); ApiNoticeDTO dto = JSON.parseObject(record.value(), ApiNoticeDTO.class);
LogUtils.info("接收到发送通知信息:{}", dto.getReportId());
// 集合报告不发送通知 // 集合报告不发送通知
if (!BooleanUtils.isTrue(dto.getIntegratedReport())) { if (!BooleanUtils.isTrue(dto.getIntegratedReport())) {
apiReportSendNoticeService.sendNotice(dto); apiReportSendNoticeService.sendNotice(dto);

View File

@ -217,7 +217,6 @@
left join project on ar.project_id = project.id left join project on ar.project_id = project.id
where where
ar.deleted = false ar.deleted = false
and ar.test_plan_id = 'NONE'
and ar.start_time BETWEEN #{startTime} AND #{endTime} and ar.start_time BETWEEN #{startTime} AND #{endTime}
and ar.exec_status in ('PENDING', 'RUNNING', 'RERUNNING') and ar.exec_status in ('PENDING', 'RUNNING', 'RERUNNING')
<if test="ids != null and ids.size() > 0"> <if test="ids != null and ids.size() > 0">

View File

@ -111,7 +111,6 @@
left join project on asr.project_id = project.id left join project on asr.project_id = project.id
where where
asr.deleted = false asr.deleted = false
and asr.test_plan_id = 'NONE'
and asr.start_time BETWEEN #{startTime} AND #{endTime} and asr.start_time BETWEEN #{startTime} AND #{endTime}
and asr.exec_status in ('PENDING', 'RUNNING', 'RERUNNING') and asr.exec_status in ('PENDING', 'RUNNING', 'RERUNNING')
<if test="ids != null and ids.size() > 0"> <if test="ids != null and ids.size() > 0">

View File

@ -0,0 +1,55 @@
package io.metersphere.plan.controller;
import io.metersphere.plan.service.TestPlanTaskCenterService;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.dto.taskcenter.TaskCenterDTO;
import io.metersphere.system.dto.taskcenter.request.TaskCenterPageRequest;
import io.metersphere.system.utils.Pager;
import io.metersphere.system.utils.SessionUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping(value = "/task/center/plan")
@Tag(name = "任务中心-实时任务-测试计划")
public class TestPlanTaskCenterController {
@Resource
private TestPlanTaskCenterService testPlanTaskCenterService;
private static final String PROJECT = "project";
private static final String ORG = "org";
private static final String SYSTEM = "system";
@PostMapping("/project/real-time/page")
@Operation(summary = "项目-任务中心-测试计划-实时任务列表")
public Pager<List<TaskCenterDTO>> projectList(@Validated @RequestBody TaskCenterPageRequest request) {
return testPlanTaskCenterService.getProjectPage(request, SessionUtils.getCurrentProjectId());
}
@PostMapping("/org/real-time/page")
@Operation(summary = "组织-任务中心-测试计划-实时任务列表")
@RequiresPermissions(PermissionConstants.ORGANIZATION_TASK_CENTER_READ)
public Pager<List<TaskCenterDTO>> orgList(@Validated @RequestBody TaskCenterPageRequest request) {
return testPlanTaskCenterService.getOrganizationPage(request, SessionUtils.getCurrentOrganizationId());
}
@PostMapping("/system/real-time/page")
@Operation(summary = "系统-任务中心-测试计划-实时任务列表")
@RequiresPermissions(PermissionConstants.SYSTEM_TASK_CENTER_READ)
public Pager<List<TaskCenterDTO>> systemList(@Validated @RequestBody TaskCenterPageRequest request) {
return testPlanTaskCenterService.getSystemPage(request);
}
}

View File

@ -5,6 +5,8 @@ import io.metersphere.plan.dto.request.TestPlanReportBatchRequest;
import io.metersphere.plan.dto.request.TestPlanReportPageRequest; import io.metersphere.plan.dto.request.TestPlanReportPageRequest;
import io.metersphere.plan.dto.response.TestPlanReportPageResponse; import io.metersphere.plan.dto.response.TestPlanReportPageResponse;
import io.metersphere.system.dto.sdk.ApiReportMessageDTO; import io.metersphere.system.dto.sdk.ApiReportMessageDTO;
import io.metersphere.system.dto.taskcenter.TaskCenterDTO;
import io.metersphere.system.dto.taskcenter.request.TaskCenterPageRequest;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@ -36,4 +38,8 @@ public interface ExtTestPlanReportMapper {
List<String> selectReportIdByProjectIdAndTime(@Param("time") long timeMills, @Param("projectId") String projectId); List<String> selectReportIdByProjectIdAndTime(@Param("time") long timeMills, @Param("projectId") String projectId);
List<String> selectReportIdTestPlanIds(@Param("testPlanIds") List<String> testPlanIds); List<String> selectReportIdTestPlanIds(@Param("testPlanIds") List<String> testPlanIds);
List<TaskCenterDTO> taskCenterlist(@Param("request") TaskCenterPageRequest request, @Param("projectIds") List<String> projectIds,
@Param("startTime") long startTime, @Param("endTime") long endTime);
} }

View File

@ -59,6 +59,43 @@
#{testPlanId} #{testPlanId}
</foreach> </foreach>
</select> </select>
<select id="taskCenterlist" resultType="io.metersphere.system.dto.taskcenter.TaskCenterDTO">
select
distinct tpr.id,
tpr.project_id,
tpr.integrated,
tpr.result_status as status,
tpr.exec_status,
tpr.start_time AS operationTime,
tpr.create_user AS operationName,
tpr.trigger_mode,
tpr.start_time,
project.organization_id,
tp.num AS resourceNum,
tp.name AS resourceName,
tp.id AS resourceId
FROM
test_plan_report tpr
INNER JOIN test_plan tp ON tpr.test_plan_id = tp.id
left join project on tpr.project_id = project.id
where
tpr.start_time BETWEEN #{startTime} AND #{endTime}
<if test="projectIds != null and projectIds.size() > 0">
and
tpr.project_id IN
<foreach collection="projectIds" item="projectId" separator="," open="(" close=")">
#{projectId}
</foreach>
</if>
<if test="request.keyword != null and request.keyword != ''">
and (tp.num like concat('%', #{request.keyword},'%')
or tp.name like concat('%', #{request.keyword},'%')
)
</if>
<include refid="filter"/>
</select>
<sql id="queryWhereCondition"> <sql id="queryWhereCondition">
<where> <where>
@ -104,10 +141,24 @@
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/> <include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
</when> </when>
<!-- 执行结果 --> <!-- 执行结果 -->
<when test="key == 'resultStatus'"> <when test="key == 'resultStatus' || key == 'status'">
and tpr.result_status in and tpr.result_status in
<include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/> <include refid="io.metersphere.system.mapper.BaseMapper.filterInWrapper"/>
</when> </when>
<!-- 项目id -->
<when test="key=='projectIds'">
and tpr.project_id in
<foreach collection="values" item="value" separator="," open="(" close=")">
#{value}
</foreach>
</when>
<!-- 组织id -->
<when test="key=='organizationIds'">
and project.organization_id in
<foreach collection="values" item="value" separator="," open="(" close=")">
#{value}
</foreach>
</when>
</choose> </choose>
</if> </if>
</foreach> </foreach>

View File

@ -0,0 +1,190 @@
package io.metersphere.plan.service;
import com.github.pagehelper.Page;
import com.github.pagehelper.page.PageMethod;
import io.metersphere.api.dto.definition.ExecuteReportDTO;
import io.metersphere.api.mapper.ExtApiScenarioReportMapper;
import io.metersphere.plan.mapper.ExtTestPlanReportMapper;
import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.DateUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.Organization;
import io.metersphere.system.dto.sdk.OptionDTO;
import io.metersphere.system.dto.taskcenter.TaskCenterDTO;
import io.metersphere.system.dto.taskcenter.request.TaskCenterPageRequest;
import io.metersphere.system.log.service.OperationLogService;
import io.metersphere.system.mapper.BaseProjectMapper;
import io.metersphere.system.mapper.ExtOrganizationMapper;
import io.metersphere.system.mapper.OrganizationMapper;
import io.metersphere.system.service.TestResourcePoolService;
import io.metersphere.system.service.UserLoginService;
import io.metersphere.system.utils.PageUtils;
import io.metersphere.system.utils.Pager;
import jakarta.annotation.Resource;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@Transactional(rollbackFor = Exception.class)
public class TestPlanTaskCenterService {
@Resource
ExtTestPlanReportMapper extTestPlanReportMapper;
@Resource
ExtOrganizationMapper extOrganizationMapper;
@Resource
BaseProjectMapper baseProjectMapper;
@Resource
UserLoginService userLoginService;
@Resource
ProjectMapper projectMapper;
@Resource
OrganizationMapper organizationMapper;
@Resource
ExtApiScenarioReportMapper extApiScenarioReportMapper;
@Resource
TestResourcePoolService testResourcePoolService;
@Resource
OperationLogService operationLogService;
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
private static final String DEFAULT_SORT = "start_time desc";
private final static String PROJECT_STOP = "/task/center/api/project/stop";
private final static String ORG_STOP = "/task/center/api/org/stop";
private final static String SYSTEM_STOP = "/task/center/api/system/stop";
/**
* 任务中心实时任务列表-项目级
*
* @param request 请求参数
* @return 任务中心实时任务列表
*/
public Pager<List<TaskCenterDTO>> getProjectPage(TaskCenterPageRequest request, String projectId) {
checkProjectExist(projectId);
List<OptionDTO> projectList = getProjectOption(projectId);
return createTaskCenterPager(request, projectList, false);
}
/**
* 任务中心实时任务列表-组织级
*
* @param request 请求参数
* @return 任务中心实时任务列表
*/
public Pager<List<TaskCenterDTO>> getOrganizationPage(TaskCenterPageRequest request, String organizationId) {
checkOrganizationExist(organizationId);
List<OptionDTO> projectList = getOrgProjectList(organizationId);
return createTaskCenterPager(request, projectList, false);
}
/**
* 任务中心实时任务列表-系统级
*
* @param request 请求参数
* @return 任务中心实时任务列表
*/
public Pager<List<TaskCenterDTO>> getSystemPage(TaskCenterPageRequest request) {
List<OptionDTO> projectList = getSystemProjectList();
return createTaskCenterPager(request, projectList, true);
}
private Pager<List<TaskCenterDTO>> createTaskCenterPager(TaskCenterPageRequest request, List<OptionDTO> projectList, boolean isSystem) {
Page<Object> page = PageMethod.startPage(request.getCurrent(), request.getPageSize(),
StringUtils.isNotBlank(request.getSortString()) ? request.getSortString() : DEFAULT_SORT);
return PageUtils.setPageInfo(page, getPage(request, projectList, isSystem));
}
public List<TaskCenterDTO> getPage(TaskCenterPageRequest request, List<OptionDTO> projectList, boolean isSystem) {
List<TaskCenterDTO> list = new ArrayList<>();
List<String> projectIds = projectList.stream().map(OptionDTO::getId).toList();
Map<String, ExecuteReportDTO> historyDeletedMap = new HashMap<>();
list = extTestPlanReportMapper.taskCenterlist(request, isSystem ? new ArrayList<>() : projectIds, DateUtils.getDailyStartTime(), DateUtils.getDailyEndTime());
//执行历史列表
/*List<String> reportIds = list.stream().map(TaskCenterDTO::getId).toList();
if (CollectionUtils.isNotEmpty(reportIds)) {
List<ExecuteReportDTO> historyDeletedList = extTestPlanReportMapper.getHistoryDeleted(reportIds);
historyDeletedMap = historyDeletedList.stream().collect(Collectors.toMap(ExecuteReportDTO::getId, Function.identity()));
}*/
processTaskCenter(list, projectList, projectIds, historyDeletedMap);
return list;
}
private void processTaskCenter(List<TaskCenterDTO> list, List<OptionDTO> projectList, List<String> projectIds, Map<String, ExecuteReportDTO> historyDeletedMap) {
if (!list.isEmpty()) {
// 取所有的userid
Set<String> userSet = list.stream()
.flatMap(item -> Stream.of(item.getOperationName()))
.collect(Collectors.toSet());
Map<String, String> userMap = userLoginService.getUserNameMap(new ArrayList<>(userSet));
// 项目
Map<String, String> projectMap = projectList.stream().collect(Collectors.toMap(OptionDTO::getId, OptionDTO::getName));
// 组织
List<OptionDTO> orgListByProjectList = getOrgListByProjectIds(projectIds);
Map<String, String> orgMap = orgListByProjectList.stream().collect(Collectors.toMap(OptionDTO::getId, OptionDTO::getName));
list.forEach(item -> {
item.setOperationName(userMap.getOrDefault(item.getOperationName(), StringUtils.EMPTY));
item.setProjectName(projectMap.getOrDefault(item.getProjectId(), StringUtils.EMPTY));
item.setOrganizationName(orgMap.getOrDefault(item.getProjectId(), StringUtils.EMPTY));
item.setHistoryDeleted(MapUtils.isNotEmpty(historyDeletedMap) && !historyDeletedMap.containsKey(item.getId()));
});
}
}
private List<OptionDTO> getProjectOption(String id) {
return baseProjectMapper.getProjectOptionsById(id);
}
private List<OptionDTO> getOrgProjectList(String orgId) {
return baseProjectMapper.getProjectOptionsByOrgId(orgId);
}
private List<OptionDTO> getSystemProjectList() {
return baseProjectMapper.getProjectOptions();
}
private List<OptionDTO> getOrgListByProjectIds(List<String> projectIds) {
return extOrganizationMapper.getOrgListByProjectIds(projectIds);
}
/**
* 查看项目是否存在
*
* @param projectId 项目ID
*/
private void checkProjectExist(String projectId) {
Project project = projectMapper.selectByPrimaryKey(projectId);
if (project == null) {
throw new MSException(Translator.get("project_not_exist"));
}
}
/**
* 查看组织是否存在
*
* @param orgId 组织ID
*/
private void checkOrganizationExist(String orgId) {
Organization organization = organizationMapper.selectByPrimaryKey(orgId);
if (organization == null) {
throw new MSException(Translator.get("organization_not_exist"));
}
}
}

View File

@ -0,0 +1,135 @@
package io.metersphere.plan.controller;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.constants.TaskCenterResourceType;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.base.BaseTest;
import io.metersphere.system.controller.handler.ResultHolder;
import io.metersphere.system.dto.taskcenter.request.TaskCenterPageRequest;
import io.metersphere.system.utils.Pager;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
public class TestPlanTaskCenterControllerTests extends BaseTest {
private static final String BASE_PATH = "/task/center/plan/";
private final static String REAL_TIME_PROJECT_PAGE = BASE_PATH + "project/real-time/page";
private final static String REAL_TIME_ORG_PAGE = BASE_PATH + "org/real-time/page";
private final static String REAL_TIME_SYSTEM_PAGE = BASE_PATH + "system/real-time/page";
private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError();
@Test
@Order(9)
@Sql(scripts = {"/dml/init_task_plan.sql"}, config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED))
public void getPage() throws Exception {
doTaskCenterPage("KEYWORD", REAL_TIME_PROJECT_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPage("FILTER", REAL_TIME_PROJECT_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPage("KEYWORD", REAL_TIME_ORG_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPage("FILTER", REAL_TIME_ORG_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPage("KEYWORD", REAL_TIME_SYSTEM_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPage("FILTER", REAL_TIME_SYSTEM_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
}
private void doTaskCenterPage(String search, String url, String moduleType) throws Exception {
TaskCenterPageRequest request = new TaskCenterPageRequest();
request.setModuleType(moduleType);
request.setCurrent(1);
request.setPageSize(10);
request.setSort(Map.of("startTime", "asc"));
// "KEYWORD", "FILTER"
switch (search) {
case "KEYWORD" -> configureKeywordSearch(request);
case "FILTER" -> configureFilterSearch(request);
default -> {
}
}
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.header(SessionConstants.CURRENT_PROJECT, DEFAULT_PROJECT_ID)
.header(SessionConstants.CURRENT_ORGANIZATION, DEFAULT_ORGANIZATION_ID)
.content(JSON.toJSONString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
// 获取返回值
String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class);
LogUtils.info(resultHolder);
// 返回请求正常
Assertions.assertNotNull(resultHolder);
Pager<?> pageData = JSON.parseObject(JSON.toJSONString(resultHolder.getData()), Pager.class);
// 返回值不为空
Assertions.assertNotNull(pageData);
// 返回值的页码和当前页码相同
Assertions.assertEquals(pageData.getCurrent(), request.getCurrent());
// 返回的数据量不超过规定要返回的数据量相同
Assertions.assertTrue(JSON.parseArray(JSON.toJSONString(pageData.getList())).size() <= request.getPageSize());
}
private void doTaskCenterPageError(String url, String moduleType) throws Exception {
TaskCenterPageRequest request = new TaskCenterPageRequest();
request.setModuleType(moduleType);
request.setCurrent(1);
request.setPageSize(10);
request.setSort(Map.of("startTime", "asc"));
configureKeywordSearch(request);
mockMvc.perform(MockMvcRequestBuilders.post(url)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.header(SessionConstants.CURRENT_PROJECT, "DEFAULT_PROJECT_ID")
.header(SessionConstants.CURRENT_ORGANIZATION, "DEFAULT_ORGANIZATION_ID")
.content(JSON.toJSONString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(ERROR_REQUEST_MATCHER);
}
private void configureKeywordSearch(TaskCenterPageRequest request) {
request.setKeyword("18");
request.setSort(Map.of("triggerMode", "asc"));
}
private void configureFilterSearch(TaskCenterPageRequest request) {
Map<String, List<String>> filters = new HashMap<>();
request.setSort(Map.of());
filters.put("triggerMode", List.of("MANUAL"));
request.setFilter(filters);
}
@Test
@Order(10)
public void getPageError() throws Exception {
doTaskCenterPageError(REAL_TIME_PROJECT_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
doTaskCenterPageError(REAL_TIME_ORG_PAGE, TaskCenterResourceType.TEST_PLAN.toString());
}
}

View File

@ -0,0 +1,12 @@
replace INTO `test_plan`(`id`, `num`, `project_id`, `group_id`, `module_id`, `name`, `status`, `type`, `tags`, `create_time`, `create_user`, `update_time`, `update_user`, `planned_start_time`, `planned_end_time`, `actual_start_time`, `actual_end_time`, `description`)
VALUES ('test_plan_id_1', 5000, '100001100001', 'NONE', '1', '测试一下计划', 'PREPARED', 'TEST_PLAN', NULL,
1714980158000, 'WX', 1714980158000, 'WX', 1714980158000, 1714980158000, 1714980158000, 1714980158000, '11');
replace INTO `test_plan_report`(`id`, `test_plan_id`, `name`, `create_user`, `create_time`, `start_time`, `end_time`, `trigger_mode`, `exec_status`, `result_status`, `pass_threshold`, `pass_rate`, `project_id`, `integrated`, `deleted`)
VALUES
('test-plan-report-id-1', 'test_plan_id_1', '测试一下计划报告1', 'admin', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, 'MANUAL', 'PENDING', 'SUCCESS', '99.99', 100.00, '100001100001', 0, 0),
('test-plan-report-id-2', 'test_plan_id_1', '测试一下计划报告1', 'admin', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, 'MANUAL', 'PENDING', '-', '99.99', 100.00, '100001100001', 0, 0),
('test-plan-report-id-3', 'test_plan_id_1', '测试一下计划报告3', 'admin', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, 'MANUAL', 'PENDING', '-', '99.99', 100.00, '100001100001',1, 0),
('test-plan-report-id-4', 'test_plan_id_1', '测试一下计划报告4', 'admin', UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, UNIX_TIMESTAMP()*1000, 'MANUAL', 'PENDING', '-', '99.99', 100.00, '100001100001', 1, 0);

View File

@ -28,8 +28,11 @@ import {
stopRealSysApiUrl, stopRealSysApiUrl,
systemRealTotal, systemRealTotal,
systemScheduleTotal, systemScheduleTotal,
taskOrgPlanRealCenterListUrl,
taskOrgRealCenterListUrl, taskOrgRealCenterListUrl,
taskProPlanRealCenterListUrl,
taskProRealCenterListUrl, taskProRealCenterListUrl,
taskSysPlanRealCenterListUrl,
taskSysRealCenterListUrl, taskSysRealCenterListUrl,
updateScheduleOrgTaskUrl, updateScheduleOrgTaskUrl,
updateScheduleProTaskUrl, updateScheduleProTaskUrl,
@ -193,4 +196,17 @@ export function getProjectRealTotal() {
return MSR.get({ url: `${projectRealTotal}` }); return MSR.get({ url: `${projectRealTotal}` });
} }
// 实时任务 测试计划
export function getRealSysPlanList(data: TableQueryParams) {
return MSR.post<CommonList<RealTaskCenterApiCaseItem>>({ url: taskSysPlanRealCenterListUrl, data });
}
export function getRealOrgPlanList(data: TableQueryParams) {
return MSR.post<CommonList<RealTaskCenterApiCaseItem>>({ url: taskOrgPlanRealCenterListUrl, data });
}
export function getRealProPlanList(data: TableQueryParams) {
return MSR.post<CommonList<RealTaskCenterApiCaseItem>>({ url: taskProPlanRealCenterListUrl, data });
}
export default {}; export default {};

View File

@ -76,3 +76,7 @@ export const projectScheduleTotal = '/task/center/project/schedule/total';
export const systemRealTotal = '/task/center/system/real/total'; export const systemRealTotal = '/task/center/system/real/total';
export const orgRealTotal = '/task/center/org/real/total'; export const orgRealTotal = '/task/center/org/real/total';
export const projectRealTotal = '/task/center/project/real/total'; export const projectRealTotal = '/task/center/project/real/total';
export const taskSysPlanRealCenterListUrl = '/task/center/plan/system/real-time/page';
export const taskOrgPlanRealCenterListUrl = '/task/center/plan/org/real-time/page';
export const taskProPlanRealCenterListUrl = '/task/center/plan/project/real-time/page';

View File

@ -86,6 +86,9 @@ export enum TableKeyEnum {
TASK_SCHEDULE_TASK_TEST_PLAN_SYSTEM = 'taskCenterScheduleTestPlanSystem', TASK_SCHEDULE_TASK_TEST_PLAN_SYSTEM = 'taskCenterScheduleTestPlanSystem',
TASK_SCHEDULE_TASK_TEST_PLAN_ORGANIZATION = 'taskCenterScheduleTestPlanOrganization', TASK_SCHEDULE_TASK_TEST_PLAN_ORGANIZATION = 'taskCenterScheduleTestPlanOrganization',
TASK_SCHEDULE_TASK_TEST_PLAN_PROJECT = 'taskCenterScheduleTestPlanProject', TASK_SCHEDULE_TASK_TEST_PLAN_PROJECT = 'taskCenterScheduleTestPlanProject',
TASK_PLAN_SYSTEM = 'taskCenterPlanSystem',
TASK_PLAN_ORGANIZATION = 'taskCenterPlanOrganization',
TASK_PLAN_PROJECT = 'taskCenterPlanProject',
} }
// 具有特殊功能的列 // 具有特殊功能的列

View File

@ -14,7 +14,20 @@
</a-tabs> </a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider> <a-divider margin="0" class="!mb-[16px]"></a-divider>
<!-- 接口用例列表--> <!-- 接口用例列表-->
<ApiCase v-if="activeTask === 'real'" :name="listName" :module-type="activeTab" :group="props.group" /> <ApiCase
v-if="
activeTask === 'real' && (activeTab === TaskCenterEnum.API_CASE || activeTab === TaskCenterEnum.API_SCENARIO)
"
:name="listName"
:module-type="activeTab"
:group="props.group"
/>
<!-- 测试计划列表-->
<TestPlan
v-if="activeTask === 'real' && activeTab === TaskCenterEnum.TEST_PLAN"
:name="listName"
:group="props.group"
/>
<ScheduledTask v-if="activeTask === 'timing'" :name="listName" :group="props.group" :module-type="activeTab" /> <ScheduledTask v-if="activeTask === 'timing'" :name="listName" :group="props.group" :module-type="activeTab" />
</div> </div>
</div> </div>
@ -25,10 +38,10 @@
import ApiCase from './apiCase.vue'; import ApiCase from './apiCase.vue';
import ScheduledTask from './scheduledTask.vue'; import ScheduledTask from './scheduledTask.vue';
import TestPlan from './testPlan.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import type { ResourceTypeMapKey } from '@/enums/taskCenter';
import { TaskCenterEnum } from '@/enums/taskCenter'; import { TaskCenterEnum } from '@/enums/taskCenter';
import type { ExtractedKeys } from './utils'; import type { ExtractedKeys } from './utils';
@ -51,6 +64,10 @@
value: TaskCenterEnum.API_SCENARIO, value: TaskCenterEnum.API_SCENARIO,
label: t('project.taskCenter.apiScenario'), label: t('project.taskCenter.apiScenario'),
}, },
{
value: TaskCenterEnum.TEST_PLAN,
label: t('project.taskCenter.testPlan'),
},
// TODO // TODO
// { // {
// value: TaskCenterEnum.UI_TEST, // value: TaskCenterEnum.UI_TEST,
@ -60,10 +77,6 @@
// value: TaskCenterEnum.LOAD_TEST, // value: TaskCenterEnum.LOAD_TEST,
// label: t('project.taskCenter.performanceTest'), // label: t('project.taskCenter.performanceTest'),
// }, // },
// {
// value: TaskCenterEnum.TEST_PLAN,
// label: t('project.taskCenter.testPlan'),
// },
]); ]);
const timingTabList = ref([ const timingTabList = ref([
@ -78,7 +91,7 @@
{ {
value: TaskCenterEnum.TEST_PLAN, value: TaskCenterEnum.TEST_PLAN,
label: t('project.taskCenter.testPlan'), label: t('project.taskCenter.testPlan'),
} },
]); ]);
const activeTask = ref(route.query.tab || 'real'); const activeTask = ref(route.query.tab || 'real');
@ -110,10 +123,12 @@
.box { .box {
display: flex; display: flex;
height: 100%; height: 100%;
.left { .left {
width: 252px; width: 252px;
height: 100%; height: 100%;
border-right: 1px solid var(--color-text-n8); border-right: 1px solid var(--color-text-n8);
.item { .item {
padding: 0 20px; padding: 0 20px;
height: 38px; height: 38px;
@ -121,18 +136,21 @@
line-height: 38px; line-height: 38px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
&.active { &.active {
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
background: rgb(var(--primary-1)); background: rgb(var(--primary-1));
} }
} }
} }
.right { .right {
width: calc(100% - 300px); width: calc(100% - 300px);
flex-grow: 1; /* 自适应 */ flex-grow: 1; /* 自适应 */
height: 100%; height: 100%;
} }
} }
.no-content { .no-content {
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
padding-top: 0; padding-top: 0;

View File

@ -0,0 +1,419 @@
<template>
<div class="px-[16px]">
<div class="mb-[16px] flex items-center justify-between">
<div class="flex items-center"></div>
<div class="items-right flex gap-[8px]">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('system.organization.searchIndexPlaceholder')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
@clear="searchList"
></a-input-search>
</div>
</div>
<ms-base-table
v-bind="propsRes"
ref="tableRef"
:action-config="tableBatchActions"
:selectable="hasOperationPermission"
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #resourceNum="{ record }">
<div
v-if="!record.integrated"
type="text"
class="one-line-text w-full"
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
@click="showDetail()"
>{{ record.resourceNum }}
</div>
</template>
<template #resourceName="{ record }">
<div
v-if="!record.integrated"
class="one-line-text max-w-[300px]"
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
@click="showDetail()"
>{{ record.resourceName }}
</div>
</template>
<template #status="{ record }">
<ExecutionStatus :status="record.status" />
</template>
<template #execStatus="{ record }">
<ExecStatus :status="record.execStatus" />
</template>
<template #[FilterSlotNameEnum.TEST_PLAN_REPORT_EXEC_STATUS]="{ filterContent }">
<ExecStatus :status="filterContent.value" />
</template>
<template #[FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER]="{ filterContent }">
<ExecutionStatus :status="filterContent.value" />
</template>
<template #projectName="{ record }">
<a-tooltip :content="`${record.projectName}`" position="tl">
<div class="one-line-text">{{ characterLimit(record.projectName) }}</div>
</a-tooltip>
</template>
<template #organizationName="{ record }">
<a-tooltip :content="`${record.organizationName}`" position="tl">
<div class="one-line-text">{{ characterLimit(record.organizationName) }}</div>
</a-tooltip>
</template>
<template #triggerMode="{ record }">
<span>{{ t(ExecutionMethodsLabel[record.triggerMode as keyof typeof ExecutionMethodsLabel]) }}</span>
</template>
<template #operationTime="{ record }">
<span>{{ dayjs(record.operationTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template #operation="{ record }">
<div v-if="record.historyDeleted">
<a-tooltip :content="t('project.executionHistory.cleared')">
<MsButton
class="!mr-0"
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
@click="viewReport(record.id)"
>{{ t('project.taskCenter.viewReport') }}
</MsButton>
</a-tooltip>
</div>
<div v-else>
<MsButton
class="!mr-0"
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
@click="viewReport(record.id)"
>{{ t('project.taskCenter.viewReport') }}
</MsButton>
</div>
<a-divider v-if="['RUNNING', 'RERUNNING'].includes(record.execStatus)" direction="vertical" />
<MsButton
v-if="
['RUNNING', 'RERUNNING'].includes(record.execStatus) && hasAnyPermission(permissionsMap[props.group].stop)
"
class="!mr-0"
@click="stop()"
>{{ t('project.taskCenter.stop') }}
</MsButton>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import ExecStatus from '@/views/test-plan/report/component/execStatus.vue';
import ExecutionStatus from '@/views/test-plan/report/component/reportStatus.vue';
import {
getRealOrgPlanList,
getRealProPlanList,
getRealSysPlanList,
} from '@/api/modules/project-management/taskCenter';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { useTableStore } from '@/store';
import { characterLimit } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { BatchApiParams } from '@/models/common';
import { ReportExecStatus } from '@/enums/apiEnum';
import { PlanReportStatus } from '@/enums/reportEnum';
import { RouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { ExecutionMethodsLabel, TaskCenterEnum } from '@/enums/taskCenter';
import { getOrgColumns, getProjectColumns, Group } from './utils';
const { openNewPage } = useOpenNewPage();
const tableStore = useTableStore();
const { t } = useI18n();
const props = defineProps<{
group: Group;
name: string;
}>();
const keyword = ref<string>('');
const permissionsMap: Record<Group, any> = {
organization: {
stop: ['ORGANIZATION_TASK_CENTER:READ+STOP', 'PROJECT_TEST_PLAN:READ+EXECUTE'],
jump: ['PROJECT_TEST_PLAN:READ'],
report: ['PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN_REPORT:READ'],
},
system: {
stop: ['SYSTEM_TASK_CENTER:READ+STOP', 'PROJECT_TEST_PLAN:READ+EXECUTE'],
jump: ['PROJECT_TEST_PLAN:READ'],
report: ['PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN_REPORT:READ'],
},
project: {
stop: ['PROJECT_TEST_PLAN:READ+EXECUTE'],
jump: ['PROJECT_TEST_PLAN:READ'],
report: ['PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN_REPORT:READ'],
},
};
const loadRealMap = ref({
system: {
list: getRealSysPlanList,
},
organization: {
list: getRealOrgPlanList,
},
project: {
list: getRealProPlanList,
},
});
const hasJumpPermission = computed(() => hasAnyPermission(permissionsMap[props.group].jump));
const hasOperationPermission = computed(() => hasAnyPermission(permissionsMap[props.group].stop));
const statusResultOptions = computed(() => {
return Object.keys(PlanReportStatus).map((key) => {
return {
value: key,
label: PlanReportStatus[key].statusText,
};
});
});
const ExecStatusList = computed(() => {
return Object.values(ReportExecStatus).map((e) => {
return {
value: e,
key: e,
};
});
});
const triggerModeList = [
{
value: 'SCHEDULE',
label: t('project.taskCenter.scheduledTask'),
},
{
value: 'MANUAL',
label: t('project.taskCenter.manualExecution'),
},
{
value: 'API',
label: t('project.taskCenter.interfaceCall'),
},
{
value: 'BATCH',
label: t('project.taskCenter.batchExecution'),
},
];
const staticColumns: MsTableColumn = [
{
title: 'project.taskCenter.resourceID',
dataIndex: 'resourceNum',
slotName: 'resourceNum',
width: 200,
sortIndex: 1,
fixed: 'left',
showTooltip: true,
showInTable: true,
showDrag: false,
columnSelectorDisabled: true,
},
{
title: 'project.taskCenter.resourceName',
slotName: 'resourceName',
dataIndex: 'resourceName',
width: 300,
showDrag: false,
showTooltip: true,
showInTable: true,
columnSelectorDisabled: true,
},
{
title: 'project.taskCenter.executionResult',
dataIndex: 'status',
slotName: 'status',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: statusResultOptions.value,
filterSlotName: FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER,
},
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'project.taskCenter.status',
dataIndex: 'execStatus',
slotName: 'execStatus',
filterConfig: {
options: ExecStatusList.value,
filterSlotName: FilterSlotNameEnum.TEST_PLAN_REPORT_EXEC_STATUS,
},
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
showInTable: true,
width: 200,
showDrag: true,
},
{
title: 'project.taskCenter.executionMode',
dataIndex: 'triggerMode',
slotName: 'triggerMode',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
filterConfig: {
options: triggerModeList,
},
showInTable: true,
width: 150,
showDrag: true,
},
{
title: 'project.taskCenter.resourcePool',
slotName: 'poolName',
dataIndex: 'poolName',
showInTable: true,
showDrag: true,
showTooltip: true,
width: 200,
},
{
title: 'project.taskCenter.operator',
slotName: 'operationName',
dataIndex: 'operationName',
showInTable: true,
showDrag: true,
showTooltip: true,
width: 200,
},
{
title: 'project.taskCenter.operating',
dataIndex: 'operationTime',
slotName: 'operationTime',
width: 180,
showDrag: true,
},
{
title: 'common.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: hasOperationPermission.value ? 180 : 100,
},
];
const tableKeysMap: Record<string, any> = {
system: TableKeyEnum.TASK_PLAN_SYSTEM,
organization: TableKeyEnum.TASK_PLAN_ORGANIZATION,
project: TableKeyEnum.TASK_PLAN_PROJECT,
};
const groupColumnsMap: Record<string, any> = {
system: [getOrgColumns(), getProjectColumns(tableKeysMap[props.group]), ...staticColumns],
organization: [getProjectColumns(tableKeysMap[props.group]), ...staticColumns],
project: staticColumns,
};
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams } = useTable(
loadRealMap.value[props.group].list,
{
tableKey: tableKeysMap[props.group],
scroll: {
x: 1400,
},
showSetting: true,
selectable: hasOperationPermission.value,
heightUsed: 330,
enableDrag: false,
showSelectAll: true,
}
);
function initData() {
setLoadListParams({
moduleType: TaskCenterEnum.TEST_PLAN,
keyword: keyword.value,
filter: {
...propsRes.value.filter,
},
});
loadList();
}
const tableBatchActions = {
baseAction: [
{
label: 'project.taskCenter.batchStop',
eventTag: 'batchStop',
anyPermission: permissionsMap[props.group].stop,
},
],
};
const batchParams = ref<BatchApiParams>({
selectIds: [],
selectAll: false,
excludeIds: [] as string[],
condition: {},
});
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = { ...params, selectIds: params?.selectedIds || [], condition: params?.condition || {} };
if (event.eventTag === 'batchStop') {
// TODO
}
}
function viewReport(id: string) {
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {
id,
});
}
function showDetail() {
// TODO
}
function stop() {
// TODO
}
function searchList() {
resetSelector();
initData();
}
onBeforeMount(async () => {
initData();
});
watch(
() => props.group,
(val) => {
if (val) {
resetSelector();
resetFilterParams();
initData();
}
}
);
await tableStore.initColumn(tableKeysMap[props.group], groupColumnsMap[props.group], 'drawer', true);
</script>
<style scoped></style>

View File

@ -121,7 +121,10 @@ export const TaskStatus: Record<ResourceTypeMapKey, Record<string, { icon: strin
export type Group = 'system' | 'organization' | 'project'; export type Group = 'system' | 'organization' | 'project';
export type ExtractedKeys = Extract<ResourceTypeMapKey, TaskCenterEnum.API_CASE | TaskCenterEnum.API_SCENARIO>; export type ExtractedKeys = Extract<
ResourceTypeMapKey,
TaskCenterEnum.API_CASE | TaskCenterEnum.API_SCENARIO | TaskCenterEnum.TEST_PLAN
>;
export const resourceTypeMap: Record<ResourceTypeMapKey, Record<string, any>> = { export const resourceTypeMap: Record<ResourceTypeMapKey, Record<string, any>> = {
[TaskCenterEnum.API_CASE]: { [TaskCenterEnum.API_CASE]: {

View File

@ -61,7 +61,7 @@
</template> </template>
<!-- 执行状态筛选 --> <!-- 执行状态筛选 -->
<template #resultStatus="{ record }"> <template #resultStatus="{ record }">
<ExecutionStatus :module-type="ReportStatusEnum.REPORT_STATUS" :status="record.resultStatus" /> <ExecutionStatus :status="record.resultStatus" />
</template> </template>
<template #execStatus="{ record }"> <template #execStatus="{ record }">
<ExecStatus :status="record.execStatus" /> <ExecStatus :status="record.execStatus" />
@ -114,7 +114,7 @@
import { BatchApiParams } from '@/models/common'; import { BatchApiParams } from '@/models/common';
import { ReportExecStatus } from '@/enums/apiEnum'; import { ReportExecStatus } from '@/enums/apiEnum';
import { PlanReportStatus, ReportStatusEnum, TriggerModeLabel } from '@/enums/reportEnum'; import { PlanReportStatus, TriggerModeLabel } from '@/enums/reportEnum';
import { TestPlanRouteEnum } from '@/enums/routeEnum'; import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum'; import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';

View File

@ -37,7 +37,7 @@
}; };
function getExecutionResult(): IconType { function getExecutionResult(): IconType {
return iconTypeStatus[props.status] ?? iconTypeStatus[props.status]?.DEFAULT; return iconTypeStatus[props.status] ? iconTypeStatus[props.status] : iconTypeStatus.DEFAULT;
} }
</script> </script>