feat: 资源池接口联调&部分组件调整

This commit is contained in:
baiqi 2023-07-24 17:46:32 +08:00 committed by fit2-zhao
parent 54640dc32d
commit 4ebc36d5a9
14 changed files with 297 additions and 109 deletions

View File

@ -1,8 +1,20 @@
import MSR from '@/api/http/index';
import { PoolListUrl, UpdatePoolUrl, AddPoolUrl, DetailPoolUrl } from '@/api/requrls/setting/resourcePool';
import {
PoolListUrl,
UpdatePoolUrl,
AddPoolUrl,
DetailPoolUrl,
DeletePoolUrl,
EnablePoolUrl,
} from '@/api/requrls/setting/resourcePool';
import type { LocationQueryValue } from 'vue-router';
import type { ResourcePoolItem, AddResourcePoolParams } from '@/models/setting/resourcePool';
import type {
ResourcePoolItem,
AddResourcePoolParams,
UpdateResourcePoolParams,
ResourcePoolDetail,
} from '@/models/setting/resourcePool';
import type { TableQueryParams } from '@/models/common';
// 获取资源池列表
@ -11,7 +23,7 @@ export function getPoolList(data: TableQueryParams) {
}
// 更新资源池信息
export function updatePoolInfo(data: ResourcePoolItem) {
export function updatePoolInfo(data: UpdateResourcePoolParams) {
return MSR.post({ url: UpdatePoolUrl, data });
}
@ -22,5 +34,15 @@ export function addPool(data: AddResourcePoolParams) {
// 获取资源池详情
export function getPoolInfo(poolId: LocationQueryValue | LocationQueryValue[]) {
return MSR.get<ResourcePoolItem>({ url: DetailPoolUrl, params: poolId });
return MSR.get<ResourcePoolDetail>({ url: DetailPoolUrl, params: poolId });
}
// 删除资源池
export function delPoolInfo(poolId: string) {
return MSR.get({ url: DeletePoolUrl, params: poolId });
}
// 启用/禁用资源池
export function togglePoolStatus(poolId: string) {
return MSR.post({ url: EnablePoolUrl, params: poolId });
}

View File

@ -3,3 +3,4 @@ export const UpdatePoolUrl = '/test/resource/pool/update';
export const AddPoolUrl = '/test/resource/pool/add';
export const DeletePoolUrl = '/test/resource/pool/delete';
export const DetailPoolUrl = '/test/resource/pool/detail';
export const EnablePoolUrl = '/test/resource/pool/set/enable/';

View File

@ -7,7 +7,13 @@
</div>
<a-divider v-if="!simple" class="mb-[16px]" />
<div class="mr-[-10px]">
<a-scrollbar class="pr-[10px]" style="overflow-y: auto; height: calc(100vh - 256px)">
<a-scrollbar
class="pr-[10px]"
:style="{
overflowY: 'auto',
height: `calc(100vh - ${256 + specialHeight}px)`,
}"
>
<slot></slot>
</a-scrollbar>
</div>
@ -20,11 +26,11 @@
</div>
<slot name="footerRight">
<a-button type="secondary" @click="back">{{ t('mscard.defaultCancelText') }}</a-button>
<a-button v-if="!props.hideContinue" type="secondary" @click="emit('saveAndContinue')">
<a-button v-if="!props.hideContinue && !props.isEdit" type="secondary" @click="emit('saveAndContinue')">
{{ t('mscard.defaultSaveAndContinueText') }}
</a-button>
<a-button type="primary" @click="emit('save')">
{{ t(idEdit ? 'mscard.defaultConfirm' : 'mscard.defaultUpdate') }}
{{ t(props.isEdit ? 'mscard.defaultUpdate' : 'mscard.defaultConfirm') }}
</a-button>
</slot>
</div>
@ -37,20 +43,24 @@
import { useRouter } from 'vue-router';
const props = withDefaults(
defineProps<{
simple?: boolean;
title?: string;
hideContinue?: boolean;
handleBack?: () => void;
hideFooter?: boolean;
loading?: boolean;
idEdit?: boolean;
}>(),
defineProps<
Partial<{
simple: boolean;
title: string;
hideContinue: boolean;
hideFooter: boolean;
loading: boolean;
isEdit: boolean;
specialHeight: number; //
handleBack: () => void;
}>
>(),
{
simple: false,
hideContinue: false,
hideFooter: false,
idEdit: false,
isEdit: false,
specialHeight: 0,
}
);

View File

@ -58,6 +58,10 @@ export const editorProps = {
},
default: 'vs-dark',
},
readOnly: {
type: Boolean as PropType<boolean>,
default: false,
},
options: {
type: Object as PropType<Options>,
default() {

View File

@ -1,5 +1,5 @@
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
// import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
// import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
// import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
@ -26,6 +26,7 @@ self.MonacoEnvironment = {
return new TsWorker();
}
const EditorWorker = ((await import('monaco-editor/esm/vs/editor/editor.worker?worker')) as any).default;
return new EditorWorker();
},
};

