feat(接口测试): 场景导入

This commit is contained in:
baiqi 2024-10-14 15:35:04 +08:00 committed by Craftsman
parent 7450a45384
commit b27da7e476
8 changed files with 310 additions and 7 deletions

View File

@ -17,6 +17,7 @@ import {
dragSortUrl, dragSortUrl,
ExecuteHistoryUrl, ExecuteHistoryUrl,
ExecuteScenarioUrl, ExecuteScenarioUrl,
ExportScenarioUrl,
FollowScenarioUrl, FollowScenarioUrl,
GetModuleCountUrl, GetModuleCountUrl,
GetModuleTreeUrl, GetModuleTreeUrl,
@ -27,6 +28,7 @@ import {
GetSystemRequestUrl, GetSystemRequestUrl,
GetTrashModuleCountUrl, GetTrashModuleCountUrl,
GetTrashModuleTreeUrl, GetTrashModuleTreeUrl,
ImportScenarioUrl,
MoveModuleUrl, MoveModuleUrl,
RecoverScenarioUrl, RecoverScenarioUrl,
RecycleScenarioUrl, RecycleScenarioUrl,
@ -63,7 +65,9 @@ import {
ApiScenarioUpdateDTO, ApiScenarioUpdateDTO,
ExecuteHistoryItem, ExecuteHistoryItem,
ExecutePageParams, ExecutePageParams,
type ExportScenarioParams,
GetSystemRequestParams, GetSystemRequestParams,
type ImportScenarioParams,
ImportSystemData, ImportSystemData,
Scenario, Scenario,
ScenarioDetail, ScenarioDetail,
@ -325,7 +329,18 @@ export function logScenarioReportBatchExport(data: BatchApiParams) {
export function getScenarioBatchExportParams(data: BatchApiParams) { export function getScenarioBatchExportParams(data: BatchApiParams) {
return MSR.post({ url: `${GetScenarioBatchExportParamsUrl}`, data }); return MSR.post({ url: `${GetScenarioBatchExportParamsUrl}`, data });
} }
// 场景导出报告id集合 // 场景导出报告id集合
export function scenarioAssociateExport(data: ImportSystemData) { export function scenarioAssociateExport(data: ImportSystemData) {
return MSR.post({ url: `${ScenarioAssociateExportUrl}`, data }); return MSR.post({ url: `${ScenarioAssociateExportUrl}`, data });
} }
// 导入场景
export function importScenario(params: ImportScenarioParams) {
return MSR.uploadFile({ url: ImportScenarioUrl }, { fileList: [params.file], request: params.request }, 'file');
}
// 导出场景
export function exportScenario(data: ExportScenarioParams) {
return MSR.post({ url: ExportScenarioUrl, data });
}

View File

@ -28,6 +28,9 @@ export const BatchEditScenarioUrl = '/api/scenario/batch-operation/edit'; // 批
export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批量执行接口场景 export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批量执行接口场景
export const UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级 export const UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级
export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态 export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态
export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景
export const ExportScenarioUrl = '/api/scenario/export'; // 导入场景
// 场景拖拽排序 // 场景拖拽排序
export const dragSortUrl = '/api/scenario/edit/pos'; export const dragSortUrl = '/api/scenario/edit/pos';
// 回收站相关 // 回收站相关

View File

@ -1,3 +1,4 @@
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import type { saveParams } from '@/components/business/ms-associate-case/types'; import type { saveParams } from '@/components/business/ms-associate-case/types';
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
@ -7,6 +8,7 @@ import {
RequestAssertionCondition, RequestAssertionCondition,
RequestComposition, RequestComposition,
RequestDefinitionStatus, RequestDefinitionStatus,
type RequestImportFormat,
RequestMethods, RequestMethods,
ScenarioExecuteStatus, ScenarioExecuteStatus,
ScenarioFailureStrategy, ScenarioFailureStrategy,
@ -521,9 +523,30 @@ export interface ScenarioAssociateCaseParams {
protocols: string[]; protocols: string[];
associateType?: string; associateType?: string;
} }
// 多模块关联 // 多模块关联
export interface ImportSystemData { export interface ImportSystemData {
API: ScenarioAssociateCaseParams; // 接口 API: ScenarioAssociateCaseParams; // 接口
CASE: ScenarioAssociateCaseParams; // 用例 CASE: ScenarioAssociateCaseParams; // 用例
SCENARIO: ScenarioAssociateCaseParams; // 场景 SCENARIO: ScenarioAssociateCaseParams; // 场景
} }
// 导入场景请求参数
export interface ImportScenarioRequest {
moduleId: string;
projectId: string;
type: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter;
coverData: boolean;
}
// 导入场景参数
export interface ImportScenarioParams {
file: File | null;
request: ImportScenarioRequest;
}
// 导出场景参数
export interface ExportScenarioParams extends BatchActionQueryParams {
apiScenarioId: string;
fileId: string;
}

View File

@ -0,0 +1,224 @@
<template>
<div>
<MsDrawer
v-model:visible="visible"
:width="960"
:title="t('apiScenario.importScenario')"
:closable="false"
:ok-disabled="!fileList.length"
:ok-text="t('common.import')"
:ok-loading="importLoading"
disabled-width-drag
desc
@confirm="confirmImport"
@cancel="cancelImport"
>
<div class="mb-[16px] flex items-center gap-[16px]">
<div
v-for="item of platformList"
:key="item.value"
:class="`import-item ${importForm.type === item.value ? 'import-item--active' : ''}`"
@click="() => setActiveImportFormat(item.value)"
>
<div class="text-[var(--color-text-1)]">{{ item.name }}</div>
</div>
</div>
<a-form ref="importFormRef" :model="importForm" layout="vertical">
<a-form-item :label="t('apiTestManagement.belongModule')">
<a-tree-select
v-model:modelValue="importForm.moduleId"
:data="innerModuleTree"
class="w-[436px]"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:draggable="false"
allow-search
allow-clear
:filter-tree-node="filterTreeNode"
:fallback-option="(key: string | number) => ({
name: t('apiTestManagement.moduleNotExist'),
key,
disabled: true,
})"
>
<template #tree-slot-title="node">
<a-tooltip :content="`${node.name}`" position="tl">
<div class="one-line-text w-[300px]">{{ node.name }}</div>
</a-tooltip>
</template>
</a-tree-select>
</a-form-item>
<a-form-item :label="t('apiTestManagement.importMode')">
<a-radio-group v-model:model-value="importForm.coverData">
<a-radio :value="true">
<div class="flex items-center gap-[2px]">
{{ t('apiTestManagement.cover') }}
<a-tooltip position="right" :content="t('apiScenario.importScenarioCoverTip')">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</a-radio>
<a-radio :value="false">
<div class="flex items-center gap-[2px]">
{{ t('apiTestManagement.uncover') }}
<a-tooltip position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('apiScenario.importScenarioUncoverTip1') }}</div>
<div>{{ t('apiScenario.importScenarioUncoverTip2') }}</div>
</template>
</a-tooltip>
</div>
</a-radio>
</a-radio-group>
</a-form-item>
<MsUpload
v-model:file-list="fileList"
:accept="fileAccept"
:auto-upload="false"
draggable
size-unit="MB"
class="w-full"
>
<template #subText>
<div class="flex">
{{ t('apiScenario.importScenarioUploadTip', { type: fileAccept, size: appStore.getFileMaxSize }) }}
</div>
</template>
</MsUpload>
</a-form>
</MsDrawer>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import { importScenario } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { filterTree, filterTreeNode, TreeNode } from '@/utils';
import { ImportScenarioRequest } from '@/models/apiTest/scenario';
import type { ModuleTreeNode } from '@/models/common';
import { RequestImportFormat } from '@/enums/apiEnum';
const props = defineProps<{
visible: boolean;
moduleTree: ModuleTreeNode[];
activeModule: string;
}>();
const emit = defineEmits(['update:visible', 'done']);
const { t } = useI18n();
const appStore = useAppStore();
const visible = useVModel(props, 'visible', emit);
const innerModuleTree = ref<TreeNode<ModuleTreeNode>[]>([]);
const platformList: { name: string; value: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter }[] = [
{
name: 'MeterSphere',
value: RequestImportFormat.MeterSphere,
},
{
name: 'Jmeter',
value: RequestImportFormat.Jmeter,
},
];
const fileList = ref<MsFileItem[]>([]);
const defaultForm: ImportScenarioRequest = {
moduleId: '',
coverData: false,
projectId: appStore.currentProjectId,
type: RequestImportFormat.MeterSphere,
};
const importForm = ref({ ...defaultForm });
const importFormRef = ref<FormInstance>();
const fileAccept = computed(() => {
return importForm.value.type === RequestImportFormat.MeterSphere ? 'json' : 'jmx';
});
watch(
() => visible.value,
(val) => {
if (val) {
importForm.value.moduleId = props.activeModule !== 'all' ? props.activeModule : '';
innerModuleTree.value = filterTree(props.moduleTree, (node) => node.type === 'MODULE');
}
},
{
immediate: true,
}
);
const importLoading = ref(false);
function setActiveImportFormat(format: RequestImportFormat.MeterSphere | RequestImportFormat.Jmeter) {
importForm.value.type = format;
}
function cancelImport() {
visible.value = false;
importForm.value = { ...defaultForm };
importFormRef.value?.resetFields();
fileList.value = [];
}
async function doImportScenario() {
try {
importLoading.value = true;
await importScenario({
file: fileList.value[0].file || null,
request: importForm.value,
});
Message.success(t('common.importSuccess'));
emit('done');
cancelImport();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
importLoading.value = false;
}
}
function confirmImport() {
importFormRef.value?.validate((errors) => {
if (!errors) {
doImportScenario();
}
});
}
</script>
<style lang="less" scoped>
.import-item {
@apply flex cursor-pointer items-center bg-white;
padding: 8px;
width: 200px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
gap: 6px;
}
.import-item--active {
border: 1px solid rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
}
:deep(.arco-form-item) {
margin-bottom: 16px;
}
:deep(.arco-select-view-value::after) {
@apply hidden;
}
</style>

