feat(测试计划): 测试计划组联调拖拽&执行&定时任务&任务中心数量

This commit is contained in:
xinxin.wu 2024-06-06 19:24:07 +08:00 committed by Craftsman
parent f1bc941411
commit 705757d8be
17 changed files with 414 additions and 163 deletions

View File

@ -12,12 +12,15 @@ import {
batchMovePlanUrl,
BatchRunCaseUrl,
BatchUpdateCaseExecutorUrl,
ConfigScheduleUrl,
copyTestPlanUrl,
deletePlanUrl,
DeleteScheduleTaskUrl,
DeleteTestPlanModuleUrl,
DisassociateCaseUrl,
dragPlanOnGroupUrl,
ExecuteHistoryUrl,
ExecutePlanUrl,
followPlanUrl,
GenerateReportUrl,
GetAssociatedBugUrl,
@ -59,9 +62,11 @@ import type {
BatchExecuteFeatureCaseParams,
BatchFeatureCaseParams,
BatchUpdateCaseExecutorParams,
CreateTask,
DisassociateCaseParams,
ExecuteHistoryItem,
ExecuteHistoryType,
ExecutePlan,
FollowPlanParams,
PassRateCountDetail,
PlanDetailApiCaseItem,
@ -281,3 +286,15 @@ export function getPlanGroupOptions(projectId: string) {
export function dragPlanOnGroup(data: DragSortParams) {
return MSR.post({ url: dragPlanOnGroupUrl, data });
}
// 测试计划-配置定时任务
export function configSchedule(data: CreateTask) {
return MSR.post({ url: ConfigScheduleUrl, data });
}
// 测试计划-计划&计划组-执行&批量执行
export function executePlanOrGroup(data: ExecutePlan) {
return MSR.post({ url: ExecutePlanUrl, data });
}
// 测试计划-计划&计划组-执行&批量执行
export function deleteScheduleTask(testPlanId: string) {
return MSR.get({ url: `${DeleteScheduleTaskUrl}/${testPlanId}` });
}

View File

@ -90,3 +90,9 @@ export const TestPlanAndGroupCopyUrl = '/test-plan/copy';
export const TestPlanGroupOptionsUrl = 'test-plan/group-list';
// 测试计划-拖拽测试计划
export const dragPlanOnGroupUrl = '/test-plan/sort';
// 测试计划-创建定时任务
export const ConfigScheduleUrl = '/test-plan/schedule-config';
// 测试计划-计划&计划组-执行&批量执行
export const ExecutePlanUrl = '/test-plan-execute/start';
// 测试计划-删除定时任务
export const DeleteScheduleTaskUrl = 'test-plan/schedule-config-delete';

View File

@ -863,6 +863,12 @@
background: var(--color-text-brand);
line-height: 16px;
}
.active-badge {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
.filter-button {
display: flex;
justify-content: space-between;

View File

@ -69,12 +69,6 @@
@apply relative right-0 top-0 transform-none shadow-none;
}
}
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
.no-content {
:deep(.arco-tabs-content) {
display: none;

View File

@ -157,11 +157,11 @@
<template v-else-if="item.showTooltip">
<a-input
v-if="
editActiveKey === `${item.dataIndex}${rowIndex}` &&
editActiveKey === `${record[rowKey || 'id']}` &&
item.editType &&
item.editType === ColumnEditTypeEnum.INPUT
"
ref="currentInputRef"
:ref="(el: any) => setRefMap(el, `${record[rowKey|| 'id']}`)"
v-model="record[item.dataIndex as string]"
:max-length="255"
@click.stop
@ -375,8 +375,16 @@
// Active
const editActiveKey = ref<string>('');
// Ref
const currentInputRef = ref();
const refMap = ref<Record<string, any>>({});
const setRefMap = (el: any, id: string) => {
if (el) {
refMap.value[id] = el;
}
};
// blur
const currentEditValue = ref<string>('');
// enter
@ -543,7 +551,7 @@
record[dataIndex] = currentEditValue.value;
}
isEnter.value = false;
currentInputRef.value = null;
refMap.value[record[rowKey || 'id']] = null;
editActiveKey.value = '';
currentEditValue.value = '';
} else {
@ -585,6 +593,16 @@
emit('sorterChange', sortOrder ? { [dataIndex]: sortOrder } : {});
};
function getCurrentList(data: TableData[], key: string, id: string) {
return data.find((item) => {
const currentChildrenIds = (item.children || []).map((e) => e[key]);
if (currentChildrenIds?.includes(id)) {
return true;
}
return false;
});
}
//
const handleDragChange = (data: TableData[], extra: TableChangeExtra, currentData: TableData[]) => {
if (!currentData || currentData.length === 1) {
@ -592,36 +610,60 @@
}
if (extra && extra.dragTarget?.id) {
let newDragData: TableData[] = data;
let oldDragData: TableData[] = currentData;
const newDragItem = getCurrentList(data, 'id', extra.dragTarget.id);
const oldDragItem = getCurrentList(currentData, 'key', extra.dragTarget.id);
if (newDragItem && newDragItem.children && oldDragItem && oldDragItem.children) {
newDragData = newDragItem.children;
oldDragData = oldDragItem.children;
}
let oldIndex = 0;
let newIndex = 0;
newIndex = newDragData.findIndex((item: any) => item.id === extra.dragTarget?.id);
oldIndex = oldDragData.findIndex((item: any) => item.key === extra.dragTarget?.id);
let position: 'AFTER' | 'BEFORE' = 'BEFORE';
position = newIndex > oldIndex ? 'AFTER' : 'BEFORE';
const params: DragSortParams = {
projectId: appStore.currentProjectId,
targetId: '', // id
moveMode: 'BEFORE',
moveMode: position,
moveId: extra.dragTarget.id as string, // id
};
const index = currentData.findIndex((item: any) => item.key === extra.dragTarget?.id);
if (index > -1 && currentData[index + 1]) {
let targetIndex;
if (position === 'AFTER' && newIndex > 0) {
targetIndex = newIndex - 1;
} else if (position === 'AFTER') {
params.moveMode = 'BEFORE';
params.targetId = currentData[index + 1].raw.id;
} else if (index > -1 && !currentData[index + 1]) {
if (index > -1 && currentData[index - 1]) {
params.moveMode = 'AFTER';
params.targetId = currentData[index - 1].raw.id;
}
targetIndex = newIndex + 1;
} else if (position === 'BEFORE' && newIndex < newDragData.length - 1) {
targetIndex = newIndex + 1;
} else {
params.moveMode = 'AFTER';
targetIndex = newIndex - 1;
}
params.targetId = newDragData[targetIndex]?.id ?? newDragData[newIndex]?.id;
emit('dragChange', params);
}
};
// input
const handleEdit = (dataIndex: string, rowIndex: number, record: TableData) => {
editActiveKey.value = dataIndex + rowIndex;
editActiveKey.value = record.id;
currentEditValue.value = record[dataIndex];
if (currentInputRef.value) {
currentInputRef.value[0].focus();
const refKey = `${record[rowKey as string]}`;
if (refMap.value[refKey]) {
refMap.value[refKey]?.focus();
} else {
nextTick(() => {
currentInputRef.value[0].focus();
refMap.value[refKey]?.focus();
});
}
};

View File

@ -38,6 +38,15 @@ export const defaultDetailCount: PassRateCountDetail = {
functionalCaseCount: 0,
apiCaseCount: 0,
apiScenarioCount: 0,
scheduleConfig: {
resourceId: '',
enable: false,
cron: '',
runConfig: {
runMode: 'SERIAL',
},
},
nextTriggerTime: 0,
};
export const defaultExecuteForm = {

View File

@ -206,7 +206,7 @@ export interface BatchUpdateCaseExecutorParams extends BatchFeatureCaseParams {
export interface SortFeatureCaseParams extends DragSortParams {
testPlanId: string;
}
export type RunModeType = 'SERIAL' | 'PARALLEL';
export interface PassRateCountDetail {
id: string;
passThreshold: number;
@ -221,6 +221,15 @@ export interface PassRateCountDetail {
functionalCaseCount: number;
apiCaseCount: number;
apiScenarioCount: number;
scheduleConfig: {
resourceId: string;
enable: boolean;
cron: string;
runConfig: {
runMode: RunModeType;
};
};
nextTriggerTime: number;
}
// 执行历史
@ -300,4 +309,16 @@ export interface PlanDetailExecuteHistoryItem {
lastExecResult: LastExecuteResults;
triggerMode: string;
}
export interface CreateTask {
resourceId: string;
enable: boolean;
cron: string;
runConfig: { runMode: 'SERIAL' | 'PARALLEL' };
}
export interface ExecutePlan {
projectId: string;
executeIds: string[];
executeMode: RunModeType;
}
export default {};

View File

@ -293,10 +293,4 @@
padding: 8px 16px;
}
}
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
</style>

View File

@ -374,10 +374,4 @@
.ms-scroll-bar();
}
}
:deep(.active-badge) {
.arco-badge-text,
.arco-badge-number {
background-color: rgb(var(--primary-5));
}
}
</style>

View File

@ -788,9 +788,6 @@
color: rgb(var(--danger-6));
}
}
:deep(.active .arco-badge-text) {
background: rgb(var(--primary-5));
}
:deep(.tags-class .arco-form-item-label-col) {
justify-content: flex-start !important;
}

View File

@ -862,7 +862,4 @@
color: rgb(var(--danger-6));
}
}
:deep(.active .arco-badge-text) {
background: rgb(var(--primary-5));
}
</style>

