This commit is contained in:
liqiang-fit2cloud 2023-01-19 16:13:17 +08:00
commit 7ee08be1c2
14 changed files with 356 additions and 89 deletions

View File

@ -40,4 +40,6 @@ public class ApiScenarioRequest extends BaseQueryRequest {
//测试计划关联场景过滤掉步骤为0的场景 //测试计划关联场景过滤掉步骤为0的场景
private String stepTotal; private String stepTotal;
//场景引用类型
private String refType;
} }

View File

@ -876,11 +876,11 @@ public class ElementUtil {
} }
} }
public static String getScriptEnv(String environmentId, ParameterConfig config) { public static String getScriptEnv(String environmentId, ParameterConfig config, String projectId) {
if (StringUtils.isEmpty(environmentId)) { if (StringUtils.isEmpty(environmentId)) {
if (config.getConfig() != null) { if (config.getConfig() != null) {
if (config.getProjectId() != null && config.getConfig().containsKey(config.getProjectId())) { if (StringUtils.isNotBlank(projectId) && config.getConfig().containsKey(projectId)) {
return config.getConfig().get(config.getProjectId()).getEnvironmentId(); return config.getConfig().get(projectId).getEnvironmentId();
} else { } else {
if (CollectionUtils.isNotEmpty(config.getConfig().values())) { if (CollectionUtils.isNotEmpty(config.getConfig().values())) {
Optional<EnvironmentConfig> values = config.getConfig().entrySet().stream().findFirst().map(Map.Entry::getValue); Optional<EnvironmentConfig> values = config.getConfig().entrySet().stream().findFirst().map(Map.Entry::getValue);

View File

@ -44,7 +44,7 @@ public class MsJSR223Processor extends MsTestElement {
if (!config.isOperating() && !this.isEnable()) { if (!config.isOperating() && !this.isEnable()) {
return; return;
} }
this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config)); this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config, this.getProjectId()));
TestElement processor = new BeanShellSampler(); TestElement processor = new BeanShellSampler();
if (jsrEnable == null || BooleanUtils.isTrue(jsrEnable)) { if (jsrEnable == null || BooleanUtils.isTrue(jsrEnable)) {

View File

@ -43,7 +43,7 @@ public class MsJSR223PostProcessor extends MsTestElement {
if (!config.isOperating() && !this.isEnable()) { if (!config.isOperating() && !this.isEnable()) {
return; return;
} }
this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config)); this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config, this.getProjectId()));
TestElement processor = new BeanShellPostProcessor(); TestElement processor = new BeanShellPostProcessor();
if (jsrEnable == null || BooleanUtils.isTrue(jsrEnable)) { if (jsrEnable == null || BooleanUtils.isTrue(jsrEnable)) {

View File

@ -43,7 +43,7 @@ public class MsJSR223PreProcessor extends MsTestElement {
} }
} }
ScriptFilter.verify(this.getScriptLanguage(), this.getName(), script); ScriptFilter.verify(this.getScriptLanguage(), this.getName(), script);
this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config)); this.setEnvironmentId(ElementUtil.getScriptEnv(this.getEnvironmentId(), config, this.getProjectId()));
final HashTree jsr223PreTree = tree.add(getShellProcessor()); final HashTree jsr223PreTree = tree.add(getShellProcessor());
if (CollectionUtils.isNotEmpty(hashTree)) { if (CollectionUtils.isNotEmpty(hashTree)) {

View File

@ -462,9 +462,11 @@
<select id="selectReference" resultType="io.metersphere.api.dto.automation.ApiScenarioDTO"> <select id="selectReference" resultType="io.metersphere.api.dto.automation.ApiScenarioDTO">
select a.id, a.name , a.num,p.name AS projectName, a.update_time, a.create_time, select a.id, a.name , a.num,p.name AS projectName, a.update_time, a.create_time,
w.name AS workspaceName, p.workspace_id AS workspaceId, p.id AS projectId from api_scenario a w.name AS workspaceName, p.workspace_id AS workspaceId, p.id AS projectId, v.name AS versionName, a.version_id
from api_scenario a
LEFT JOIN project p ON a.project_id = p.id LEFT JOIN project p ON a.project_id = p.id
LEFT JOIN `workspace` w ON p.workspace_id = w.id LEFT JOIN `workspace` w ON p.workspace_id = w.id
LEFT JOIN `project_version` v ON a.version_id = v.id
<where> <where>
a.status != 'Trash' a.status != 'Trash'
<if test="request.ids != null and request.ids.size() > 0"> <if test="request.ids != null and request.ids.size() > 0">
@ -489,6 +491,12 @@
#{value} #{value}
</foreach> </foreach>
</when> </when>
<when test="key=='version_id'">
and a.version_id in
<foreach collection="values" item="value" separator="," open="(" close=")">
#{value}
</foreach>
</when>
</choose> </choose>
</if> </if>
</foreach> </foreach>

View File

@ -1024,7 +1024,7 @@ public class ApiDefinitionService {
public void getReferenceIds(ApiScenarioRequest request) { public void getReferenceIds(ApiScenarioRequest request) {
ApiScenarioReferenceIdExample example = new ApiScenarioReferenceIdExample(); ApiScenarioReferenceIdExample example = new ApiScenarioReferenceIdExample();
example.createCriteria().andReferenceIdEqualTo(request.getId()).andReferenceTypeEqualTo(MsTestElementConstants.REF.name()); example.createCriteria().andReferenceIdEqualTo(request.getId()).andReferenceTypeEqualTo(request.getRefType());
List<ApiScenarioReferenceId> scenarioReferenceIds = apiScenarioReferenceIdMapper.selectByExample(example); List<ApiScenarioReferenceId> scenarioReferenceIds = apiScenarioReferenceIdMapper.selectByExample(example);
List<String> scenarioIds = scenarioReferenceIds.stream().map(ApiScenarioReferenceId::getApiScenarioId).collect(Collectors.toList()); List<String> scenarioIds = scenarioReferenceIds.stream().map(ApiScenarioReferenceId::getApiScenarioId).collect(Collectors.toList());
request.setIds(scenarioIds); request.setIds(scenarioIds);

View File

@ -44,7 +44,7 @@ export function getAll() {
} }
export function getUserWorkspace() { export function getUserWorkspace() {
return get('/workspace/list/userworkspace/'); return get('/workspace/list/userworkspace');
} }
export function projectRelated(params) { export function projectRelated(params) {

View File

@ -0,0 +1,54 @@
<template>
<el-dropdown class="scenario-ext-btn">
<el-link type="primary" :underline="false">
<el-icon class="el-icon-more"></el-icon>
</el-link>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(item,index) in dropdownItems" :key="index" @click.native.stop="click(item)"
:disabled="isDisable(item)"
:command="item.value"
v-modules="item.modules"
>{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import {hasPermissions} from 'metersphere-frontend/src/utils/permission';
export default {
name: "TableExtendBtns",
props: {
dropdownItems: Array,
row: Object,
},
methods: {
click(btn) {
if (btn.exec instanceof Function) {
btn.exec(this.row);
}
},
isDisable(item) {
if (item.isDisable) {
if (item.isDisable instanceof Function) {
return item.isDisable();
} else {
return item.isDisable;
}
}
if (item.permissions && item.permissions.length > 0) {
return !hasPermissions(...item.permissions);
}
return false;
}
}
}
</script>
<style scoped>
.scenario-ext-btn {
margin-left: 10px;
}
</style>

View File

@ -210,6 +210,11 @@
:fields-width="fieldsWidth" :fields-width="fieldsWidth"
:label="$t('commons.description')"/> :label="$t('commons.description')"/>
</span> </span>
<template v-if="!trashEnable" v-slot:opt-behind="scope">
<table-extend-btns
:dropdown-items="dropdownItems"
:row="scope.row"/>
</template>
</ms-table> </ms-table>
<ms-table-pagination <ms-table-pagination
:change="initTable" :change="initTable"
@ -229,6 +234,8 @@
<version-selector @handleSave="handleCopyDataFromVersion" ref="versionSelector"/> <version-selector @handleSave="handleCopyDataFromVersion" ref="versionSelector"/>
<!--高级搜索--> <!--高级搜索-->
<ms-table-adv-search-bar :condition.sync="condition" :showLink="false" ref="searchBar" @search="search"/> <ms-table-adv-search-bar :condition.sync="condition" :showLink="false" ref="searchBar" @search="search"/>
<!--查看引用-->
<ms-show-reference ref="viewRef" :show-plan="false" :is-has-ref="false" api-type="API"/>
<case-batch-move @refresh="initTable" @moveSave="moveSave" ref="testCaseBatchMove"/> <case-batch-move @refresh="initTable" @moveSave="moveSave" ref="testCaseBatchMove"/>
<mx-relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/> <mx-relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/>
@ -296,6 +303,8 @@ import ListItemDeleteConfirm from 'metersphere-frontend/src/components/ListItemD
import MsSearch from 'metersphere-frontend/src/components/search/MsSearch'; import MsSearch from 'metersphere-frontend/src/components/search/MsSearch';
import {buildNodePath} from 'metersphere-frontend/src/model/NodeTree'; import {buildNodePath} from 'metersphere-frontend/src/model/NodeTree';
import VersionSelector from '@/business/definition/components/version/VersionSelector'; import VersionSelector from '@/business/definition/components/version/VersionSelector';
import TableExtendBtns from "@/business/definition/components/complete/table/TableExtendBtns";
import MsShowReference from "@/business/definition/components/reference/ShowReference";
export default { export default {
name: 'ApiList', name: 'ApiList',
@ -321,6 +330,8 @@ export default {
MsTableColumn, MsTableColumn,
MsSearch, MsSearch,
VersionSelector, VersionSelector,
TableExtendBtns,
MsShowReference,
MsApiReportStatus: () => import('../../../automation/report/ApiReportStatus'), MsApiReportStatus: () => import('../../../automation/report/ApiReportStatus'),
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'), MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'),
}, },
@ -401,13 +412,6 @@ export default {
exec: this.editApi, exec: this.editApi,
permissions: ['PROJECT_API_DEFINITION:READ+EDIT_API'], permissions: ['PROJECT_API_DEFINITION:READ+EDIT_API'],
}, },
{
tip: 'CASE',
exec: this.handleTestCase,
isDivButton: true,
type: 'primary',
permissions: ['PROJECT_API_DEFINITION:READ'],
},
{ {
tip: this.$t('commons.delete'), tip: this.$t('commons.delete'),
exec: this.handleDelete, exec: this.handleDelete,
@ -437,6 +441,20 @@ export default {
permissions: ['PROJECT_API_DEFINITION:READ+DELETE_API'], permissions: ['PROJECT_API_DEFINITION:READ+DELETE_API'],
}, },
], ],
dropdownItems: [
{
name: this.$t('api_test.automation.view_ref'),
value: "ref",
permissions: ['PROJECT_API_DEFINITION:READ'],
exec: this.showCaseRef,
},
{
name: this.$t('commons.view') + "CASE",
value: "case",
permissions: ['PROJECT_API_DEFINITION:READ'],
exec: this.handleTestCase,
}
],
typeArr: [ typeArr: [
{id: 'status', name: this.$t('api_test.definition.api_status')}, {id: 'status', name: this.$t('api_test.definition.api_status')},
{id: 'method', name: this.$t('api_test.definition.api_type')}, {id: 'method', name: this.$t('api_test.definition.api_type')},
@ -1024,6 +1042,12 @@ export default {
this.$emit('handleTestCase', api); this.$emit('handleTestCase', api);
// this.$refs.caseList.open(this.selectApi); // this.$refs.caseList.open(this.selectApi);
}, },
showCaseRef(row) {
let param = {};
Object.assign(param, row);
param.moduleId = undefined;
this.$refs.viewRef.open(param, 'API');
},
handleDelete(api) { handleDelete(api) {
if (this.trashEnable) { if (this.trashEnable) {
delDefinition(api.id).then(() => { delDefinition(api.id).then(() => {

View File

@ -1,9 +1,12 @@
<template> <template>
<el-dialog :visible.sync="isVisible" class="advanced-item-value" width="50%"> <el-dialog :visible.sync="isVisible" class="advanced-item-value" width="50%">
<el-tabs tab-position="top" style="width: 100%" v-model="activeName" @tab-click="handleClick"> <el-tabs tab-position="top" style="width: 100%" v-model="activeName" @tab-click="handleClick">
<el-tab-pane :label="$t('api_test.automation.scenario_ref')" name="scenario"> <el-tab-pane
:label="$t('api_test.home_page.api_details_card.title')+$t('api_test.home_page.test_scene_details_card.title')"
name="scenario">
<ms-table <ms-table
:data="scenarioData" v-if="!isHasRef"
:data="scenarioCopyData"
style="width: 100%" style="width: 100%"
:screen-height="screenHeight" :screen-height="screenHeight"
:total="total" :total="total"
@ -31,10 +34,101 @@
column-key="projectId" column-key="projectId"
width="200"> width="200">
</ms-table-column> </ms-table-column>
<ms-table-column
prop="versionName"
:label="$t('project.version.name')"
width="200"
column-key="versionId"
:filters="versionFilters">
</ms-table-column>
</ms-table> </ms-table>
<ms-tab-button
v-if="isHasRef"
:active-dom.sync="activeDom"
:left-content="$t('api_test.scenario.clone')"
:right-content="$t('api_test.scenario.reference')"
@changeTab="changeTab"
:middle-button-enable="false">
<ms-table
v-if="activeDom === 'right'"
:data="scenarioRefData"
style="width: 100%"
:screen-height="screenHeight"
:total="total"
:page-size="pageSize"
:enable-selection="false"
@refresh="search"
:condition="condition">
<ms-table-column prop="num" label="ID" sortable width="80"/>
<ms-table-column prop="name" :label="$t('api_report.scenario_name')" width="200">
<template v-slot:default="{ row }">
<el-link @click="openScenario(row)" style="cursor: pointer">{{ row.name }}</el-link>
</template>
</ms-table-column>
<ms-table-column
prop="workspaceName"
:label="$t('group.belong_workspace')"
width="200"
column-key="workspaceId"
:filters="workspaceFilters">
</ms-table-column>
<ms-table-column
prop="projectName"
:label="$t('group.belong_project')"
:filters="projectFilters"
column-key="projectId"
width="200">
</ms-table-column>
<ms-table-column
prop="versionName"
:label="$t('project.version.name')"
width="200"
column-key="versionId"
:filters="versionFilters">
</ms-table-column>
</ms-table>
<ms-table
v-if="activeDom === 'left'"
:data="scenarioCopyData"
style="width: 100%"
:screen-height="screenHeight"
:total="total"
:page-size="pageSize"
:enable-selection="false"
@refresh="search"
:condition="condition">
<ms-table-column prop="num" label="ID" sortable width="80"/>
<ms-table-column prop="name" :label="$t('api_report.scenario_name')" width="200">
<template v-slot:default="{ row }">
<el-link @click="openScenario(row)" style="cursor: pointer">{{ row.name }}</el-link>
</template>
</ms-table-column>
<ms-table-column
prop="workspaceName"
:label="$t('group.belong_workspace')"
width="200"
column-key="workspaceId"
:filters="workspaceFilters">
</ms-table-column>
<ms-table-column
prop="projectName"
:label="$t('group.belong_project')"
:filters="projectFilters"
column-key="projectId"
width="200">
</ms-table-column>
<ms-table-column
prop="versionName"
:label="$t('project.version.name')"
width="200"
column-key="versionId"
:filters="versionFilters">
</ms-table-column>
</ms-table>
</ms-tab-button>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="showPlan" :label="$t('api_test.home_page.running_task_list.test_plan_schedule')"
<el-tab-pane :label="$t('api_test.automation.plan_ref')" name="testPlan"> name="testPlan">
<ms-table <ms-table
:data="planData" :data="planData"
style="width: 100%" style="width: 100%"
@ -73,16 +167,23 @@ import MsTablePagination from 'metersphere-frontend/src/components/pagination/Ta
import { apiProjectRelated, getOwnerProjectIds, getProject, getUserWorkspace, projectRelated } from '@/api/project'; import { apiProjectRelated, getOwnerProjectIds, getProject, getUserWorkspace, projectRelated } from '@/api/project';
import { getCurrentProjectID, getCurrentUserId, getCurrentWorkspaceId } from 'metersphere-frontend/src/utils/token'; import { getCurrentProjectID, getCurrentUserId, getCurrentWorkspaceId } from 'metersphere-frontend/src/utils/token';
import {getUUID} from 'metersphere-frontend/src/utils'; import {getUUID} from 'metersphere-frontend/src/utils';
import {hasLicense} from 'metersphere-frontend/src/utils/permission';
import {getDefinitionReference, getPlanReference} from '@/api/definition'; import {getDefinitionReference, getPlanReference} from '@/api/definition';
import MsTable from 'metersphere-frontend/src/components/table/MsTable'; import MsTable from 'metersphere-frontend/src/components/table/MsTable';
import MsTableColumn from 'metersphere-frontend/src/components/table/MsTableColumn'; import MsTableColumn from 'metersphere-frontend/src/components/table/MsTableColumn';
import MsTabButton from '@/business/commons/MsTabs';
import {getProjectVersions} from "@/api/xpack";
export default { export default {
name: 'ShowReference', name: 'ShowReference',
data() { data() {
return { return {
isVisible: false, isVisible: false,
scenarioData: [], isCopy: true,
showTextColor: "showTextColor",
unShowTextColor: "unShowTextColor",
scenarioRefData: [],
scenarioCopyData: [],
planData: [], planData: [],
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
@ -92,19 +193,36 @@ export default {
workspaceList: [], workspaceList: [],
workspaceFilters: [], workspaceFilters: [],
projectFilters: [], projectFilters: [],
versionFilters: [],
projectList: [], projectList: [],
screenHeight: 'calc(100vh - 400px)', screenHeight: 'calc(100vh - 400px)',
condition: {}, condition: {},
type: '', type: '',
projectPlanFilters: [], projectPlanFilters: [],
activeDom: 'left',
}; };
}, },
props: {
apiType: String,
showPlan: {
type: Boolean,
default: true,
},
isHasRef: {
type: Boolean,
default: true,
},
},
components: { components: {
MsTablePagination, MsTablePagination,
MsTable, MsTable,
MsTableColumn, MsTableColumn,
MsTabButton
}, },
watch: { watch: {
activeDom() {
this.getScenarioData();
},
activeName(o) { activeName(o) {
if (o) { if (o) {
this.init(); this.init();
@ -126,6 +244,23 @@ export default {
this.projectList = res.data ? res.data : []; this.projectList = res.data ? res.data : [];
}); });
}, },
getVersionOptions(currentVersion) {
if (hasLicense()) {
getProjectVersions(getCurrentProjectID()).then((response) => {
if (currentVersion) {
this.versionFilters = response.data
.filter((u) => u.id === currentVersion)
.map((u) => {
return {text: u.name, value: u.id};
});
} else {
this.versionFilters = response.data.map((u) => {
return {text: u.name, value: u.id};
});
}
});
}
},
/** /**
* 操作方法 * 操作方法
*/ */
@ -134,7 +269,8 @@ export default {
this.pageSize = 10; this.pageSize = 10;
this.total = 0; this.total = 0;
this.condition = {}; this.condition = {};
this.scenarioData = []; this.scenarioRefData = [];
this.scenarioCopyData = [];
this.planData = []; this.planData = [];
}, },
open(row, type) { open(row, type) {
@ -142,6 +278,7 @@ export default {
this.init(); this.init();
this.getUserProjectList(); this.getUserProjectList();
this.getWorkSpaceList(); this.getWorkSpaceList();
this.getVersionOptions();
this.isVisible = true; this.isVisible = true;
this.scenarioId = row.id; this.scenarioId = row.id;
this.type = type; this.type = type;
@ -150,17 +287,8 @@ export default {
close() { close() {
this.isVisible = false; this.isVisible = false;
}, },
search(row) { getReferenceData(condition) {
this.condition.id = this.scenarioId; getDefinitionReference(this.currentPage, this.pageSize, condition).then((res) => {
if (row) {
this.condition.id = row.id;
this.condition.projectId = row.projectId;
this.condition.workspaceId = row.workspaceId;
}
this.condition.workspaceId = getCurrentWorkspaceId();
this.condition.scenarioType = this.type;
if (this.activeName === 'scenario') {
getDefinitionReference(this.currentPage, this.pageSize, this.condition).then((res) => {
let data = res.data || []; let data = res.data || [];
this.total = data.itemCount || 0; this.total = data.itemCount || 0;
if (this.workspaceList) { if (this.workspaceList) {
@ -200,9 +328,32 @@ export default {
}); });
}); });
} }
if (this.activeDom === 'left') {
this.scenarioCopyData = data.listObject || [];
} else {
this.scenarioRefData = data.listObject || [];
}
this.scenarioData = data.listObject || [];
}); });
},
search(row) {
this.condition.id = this.scenarioId;
if (row) {
this.condition.id = row.id;
this.condition.projectId = row.projectId;
this.condition.workspaceId = row.workspaceId;
}
this.condition.workspaceId = getCurrentWorkspaceId();
this.condition.scenarioType = this.type;
if (this.activeName === 'scenario') {
if (!this.isHasRef) {
this.condition.refType = "Copy"
this.activeDom = 'left'
} else {
this.condition.refType = "REF"
this.activeDom = 'right'
}
this.getReferenceData(this.condition);
} else { } else {
getPlanReference(this.currentPage, this.pageSize, this.condition).then((res) => { getPlanReference(this.currentPage, this.pageSize, this.condition).then((res) => {
let data = res.data || []; let data = res.data || [];
@ -279,9 +430,37 @@ export default {
}); });
window.open(automationData.href, '_blank'); window.open(automationData.href, '_blank');
}, },
getScenarioData() {
if (this.activeDom === 'left') {
this.condition.refType = 'Copy'
this.getReferenceData(this.condition);
} else {
this.condition.refType = 'REF'
this.getReferenceData(this.condition);
}
},
changeTab(active) {
this.activeDom = active;
},
}, },
}; };
</script> </script>
<style type="text/css" scoped>
.showTextColor {
color: var(--primary_color);
cursor: pointer;
}
.unShowTextColor {
cursor: pointer;
}
.changeTap {
margin: auto;
width: 50%;
text-align: center;
}
</style>
<style scoped> <style scoped>
:deep(.el-table__empty-block) { :deep(.el-table__empty-block) {
padding-right: 0 !important; padding-right: 0 !important;

View File

@ -15,7 +15,7 @@ import io.metersphere.quota.service.QuotaManagementService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View File

@ -22,7 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import jakarta.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;

View File

@ -21,7 +21,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import jakarta.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;