View File

@ -1,24 +1,37 @@
<template> <template>
<div class="h-full"> <div class="h-full">
<div class="mb-[8px] flex items-center gap-[8px]"> <div class="mb-[8px] flex items-center gap-[8px]">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('apiScenario.tree.selectorPlaceholder')"
allow-clear
/>
<a-button <a-button
v-permission="['PROJECT_API_SCENARIO:READ+ADD']" v-permission="['PROJECT_API_SCENARIO:READ+ADD']"
type="primary" type="primary"
value="newScenario" long
@click=" @click="
() => { () => {
emit('newScenario'); emit('newScenario');
} }
" "
> >
{{ t('common.newCreate') }}</a-button {{ t('apiScenario.createScenario') }}
</a-button>
<a-button
v-permission="['PROJECT_API_SCENARIO:READ+ADD']"
type="outline"
long
@click="
() => {
emit('import');
}
"
> >
{{ t('apiScenario.importScenario') }}
</a-button>
</div> </div>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('apiScenario.tree.selectorPlaceholder')"
class="mb-[8px]"
allow-clear
/>
<div class="folder" @click="setActiveFolder('all')"> <div class="folder" @click="setActiveFolder('all')">
<div :class="allFolderClass"> <div :class="allFolderClass">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" /> <MsIcon type="icon-icon_folder_filled1" class="folder-icon" />

