feat(接口测试): 接口场景变更历史tab&执行历史tab

This commit is contained in:
WangXu10 2024-03-15 14:55:37 +08:00 committed by Craftsman
parent ce94dd62de
commit 5fb9b926e4
10 changed files with 496 additions and 6 deletions

View File

@ -6,10 +6,12 @@ import {
BatchMoveScenarioUrl,
BatchRecycleScenarioUrl,
DeleteModuleUrl,
ExecuteHistoryUrl,
GetModuleCountUrl,
GetModuleTreeUrl,
MoveModuleUrl,
RecycleScenarioUrl,
ScenarioHistoryUrl,
ScenarioPageUrl,
UpdateModuleUrl,
UpdateScenarioUrl,
@ -23,6 +25,10 @@ import {
ApiScenarioModuleUpdateParams,
ApiScenarioPageParams,
ApiScenarioUpdateDTO,
ExecuteHistoryItem,
ExecutePageParams,
ScenarioHistoryItem,
ScenarioHistoryPageParams,
} from '@/models/apiTest/scenario';
import { AddModuleParams, CommonList, ModuleTreeNode, MoveModules } from '@/models/common';
@ -101,3 +107,13 @@ export function batchOptionScenario(
export function batchEditScenario(params: ApiScenarioBatchEditParams) {
return MSR.post({ url: BatchEditScenarioUrl, params });
}
// 场景执行历史接口
export function getExecuteHistory(data: ExecutePageParams) {
return MSR.post<CommonList<ExecuteHistoryItem>>({ url: ExecuteHistoryUrl, data });
}
// 场景变更历史接口
export function getScenarioHistory(data: ScenarioHistoryPageParams) {
return MSR.post<CommonList<ScenarioHistoryItem>>({ url: ScenarioHistoryUrl, data });
}

View File

@ -33,3 +33,6 @@ export const BatchEditScenarioUrl = '/api/scenario/batch-operation/edit'; // 批
// export const GetDefinitionScheduleUrl = '/api/scenario/schedule/get'; // 接口场景-定时同步-查询
// export const DeleteDefinitionScheduleUrl = '/api/scenario/schedule/delete'; // 接口场景-定时同步-删除
// export const DebugDefinitionUrl = '/api/scenario/debug'; // 接口场景-调试
export const ExecuteHistoryUrl = '/api/scenario/execute/page'; // 场景执行历史
export const ScenarioHistoryUrl = '/api/scenario/operation-history/page'; // 场景变更历史

View File

@ -267,3 +267,14 @@ export enum ScenarioAddStepActionType {
SCRIPT_OPERATION = 'SCRIPT_OPERATION',
WAIT_TIME = 'WAIT_TIME',
}
// 接口场景-执行结果状态
export enum ExecuteStatusFilters {
PENDING = 'PENDING',
RUNNING = 'RUNNING',
RERUNNING = 'RERUNNING',
ERROR = 'ERROR',
SUCCESS = 'SUCCESS',
FAKE_ERROR = 'FAKE_ERROR',
STOPPED = 'STOPPED',
}

View File

@ -131,3 +131,43 @@ export interface ApiScenarioBatchEditParams extends BatchOptionParams {
export interface ApiScenarioBatchDeleteParams extends BatchApiParams {
deleteAll: boolean;
}
// 场景-执行历史-请求参数
export interface ExecutePageParams extends TableQueryParams {
id: string;
}
// 场景-执行历史-请求参数
export interface ExecuteHistoryItem {
id: string;
num: string;
name: string;
operationUser: string;
createUser: string;
startTime: number;
status: string;
triggerMode: string;
}
// 场景-变更历史列表查询参数
export interface ScenarioHistoryPageParams extends TableQueryParams {
projectId: string;
sourceId: string;
createUser: string;
types: string[];
modules: string[];
}
// 场景-变更历史列表项
export interface ScenarioHistoryItem {
id: number;
projectId: string;
createTime: number;
createUser: string;
sourceId: string;
type: string;
module: string;
refId: number;
createUserName: string;
versionName: string;
}

View File

@ -1,7 +1,96 @@
<template>
<div> changeHistory </div>
<div>
<a-alert v-if="isShowTip" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiScenario.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
</template>
</a-alert>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent"></ms-base-table>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getScenarioHistory } from '@/api/modules/api-test/scenario';
import { operationTypeOptions } from '@/config/common';
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const { t } = useI18n();
const isShowTip = ref<boolean>(true);
const visitedKey = 'scenarioHistoryTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const props = defineProps<{
sourceId: string | number;
}>();
const columns: MsTableColumn = [
{
title: 'apiScenario.changeOrder',
dataIndex: 'id',
width: 150,
},
{
title: 'apiScenario.type',
dataIndex: 'type',
slotName: 'type',
titleSlotName: 'typeFilter',
width: 150,
},
{
title: 'apiScenario.operationUser',
dataIndex: 'createUserName',
showTooltip: true,
width: 150,
},
{
title: 'apiScenario.updateTime',
dataIndex: 'updateTime',
showTooltip: true,
width: 180,
},
// {
// title: 'common.operation',
// slotName: 'action',
// dataIndex: 'operation',
// width: 50,
// },
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getScenarioHistory,
{
columns,
scroll: { x: '100%' },
selectable: false,
heightUsed: 374,
},
(item) => ({
...item,
type: t(operationTypeOptions.find((e) => e.value === item.type)?.label || ''),
updateTime: dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
function loadHistory() {
setLoadListParams({
projectId: appStore.currentProjectId,
sourceId: props.sourceId,
});
loadList();
}
onMounted(() => {
loadHistory();
});
</script>
<style lang="less" scoped></style>

View File

@ -1,7 +1,224 @@
<template>
<div> executeHistory </div>
<div>
<div class="mb-[16px] flex items-center justify-end">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.executeHistory.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadExecuteHistoryList"
@press-enter="loadExecuteHistoryList"
/>
</div>
<ms-base-table
v-bind="propsRes"
:first-column-width="44"
:secnario-id="props.scenarioId"
no-disable
filter-icon-align-left
v-on="propsEvent"
>
<template #num="{ record }">
<span type="text" class="px-0">{{ record.num }}</span>
</template>
<template #triggerModeFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="triggerModeFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<a-button type="text" class="arco-btn-text--secondary p-[8px_4px]" @click="triggerModeFilterVisible = true">
<div class="font-medium">
{{ t(columnConfig.title as string) }}
</div>
<icon-down :class="triggerModeFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</a-button>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="triggerModeListFilters" direction="vertical" size="small">
<a-checkbox v-for="(key, value) of TriggerModeLabel" :key="key" :value="value">
<div class="font-medium">{{ t(key) }}</div>
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #statusFilter="{ columnConfig }">
<a-trigger
v-model:popup-visible="statusFilterVisible"
trigger="click"
@popup-visible-change="handleFilterHidden"
>
<MsButton type="text" class="arco-btn-text--secondary ml-[10px]" @click="statusFilterVisible = true">
{{ t(columnConfig.title as string) }}
<icon-down :class="statusFilterVisible ? 'text-[rgb(var(--primary-5))]' : ''" />
</MsButton>
<template #content>
<div class="arco-table-filters-content">
<div class="flex items-center justify-center px-[6px] py-[2px]">
<a-checkbox-group v-model:model-value="statusFilters" direction="vertical" size="small">
<a-checkbox v-for="val of Object.values(ExecuteStatusFilters)" :key="val" :value="val">
<executeStatus :status="val" />
</a-checkbox>
</a-checkbox-group>
</div>
</div>
</template>
</a-trigger>
</template>
<template #triggerMode="{ record }">
<span>{{ t(TriggerModeLabel[record.triggerMode]) }}</span>
</template>
<template #status="{ record }">
<executeStatus :status="record.status" />
</template>
<template #operation="{ record }">
<MsButton class="!mr-0" @click="showResult(record)"
>{{ t('apiScenario.executeHistory.execution.operation') }}
</MsButton>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { 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 { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import ExecuteStatus from '@/views/api-test/scenario/components/executeStatus.vue';
import { getExecuteHistory } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ExecuteHistoryItem } from '@/models/apiTest/scenario';
import { ExecuteStatusFilters } from '@/enums/apiEnum';
import { TriggerModeLabel } from '@/enums/reportEnum';
const triggerModeListFilters = ref<string[]>(Object.keys(TriggerModeLabel));
const triggerModeFilterVisible = ref(false);
const statusFilterVisible = ref(false);
const statusFilters = ref(Object.keys(ExecuteStatusFilters));
const tableQueryParams = ref<any>();
const appStore = useAppStore();
const keyword = ref('');
const { t } = useI18n();
const props = defineProps<{
scenarioId: string; // id
readOnly?: boolean;
}>();
const columns: MsTableColumn = [
{
title: 'apiScenario.executeHistory.num',
dataIndex: 'id',
slotName: 'num',
fixed: 'left',
width: 100,
},
{
title: 'apiScenario.executeHistory.execution.triggerMode',
dataIndex: 'triggerMode',
slotName: 'triggerMode',
showTooltip: true,
titleSlotName: 'triggerModeFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiScenario.executeHistory.execution.status',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
},
{
title: 'apiScenario.executeHistory.execution.operator',
dataIndex: 'createUser',
slotName: 'operationUser',
showTooltip: true,
width: 150,
},
{
title: 'apiScenario.executeHistory.execution.operatorTime',
dataIndex: 'startTime',
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
},
{
title: 'common.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
showInTable: true,
showDrag: false,
width: 150,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getExecuteHistory,
{
columns,
scroll: { x: '100%' },
showSetting: false,
selectable: false,
heightUsed: 374,
},
(item) => ({
...item,
startTime: dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss'),
})
);
//
function loadExecuteHistoryList() {
const params = {
keyword: keyword.value,
id: props.scenarioId,
filter: {
triggerMode: triggerModeListFilters.value,
status: statusFilters.value,
},
};
setLoadListParams(params);
loadList();
tableQueryParams.value = {
...params,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
};
}
function handleFilterHidden(val: boolean) {
if (!val) {
loadExecuteHistoryList();
}
}
function showResult(record: ExecuteHistoryItem) {}
onBeforeMount(() => {
loadExecuteHistoryList();
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,68 @@
<template>
<MsTag :self-style="status.style" :size="props.size"> {{ status.text }}</MsTag>
</template>
<script setup lang="ts">
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteStatusFilters } from '@/enums/apiEnum';
const props = defineProps<{
status: ExecuteStatusFilters;
size?: Size;
}>();
const { t } = useI18n();
const statusMap = {
[ExecuteStatusFilters.PENDING]: {
bgColor: 'var(--color-text-n8)',
color: 'var(--color-text-4)',
text: 'apiScenario.executeHistory.status.pending',
},
[ExecuteStatusFilters.RUNNING]: {
bgColor: 'rgb(var(--link-2))',
color: 'rgb(var(--link-5))',
text: 'apiScenario.executeHistory.status.running',
},
[ExecuteStatusFilters.RERUNNING]: {
bgColor: 'rgb(var(--link-2))',
color: 'rgb(var(--link-6))',
text: 'apiScenario.executeHistory.status.rerunning',
},
[ExecuteStatusFilters.ERROR]: {
bgColor: 'rgb(var(--danger-2))',
color: 'rgb(var(--danger-5))',
text: 'apiScenario.executeHistory.status.error',
},
[ExecuteStatusFilters.SUCCESS]: {
bgColor: 'rgb(var(--success-2))',
color: 'rgb(var(--success-5))',
text: 'apiScenario.executeHistory.status.success',
},
[ExecuteStatusFilters.FAKE_ERROR]: {
bgColor: 'rgb(var(--warning-2))',
color: 'rgb(var(--warning-5))',
text: 'apiScenario.executeHistory.status.fake.error',
},
[ExecuteStatusFilters.STOPPED]: {
bgColor: 'rgb(var(--link-2))',
color: 'rgb(var(--color-border-2))',
text: 'apiScenario.executeHistory.status.fake.stopped',
},
};
const status = computed(() => {
const config = statusMap[props.status];
return {
style: {
backgroundColor: config?.bgColor,
color: config?.color,
},
text: t(config?.text),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -72,14 +72,17 @@
:title="t('apiScenario.executeHistory')"
class="px-[24px] py-[16px]"
>
<executeHistory v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY" />
<executeHistory
v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY"
:scenario-id="previewDetail.id"
/>
</a-tab-pane>
<a-tab-pane
:key="ScenarioDetailComposition.CHANGE_HISTORY"
:title="t('apiScenario.changeHistory')"
class="px-[24px] py-[16px]"
>
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" />
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" :source-id="previewDetail.id" />
</a-tab-pane>
<a-tab-pane
:key="ScenarioDetailComposition.DEPENDENCY"

View File

@ -50,4 +50,26 @@ export default {
// 批量操作文案
'api_scenario.batch_operation.success': 'Success {opt} to {name}',
'api_scenario.table.batchMoveConfirm': 'Ready to {opt} {count} scenarios',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name',
'apiScenario.executeHistory.num': 'Number',
'apiScenario.executeHistory.execution.triggerMode': 'Trigger mode',
'apiScenario.executeHistory.execution.status': 'Execution result',
'apiScenario.executeHistory.execution.operator': 'Operator',
'apiScenario.executeHistory.execution.operatorTime': 'Operation time',
'apiScenario.executeHistory.execution.operation': 'Execution result',
'apiScenario.executeHistory.status.pending': 'Pending',
'apiScenario.executeHistory.status.running': 'Running',
'apiScenario.executeHistory.status.rerunning': 'Rerunning',
'apiScenario.executeHistory.status.error': 'Error',
'apiScenario.executeHistory.status.success': 'Success',
'apiScenario.executeHistory.status.fake.error': 'Fake error',
'apiScenario.executeHistory.status.fake.stopped': 'Stopped',
// 操作历史
'apiScenario.historyListTip':
'View and compare historical changes. According to the rules set by the administrator, the change history data will be automatically deleted.',
'apiScenario.changeOrder': 'Change serial number',
'apiScenario.type': 'Type',
'apiScenario.operationUser': 'Operator',
'apiScenario.updateTime': 'Update time',
};

View File

@ -98,4 +98,25 @@ export default {
'apiScenario.case': '用例',
'apiScenario.scenario': '场景',
'apiScenario.sumSelected': '共选择',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号',
'apiScenario.executeHistory.execution.triggerMode': '执行方式',
'apiScenario.executeHistory.execution.status': '执行结果',
'apiScenario.executeHistory.execution.operator': '操作人',
'apiScenario.executeHistory.execution.operatorTime': '操作时间',
'apiScenario.executeHistory.execution.operation': '执行结果',
'apiScenario.executeHistory.status.pending': '排队中',
'apiScenario.executeHistory.status.running': '执行中',
'apiScenario.executeHistory.status.rerunning': '重跑中',
'apiScenario.executeHistory.status.error': '失败',
'apiScenario.executeHistory.status.success': '成功',
'apiScenario.executeHistory.status.fake.error': '误报',
'apiScenario.executeHistory.status.fake.stopped': '停止',
// 操作历史
'apiScenario.historyListTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'apiScenario.changeOrder': '变更序号',
'apiScenario.type': '类型',
'apiScenario.operationUser': '操作人',
'apiScenario.updateTime': '更新时间',
};