View File

@ -13,8 +13,17 @@
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
</div>
<template v-if="fileList.length === 0">
<div v-if="props.mainText" class="ms-upload-main-text">{{ t(props.mainText) }}</div>
<div v-if="props.subText" class="ms-upload-sub-text">{{ t(props.subText) }}</div>
<div class="ms-upload-main-text">
{{ t(props.mainText || 'ms.upload.importModalDragtext') }}
</div>
<div class="ms-upload-sub-text">
{{
t(props.subText || 'ms.upload.importModalFileTip', {
type: UploadAcceptEnum[props.accept],
size: props.maxSize || defaultMaxSize,
})
}}
</div>
</template>
<template v-else>
<div class="ms-upload-main-text">
@ -31,23 +40,24 @@
import { ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { UploadAcceptEnum } from '@/enums/uploadEnum';
import { formatFileSize, sleep } from '@/utils';
import { formatFileSize } from '@/utils';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import type { FileItem } from '@arco-design/web-vue';
import { FileItem, Message } from '@arco-design/web-vue';
import type { UploadType } from './types';
const { t } = useI18n();
// props
type UploadProps = Partial<{
mainText: string;
subText: string;
mainText: string; //
subText: string; //
class: string;
multiple: boolean;
imagePreview: boolean;
showFileList: boolean;
iconType: string;
maxSize: number; // MB
[key: string]: any;
}> & {
accept: UploadType;
@ -57,6 +67,8 @@
const props = defineProps<UploadProps>();
const emit = defineEmits(['update:fileList']);
const defaultMaxSize = 50;
const fileList = ref<FileItem[]>(props.fileList);
watch(
@ -80,11 +92,16 @@
jar: 'icon-icon_file-jar_colorful',
};
async function beforeUpload() {
async function beforeUpload(file: File) {
if (!props.multiple && fileList.value.length > 0) {
//
fileList.value = [];
}
const maxSize = props.maxSize || defaultMaxSize;
if (file.size > maxSize * 1024 * 1024) {
Message.warning(t('ms.upload.overSize'));
return Promise.resolve(false);
}
return Promise.resolve(true);
}
</script>

View File

@ -1,3 +1,6 @@
export default {
'ms.upload.changeFile': 'Change file',
'ms.upload.overSize': 'The file size exceeds the limit, please reselect the file',
'ms.upload.importModalDragtext': 'Drag or click this area to select a file',
'ms.upload.importModalFileTip': 'Only {type} format files are supported, and the file size does not exceed {size} MB',
};

View File

@ -1,3 +1,6 @@
export default {
'ms.upload.changeFile': '更改文件',
'ms.upload.overSize': '文件大小超出限制,请重新选择文件',
'ms.upload.importModalDragtext': '拖拽或点击此区域选择文件',
'ms.upload.importModalFileTip': '只支持 {type} 格式文件,文件大小不超过 {size} MB',
};

View File

@ -9,4 +9,6 @@ export enum TableModuleEnum {
export enum TableKeyEnum {
API_TEST = 'apiTest',
USERGROUPUSER = 'userGroupUser',
SYSTEM_USER = 'systemUser',
SYSTEM_RESOURCEPOOL = 'systemResourcePool',
}

View File

@ -39,7 +39,7 @@
:style="{
overflow: 'auto',
width: `calc(100vw - ${menuWidth}px)`,
height: 'calc(100vh - 72px)',
height: 'calc(100vh - 64px)',
}"
>
<MsBreadCrumb />

View File

@ -6,6 +6,12 @@ export interface NodesListItem {
concurrentNumber: number;
}
// 应用组织id和name映射对象
export interface OrgIdNameMap {
id: string;
name: string;
}
// 资源池配置信息对象
export interface TestResourceDTO {
loadTestImage: string; // 镜像
@ -22,6 +28,7 @@ export interface TestResourceDTO {
uiGrid: string; // ui测试selenium-grid
girdConcurrentNumber: number; // ui测试selenium-grid最大并发数
orgIds: string[]; // 应用范围选择指定组织时的id集合
orgIdNameMap: OrgIdNameMap[]; // 应用范围选择指定组织时的id和name映射
}
// 资源池信息对象
@ -43,7 +50,18 @@ export interface ResourcePoolItem extends ResourcePoolInfo {
id: string;
}
export type ResourcePoolDetail = Omit<ResourcePoolInfo, 'testResourceDTO'> & {
id: string;
testResourceReturnDTO: TestResourceDTO;
};
// 添加资源池参数对象
export type AddResourcePoolParams = Omit<ResourcePoolInfo, 'testResourceDTO'> & {
testResourceDTO?: Partial<TestResourceDTO>;
};
// 更新资源池参数对象
export type UpdateResourcePoolParams = Omit<ResourcePoolInfo, 'testResourceDTO'> & {
id: string;
testResourceDTO?: Partial<TestResourceDTO>;
};

View File

@ -1,11 +1,18 @@
<template>
<MsDrawer v-model:visible="showJobDrawer" width="680px" :title="t('system.resourcePool.customJobTemplate')">
<MsDrawer
v-model:visible="showJobDrawer"
width="680px"
:title="t('system.resourcePool.customJobTemplate')"
:footer="false"
@close="handleClose"
>
<MsCodeEditor
v-model:model-value="jobDefinition"
title="YAML"
width="100%"
height="calc(100vh - 205px)"
theme="MS-text"
:read-only="props.readOnly"
/>
</MsDrawer>
</template>
@ -18,14 +25,16 @@
const props = defineProps<{
visible: boolean;
value: string | null;
value?: string | null;
defaultVal?: string | null;
readOnly?: boolean;
}>();
const emit = defineEmits(['update:value', 'update:visible']);
const { t } = useI18n();
const showJobDrawer = ref(props.visible);
const jobDefinition = ref(props.value || '');
const jobDefinition = ref(props.defaultVal || props.value || '');
watch(
() => props.visible,
@ -34,12 +43,34 @@
}
);
watch(
() => props.defaultVal,
(val) => {
if (val) {
jobDefinition.value = val;
}
}
);
watch(
() => props.value,
(val) => {
if (val) {
jobDefinition.value = val;
}
}
);
watch(
() => showJobDrawer.value,
(val) => {
emit('update:visible', val);
}
);
function handleClose() {
emit('update:value', jobDefinition.value);
}
</script>
<style lang="less" scoped></style>

View File

@ -1,5 +1,12 @@
<template>
<MsCard :loading="loading" :title="title" @save="beforeSave" @save-and-continue="beforeSave(true)">
<MsCard
:loading="loading"
:title="title"
:is-edit="isEdit"
:special-height="34"
@save="beforeSave"
@save-and-continue="beforeSave(true)"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
:label="t('system.resourcePool.name')"
@ -318,15 +325,15 @@
</template>
</a-form>
<template #footerLeft>
<a-button v-if="isCheckedPerformance" type="text" @click="showJobDrawer = true">
<MsButton v-if="isCheckedPerformance && isShowK8SResources" @click="showJobDrawer = true">
{{ t('system.resourcePool.customJobTemplate') }}
<a-tooltip :content="t('system.resourcePool.jobTemplateTip')" position="tl" mini>
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
</a-tooltip>
</a-button>
</MsButton>
</template>
</MsCard>
<JobTemplateDrawer v-model:visible="showJobDrawer" :value="form.testResourceDTO.jobDefinition" />
<JobTemplateDrawer v-model:visible="showJobDrawer" v-model:value="form.testResourceDTO.jobDefinition" />
</template>
<script setup lang="ts">
@ -336,16 +343,17 @@
import { useI18n } from '@/hooks/useI18n';
import useVisit from '@/hooks/useVisit';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBatchForm from '@/components/bussiness/ms-batch-form/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
import { getYaml, YamlType, job } from './template';
import { downloadStringFile, sleep } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { addPool, getPoolInfo } from '@/api/modules/setting/resourcePool';
import { addPool, getPoolInfo, updatePoolInfo } from '@/api/modules/setting/resourcePool';
import type { MsBatchFormInstance, FormItemModel } from '@/components/bussiness/ms-batch-form/types';
import type { AddResourcePoolParams, NodesListItem } from '@/models/setting/resourcePool';
import type { UpdateResourcePoolParams, NodesListItem } from '@/models/setting/resourcePool';
const route = useRoute();
const router = useRouter();
@ -402,18 +410,19 @@
try {
loading.value = true;
const res = await getPoolInfo(route.query.id);
const { testResourceDTO } = res;
const { girdConcurrentNumber, podThreads, concurrentNumber } = testResourceDTO;
const { testResourceReturnDTO } = res;
const { girdConcurrentNumber, podThreads, concurrentNumber, orgIdNameMap } = testResourceReturnDTO;
form.value = {
...res,
addType: 'single',
orgType: res.allOrg ? 'allOrg' : 'set',
use: [res.loadTest ? 'performance' : '', res.apiTest ? 'API' : '', res.uiTest ? 'UI' : ''].filter((e) => e),
testResourceDTO: {
...testResourceDTO,
...testResourceReturnDTO,
girdConcurrentNumber: girdConcurrentNumber || 1,
podThreads: podThreads || 1,
concurrentNumber: concurrentNumber || 1,
orgIds: orgIdNameMap?.map((e) => e.id) || [],
},
};
} catch (error) {
@ -558,10 +567,9 @@
* 解析代码编辑器内容
*/
function analyzeCode() {
const arr = editorContent.value.split('\r'); //
const arr = editorContent.value.replaceAll('\r', '\n').split('\n'); //
//
arr.forEach((e, i) => {
e = e.replaceAll('\n', ''); //
if (e.trim() !== '') {
//
const line = e.split(',');
@ -648,7 +656,7 @@
/**
* 拼接添加资源池参数
*/
function makeResourcePoolParams(): AddResourcePoolParams {
function makeResourcePoolParams(): UpdateResourcePoolParams {
const { type, testResourceDTO } = form.value;
const {
ip,
@ -678,7 +686,6 @@
nameSpaces,
concurrentNumber,
podThreads,
jobDefinition,
apiTestImage,
deployName,
}
@ -706,14 +713,16 @@
girdConcurrentNumber,
}
: {};
const jobDTO = isCheckedPerformance.value && isShowK8SResources ? { jobDefinition } : {};
return {
...form.value,
type: isShowTypeItem.value ? form.value.type : '',
type: isShowTypeItem.value ? form.value.type : 'Node', // Node
allOrg: form.value.orgType === 'allOrg',
apiTest: form.value.use.includes('API'), // api
loadTest: form.value.use.includes('performance'), //
uiTest: form.value.use.includes('UI'), // ui
testResourceDTO: { ...performanceDTO, ...apiDTO, ...uiDTO, orgIds: form.value.testResourceDTO.orgIds },
testResourceDTO: { ...performanceDTO, ...apiDTO, ...uiDTO, ...jobDTO, orgIds: form.value.testResourceDTO.orgIds },
};
}
@ -721,8 +730,13 @@
try {
loading.value = true;
const params = makeResourcePoolParams();
if (isEdit.value) {
await updatePoolInfo(params);
Message.success(t('system.resourcePool.updateSuccess'));
} else {
await addPool(params);
Message.success(t('system.resourcePool.addSuccess'));
}
if (isContinueAdd.value) {
resetForm();
} else {
@ -795,4 +809,3 @@
}
}
</style>
@/models/setting/resourcePool @/api/modules/setting/resourcePool

View File

@ -1,12 +1,17 @@
<template>
<div>
<MsCard :loading="loading" simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="primary" @click="addPool">
{{ t('system.resourcePool.createPool') }}
</a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('system.resourcePool.searchPool')"
class="w-[230px]"
allow-clear
@search="searchPool"
@press-enter="searchPool"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
@ -32,6 +37,7 @@
<MsTableMoreAction :list="tableActions" @select="handleSelect($event, record)"></MsTableMoreAction>
</template>
</ms-base-table>
</MsCard>
<MsDrawer
v-model:visible="showDetailDrawer"
width="480px"
@ -48,7 +54,12 @@
</a-button>
</template>
</MsDrawer>
<JobTemplateDrawer v-model:visible="showJobDrawer" :value="activePool?.testResourceDTO.jobDefinition || ''" />
<JobTemplateDrawer
v-model:visible="showJobDrawer"
:default-val="activePool?.testResourceDTO.jobDefinition || ''"
read-only
/>
</div>
</template>
<script setup lang="ts">
@ -56,14 +67,17 @@
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { getPoolList } from '@/api/modules/setting/resourcePool';
import { getPoolList, delPoolInfo, togglePoolStatus } from '@/api/modules/setting/resourcePool';
import useModal from '@/hooks/useModal';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import useTable from '@/components/pure/ms-table/useTable';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
import { TableKeyEnum } from '@/enums/tableEnum';
import { useTableStore } from '@/store';
import type { Description } from '@/components/pure/ms-description/index.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
@ -79,37 +93,46 @@
slotName: 'name',
dataIndex: 'name',
width: 200,
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmStatus',
slotName: 'enable',
dataIndex: 'enable',
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmDescription',
dataIndex: 'description',
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmType',
dataIndex: 'type',
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmCreateTime',
dataIndex: 'createTime',
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmUpdateTime',
dataIndex: 'updateTime',
showInTable: true,
},
{
title: 'system.resourcePool.tableColunmActions',
slotName: 'action',
fixed: 'right',
width: 120,
showInTable: true,
},
];
const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.SYSTEM_RESOURCEPOOL, columns, 'drawer');
const { propsRes, propsEvent, loadList, setKeyword } = useTable(getPoolList, {
tableKey: TableKeyEnum.SYSTEM_RESOURCEPOOL,
columns,
scroll: { y: 'auto' },
selectable: false,
@ -138,19 +161,25 @@
},
];
const loading = ref(false);
/**
* 启用资源池
*/
function enablePool(record: any) {
async function enablePool(record: any) {
try {
loading.value = true;
await togglePoolStatus(record.id);
Message.success(t('system.resourcePool.enablePoolSuccess'));
return true;
loadList();
} catch (error) {
console.log(error);
return false;
} finally {
loading.value = false;
}
}
const disableLoading = ref(false);
/**
* 禁用资源池
*/
@ -164,19 +193,30 @@
okButtonProps: {
status: 'danger',
},
cancelButtonProps: {
disabled: disableLoading.value,
},
okLoading: disableLoading.value,
maskClosable: false,
onBeforeOk: async () => {
try {
disableLoading.value = true;
await togglePoolStatus(record.id);
Message.success(t('system.resourcePool.disablePoolSuccess'));
loadList();
return true;
} catch (error) {
console.log(error);
return false;
} finally {
disableLoading.value = false;
}
},
hideCancel: false,
});
}
const delLoading = ref(false);
/**
* 删除资源池
*/
@ -190,13 +230,23 @@
okButtonProps: {
status: 'danger',
},
cancelButtonProps: {
disabled: delLoading.value,
},
maskClosable: false,
okLoading: delLoading.value,
onBeforeOk: async () => {
try {
delLoading.value = true;
await delPoolInfo(record.id);
Message.success(t('system.resourcePool.deletePoolSuccess'));
loadList();
return true;
} catch (error) {
console.log(error);
return false;
} finally {
delLoading.value = false;
}
},
hideCancel: false,
@ -242,6 +292,7 @@
podThreads, // k8s pod线
apiTestImage, // k8s api
deployName, // k8s api
girdConcurrentNumber,
nodesList,
loadTestImage,
loadTestHeap,
@ -290,6 +341,11 @@
label: t('system.resourcePool.testResourceDTO.podThreads'),
value: podThreads,
},
]
: [];
const jobTemplate =
loadTest && type === 'Kubernetes'
? [
{
label: t('system.resourcePool.jobTemplate'),
value: t('system.resourcePool.customJobTemplate'),
@ -324,7 +380,17 @@
},
{
label: t('system.resourcePool.concurrentNumber'),
value: concurrentNumber,
value: girdConcurrentNumber,
},
]
: [];
const detailType =
apiTest || loadTest
? [
{
label: t('system.resourcePool.detailType'),
value: activePool.value.type,
},
]
: [];
@ -341,7 +407,7 @@
label: t('system.resourcePool.detailRange'),
value: activePool.value.allOrg
? [t('system.resourcePool.orgAll')]
: activePool.value.testResourceDTO.orgIds.join(','),
: activePool.value.testResourceDTO.orgIdNameMap.map((e) => e.name),
isTag: true,
},
{
@ -351,11 +417,9 @@
},
...performanceDesc,
...uiDesc,
{
label: t('system.resourcePool.detailType'),
value: activePool.value.type,
},
...detailType,
...resourceDesc,
...jobTemplate,
];
}
@ -379,7 +443,7 @@
* 添加资源池
* @param record
*/
function addPool(record: any) {
function addPool() {
router.push({
name: 'settingSystemResourcePoolDetail',
});
@ -387,4 +451,3 @@
</script>
<style lang="less" scoped></style>
@/models/setting/resourcePool @/api/modules/setting/resourcePool