View File

@ -1,11 +1,21 @@
<template>
<div class="box">
<div class="left" :class="getStyleClass()">
<div class="item" :class="[activeTask === 'real' ? 'active' : '']" @click="toggleTask('real')">
{{ t('project.taskCenter.realTimeTask') }}
</div>
<div class="item" :class="[activeTask === 'timing' ? 'active' : '']" @click="toggleTask('timing')">
{{ t('project.taskCenter.scheduledTask') }}
<div
v-for="item of menuTab"
:key="item.value"
:class="`${activeTask === item.value ? 'active' : ''} item flex items-center`"
@click="toggleTask(item.value)"
>
<div class="mr-2">
{{ item.label }}
</div>
<a-badge
v-if="getTextFunc(item.value) !== ''"
:class="`${item.value === activeTask ? 'active-badge' : ''} mt-[2px]`"
:max-count="99"
:text="getTextFunc(item.value)"
/>
</div>
</div>
<div class="right">
@ -40,11 +50,20 @@
import ScheduledTask from './scheduledTask.vue';
import TestPlan from './testPlan.vue';
import {
getOrgRealTotal,
getOrgScheduleTotal,
getProjectRealTotal,
getProjectScheduleTotal,
getSystemRealTotal,
getSystemScheduleTotal,
} from '@/api/modules/project-management/taskCenter';
import { useI18n } from '@/hooks/useI18n';
import { TaskCenterEnum } from '@/enums/taskCenter';
import type { ExtractedKeys } from './utils';
import { on } from 'events';
const { t } = useI18n();
@ -94,7 +113,7 @@
},
]);
const activeTask = ref(route.query.tab || 'real');
const activeTask = ref<string>((route.query.tab as string) || 'real');
const activeTab = ref<ExtractedKeys>((route.query.type as ExtractedKeys) || TaskCenterEnum.API_CASE);
const rightTabList = computed(() => {
@ -117,40 +136,86 @@
const listName = computed(() => {
return rightTabList.value.find((item) => item.value === activeTab.value)?.label || '';
});
export type menuType = 'real' | 'timing';
const menuTab: { value: menuType; label: string }[] = [
{
value: 'real',
label: t('project.taskCenter.realTimeTask'),
},
{
value: 'timing',
label: t('project.taskCenter.scheduledTask'),
},
];
const getTotalMap: Record<menuType, any> = {
real: {
system: getSystemRealTotal,
organization: getOrgRealTotal,
project: getProjectRealTotal,
},
timing: {
system: getSystemScheduleTotal,
organization: getOrgScheduleTotal,
project: getProjectScheduleTotal,
},
};
const totalMap = ref<Record<menuType, number>>({
real: 0,
timing: 0,
});
async function getTotal() {
try {
const [timingTotal, realTotal] = await Promise.all([
getTotalMap.timing[props.group](),
getTotalMap.real[props.group](),
]);
totalMap.value.timing = timingTotal;
totalMap.value.real = realTotal;
} catch (error) {
console.log(error);
}
}
function getTextFunc(activeKey: menuType) {
return totalMap.value[activeKey] > 99 ? '99+' : `${totalMap.value[activeKey]}` || '';
}
onMounted(() => {
getTotal();
});
</script>
<style scoped lang="less">
.box {
display: flex;
height: 100%;
.left {
width: 252px;
height: 100%;
border-right: 1px solid var(--color-text-n8);
.item {
padding: 0 20px;
height: 38px;
font-size: 14px;
line-height: 38px;
border-radius: 4px;
cursor: pointer;
&.active {
color: rgb(var(--primary-5));
background: rgb(var(--primary-1));
}
}
}
.right {
width: calc(100% - 300px);
flex-grow: 1; /* 自适应 */
height: 100%;
}
}
.no-content {
:deep(.arco-tabs-content) {
padding-top: 0;

View File

@ -26,7 +26,7 @@
{{ t('common.confirmDelete') }}
</a-button>
<a-button
v-if="props.record?.status === 'COMPLETED'"
v-if="showArchive"
:loading="confirmLoading"
class="ml-3"
type="primary"
@ -105,6 +105,10 @@
return t('testPlan.testPlanIndex.deletePendingPlan');
}
});
const showArchive = computed(() => {
return props.record?.status === 'COMPLETED' && props.record.groupId && props.record.groupId === 'NONE';
});
</script>
<style scoped lang="less">

View File

@ -108,24 +108,24 @@
:class="`${
record.type === testPlanTypeEnum.TEST_PLAN ? 'text-[rgb(var(--primary-5))]' : ''
} one-line-text ${hasIndent(record)}`"
@click="openDetail(record.id, record.type)"
@click="openDetail(record.id)"
>{{ record.num }}</div
>
<!-- TODO 待联调定时任务 -->
<a-tooltip position="right" :disabled="record.schedule" :mouse-enter-delay="300">
<a-tooltip position="right" :disabled="!getSchedule(record.id)" :mouse-enter-delay="300">
<MsTag
v-if="record.schedule"
v-if="getSchedule(record.id)"
size="small"
:type="record.schedule ? 'link' : 'default'"
:type="getScheduleEnable(record.id) ? 'link' : 'default'"
theme="outline"
class="ml-2"
:tooltip-disabled="true"
>{{ t('testPlan.testPlanIndex.timing') }}</MsTag
>
<template #content>
<div v-if="record.schedule">
<div v-if="getScheduleEnable(record.id)">
<div>{{ t('testPlan.testPlanIndex.scheduledTaskOpened') }}</div>
<div>{{ t('testPlan.testPlanIndex.nextExecutionTime') }}</div>
<div> {{ dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div> {{ dayjs(defaultCountDetailMap[record.id]?.nextTriggerTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<div v-else> {{ t('testPlan.testPlanIndex.scheduledTaskUnEnable') }} </div>
</template>
@ -176,8 +176,8 @@
</div>
</template>
<template #functionalCaseCount="{ record }">
<a-popover position="bottom" content-class="p-[16px]" :disabled="record.functionalCaseCount < 1">
<div>{{ record.functionalCaseCount }}</div>
<a-popover position="bottom" content-class="p-[16px]" :disabled="getFunctionalCount(record.id) < 1">
<div>{{ getFunctionalCount(record.id) }}</div>
<template #content>
<table class="min-w-[140px] max-w-[176px]">
<tr>
@ -185,7 +185,7 @@
<div>{{ t('testPlan.testPlanIndex.TotalCases') }}</div>
</td>
<td class="popover-value-td">
{{ record.caseTotal }}
{{ defaultCountDetailMap[record.id]?.caseTotal ?? '0' }}
</td>
</tr>
<tr>
@ -193,7 +193,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.functionalUseCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.functionalCaseCount }}
{{ getFunctionalCount(record.id) }}
</td>
</tr>
<tr>
@ -201,7 +201,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.apiCaseCount }}
{{ defaultCountDetailMap[record.id]?.apiCaseCount ?? '0' }}
</td>
</tr>
<tr>
@ -209,7 +209,7 @@
<div class="text-[var(--color-text-1)]">{{ t('testPlan.testPlanIndex.apiScenarioCase') }}</div>
</td>
<td class="popover-value-td">
{{ record.apiScenarioCount }}
{{ defaultCountDetailMap[record.id]?.apiScenarioCount ?? '0' }}
</td>
</tr>
</table>
@ -221,17 +221,17 @@
<div class="flex items-center">
<MsButton
v-if="
record.functionalCaseCount > 0 &&
getFunctionalCount(record.id) > 0 &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
record.status !== 'ARCHIVED'
"
class="!mx-0"
@click="openDetail(record.id)"
@click="executePlan(record)"
>{{ t('testPlan.testPlanIndex.execution') }}</MsButton
>
<a-divider
v-if="
record.functionalCaseCount > 0 &&
getFunctionalCount(record.id) > 0 &&
hasAnyPermission(['PROJECT_TEST_PLAN:READ+EXECUTE']) &&
record.status !== 'ARCHIVED'
"
@ -254,7 +254,7 @@
<MsButton
v-if="
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.functionalCaseCount < 1 &&
getFunctionalCount(record.id) < 1 &&
record.status !== 'ARCHIVED'
"
class="!mx-0"
@ -264,7 +264,7 @@
<a-divider
v-if="
hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) &&
record.functionalCaseCount < 1 &&
getFunctionalCount(record.id) < 1 &&
record.status !== 'ARCHIVED'
"
direction="vertical"
@ -286,9 +286,9 @@
<template #title>
{{ t('testPlan.testPlanIndex.batchExecution') }}
</template>
<a-radio-group v-model="executeType">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
<a-radio-group v-model="executeForm.executeMode">
<a-radio value="SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
<template #footer>
<div class="flex justify-end">
@ -311,8 +311,14 @@
:type="showType"
@save="handleMoveOrCopy"
/>
<!-- TODO 待联调定时任务 -->
<ScheduledModal v-model:visible="showScheduledTaskModal" :type="currentPlanType" @close="resetPlanType" />
<!-- TODO 待联调[编辑] 字段加到统计里边 -->
<ScheduledModal
v-model:visible="showScheduledTaskModal"
:type="planType"
:source-id="planSourceId"
:task-config="taskForm"
@handle-success="fetchData()"
/>
<ActionModal v-model:visible="showStatusDeleteModal" :record="activeRecord" @success="fetchData()" />
<BatchEditModal
v-model:visible="showEditModel"
@ -361,7 +367,9 @@
batchDeletePlan,
batchMovePlan,
deletePlan,
deleteScheduleTask,
dragPlanOnGroup,
executePlanOrGroup,
getPlanPassRate,
getTestPlanDetail,
getTestPlanList,
@ -379,6 +387,8 @@
import type {
AddTestPlanParams,
BatchMoveParams,
CreateTask,
ExecutePlan,
moduleForm,
PassRateCountDetail,
TestPlanItem,
@ -414,7 +424,6 @@
const isArchived = ref<boolean>(false);
const keyword = ref<string>('');
const currentPlanType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
const hasOperationPermission = computed(() =>
hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE', 'PROJECT_TEST_PLAN:READ+EXECUTE', 'PROJECT_TEST_PLAN:READ+ADD'])
@ -685,15 +694,28 @@
},
];
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
function getFunctionalCount(id: string) {
return defaultCountDetailMap.value[id]?.functionalCaseCount ?? 0;
}
function getSchedule(id: string) {
return !!defaultCountDetailMap.value[id]?.scheduleConfig;
}
function getScheduleEnable(id: string) {
return defaultCountDetailMap.value[id].scheduleConfig.enable;
}
function getMoreActions(record: TestPlanItem) {
const { status: planStatus, functionalCaseCount: useCount, schedule } = record;
const { status: planStatus } = record;
const useCount = defaultCountDetailMap.value[record.id]?.functionalCaseCount ?? 0;
//
const copyAction =
useCount > 0 && hasAnyPermission(['PROJECT_TEST_PLAN:READ+ADD']) && planStatus !== 'ARCHIVED' ? copyActions : [];
// TODO
let scheduledTaskAction: ActionsItem[] = [];
if (planStatus !== 'ARCHIVED') {
scheduledTaskAction = schedule ? updateAndDeleteScheduledActions : createScheduledActions;
if (planStatus !== 'ARCHIVED' && record.groupId && record.groupId === 'NONE') {
scheduledTaskAction = getSchedule(record.id) ? updateAndDeleteScheduledActions : createScheduledActions;
}
// &
const archiveAction =
@ -701,6 +723,7 @@
(record.type === testPlanTypeEnum.TEST_PLAN && record.groupId && record.groupId !== 'NONE')
? []
: archiveActions;
//
if (planStatus === 'ARCHIVED' || planStatus === 'PREPARED' || planStatus === 'UNDERWAY') {
return [
@ -741,17 +764,18 @@
draggableCondition: true,
});
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams } = useTable(
getTestPlanList,
tableProps.value,
(item) => {
return {
...item,
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
};
},
updatePlanName
);
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, resetFilterParams, setPagination } =
useTable(
getTestPlanList,
tableProps.value,
(item) => {
return {
...item,
tags: (item.tags || []).map((e: string) => ({ id: e, name: e })),
};
},
updatePlanName
);
const batchParams = ref<BatchActionQueryParams>({
selectedIds: [],
@ -816,8 +840,6 @@
});
}
const defaultCountDetailMap = ref<Record<string, PassRateCountDetail>>({});
async function getStatistics(selectedPlanIds: (string | undefined)[]) {
try {
const result = await getPlanPassRate(selectedPlanIds);
@ -829,18 +851,8 @@
console.log(error);
}
}
async function fetchData() {
resetSelector();
await loadPlanList();
emitTableParams();
}
//
function openDetail(id: string, type?: keyof typeof testPlanTypeEnum) {
if (type && type === testPlanTypeEnum.GROUP) {
return;
}
function openDetail(id: string) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: {
@ -849,29 +861,82 @@
});
}
async function fetchData() {
resetSelector();
await loadPlanList();
emitTableParams();
}
/**
* 批量执行
*/
const executeType = ref('serial');
const initExecuteForm: ExecutePlan = {
projectId: appStore.currentProjectId,
executeIds: [],
executeMode: 'SERIAL',
};
const executeForm = ref<ExecutePlan>(cloneDeep(initExecuteForm));
const executeVisible = ref<boolean>(false);
function handleExecute() {
function handleExecute(isBatch: boolean) {
if (isBatch) {
executeForm.value.executeIds = batchParams.value.selectedIds || [];
}
executeVisible.value = true;
}
function cancelHandler() {
executeVisible.value = false;
executeForm.value = cloneDeep(initExecuteForm);
}
const confirmLoading = ref<boolean>(false);
/**
* 执行 TODO 待联调
* 批量执行
*/
function executeHandler() {
const confirmLoading = ref<boolean>(false);
async function executeHandler() {
confirmLoading.value = true;
try {
await executePlanOrGroup(executeForm.value);
cancelHandler();
Message.success(t('case.detail.execute.success'));
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
//
function executePlan(record: TestPlanItem) {
const { type, id } = record;
if (type === testPlanTypeEnum.GROUP) {
handleExecute(false);
executeForm.value.executeIds = [id];
return;
}
if (type === testPlanTypeEnum.TEST_PLAN) {
//
if (defaultCountDetailMap.value[id]) {
const { apiScenarioCount, apiCaseCount } = defaultCountDetailMap.value[id];
if (!apiScenarioCount && !apiCaseCount) {
router.push({
name: TestPlanRouteEnum.TEST_PLAN_INDEX_DETAIL,
query: {
id,
},
});
} else {
executeForm.value.executeIds = [id];
executeHandler();
}
}
}
}
@ -920,6 +985,7 @@
showBatchModal.value = false;
fetchData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
okLoading.value = false;
@ -1042,7 +1108,7 @@
batchParams.value = params;
switch (event.eventTag) {
case 'execute':
handleExecute();
handleExecute(true);
break;
case 'copy':
handleCopyOrMove('copy');
@ -1072,17 +1138,19 @@
}
const showScheduledTaskModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
const taskForm = ref<CreateTask>();
const planSourceId = ref<string>();
const planType = ref<keyof typeof testPlanTypeEnum>(testPlanTypeEnum.TEST_PLAN);
function handleScheduledTask(record: TestPlanItem) {
currentPlanType.value = record.type;
planType.value = record.type;
planSourceId.value = record.id;
taskForm.value = defaultCountDetailMap.value[record.id]?.scheduleConfig;
showScheduledTaskModal.value = true;
}
function resetPlanType() {
currentPlanType.value = testPlanTypeEnum.TEST_PLAN;
}
const showStatusDeleteModal = ref<boolean>(false);
const activeRecord = ref<TestPlanItem>();
// :
async function handleDeleteGroup(record: TestPlanItem) {
@ -1158,6 +1226,16 @@
}
}
async function handleDeleteScheduled(record: TestPlanItem) {
try {
await deleteScheduleTask(record.id);
Message.success(t('testPlan.testPlanGroup.deleteScheduleTaskSuccess'));
fetchData();
} catch (error) {
console.log(error);
}
}
function handleMoreActionSelect(item: ActionsItem, record: TestPlanItem) {
switch (item.eventTag) {
case 'copy':
@ -1166,6 +1244,12 @@
case 'createScheduledTask':
handleScheduledTask(record);
break;
case 'updateScheduledTask':
handleScheduledTask(record);
break;
case 'deleteScheduledTask':
handleDeleteScheduled(record);
break;
case 'delete':
deleteStatusHandler(record);
break;
@ -1206,6 +1290,9 @@
(val) => {
if (val) {
tableProps.value.draggableCondition = hasAnyPermission(['PROJECT_TEST_PLAN:READ+UPDATE']) && val === 'ALL';
setPagination({
current: 1,
});
expandedKeys.value = [];
resetFilterParams();
fetchData();

View File

@ -6,11 +6,15 @@
:mask-closable="false"
>
<template #title>
{{ form.id ? t('testPlan.testPlanIndex.updateScheduledTask') : t('testPlan.testPlanIndex.createScheduledTask') }}
{{
props.taskConfig
? t('testPlan.testPlanIndex.updateScheduledTask')
: t('testPlan.testPlanIndex.createScheduledTask')
}}
</template>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item :label="t('testPlan.testPlanIndex.triggerTime')" asterisk-position="end">
<a-select v-model:model-value="form.time" :placeholder="t('common.pleaseSelect')">
<a-select v-model:model-value="form.cron" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of syncFrequencyOptions" :key="item.value" :value="item.value">
<span class="text-[var(--color-text-2)]"> {{ item.value }}</span
><span class="ml-1 text-[var(--color-text-n4)] hover:text-[rgb(var(--primary-5))]">
@ -39,9 +43,9 @@
</a-radio>
<a-radio value="new"> {{ t('testPlan.testPlanIndex.newEnv') }}</a-radio>
</a-radio-group> -->
<a-radio-group v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.methods">
<a-radio value="serial">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="parallel">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
<a-radio-group v-if="props.type === testPlanTypeEnum.GROUP" v-model="form.runConfig.runMode">
<a-radio value="SERIAL">{{ t('testPlan.testPlanIndex.serial') }}</a-radio>
<a-radio value="PARALLEL">{{ t('testPlan.testPlanIndex.parallel') }}</a-radio>
</a-radio-group>
<!-- TODO 资源池暂时不做 -->
<!-- <a-form-item :label="t('testPlan.testPlanIndex.resourcePool')" asterisk-position="end" class="mb-0">
@ -89,7 +93,9 @@
</div>
<div>
<a-button type="secondary" class="mr-3" @click="handleCancel">{{ t('system.plugin.pluginCancel') }}</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleCreate">{{ t('common.create') }}</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleCreate">{{
props.taskConfig ? t('common.update') : t('common.create')
}}</a-button>
</div>
</div>
</template>
@ -99,54 +105,72 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { type FormInstance, Message, type ValidatedError } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { configSchedule } from '@/api/modules/test-plan/testPlan';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { ResourcesItem } from '@/models/testPlan/testPlan';
import type { CreateTask } from '@/models/testPlan/testPlan';
import { testPlanTypeEnum } from '@/enums/testPlanEnum';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
taskConfig?: CreateTask;
type: keyof typeof testPlanTypeEnum;
sourceId?: string;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'close'): void;
(e: 'handleSuccess'): void;
}>();
const showModalVisible = useVModel(props, 'visible', emit);
const initForm = {
id: '',
time: '',
env: '',
resourcePoolIds: '',
const initForm: CreateTask = {
resourceId: '',
cron: '',
enable: false,
methods: 'parallel',
runConfig: { runMode: 'SERIAL' },
};
const form = ref({ ...initForm });
const form = ref<CreateTask>(cloneDeep(initForm));
const confirmLoading = ref<boolean>(false);
const formRef = ref();
function handleCreate() {}
function resetForm() {
form.value = { ...initForm };
}
const formRef = ref<FormInstance | null>(null);
function handleCancel() {
showModalVisible.value = false;
formRef.value?.resetFields();
resetForm();
emit('close');
form.value = cloneDeep(initForm);
}
function handleCreate() {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
confirmLoading.value = true;
try {
if (props.sourceId) {
const params = {
...form.value,
resourceId: props.sourceId,
};
await configSchedule(params);
handleCancel();
emit('handleSuccess');
Message.success(t('common.createSuccess'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
confirmLoading.value = false;
}
}
});
}
const syncFrequencyOptions = [
@ -156,22 +180,14 @@
{ label: t('apiTestManagement.timeTaskDay'), value: '0 0 0 * * ?' },
];
const resourcesList = ref<ResourcesItem[]>([
{
id: '1',
name: '200.4',
cpuRate: '80%',
status: true,
},
{
id: '2',
name: 'LOCAL',
cpuRate: '80%',
status: true,
},
]);
function createCustomFrequency() {}
watch(
() => props.taskConfig,
(val) => {
if (val) {
form.value = cloneDeep(val);
}
}
);
</script>
<style scoped lang="less">

View File

@ -133,4 +133,5 @@ export default {
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': 'Please select the Plan group',
'testPlan.testPlanGroup.batchArchivedGroup': 'Confirm archive: {count} test plan groups',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': 'Are you sure to delete {count} test plan groups?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': 'Delete the scheduled task successfully',
};

View File

@ -122,4 +122,5 @@ export default {
'testPlan.testPlanGroup.selectTestPlanGroupPlaceHolder': '请选择计划组',
'testPlan.testPlanGroup.batchArchivedGroup': '确认归档:{count} 个测试计划组吗',
'testPlan.testPlanGroup.confirmBatchDeletePlanGroup': '确认删除 {count} 个测试计划组吗?',
'testPlan.testPlanGroup.deleteScheduleTaskSuccess': '删除定时任务成功',
};