feat(性能测试): 性能测试保存时根据配置来发送脚本审核的通知

--story=1012024 --user=宋天阳 接口脚本增加预警审核机制
https://www.tapd.cn/55049933/s/1371891
This commit is contained in:
song-tianyang 2023-05-16 16:02:43 +08:00 committed by fit2-zhao
parent 402ce846cf
commit 3222ea22b2
17 changed files with 1103 additions and 427 deletions

View File

@ -3,41 +3,72 @@
<div class="ms-header-menu align-right">
<el-tooltip effect="light">
<template v-slot:content>
<span>{{ $t('commons.notice_center') }}</span>
<span>{{ $t("commons.notice_center") }}</span>
</template>
<div @click="showNoticeCenter" v-if="noticeCount > 0 || noticeShow">
<el-badge is-dot class="item" type="danger">
<font-awesome-icon class="icon global focusing" :icon="['fas', 'bell']"/>
<font-awesome-icon
class="icon global focusing"
:icon="['fas', 'bell']"
/>
</el-badge>
</div>
<font-awesome-icon @click="showNoticeCenter" class="icon global focusing" :icon="['fas', 'bell']" v-else/>
<font-awesome-icon
@click="showNoticeCenter"
class="icon global focusing"
:icon="['fas', 'bell']"
v-else
/>
</el-tooltip>
</div>
<el-drawer :visible.sync="taskVisible" :destroy-on-close="true" direction="rtl"
:withHeader="true" :modal="false" :title="$t('commons.notice_center')" size="550px"
custom-class="ms-drawer-task">
<el-drawer
:visible.sync="taskVisible"
:destroy-on-close="true"
direction="rtl"
:withHeader="true"
:modal="false"
:title="$t('commons.notice_center')"
size="550px"
custom-class="ms-drawer-task"
>
<div style="margin: 0 20px 0">
<el-tabs :active-name="activeName">
<el-tab-pane :label="$t('commons.mentioned_me_notice')" name="mentionedMe">
<notification-data ref="mentionedMe" :user-list="userList" type="MENTIONED_ME"/>
<el-tab-pane
:label="$t('commons.mentioned_me_notice')"
name="mentionedMe"
>
<notification-data
ref="mentionedMe"
:user-list="userList"
type="MENTIONED_ME"
/>
</el-tab-pane>
<el-tab-pane :label="$t('commons.system_notice')" name="systemNotice">
<notification-data ref="systemNotice" :user-list="userList" type="SYSTEM_NOTICE"/>
<notification-data
ref="systemNotice"
:user-list="userList"
type="SYSTEM_NOTICE"
/>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
</div>
</template>
<script>
import MsDrawer from "../MsDrawer";
import MsTipButton from "../MsTipButton";
import {getOperation, getResource} from "./util";
import { getOperation, getResource } from "./util";
import NotificationData from "./components/NotificationData";
import {getWsMemberList, initNoticeSocket, read, readAll, searchNotifications} from "../../api/notification";
import {
getWsMemberList,
initNoticeSocket,
read,
readAll,
searchNotifications,
} from "../../api/notification";
export default {
name: "MsNotification",
@ -46,9 +77,7 @@ export default {
MsTipButton,
MsDrawer,
},
inject: [
'reload'
],
inject: ["reload"],
data() {
return {
noticeCount: 0,
@ -63,7 +92,7 @@ export default {
userList: [],
userMap: {},
websocket: Object,
activeName: 'mentionedMe',
activeName: "mentionedMe",
pageSize: 20,
goPage: 1,
totalPage: 0,
@ -83,18 +112,17 @@ export default {
if (!v) {
this.close();
}
}
},
},
methods: {
getUserList() {
getWsMemberList()
.then(response => {
this.userList = response.data;
this.userMap = this.userList.reduce((r, c) => {
r[c.id] = c;
return r;
}, {});
});
getWsMemberList().then((response) => {
this.userList = response.data;
this.userMap = this.userList.reduce((r, c) => {
r[c.id] = c;
return r;
}, {});
});
},
initWebSocket() {
this.websocket = initNoticeSocket();
@ -103,10 +131,9 @@ export default {
this.websocket.onerror = this.onError;
this.websocket.onclose = this.onClose;
},
onOpen() {
},
onOpen() {},
onError(e) {
console.warn('socket error: ', e)
console.warn("socket error: ", e);
},
onMessage(e) {
let m = JSON.parse(e.data);
@ -121,8 +148,7 @@ export default {
this.$refs.mentionedMe.init();
}
},
onClose(e) {
},
onClose(e) {},
showNoticeCenter() {
this.noticeCount = 0;
this.readAll();
@ -138,43 +164,61 @@ export default {
this.initIndex = 0;
},
readAll() {
readAll()
readAll();
this.noticeShow = false;
},
getNotifications() {
this.initWebSocket();
},
showNotification() {
searchNotifications({}, 1, 10)
.then(response => {
let data = response.data.listObject;
let now = this.serverTime;
data.filter(d => d.status === 'UNREAD').forEach(d => {
searchNotifications({}, 1, 10).then((response) => {
let data = response.data.listObject;
let now = this.serverTime;
data
.filter((d) => d.status === "UNREAD")
.forEach((d) => {
if (now - d.createTime > 10 * 1000) {
return;
}
d.user = this.userMap[d.operator] || {name: 'MS'};
let message = d.user.name + getOperation(d.operation) + getResource(d) + ": " + d.resourceName;
let title = d.type === 'MENTIONED_ME' ? this.$t('commons.mentioned_me_notice') : this.$t('commons.system_notice');
d.user = this.userMap[d.operator] || { name: "MS" };
let message = "";
if (d.operation === "REVIEW") {
message =
getResource(d) +
"" +
d.resourceName +
" " +
this.$t("commons.contains_script_review");
} else {
message =
d.user.name +
getOperation(d.operation) +
getResource(d) +
": " +
d.resourceName;
}
let title =
d.type === "MENTIONED_ME"
? this.$t("commons.mentioned_me_notice")
: this.$t("commons.system_notice");
setTimeout(() => {
this.$notify({
title: title,
type: 'info',
type: "info",
message: message,
});
//
read(d.id)
read(d.id);
this.noticeShow = true;
});
});
});
}
}
});
},
},
};
</script>
<style scoped>
.el-icon-check {
color: #44b349;
margin-left: 10px;
@ -197,8 +241,8 @@ export default {
:deep(.el-drawer__header) {
font-size: 18px;
color: #0a0a0a;
border-bottom: 1px solid #E6E6E6;
background-color: #FFF;
border-bottom: 1px solid #e6e6e6;
background-color: #fff;
margin-bottom: 10px;
padding: 10px;
}
@ -238,7 +282,6 @@ export default {
border-color: #783887;
}
:deep(.el-menu-item) {
padding-left: 0;
padding-right: 0;
@ -259,11 +302,11 @@ export default {
}
.ms-task-error {
color: #F56C6C;
color: #f56c6c;
}
.ms-task-success {
color: #67C23A;
color: #67c23a;
}
.ms-header-menu {
@ -275,5 +318,4 @@ export default {
cursor: pointer;
border-color: var(--color);
}
</style>

View File

@ -1,31 +1,73 @@
<template>
<div>
<div style="padding-bottom: 5px; width: 100%; height: 50px;">
<div style="float:right;">
<span style="color: gray; padding-right: 10px">({{ totalCount }} {{ $t('commons.notice_count') }})</span>
<div style="padding-bottom: 5px; width: 100%; height: 50px">
<div style="float: right">
<span style="color: gray; padding-right: 10px"
>({{ totalCount }} {{ $t("commons.notice_count") }})</span
>
<el-dropdown @command="handleCommand" style="padding-right: 10px">
<span class="el-dropdown-link" v-if="totalPage > 0">
{{ goPage }}/{{ totalPage }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<span class="el-dropdown-link" v-if="totalPage > 0">
{{ goPage }}/{{ totalPage
}}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<span v-else class="el-dropdown-link">0/0</span>
<el-dropdown-menu slot="dropdown">
<div class="dropdown-content">
<el-dropdown-item v-for="i in totalPage" :key="i" :command="i">{{ i }}</el-dropdown-item>
<el-dropdown-item v-for="i in totalPage" :key="i" :command="i"
>{{ i }}
</el-dropdown-item>
</div>
</el-dropdown-menu>
</el-dropdown>
<el-button icon="el-icon-arrow-left" size="mini" :disabled="goPage === 1" @click="prevPage"/>
<el-button icon="el-icon-arrow-right" size="mini" :disabled="goPage === totalPage" @click="nextPage"/>
<el-button
icon="el-icon-arrow-left"
size="mini"
:disabled="goPage === 1"
@click="prevPage"
/>
<el-button
icon="el-icon-arrow-right"
size="mini"
:disabled="goPage === totalPage"
@click="nextPage"
/>
</div>
</div>
<div class="report-container">
<el-table :data="systemNoticeData"
:show-header="false"
:highlight-current-row="true"
style="width: 100%">
<el-table
:data="systemNoticeData"
:show-header="false"
:highlight-current-row="true"
style="width: 100%"
>
<el-table-column prop="content" :label="$t('commons.name')">
<template v-slot="{row}">
<el-row type="flex" align="start" class="current-user">
<template v-slot="{ row }">
<!--评审信息的格式与其余的不一样-->
<el-row
v-if="isReviewNotice(row)"
type="flex"
align="start"
class="current-user"
>
<el-col :span="2">
<div class="icon-title">
{{ row.resourceName.substring(0, 1) }}
</div>
</el-col>
<el-col :span="22">
<span class="operation">
<span>{{ getResource(row) }}:</span>
<span
@click="clickResource(row)"
style="color: #783887; cursor: pointer"
>
{{ row.resourceName }}
</span>
<span> {{ $t("commons.contains_script_review") }} </span>
</span>
</el-col>
</el-row>
<el-row v-else type="flex" align="start" class="current-user">
<el-col :span="2">
<div class="icon-title">
{{ row.user.name.substring(0, 1) }}
@ -33,9 +75,13 @@
</el-col>
<el-col :span="22">
<span class="username">{{ row.user.name }}</span>
<span class="operation">{{ getOperation(row.operation) }}{{ getResource(row) }}:
<span v-if="row.resourceId && row.operation.indexOf('DELETE') < 0" @click="clickResource(row)"
style="color: #783887; cursor: pointer;">
<span class="operation"
>{{ getOperation(row.operation) }}{{ getResource(row) }}:
<span
v-if="row.resourceId && row.operation.indexOf('DELETE') < 0"
@click="clickResource(row)"
style="color: #783887; cursor: pointer"
>
{{ row.resourceName }}
</span>
<span v-else>{{ row.resourceName }}</span>
@ -43,26 +89,29 @@
</el-col>
</el-row>
<el-row type="flex" justify="space-between">
<el-col :span="12">
</el-col>
<el-col :span="12"></el-col>
<el-col :span="6">
<span class="time-style">{{ row.createTime | datetimeFormat }}</span>
<span class="time-style">{{
row.createTime | datetimeFormat
}}</span>
</el-col>
</el-row>
</template>
</el-table-column>
</el-table>
</div>
<div style="color: gray; padding-top:20px; text-align: center">
- {{ $t('commons.notice_tips') }} -
<div style="color: gray; padding-top: 20px; text-align: center">
- {{ $t("commons.notice_tips") }} -
</div>
</div>
</template>
<script>
import {getOperation, getResource, getUrl} from "../util";
import {searchNotifications, updateUserByResourceId} from "../../../api/notification";
import { getOperation, getResource, getUrl } from "../util";
import {
searchNotifications,
updateUserByResourceId,
} from "../../../api/notification";
export default {
name: "NotificationData",
@ -73,7 +122,7 @@ export default {
goPage: 1,
totalPage: 0,
totalCount: 0,
userMap: {}
userMap: {},
};
},
props: {
@ -95,17 +144,18 @@ export default {
this.init();
},
init() {
let param = {type: this.type};
this.result = searchNotifications(param, this.goPage, this.pageSize)
.then(response => {
let param = { type: this.type };
this.result = searchNotifications(param, this.goPage, this.pageSize).then(
(response) => {
this.systemNoticeData = response.data.listObject;
this.totalPage = response.data.pageCount;
this.totalCount = response.data.itemCount;
this.systemNoticeData.forEach(n => {
n.user = this.userMap[n.operator] || {name: "MS"};
this.systemNoticeData.forEach((n) => {
n.user = this.userMap[n.operator] || { name: "MS" };
});
});
}
);
},
prevPage() {
if (this.goPage < 1) {
@ -130,10 +180,9 @@ export default {
}
let uri = getUrl(resource);
updateUserByResourceId(resourceId)
.then(() => {
this.toPage(uri);
});
updateUserByResourceId(resourceId).then(() => {
this.toPage(uri);
});
},
toPage(uri) {
let id = "new_a";
@ -146,8 +195,11 @@ export default {
let element = document.getElementById(id);
element.parentNode.removeChild(element);
}
}
},
isReviewNotice(row) {
return row.operation === "REVIEW";
},
},
};
</script>

View File

@ -395,6 +395,7 @@ const message = {
},
reviewer: "Reviewer",
append_reviewer: "Append Reviewer",
contains_script_review: "has script stepreview it please.",
report_statistics: {
reserved: "Reserved",
menu: {
@ -520,10 +521,10 @@ const message = {
ui_module: "default",
},
other: "Other",
function_introduction: 'Function introduction',
page_guidance: 'Page guidance',
novice_journey: 'Novice Journey',
minder_operation: 'Minder Operation'
function_introduction: "Function introduction",
page_guidance: "Page guidance",
novice_journey: "Novice Journey",
minder_operation: "Minder Operation",
},
login: {
normal_Login: "Normal Login",
@ -1381,7 +1382,8 @@ const message = {
title: "Upload jar package",
jar_file: "Jar Package",
jar_manage: "JAR package management",
delete_tip: "Deleting the plug-in requires restarting the service to take effect",
delete_tip:
"Deleting the plug-in requires restarting the service to take effect",
delete_confirm: "Confirm to delete the plugin",
file_exist: "The name already exists in the project",
upload_limit_size: "Upload file size cannot exceed 30MB!",
@ -3539,56 +3541,56 @@ const message = {
},
shepherd: {
step1: {
title: 'A Workspaces and Projects',
text: 'MeterSphere uses [workspace] and [project] to isolate test data, and you can switch between workspace and project in the top menu.'
title: "A Workspaces and Projects",
text: "MeterSphere uses [workspace] and [project] to isolate test data, and you can switch between workspace and project in the top menu.",
},
step2: {
title: 'Side navigation menu',
text: 'The navigation menu shows which function module you are in.'
title: "Side navigation menu",
text: "The navigation menu shows which function module you are in.",
},
step3: {
title: 'One workspace holds multiple projects',
text: 'A "project" is a collection of use cases and members. Various types of tests on MeterSphere are viewed and managed through projects.'
title: "One workspace holds multiple projects",
text: 'A "project" is a collection of use cases and members. Various types of tests on MeterSphere are viewed and managed through projects.',
},
step4: {
title: 'Top function menu',
text: 'The topl function menu supports switching subdivision functions under the current first-class module.'
title: "Top function menu",
text: "The topl function menu supports switching subdivision functions under the current first-class module.",
},
step5: {
title: "Where are you?",
text: "Now, that you have joined a workspace and become a member of the current project, start your testing journey from here."
text: "Now, that you have joined a workspace and become a member of the current project, start your testing journey from here.",
},
exit:'skip',
next: 'Next',
know:'know',
exit: "skip",
next: "Next",
know: "know",
},
guide: {
home: {
title: 'Welcome to MeterSphere!',
desc: 'A quickstart guide to see what MeterSphere can do for you.',
button: 'Lets get started'
title: "Welcome to MeterSphere!",
desc: "A quickstart guide to see what MeterSphere can do for you.",
button: "Lets get started",
},
test: {
title: 'Test cases are the cornerstone of testing',
desc: '<span>Maintain your test cases through online editing/file import/URL synchronization/multi-person review,</span><br><span>add them to your test plan,quantitatively manage test progress, record results, synchronize issues, </span><br><span>retain/share test reports, and cover the entire software testing life cycle.</span>',
button: 'Next: Interface Test'
title: "Test cases are the cornerstone of testing",
desc: "<span>Maintain your test cases through online editing/file import/URL synchronization/multi-person review,</span><br><span>add them to your test plan,quantitatively manage test progress, record results, synchronize issues, </span><br><span>retain/share test reports, and cover the entire software testing life cycle.</span>",
button: "Next: Interface Test",
},
api: {
title: 'Simulate real scenarios to automate API testing',
desc: '<span>API testing is triggered by manual/scheduled tasks/plug-ins, supporting multiple communication protocols; </span><br><span>scenario case-sets are arranged based on real business processes, </span><br><span>and multi-type controllers/custom scripts/assertions are supported to meet various user needs.</span>',
button: 'Next: UI Test'
title: "Simulate real scenarios to automate API testing",
desc: "<span>API testing is triggered by manual/scheduled tasks/plug-ins, supporting multiple communication protocols; </span><br><span>scenario case-sets are arranged based on real business processes, </span><br><span>and multi-type controllers/custom scripts/assertions are supported to meet various user needs.</span>",
button: "Next: UI Test",
},
ui: {
title: 'Portable UI element library and command set',
title: "Portable UI element library and command set",
desc: '<span>Arrange scenario cases based on reusable element library and commands;</span><br><span>combine your commonly used test steps into new command, which can be flexibly called in automation scenarios.</span><br><span style="background-color: #ffffff;color:#ffffff">/</span>',
button: 'Next: Performance Test'
button: "Next: Performance Test",
},
performance: {
title: 'One-click launch performance testing',
desc: '<span>Provides a distributed performance testing solution, supporting multiple types of testing resource pools </span><br><span>such as physical machines/virtual machines/k8s container clusters; </span><br><span>One-click to initiate API scenario case to performance testing, and view real-time reports;</span><br><span>Provides report comparison under different configurations to control performance bottlenecks and optimize them.</span>',
button: 'To create your first test case'
title: "One-click launch performance testing",
desc: "<span>Provides a distributed performance testing solution, supporting multiple types of testing resource pools </span><br><span>such as physical machines/virtual machines/k8s container clusters; </span><br><span>One-click to initiate API scenario case to performance testing, and view real-time reports;</span><br><span>Provides report comparison under different configurations to control performance bottlenecks and optimize them.</span>",
button: "To create your first test case",
},
go_prev: 'Return to previous'
go_prev: "Return to previous",
},
side_task: {
test_tracking: {
@ -3638,7 +3640,7 @@ const message = {
subtitle: "You have completed all the novice journey, full of energy",
desc: "If you want to continue to learn about advanced tutorials, please follow our technical blog and live channel",
blog_url: "Technical Blog",
live_url: "Live Channel"
live_url: "Live Channel",
},
close: {
title: "Close the novice journey",
@ -3649,8 +3651,8 @@ const message = {
},
save_success: "Closed successfully",
to_try: "Go to try it",
view_video: "View video tutorial"
}
view_video: "View video tutorial",
},
};
export default {

View File

@ -387,6 +387,7 @@ const message = {
},
reviewer: "评审人",
append_reviewer: "追加评审人",
contains_script_review: "包含脚本步骤,请审核",
report_statistics: {
reserved: "预留模块敬请期待",
menu: {
@ -512,10 +513,10 @@ const message = {
template_delete: "模版删除",
scope: "应用场景",
other: "其他",
function_introduction: '功能介绍',
page_guidance: '页面指引',
novice_journey: '新手旅程',
minder_operation: '脑图操作'
function_introduction: "功能介绍",
page_guidance: "页面指引",
novice_journey: "新手旅程",
minder_operation: "脑图操作",
},
login: {
normal_Login: "普通登录",
@ -3413,56 +3414,56 @@ const message = {
},
shepherd: {
step1: {
title: '工作空间和项目',
text: 'MeterSphere 使用 [工作空间] 和 [项目] 来隔离测试数据, 你可以在顶部菜单进行工作空间和项目切换。'
title: "工作空间和项目",
text: "MeterSphere 使用 [工作空间] 和 [项目] 来隔离测试数据, 你可以在顶部菜单进行工作空间和项目切换。",
},
step2: {
title: '功能主菜单',
text: '主菜单显示你所在的功能模块。'
title: "功能主菜单",
text: "主菜单显示你所在的功能模块。",
},
step3: {
title: '一个空间可以创建多个项目',
text: '「项目」是一组用例和成员的集合。MeterSphere 上各种类型的测试均通过项目进行分权分域查看和管理。'
title: "一个空间可以创建多个项目",
text: "「项目」是一组用例和成员的集合。MeterSphere 上各种类型的测试均通过项目进行分权分域查看和管理。",
},
step4: {
title: '一级功能菜单',
text: '顶部一级功能菜单栏,支持在当前功能模块下切换细分功能。'
title: "一级功能菜单",
text: "顶部一级功能菜单栏,支持在当前功能模块下切换细分功能。",
},
step5: {
title: "你在哪?",
text: "现在,你已加入了一个工作空间并成为当前项目的一员,就从这里开始你的测试之旅吧。"
text: "现在,你已加入了一个工作空间并成为当前项目的一员,就从这里开始你的测试之旅吧。",
},
exit:'跳过',
next:'下一步',
know:'知道啦',
exit: "跳过",
next: "下一步",
know: "知道啦",
},
guide: {
home: {
title: '欢迎来到 MeterSphere',
desc: '通过一个快捷指引来了解 MeterSphere 究竟能为你做哪些事。',
button: '让我们开始吧'
title: "欢迎来到 MeterSphere",
desc: "通过一个快捷指引来了解 MeterSphere 究竟能为你做哪些事。",
button: "让我们开始吧",
},
test: {
title: '测试用例是测试任务的基石',
desc: '<span>使用在线编辑/文件导入/URL同步/多人评审的方式维护你的用例,</span><br><span>将它们加入你的测试计划中,量化管理测试进度,记录结果,同步缺陷,</span><br><span>留存/分享测试报告,覆盖整个测试生命周期。</span>',
button: '下一个:接口测试'
title: "测试用例是测试任务的基石",
desc: "<span>使用在线编辑/文件导入/URL同步/多人评审的方式维护你的用例,</span><br><span>将它们加入你的测试计划中,量化管理测试进度,记录结果,同步缺陷,</span><br><span>留存/分享测试报告,覆盖整个测试生命周期。</span>",
button: "下一个:接口测试",
},
api: {
title: '模拟真实场景 让接口自动化',
desc: '<span>通过手动/定时任务/插件触发接口测试,支持多种通信协议;</span><br><span>基于真实业务流程编排场景化用例集,支持添加多类型控制器/自定义脚本/断言,</span><br><span>满足各种复杂场景所需。</span>',
button: '下一个UI测试'
title: "模拟真实场景 让接口自动化",
desc: "<span>通过手动/定时任务/插件触发接口测试,支持多种通信协议;</span><br><span>基于真实业务流程编排场景化用例集,支持添加多类型控制器/自定义脚本/断言,</span><br><span>满足各种复杂场景所需。</span>",
button: "下一个UI测试",
},
ui: {
title: '可移植的 UI 元素库与指令集',
title: "可移植的 UI 元素库与指令集",
desc: '<span>基于可复用的元素库及指令快速编排场景化用例;</span><br><span>将你常用的测试步骤组合成新的自定义指令,在自动化场景中灵活调用。</span><br><span style="background-color: #ffffff;color:#ffffff">/</span>',
button: '下一个:性能测试'
button: "下一个:性能测试",
},
performance: {
title: '性能测试 一键就可以',
desc: '<span>提供分布式压测解决方案,支持物理机/虚拟机/k8s容器集群等多类型压测资源池</span><br><span>使用接口测试转性能一键发起,实时查看报告;</span><br><span>提供差异配置下的报告对比,掌控性能瓶颈及调优。</span>',
button: '完成!去创建你的第 1 条测试用例'
title: "性能测试 一键就可以",
desc: "<span>提供分布式压测解决方案,支持物理机/虚拟机/k8s容器集群等多类型压测资源池</span><br><span>使用接口测试转性能一键发起,实时查看报告;</span><br><span>提供差异配置下的报告对比,掌控性能瓶颈及调优。</span>",
button: "完成!去创建你的第 1 条测试用例",
},
go_prev: '返回上一个'
go_prev: "返回上一个",
},
side_task: {
test_tracking: {
@ -3512,7 +3513,7 @@ const message = {
subtitle: "你已完成全部新手旅程 能量满载~",
desc: "想继续了解进阶教程,请关注我们的技术博客和直播",
blog_url: "博客地址",
live_url: "直播间地址"
live_url: "直播间地址",
},
close: {
title: "关闭新手旅程",
@ -3523,8 +3524,8 @@ const message = {
},
save_success: "关闭成功",
to_try: "去完成",
view_video: "观看视频教程"
}
view_video: "观看视频教程",
},
};
export default {

View File

@ -387,6 +387,7 @@ const message = {
},
reviewer: "評審人",
append_reviewer: "追加評審人",
contains_script_review: "包含腳本步驟,請審核",
report_statistics: {
reserved: "預留模塊敬請期待",
menu: {
@ -511,10 +512,10 @@ const message = {
},
template_delete: "模版刪除",
other: "其他",
function_introduction: '功能介紹',
page_guidance: '頁面指引',
novice_journey: '新手旅程',
minder_operation: '腦圖操作'
function_introduction: "功能介紹",
page_guidance: "頁面指引",
novice_journey: "新手旅程",
minder_operation: "腦圖操作",
},
login: {
normal_Login: "普通登錄",
@ -3410,56 +3411,56 @@ const message = {
},
shepherd: {
step1: {
title: '工作空間和項目',
text: 'MeterSphere 使用 [工作空間] 和 [項目] 來隔離測試數據, 你可以在頂部菜單進行工作空間和項目切換。'
title: "工作空間和項目",
text: "MeterSphere 使用 [工作空間] 和 [項目] 來隔離測試數據, 你可以在頂部菜單進行工作空間和項目切換。",
},
step2: {
title: '功能主菜單',
text: '主菜單顯示您所在的功能模塊。'
title: "功能主菜單",
text: "主菜單顯示您所在的功能模塊。",
},
step3: {
title: '一個空間可以創建多個項目',
text: '「項目」是一組用例和成員的集合。 MeterSphere 上各種類型的測試均通過項目進行分權分域查看和管理。'
title: "一個空間可以創建多個項目",
text: "「項目」是一組用例和成員的集合。 MeterSphere 上各種類型的測試均通過項目進行分權分域查看和管理。",
},
step4: {
title: '一級功能菜單',
text: '頂部一級功能菜單欄,支持在當前功能模塊下切換細分功能。'
title: "一級功能菜單",
text: "頂部一級功能菜單欄,支持在當前功能模塊下切換細分功能。",
},
step5: {
title: "你在哪?",
text: "現在,你已加入了一個工作空間並成為當前項目的一員,就從這裡開始你的測試之旅吧。"
text: "現在,你已加入了一個工作空間並成為當前項目的一員,就從這裡開始你的測試之旅吧。",
},
exit:'跳過',
next:'下一步',
know:'知道啦',
exit: "跳過",
next: "下一步",
know: "知道啦",
},
guide: {
home: {
title: '歡迎來到 MeterSphere',
desc: '通過一個快捷指引來了解 MeterSphere 究竟能為你做哪些事。',
button: '讓我們開始吧'
title: "歡迎來到 MeterSphere",
desc: "通過一個快捷指引來了解 MeterSphere 究竟能為你做哪些事。",
button: "讓我們開始吧",
},
test: {
title: '測試用例是測試任務的基石',
desc: '<span>使用在線編輯/文件導入/URL同步/多人評審的方式維護你的用例,</span><br><span>將它們加入你的測試計劃中,量化管理測試進度,記錄結果,同步缺陷,</span><br><span>留存/分享測試報告,覆蓋整個測試生命週期。</span>',
button: '下一個:接口測試'
title: "測試用例是測試任務的基石",
desc: "<span>使用在線編輯/文件導入/URL同步/多人評審的方式維護你的用例,</span><br><span>將它們加入你的測試計劃中,量化管理測試進度,記錄結果,同步缺陷,</span><br><span>留存/分享測試報告,覆蓋整個測試生命週期。</span>",
button: "下一個:接口測試",
},
api: {
title: '模擬真實場景 讓接口自動化',
desc: '<span>通過手動/定時任務/插件觸發接口測試,支持多種通信協議;</span><br><span> 基於真實業務流程編排場景化用例集,支持添加多類型控制器/自定義腳本/斷言,</span><br><span>滿足各種複雜場景所需。</span>',
button: '下一個UI測試'
title: "模擬真實場景 讓接口自動化",
desc: "<span>通過手動/定時任務/插件觸發接口測試,支持多種通信協議;</span><br><span> 基於真實業務流程編排場景化用例集,支持添加多類型控制器/自定義腳本/斷言,</span><br><span>滿足各種複雜場景所需。</span>",
button: "下一個UI測試",
},
ui: {
title: '可移植的 UI 元素庫與指令集',
title: "可移植的 UI 元素庫與指令集",
desc: '<span>基於可複用的元素庫及指令快速編排場景化用例;</span><br><span>將你常用的測試步驟組合成新的自定義指令,在自動化場景中靈活調用。</span><br><span style="background-color: #ffffff;color:#ffffff">/</span>',
button: '下一個:性能測試'
button: "下一個:性能測試",
},
performance: {
title: '性能測試 一鍵就可以',
desc: '<span>提供分佈式壓測解決方案,支持物理機/虛擬機/k8s容器集群等多類型壓測資源池</span><br><span>使用接口測試轉性能一鍵發起,實時查看報告;</span><br><span>提供差異配置下的報告對比,掌控性能瓶頸及調優。</span>',
button: '完成!去創建你的第 1 條測試用例'
title: "性能測試 一鍵就可以",
desc: "<span>提供分佈式壓測解決方案,支持物理機/虛擬機/k8s容器集群等多類型壓測資源池</span><br><span>使用接口測試轉性能一鍵發起,實時查看報告;</span><br><span>提供差異配置下的報告對比,掌控性能瓶頸及調優。</span>",
button: "完成!去創建你的第 1 條測試用例",
},
go_prev: '返回上一個'
go_prev: "返回上一個",
},
side_task: {
test_tracking: {
@ -3509,7 +3510,7 @@ const message = {
subtitle: "您已完成全部新手旅程 能量滿載~",
desc: "想繼續了解進階教程,請關注我們的技術博客和直播",
blog_url: "博客地址",
live_url: "直播間地址"
live_url: "直播間地址",
},
close: {
title: "關閉新手旅程",
@ -3520,8 +3521,8 @@ const message = {
},
save_success: "關閉成功",
to_try: "去完成",
view_video: "觀看視頻教程"
}
view_video: "觀看視頻教程",
},
};
export default {

View File

@ -46,6 +46,7 @@ public interface NoticeConstants {
String UPDATE = "UPDATE";
String DELETE = "DELETE";
String COMPLETE = "COMPLETE";
String REVIEW = "REVIEW";
String CASE_CREATE = "CASE_CREATE";
String CASE_UPDATE = "CASE_UPDATE";

View File

@ -107,4 +107,13 @@ public enum ProjectApplicationType {
* 资源池ID
*/
RESOURCE_POOL_ID,
/**
* 性能测试是否评审脚本
*/
PERFORMANCE_REVIEW_LOAD_TEST_SCRIPT,
/**
* 性能测试脚本评审人
*/
PERFORMANCE_SCRIPT_REVIEWER,
}

View File

@ -36,4 +36,6 @@ public class ProjectConfig {
private String resourcePoolId;
private Boolean poolEnable = false;
private Boolean reReview = false;
private String performanceScriptReviewer;
private Boolean performanceReviewLoadTestScript = false;
}

View File

@ -784,4 +784,10 @@ public class FileMetadataService {
return new ArrayList<>();
}
}
public List<FileMetadataWithBLOBs> selectByIdAndType(List<String> idList, String jmx) {
FileMetadataExample fileMetadataExample = new FileMetadataExample();
fileMetadataExample.createCriteria().andIdIn(idList).andTypeEqualTo("JMX");
return fileMetadataMapper.selectByExampleWithBLOBs(fileMetadataExample);
}
}

View File

@ -22,6 +22,7 @@ import io.metersphere.request.*;
import io.metersphere.service.BaseCheckPermissionService;
import io.metersphere.service.PerformanceTestService;
import io.metersphere.task.dto.TaskRequestDTO;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -29,7 +30,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -96,12 +97,12 @@ public class PerformanceTestController {
@RequestPart(value = "file", required = false) List<MultipartFile> files
) {
request.setId(UUID.randomUUID().toString());
// checkPermissionService.checkProjectOwner(request.getProjectId());
LoadTest loadTest = performanceTestService.save(request, files);
List<ApiLoadTest> apiList = request.getApiList();
apiPerformanceService.add(apiList, loadTest.getId());
//检查并发送审核脚本的通知
performanceTestService.checkAndSendReviewMessage(new ArrayList<>(request.getUpdatedFileList()), files, request.getId(), request.getName(), request.getProjectId());
return loadTest;
}
@ -114,8 +115,10 @@ public class PerformanceTestController {
@RequestPart("request") EditTestPlanRequest request,
@RequestPart(value = "file", required = false) List<MultipartFile> files
) {
// // checkPermissionService.checkPerformanceTestOwner(request.getId());
return performanceTestService.edit(request, files);
LoadTest returnModel = performanceTestService.edit(request, files);
//检查并发送审核脚本的通知
performanceTestService.checkAndSendReviewMessage(new ArrayList<>(request.getUpdatedFileList()), files, request.getId(), request.getName(), request.getProjectId());
return returnModel;
}
@ -160,7 +163,7 @@ public class PerformanceTestController {
public Pager<List<FileMetadata>> getProjectFiles(@PathVariable String projectId, @PathVariable String loadType,
@PathVariable int goPage, @PathVariable int pageSize,
@RequestBody QueryProjectFileRequest request) {
// // checkPermissionService.checkProjectOwner(projectId);
// // checkPermissionService.checkProjectOwner(projectId);
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, performanceTestService.getProjectFiles(projectId, loadType, request));
}
@ -283,6 +286,7 @@ public class PerformanceTestController {
public LoadTestDTO getLoadTestByVersion(@PathVariable String version, @PathVariable String refId) {
return performanceTestService.getLoadTestByVersion(version, refId);
}
@GetMapping("check-file-is-related/{fileId}")
public void checkFileIsRelated(@PathVariable String fileId) {
performanceTestService.checkFileIsRelated(fileId);

View File

@ -24,12 +24,16 @@ import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.performance.PerformanceReference;
import io.metersphere.metadata.service.FileMetadataService;
import io.metersphere.notice.service.NotificationService;
import io.metersphere.quota.service.BaseQuotaService;
import io.metersphere.request.*;
import io.metersphere.task.dto.TaskRequestDTO;
import io.metersphere.utils.JmxParseUtil;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
@ -38,7 +42,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
@ -94,6 +97,10 @@ public class PerformanceTestService {
private TestCaseTestMapper testCaseTestMapper;
@Resource
private BaseQuotaService baseQuotaService;
@Resource
private BaseProjectApplicationService baseProjectApplicationService;
@Resource
private NotificationService notificationService;
public List<LoadTestDTO> list(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultSortOrder(request.getOrders()));
@ -794,29 +801,6 @@ public class PerformanceTestService {
return null;
}
// /**
// * 初始化场景与性能测试的关联关系
// */
// public void initScenarioLoadTest() {
// LoadTestExample example = new LoadTestExample();
// example.createCriteria().andScenarioIdIsNotNull();
// List<LoadTest> loadTests = loadTestMapper.selectByExample(example);
// SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// ApiLoadTestMapper mapper = sqlSession.getMapper(ApiLoadTestMapper.class);
// loadTests.forEach(item -> {
// ApiLoadTest scenarioLoadTest = new ApiLoadTest();
// scenarioLoadTest.setType(ApiLoadType.SCENARIO.name());
// scenarioLoadTest.setApiId(item.getScenarioId());
// scenarioLoadTest.setApiVersion(item.getScenarioVersion() == null ? 0 : item.getScenarioVersion());
// scenarioLoadTest.setLoadTestId(item.getId());
// scenarioLoadTest.setId(UUID.randomUUID().toString());
// mapper.insert(scenarioLoadTest);
// });
// sqlSession.flushStatements();
// if (sqlSession != null && sqlSessionFactory != null) {
// SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
// }
// }
public Integer getGranularity(String reportId) {
Integer granularity = CommonBeanFactory.getBean(JmeterProperties.class).getReport().getGranularity();
@ -1017,4 +1001,40 @@ public class PerformanceTestService {
public List<BaseCase> getBaseCaseByProjectId(String projectId) {
return extLoadTestMapper.selectBaseCaseByProjectId(projectId);
}
//检查并发送脚本审核的通知
public void checkAndSendReviewMessage(List<FileMetadata> fileMetadataList, List<MultipartFile> files, String loadTestId, String loadTestName, String projectId) {
ProjectApplication reviewLoadTestScript = baseProjectApplicationService.getProjectApplication(
projectId, ProjectApplicationType.PERFORMANCE_REVIEW_LOAD_TEST_SCRIPT.name());
if (BooleanUtils.toBoolean(reviewLoadTestScript.getTypeValue())) {
ProjectApplication loadTestScriptReviewerConfig = baseProjectApplicationService.getProjectApplication(
projectId, ProjectApplicationType.PERFORMANCE_SCRIPT_REVIEWER.name());
if (StringUtils.isNotEmpty(loadTestScriptReviewerConfig.getTypeValue())) {
boolean isSend = this.isSendScriptReviewMessage(fileMetadataList, files);
if (isSend) {
Notification notification = new Notification();
notification.setTitle("性能测试通知");
notification.setOperator(SessionUtils.getUserId());
notification.setOperation(NoticeConstants.Event.REVIEW);
notification.setResourceId(loadTestId);
notification.setResourceName(loadTestName);
notification.setResourceType(NoticeConstants.TaskType.PERFORMANCE_TEST_TASK);
notification.setType(NotificationConstants.Type.SYSTEM_NOTICE.name());
notification.setStatus(NotificationConstants.Status.UNREAD.name());
notification.setCreateTime(System.currentTimeMillis());
notification.setReceiver(loadTestScriptReviewerConfig.getTypeValue());
notificationService.sendAnnouncement(notification);
}
}
}
}
private boolean isSendScriptReviewMessage(List<FileMetadata> fileMetadataList, List<MultipartFile> files) {
List<FileMetadataWithBLOBs> fileMetadataWithBLOBsList = new ArrayList<>();
if (CollectionUtils.isNotEmpty(fileMetadataList)) {
List<String> idList = fileMetadataList.stream().map(FileMetadata::getId).toList();
fileMetadataWithBLOBsList = fileMetadataService.selectByIdAndType(idList, "JMX");
}
return JmxParseUtil.isJmxHasScriptByFiles(files) || JmxParseUtil.isJmxHasScriptByStorage(fileMetadataWithBLOBsList);
}
}

View File

@ -0,0 +1,132 @@
package io.metersphere.utils;
import io.metersphere.base.domain.FileMetadataWithBLOBs;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.environment.utils.XMLUtils;
import io.metersphere.metadata.service.FileCenter;
import io.metersphere.metadata.vo.FileRequest;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.util.List;
//Jmx解析工具类
public class JmxParseUtil {
public static boolean isJmxHasScriptByFiles(List<MultipartFile> files) {
if (CollectionUtils.isNotEmpty(files)) {
for (MultipartFile file : files) {
if (jmxMultipartFileHasScript(file)) {
return true;
}
}
}
return false;
}
public static boolean isJmxHasScriptByStorage(List<FileMetadataWithBLOBs> fileMetadataWithBLOBs) {
for (FileMetadataWithBLOBs fileMetadata : fileMetadataWithBLOBs) {
if (StringUtils.equalsIgnoreCase(fileMetadata.getType(), "jmx")) {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(fileMetadata.getName());
fileRequest.setProjectId(fileMetadata.getProjectId());
fileRequest.setResourceId(fileMetadata.getId());
fileRequest.setStorage(fileMetadata.getStorage());
fileRequest.setResourceType(fileMetadata.getResourceType());
fileRequest.setType(fileMetadata.getType());
fileRequest.setPath(fileMetadata.getPath());
fileRequest.setFileAttachInfoByString(fileMetadata.getAttachInfo());
if (jmxStorageFileHasScript(fileRequest)) {
return true;
}
}
}
return false;
}
private static boolean jmxMultipartFileHasScript(MultipartFile file) {
if (file != null) {
try {
String fileName = file.getOriginalFilename();
if (StringUtils.endsWithIgnoreCase(fileName, ".jmx")) {
return jmxBytesHasScript(file.getBytes());
}
} catch (Exception e) {
LogUtil.error("检查上传的jmx文件是否含有脚本数据失败", e);
}
}
return false;
}
private static boolean jmxStorageFileHasScript(FileRequest fileRequest) {
try {
return jmxBytesHasScript(FileCenter.getRepository(fileRequest.getStorage()).getFile(fileRequest));
} catch (Exception e) {
LogUtil.error("下载jmx文件解析是否含有脚本数据失败", e);
}
return false;
}
private static boolean jmxBytesHasScript(byte[] jmxFileByte) throws Exception {
if (jmxFileByte != null) {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
XMLUtils.setExpandEntityReferencesFalse(documentBuilderFactory);
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
final Document document = builder.parse(new InputSource(new ByteArrayInputStream(jmxFileByte)));
final Element jmxDoc = document.getDocumentElement();
NodeList childNodes = jmxDoc.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (node instanceof Element) {
if (hashTreeNodeHasScript((Element) node)) {
return true;
}
}
}
}
return false;
}
private static boolean hashTreeNodeHasScript(Element hashTree) {
if (invalid(hashTree)) {
return false;
}
if (hashTree.getChildNodes().getLength() > 0) {
final NodeList childNodes = hashTree.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
if (node instanceof Element elementItem) {
if (invalid(elementItem)) {
continue;
}
if (nodeIsScript(elementItem)) {
return true;
} else {
//递归子节点是否有脚本
if (hashTreeNodeHasScript(elementItem)) {
return true;
}
}
}
}
}
return false;
}
private static boolean invalid(Element ele) {
return !StringUtils.isBlank(ele.getAttribute("enabled")) && !Boolean.parseBoolean(ele.getAttribute("enabled"));
}
private static boolean nodeIsScript(Node node) {
return StringUtils.containsAnyIgnoreCase(node.getNodeName(), "JSR223", "Processor");
}
}

View File

@ -0,0 +1,82 @@
<template>
<app-manage-item
:title="name"
:description="popTitle"
:append-span="3"
:middle-span="12"
:prepend-span="9"
>
<template #prepend>
<span style="margin-left: 10px; line-height: 35px">{{ name }}</span>
<el-tooltip
v-show="popTitle"
class="item"
effect="dark"
:content="popTitle"
placement="right-start"
>
<i class="el-icon-info" />
</el-tooltip>
</template>
<template #middle>
<span>{{ $t("pj.reviewers") }}</span>
<el-select
v-model="config.performanceScriptReviewer"
@change="reviewerChange"
size="mini"
style="margin-left: 5px"
filterable
:placeholder="$t('api_test.definition.api_principal')"
>
<el-option
v-for="item in reviewers"
:key="item.id"
:value="item.id"
:label="item.name"
>
</el-option>
</el-select>
</template>
<template #append>
<el-switch
v-model="config.performanceReviewLoadTestScript"
@change="switchChange"
></el-switch>
</template>
</app-manage-item>
</template>
<script>
import AppManageItem from "@/business/menu/appmanage/AppManageItem.vue";
export default {
name: "ReviewerConfig",
components: { AppManageItem },
props: {
name: String,
popTitle: String,
reviewers: Array,
config: Object,
},
setup() {
return {};
},
data() {
return {};
},
computed: {},
watch: {},
created() {},
mounted() {},
methods: {
switchChange() {
this.$emit("chooseChange");
},
reviewerChange() {
this.$emit("reviewerChange");
},
},
};
</script>
<style scoped></style>

View File

@ -8,6 +8,10 @@ const message = {
"(Environment configuration with the same name filtered {0})",
check_third_project_success: "inspection passed",
api_run_pool_title: "Interface execution resource pool",
reviewers: "Reviewers",
load_test_script_review: "Performance test script review",
load_test_script_review_detail:
"Performance test script file upload must specify user review",
},
file_manage: {
my_file: "My File",

View File

@ -7,6 +7,9 @@ const message = {
environment_import_repeat_tip: "(已过滤同名称的环境配置 {0})",
check_third_project_success: "检查通过",
api_run_pool_title: "接口执行资源池",
reviewers: "审核人",
load_test_script_review: "性能脚本审核",
load_test_script_review_detail: "上传性能测试脚本文件须指定用户审核",
},
file_manage: {
my_file: "我的文件",

View File

@ -7,6 +7,9 @@ const message = {
environment_import_repeat_tip: "(已過濾同名稱的環境配置 {0})",
check_third_project_success: "檢查通過",
api_run_pool_title: "接口執行資源池",
reviewers: "審核人",
load_test_script_review: "性能腳本審核",
load_test_script_review_detail: "上傳性能測試腳本文件須指定用戶審核",
},
file_manage: {
my_file: "我的文件",