View File

@ -12,6 +12,7 @@
@init="handleModuleInit" @init="handleModuleInit"
@new-scenario="() => newTab()" @new-scenario="() => newTab()"
@change="handleModuleChange" @change="handleModuleChange"
@import="importDrawerVisible = true"
></scenarioModuleTree> ></scenarioModuleTree>
</div> </div>
<a-divider margin="0" /> <a-divider margin="0" />
@ -104,6 +105,13 @@
</template> </template>
</MsSplitBox> </MsSplitBox>
</MsCard> </MsCard>
<importScenario
v-model:visible="importDrawerVisible"
:module-tree="moduleTree"
:active-module="activeModule"
popup-container="#managementContainer"
@done="handleImportDone"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -123,6 +131,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue'; import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsEnvironmentSelect from '@/components/business/ms-environment-select/index.vue'; import MsEnvironmentSelect from '@/components/business/ms-environment-select/index.vue';
import importScenario from './components/import.vue';
import scenarioModuleTree from './components/scenarioModuleTree.vue'; import scenarioModuleTree from './components/scenarioModuleTree.vue';
import executeButton from '@/views/api-test/components/executeButton.vue'; import executeButton from '@/views/api-test/components/executeButton.vue';
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue'; import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
@ -717,6 +726,13 @@
} }
} }
const importDrawerVisible = ref(false);
async function handleImportDone() {
await scenarioModuleTreeRef.value?.refresh();
apiTableRef.value?.loadScenarioList();
}
const { registerCatchSaveShortcut, removeCatchSaveShortcut } = useShortcutSave(saveScenario); const { registerCatchSaveShortcut, removeCatchSaveShortcut } = useShortcutSave(saveScenario);
onBeforeMount(async () => { onBeforeMount(async () => {
selectRecycleCount(); selectRecycleCount();

View File

@ -3,6 +3,11 @@ export default {
'apiScenario.allScenario': 'All Scenarios', 'apiScenario.allScenario': 'All Scenarios',
'apiScenario.createScenario': 'Create Scenario', 'apiScenario.createScenario': 'Create Scenario',
'apiScenario.importScenario': 'Import Scenario', 'apiScenario.importScenario': 'Import Scenario',
'apiScenario.importScenarioCoverTip': 'If the same scene already exists in the system, it will be overwritten.',
'apiScenario.importScenarioUncoverTip1':
'1. If the same scene already exists in the system (with a unique name under the module), no changes will be made',
'apiScenario.importScenarioUncoverTip2': '2. If the scenario does not exist in the system, add',
'apiScenario.importScenarioUploadTip': '仅支持 json格式文件单个大小不超过 {size} MB',
'apiScenario.tree.selectorPlaceholder': 'Please enter module name', 'apiScenario.tree.selectorPlaceholder': 'Please enter module name',
'apiScenario.tree.folder.allScenario': 'All Scenarios', 'apiScenario.tree.folder.allScenario': 'All Scenarios',
'apiScenario.tree.noMatchModule': 'No matching module found', 'apiScenario.tree.noMatchModule': 'No matching module found',

View File

@ -3,6 +3,10 @@ export default {
'apiScenario.allScenario': '全部场景', 'apiScenario.allScenario': '全部场景',
'apiScenario.createScenario': '新建场景', 'apiScenario.createScenario': '新建场景',
'apiScenario.importScenario': '导入场景', 'apiScenario.importScenario': '导入场景',
'apiScenario.importScenarioCoverTip': '系统已存在同一场景则覆盖',
'apiScenario.importScenarioUncoverTip1': '1.系统已存在的同一场景(模块下名称唯一),则不做变更',
'apiScenario.importScenarioUncoverTip2': '2.系统不存在的场景,则新增',
'apiScenario.importScenarioUploadTip': '仅支持 {type} 格式文件,单个大小不超过 {size} MB',
'apiScenario.tree.selectorPlaceholder': '请输入模块名称进行搜索', 'apiScenario.tree.selectorPlaceholder': '请输入模块名称进行搜索',
'apiScenario.tree.folder.allScenario': '全部场景', 'apiScenario.tree.folder.allScenario': '全部场景',
'apiScenario.tree.noMatchModule': '暂无匹配的模块', 'apiScenario.tree.noMatchModule': '暂无匹配的模块',