feat: 资源池页面
This commit is contained in:
parent
cd57074414
commit
025965b4ee
|
@ -1,6 +1,7 @@
|
||||||
import MSR from '@/api/http/index';
|
import MSR from '@/api/http/index';
|
||||||
import { PoolListUrl, UpdatePoolUrl, AddPoolUrl, DetailPoolUrl } from '@/api/requrls/system/resourcePool';
|
import { PoolListUrl, UpdatePoolUrl, AddPoolUrl, DetailPoolUrl } from '@/api/requrls/system/resourcePool';
|
||||||
|
|
||||||
|
import type { LocationQueryValue } from 'vue-router';
|
||||||
import type { ResourcePoolItem, AddResourcePoolParams } from '@/models/system/resourcePool';
|
import type { ResourcePoolItem, AddResourcePoolParams } from '@/models/system/resourcePool';
|
||||||
import type { TableQueryParams } from '@/models/common';
|
import type { TableQueryParams } from '@/models/common';
|
||||||
|
|
||||||
|
@ -20,6 +21,6 @@ export function addPool(data: AddResourcePoolParams) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取资源池详情
|
// 获取资源池详情
|
||||||
export function getPoolInfo(poolId: string) {
|
export function getPoolInfo(poolId: LocationQueryValue | LocationQueryValue[]) {
|
||||||
return MSR.get<ResourcePoolItem>({ url: DetailPoolUrl, params: poolId });
|
return MSR.get<ResourcePoolItem>({ url: DetailPoolUrl, params: poolId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,32 +3,32 @@
|
||||||
<div class="mb-[16px] overflow-y-auto rounded-[4px] bg-[var(--color-fill-1)] p-[12px]">
|
<div class="mb-[16px] overflow-y-auto rounded-[4px] bg-[var(--color-fill-1)] p-[12px]">
|
||||||
<a-scrollbar class="overflow-y-auto" :style="{ 'max-height': props.maxHeight }">
|
<a-scrollbar class="overflow-y-auto" :style="{ 'max-height': props.maxHeight }">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-[8px]">
|
<div class="flex flex-wrap items-start justify-between gap-[8px]">
|
||||||
<template v-for="(order, i) of form.list" :key="`form-item-${order}`">
|
<template v-for="(item, i) of form.list" :key="`form-item-${i}`">
|
||||||
<div class="flex w-full items-start justify-between gap-[8px]">
|
<div class="flex w-full items-start justify-between gap-[8px]">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
v-for="item of props.models"
|
v-for="model of props.models"
|
||||||
:key="`${item.filed}${order}`"
|
:key="`${model.filed}${i}`"
|
||||||
:field="`${item.filed}${order}`"
|
:field="`list[${i}].${model.filed}`"
|
||||||
:class="i > 0 ? 'hidden-item' : 'mb-0 flex-1'"
|
:class="i > 0 ? 'hidden-item' : 'mb-0 flex-1'"
|
||||||
:label="i === 0 && item.label ? t(item.label) : ''"
|
:label="i === 0 && model.label ? t(model.label) : ''"
|
||||||
:rules="item.rules"
|
:rules="model.rules"
|
||||||
asterisk-position="end"
|
asterisk-position="end"
|
||||||
>
|
>
|
||||||
<a-input
|
<a-input
|
||||||
v-if="item.type === 'input'"
|
v-if="model.type === 'input'"
|
||||||
v-model="form[`${item.filed}${order}`]"
|
v-model="item[model.filed]"
|
||||||
class="mb-[4px] flex-1"
|
class="mb-[4px] flex-1"
|
||||||
:placeholder="t(item.placeholder || '')"
|
:placeholder="t(model.placeholder || '')"
|
||||||
:max-length="item.maxLength || 250"
|
:max-length="model.maxLength || 250"
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
<a-input-number
|
<a-input-number
|
||||||
v-if="item.type === 'inputNumber'"
|
v-if="model.type === 'inputNumber'"
|
||||||
v-model="form[`${item.filed}${order}`]"
|
v-model="item[model.filed]"
|
||||||
class="mb-[4px] flex-1"
|
class="mb-[4px] flex-1"
|
||||||
:placeholder="t(item.placeholder || '')"
|
:placeholder="t(model.placeholder || '')"
|
||||||
:min="item.min"
|
:min="model.min"
|
||||||
:max="item.max || 9999999"
|
:max="model.max || 9999999"
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
'text-[var(--color-text-brand)]',
|
'text-[var(--color-text-brand)]',
|
||||||
i === 0 ? 'mt-[36px]' : 'mt-[5px]',
|
i === 0 ? 'mt-[36px]' : 'mt-[5px]',
|
||||||
]"
|
]"
|
||||||
@click="removeField(order, i)"
|
@click="removeField(i)"
|
||||||
>
|
>
|
||||||
<icon-minus-circle />
|
<icon-minus-circle />
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,8 +65,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watchEffect } from 'vue';
|
import { ref, watchEffect, unref } from 'vue';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { scrollIntoView } from '@/utils/dom';
|
||||||
|
|
||||||
import type { ValidatedError, FormInstance } from '@arco-design/web-vue';
|
import type { ValidatedError, FormInstance } from '@arco-design/web-vue';
|
||||||
import type { FormItemModel, FormMode, ValueType } from './types';
|
import type { FormItemModel, FormMode, ValueType } from './types';
|
||||||
|
@ -81,7 +82,7 @@
|
||||||
maxHeight?: string;
|
maxHeight?: string;
|
||||||
valueType?: ValueType;
|
valueType?: ValueType;
|
||||||
delimiter?: string; // 当valueType为 string 类型时的分隔符,默认为英文逗号,
|
delimiter?: string; // 当valueType为 string 类型时的分隔符,默认为英文逗号,
|
||||||
defaultVals?: Record<string, string | string[] | number[]>; // 当外层是编辑状态时,可传入已填充的数据
|
defaultVals?: any[]; // 当外层是编辑状态时,可传入已填充的数据
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
valueType: 'Array',
|
valueType: 'Array',
|
||||||
|
@ -91,10 +92,11 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
list: [0],
|
list: [] as Record<string, any>[],
|
||||||
};
|
};
|
||||||
const form = ref<Record<string, any>>({ ...defaultForm });
|
const form = ref<Record<string, any>>({ list: [...defaultForm.list] });
|
||||||
const formRef = ref<FormInstance | null>(null);
|
const formRef = ref<FormInstance | null>(null);
|
||||||
|
const formItem: Record<string, any> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监测defaultVals和models的变化
|
* 监测defaultVals和models的变化
|
||||||
|
@ -103,42 +105,17 @@
|
||||||
*/
|
*/
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
props.models.forEach((e) => {
|
props.models.forEach((e) => {
|
||||||
form.value[`${e.filed}0`] = e.type === 'inputNumber' ? null : '';
|
formItem[e.filed] = e.type === 'inputNumber' ? null : '';
|
||||||
});
|
});
|
||||||
if (props.defaultVals) {
|
form.value.list = [{ ...formItem }];
|
||||||
// 重置表单,因为组件初始化后可能输入过值或创建过表单项
|
if (props.defaultVals?.length) {
|
||||||
form.value = { list: [0] };
|
|
||||||
// 取出defaultVals的表单 filed
|
// 取出defaultVals的表单 filed
|
||||||
const arr = Object.keys(props.defaultVals);
|
form.value.list = props.defaultVals.map((e) => e);
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
const filed = arr[i];
|
|
||||||
// 取出当前 filed 的默认值
|
|
||||||
const dVals = props.defaultVals[filed];
|
|
||||||
// 判断默认值为数组还是字符串,字符串需要根据传入的分隔符delimiter分割
|
|
||||||
const vals = Array.isArray(dVals) ? dVals : dVals.split(`${props.delimiter}`);
|
|
||||||
// 遍历当前 filed 的默认值数组,填充至表单对象
|
|
||||||
vals.forEach((val, order) => {
|
|
||||||
form.value[`${filed}${order}`] = val;
|
|
||||||
if (i === 0 && order > 0) {
|
|
||||||
// 行数只需要遍历一次字段的值数组长度即可
|
|
||||||
form.value.list.push(order);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFormResult() {
|
function getFormResult() {
|
||||||
const res: Record<string, any> = {};
|
return unref<Record<string, any>[]>(form.value.list);
|
||||||
props.models.forEach((e) => {
|
|
||||||
res[e.filed] = [];
|
|
||||||
});
|
|
||||||
form.value.list.forEach((e: number) => {
|
|
||||||
props.models.forEach((m) => {
|
|
||||||
res[m.filed].push(form.value[`${m.filed}${e}`]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -146,15 +123,15 @@
|
||||||
* @param cb 校验通过后执行回调
|
* @param cb 校验通过后执行回调
|
||||||
* @param isSubmit 是否需要将表单值拼接后传入回调函数
|
* @param isSubmit 是否需要将表单值拼接后传入回调函数
|
||||||
*/
|
*/
|
||||||
function formValidate(cb: (res?: Record<string, string[] | string>) => void, isSubmit = true) {
|
function formValidate(cb: (res?: Record<string, any>[]) => void, isSubmit = true) {
|
||||||
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
|
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
|
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === 'function') {
|
||||||
if (isSubmit) {
|
if (isSubmit) {
|
||||||
const res = getFormResult();
|
cb(getFormResult());
|
||||||
cb(props.valueType === 'Array' ? res : res.join(','));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
|
@ -167,24 +144,15 @@
|
||||||
*/
|
*/
|
||||||
function addField() {
|
function addField() {
|
||||||
formValidate(() => {
|
formValidate(() => {
|
||||||
const lastIndex = form.value.list.length - 1;
|
form.value.list.push({ ...formItem }); // 序号自增,不会因为删除而重复
|
||||||
const lastOrder = form.value.list[lastIndex] + 1;
|
|
||||||
form.value.list.push(lastOrder); // 序号自增,不会因为删除而重复
|
|
||||||
props.models.forEach((e) => {
|
|
||||||
form.value[`${e.filed}${lastOrder}`] = e.type === 'inputNumber' ? null : '';
|
|
||||||
});
|
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除表单项
|
* 移除表单项
|
||||||
* @param index 表单项的序号
|
|
||||||
* @param i 表单项对应 list 的下标
|
* @param i 表单项对应 list 的下标
|
||||||
*/
|
*/
|
||||||
function removeField(index: number, i: number) {
|
function removeField(i: number) {
|
||||||
props.models.forEach((e) => {
|
|
||||||
delete form.value[`${e.filed}${index}`];
|
|
||||||
});
|
|
||||||
form.value.list.splice(i, 1);
|
form.value.list.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,8 @@ declare const _default: import('vue').DefineComponent<
|
||||||
import('vue').ComponentOptionsMixin,
|
import('vue').ComponentOptionsMixin,
|
||||||
import('vue').ComponentOptionsMixin,
|
import('vue').ComponentOptionsMixin,
|
||||||
{
|
{
|
||||||
formValidate: (cb: (res?: Record<string, string[] | string>) => void, isSubmit = true) => void;
|
formValidate: (cb: (res?: Record<string, any>) => void, isSubmit = true) => void;
|
||||||
|
getFormResult: <T>() => T[];
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ export interface TestResourceDTO {
|
||||||
apiTestImage: string; // k8s api测试镜像
|
apiTestImage: string; // k8s api测试镜像
|
||||||
deployName: string; // k8s api测试部署名称
|
deployName: string; // k8s api测试部署名称
|
||||||
uiGrid: string; // ui测试selenium-grid
|
uiGrid: string; // ui测试selenium-grid
|
||||||
|
girdConcurrentNumber: number; // ui测试selenium-grid最大并发数
|
||||||
orgIds: string[]; // 应用范围选择指定组织时的id集合
|
orgIds: string[]; // 应用范围选择指定组织时的id集合
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,3 +88,14 @@ export const downloadStringFile = (type: string, content: string, fileName: stri
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 休眠
|
||||||
|
* @param ms 睡眠时长,单位毫秒
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(), ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<MsDrawer v-model:visible="showJobDrawer" width="680px" :title="t('system.resourcePool.customJobTemplate')">
|
||||||
|
<MsCodeEditor
|
||||||
|
v-model:model-value="jobDefinition"
|
||||||
|
title="YAML"
|
||||||
|
width="100%"
|
||||||
|
height="calc(100vh - 205px)"
|
||||||
|
theme="MS-text"
|
||||||
|
/>
|
||||||
|
</MsDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
value: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value', 'update:visible']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const showJobDrawer = ref(props.visible);
|
||||||
|
const jobDefinition = ref(props.value);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
showJobDrawer.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => showJobDrawer.value,
|
||||||
|
(val) => {
|
||||||
|
emit('update:visible', val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
|
@ -1,7 +1,789 @@
|
||||||
<template>
|
<template>
|
||||||
|
<MsCard :loading="loading" :title="title" @save="beforeSave" @save-and-continue="beforeSave(true)">
|
||||||
|
<a-form ref="formRef" :model="form" layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.name')"
|
||||||
|
field="name"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.nameRequired') }]"
|
||||||
|
class="form-item"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.name"
|
||||||
|
:placeholder="t('system.resourcePool.namePlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('system.resourcePool.desc')" field="description" class="form-item">
|
||||||
|
<a-textarea
|
||||||
|
v-model:model-value="form.description"
|
||||||
|
:placeholder="t('system.resourcePool.descPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-textarea>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('system.resourcePool.serverUrl')" field="serverUrl" class="form-item">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.serverUrl"
|
||||||
|
:placeholder="t('system.resourcePool.rootUrlPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('system.resourcePool.orgRange')" field="orgType" class="form-item">
|
||||||
|
<a-radio-group v-model:model-value="form.orgType">
|
||||||
|
<a-radio value="allOrg">
|
||||||
|
{{ t('system.resourcePool.orgAll') }}
|
||||||
|
<a-tooltip :content="t('system.resourcePool.orgRangeTip')" position="top" mini>
|
||||||
|
<icon-question-circle class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
|
||||||
|
</a-tooltip>
|
||||||
|
</a-radio>
|
||||||
|
<a-radio value="set">{{ t('system.resourcePool.orgSetup') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
v-if="isSpecifiedOrg"
|
||||||
|
:label="t('system.resourcePool.orgSelect')"
|
||||||
|
class="form-item"
|
||||||
|
field="testResourceDTO.orgIds"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.orgRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model="form.testResourceDTO.orgIds"
|
||||||
|
:placeholder="t('system.resourcePool.orgPlaceholder')"
|
||||||
|
multiple
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
|
<a-option v-for="org of orgOptons" :key="org.id" :value="org.value">{{ org.label }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.use')"
|
||||||
|
field="use"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.useRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-checkbox-group v-model:model-value="form.use">
|
||||||
|
<a-checkbox v-for="use of useList" :key="use.value" :value="use.value">{{ t(use.label) }}</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="isCheckedPerformance">
|
||||||
|
<a-form-item :label="t('system.resourcePool.mirror')" field="testResourceDTO.loadTestImage" class="form-item">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.loadTestImage"
|
||||||
|
:placeholder="t('system.resourcePool.mirrorPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('system.resourcePool.testHeap')" field="testResourceDTO.loadTestHeap" class="form-item">
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.loadTestHeap"
|
||||||
|
:placeholder="t('system.resourcePool.testHeapPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
<div class="mt-[4px] text-[12px] text-[var(--color-text-4)]">
|
||||||
|
{{ t('system.resourcePool.testHeapExample', { heap: defaultHeap }) }}
|
||||||
|
<MsIcon
|
||||||
|
type="icon-icon_corner_right_up"
|
||||||
|
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
||||||
|
@click="fillHeapByDefault"
|
||||||
|
></MsIcon>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="isCheckedUI">
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.uiGrid')"
|
||||||
|
field="testResourceDTO.uiGrid"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.uiGridRequired') }]"
|
||||||
|
asterisk-position="end"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.uiGrid"
|
||||||
|
:placeholder="t('system.resourcePool.uiGridPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
<div class="mt-[4px] text-[12px] text-[var(--color-text-4)]">
|
||||||
|
{{ t('system.resourcePool.uiGridExample', { grid: defaultGrid }) }}
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.girdConcurrentNumber')"
|
||||||
|
field="girdConcurrentNumber"
|
||||||
|
class="form-item"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:model-value="form.testResourceDTO.girdConcurrentNumber"
|
||||||
|
:min="1"
|
||||||
|
:max="9999999"
|
||||||
|
:step="1"
|
||||||
|
mode="button"
|
||||||
|
class="w-[160px]"
|
||||||
|
></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-form-item v-if="isShowTypeItem" :label="t('system.resourcePool.type')" field="type" class="form-item">
|
||||||
|
<a-radio-group v-model:model-value="form.type" type="button" @change="changeResourceType">
|
||||||
|
<a-radio value="Node">Node</a-radio>
|
||||||
|
<a-radio value="Kubernetes">Kubernetes</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="isShowNodeResources">
|
||||||
|
<a-form-item field="addType" class="form-item">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ t('system.resourcePool.addResource') }}
|
||||||
|
<a-tooltip :content="t('system.resourcePool.changeAddTypeTip')" position="tl" mini>
|
||||||
|
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="!getIsVisited()"
|
||||||
|
class="ms-pop-confirm"
|
||||||
|
position="br"
|
||||||
|
:ok-text="t('system.resourcePool.batchAddTipConfirm')"
|
||||||
|
@popup-visible-change="handlePopChange"
|
||||||
|
>
|
||||||
|
<template #cancel-text>
|
||||||
<div> </div>
|
<div> </div>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="font-semibold text-[var(--color-text-1)]">
|
||||||
|
{{ t('system.resourcePool.changeAddTypePopTitle') }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-[8px] w-[290px] text-[12px] text-[var(--color-text-2)]">
|
||||||
|
{{ t('system.resourcePool.changeAddTypeTip') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a-radio-group v-model:model-value="form.addType" type="button" @change="handleTypeChange">
|
||||||
|
<a-radio value="single">{{ t('system.resourcePool.singleAdd') }}</a-radio>
|
||||||
|
<a-radio value="multiple">{{ t('system.resourcePool.batchAdd') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-radio-group v-else v-model:model-value="form.addType" type="button" @change="handleTypeChange">
|
||||||
|
<a-radio value="single">{{ t('system.resourcePool.singleAdd') }}</a-radio>
|
||||||
|
<a-radio value="multiple">{{ t('system.resourcePool.batchAdd') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<MsBatchForm
|
||||||
|
v-show="form.addType === 'single'"
|
||||||
|
ref="batchFormRef"
|
||||||
|
:models="batchFormModels"
|
||||||
|
:form-mode="isEdit ? 'edit' : 'create'"
|
||||||
|
add-text="system.resourcePool.addResource"
|
||||||
|
:default-vals="defaultVals"
|
||||||
|
max-height="250px"
|
||||||
|
></MsBatchForm>
|
||||||
|
<!-- TODO:代码编辑器懒加载 -->
|
||||||
|
<div v-show="form.addType === 'multiple'">
|
||||||
|
<MsCodeEditor v-model:model-value="editorContent" width="100%" height="400px" theme="MS-text">
|
||||||
|
<template #title>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.batchAddResource')"
|
||||||
|
asterisk-position="end"
|
||||||
|
class="hide-wrapper mb-0"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</MsCodeEditor>
|
||||||
|
<div class="mb-[24px] text-[12px] text-[var(--color-text-4)]">
|
||||||
|
{{ t('system.resourcePool.nodeConfigEditorTip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isShowK8SResources">
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.ip')"
|
||||||
|
field="testResourceDTO.ip"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.testResourceDTO.ipRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.ip"
|
||||||
|
:placeholder="t('system.resourcePool.testResourceDTO.ipPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
<div class="mt-[4px] text-[12px] text-[var(--color-text-4)]">
|
||||||
|
{{ t('system.resourcePool.testResourceDTO.ipSubTip', { ip: '100.0.0.100', domain: 'example.com' }) }}
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.token')"
|
||||||
|
field="testResourceDTO.token"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.testResourceDTO.tokenRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.token"
|
||||||
|
:placeholder="t('system.resourcePool.testResourceDTO.tokenPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.nameSpaces')"
|
||||||
|
field="testResourceDTO.nameSpaces"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.testResourceDTO.nameSpacesRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.nameSpaces"
|
||||||
|
:placeholder="t('system.resourcePool.testResourceDTO.nameSpacesPlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
class="mr-[8px] flex-1"
|
||||||
|
></a-input>
|
||||||
|
<a-tooltip
|
||||||
|
:content="t('system.resourcePool.testResourceDTO.downloadRoleYamlTip')"
|
||||||
|
:disabled="isFillNameSpaces"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<a-button type="outline" :disabled="!isFillNameSpaces" @click="downloadYaml('role')">
|
||||||
|
{{ t('system.resourcePool.testResourceDTO.downloadRoleYaml') }}
|
||||||
|
</a-button>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.apiTestImage')"
|
||||||
|
field="testResourceDTO.apiTestImage"
|
||||||
|
class="form-item"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.apiTestImage"
|
||||||
|
:placeholder="t('system.resourcePool.testResourceDTO.apiTestImagePlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.deployName')"
|
||||||
|
field="testResourceDTO.deployName"
|
||||||
|
class="form-item"
|
||||||
|
:rules="[{ required: true, message: t('system.resourcePool.testResourceDTO.deployNameRequired') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.testResourceDTO.deployName"
|
||||||
|
:placeholder="t('system.resourcePool.testResourceDTO.deployNamePlaceholder')"
|
||||||
|
:max-length="250"
|
||||||
|
class="mr-[8px] flex-1"
|
||||||
|
></a-input>
|
||||||
|
<a-tooltip
|
||||||
|
:content="t('system.resourcePool.testResourceDTO.downloadDeployYamlTip')"
|
||||||
|
:disabled="isFillNameSpacesAndDeployName"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<a-dropdown :popup-max-height="false" @select="downloadYaml($event as YamlType)">
|
||||||
|
<a-button type="outline" :disabled="!isFillNameSpacesAndDeployName">
|
||||||
|
{{ t('system.resourcePool.testResourceDTO.downloadRoleYaml') }}<icon-down />
|
||||||
|
</a-button>
|
||||||
|
<template #content>
|
||||||
|
<a-doption value="DaemonSet">DaemonSet.yml</a-doption>
|
||||||
|
<a-doption value="Deployment">Deployment.yml</a-doption>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.concurrentNumber')"
|
||||||
|
field="testResourceDTO.concurrentNumber"
|
||||||
|
class="form-item"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:model-value="form.testResourceDTO.concurrentNumber"
|
||||||
|
:min="1"
|
||||||
|
:max="9999999"
|
||||||
|
:step="1"
|
||||||
|
mode="button"
|
||||||
|
class="w-[160px]"
|
||||||
|
></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
:label="t('system.resourcePool.testResourceDTO.podThreads')"
|
||||||
|
field="testResourceDTO.podThreads"
|
||||||
|
class="form-item"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:model-value="form.testResourceDTO.podThreads"
|
||||||
|
:min="1"
|
||||||
|
:max="9999999"
|
||||||
|
:step="1"
|
||||||
|
mode="button"
|
||||||
|
class="w-[160px]"
|
||||||
|
></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
</a-form>
|
||||||
|
<template #footerLeft>
|
||||||
|
<a-button v-if="isCheckedPerformance" type="text" @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>
|
||||||
|
</template>
|
||||||
|
</MsCard>
|
||||||
|
<JobTemplateDrawer v-model:visible="showJobDrawer" :value="form.testResourceDTO.jobDefinition" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { computed, Ref, ref, watchEffect } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { Message, FormInstance, SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useVisit from '@/hooks/useVisit';
|
||||||
|
import MsCard from '@/components/pure/ms-card/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/system/resourcePool';
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
import type { MsBatchFormInstance, FormItemModel } from '@/components/bussiness/ms-batch-form/types';
|
||||||
|
import type { AddResourcePoolParams, NodesListItem } from '@/models/system/resourcePool';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const title = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const defaultForm = {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
enable: true,
|
||||||
|
description: '',
|
||||||
|
serverUrl: '',
|
||||||
|
orgType: 'allOrg',
|
||||||
|
use: ['performance', 'API'],
|
||||||
|
type: 'Node',
|
||||||
|
addType: 'single',
|
||||||
|
testResourceDTO: {
|
||||||
|
loadTestImage: '',
|
||||||
|
loadTestHeap: '',
|
||||||
|
uiGrid: '',
|
||||||
|
girdConcurrentNumber: 1,
|
||||||
|
podThreads: 1,
|
||||||
|
concurrentNumber: 1,
|
||||||
|
nodesList: [] as NodesListItem[],
|
||||||
|
ip: '',
|
||||||
|
token: '',
|
||||||
|
nameSpaces: '',
|
||||||
|
jobDefinition: job,
|
||||||
|
apiTestImage: '',
|
||||||
|
deployName: '',
|
||||||
|
orgIds: [] as string[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const form = ref({ ...defaultForm });
|
||||||
|
const formRef = ref<FormInstance | null>(null);
|
||||||
|
const orgOptons = ref<SelectOptionData>([]);
|
||||||
|
const useList = [
|
||||||
|
{
|
||||||
|
label: 'system.resourcePool.usePerformance',
|
||||||
|
value: 'performance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'system.resourcePool.useAPI',
|
||||||
|
value: 'API',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'system.resourcePool.useUI',
|
||||||
|
value: 'UI',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const defaultGrid = 'http://selenium-hub:4444';
|
||||||
|
|
||||||
|
async function initPoolInfo() {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getPoolInfo(route.query.id);
|
||||||
|
form.value = {
|
||||||
|
...res,
|
||||||
|
addType: 'single',
|
||||||
|
orgType: res.allOrg ? 'allOrg' : 'set',
|
||||||
|
use: [res.loadTest ? 'performance' : '', res.apiTest ? 'API' : '', res.uiTest ? 'UI' : ''].filter((e) => e),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEdit = ref(false);
|
||||||
|
watchEffect(() => {
|
||||||
|
// 判断当前是编辑还是新增
|
||||||
|
if (route.query.id) {
|
||||||
|
title.value = t('menu.settings.system.resourcePoolEdit');
|
||||||
|
isEdit.value = true;
|
||||||
|
initPoolInfo();
|
||||||
|
} else {
|
||||||
|
title.value = t('menu.settings.system.resourcePoolDetail');
|
||||||
|
isEdit.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultHeap = '-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m';
|
||||||
|
function fillHeapByDefault() {
|
||||||
|
form.value.testResourceDTO.loadTestHeap = defaultHeap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitedKey = 'changeAddResourceType';
|
||||||
|
const { addVisited, getIsVisited } = useVisit(visitedKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换类型提示确认框隐藏时,设置已访问标志
|
||||||
|
* @param visible 显示/隐藏
|
||||||
|
*/
|
||||||
|
function handlePopChange(visible: boolean) {
|
||||||
|
if (!visible) {
|
||||||
|
addVisited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制表单项显示隐藏逻辑计算器
|
||||||
|
*/
|
||||||
|
// 是否选择应用组织为指定组织
|
||||||
|
const isSpecifiedOrg = computed(() => form.value.orgType === 'set');
|
||||||
|
// 是否勾选了性能测试
|
||||||
|
const isCheckedPerformance = computed(() => form.value.use.includes('performance'));
|
||||||
|
// 是否勾选了UI测试
|
||||||
|
const isCheckedUI = computed(() => form.value.use.includes('UI'));
|
||||||
|
// 是否勾选了接口测试
|
||||||
|
const isCheckedAPI = computed(() => form.value.use.includes('API'));
|
||||||
|
// 是否显示类型切换表单项
|
||||||
|
const isShowTypeItem = computed(() => ['API', 'performance'].some((s) => form.value.use.includes(s)));
|
||||||
|
// 是否显示Node资源配置信息
|
||||||
|
const isShowNodeResources = computed(() => form.value.type === 'Node' && isShowTypeItem.value);
|
||||||
|
// 是否显示K8S资源配置信息
|
||||||
|
const isShowK8SResources = computed(() => form.value.type === 'Kubernetes' && isShowTypeItem.value);
|
||||||
|
// 是否填写了命名空间
|
||||||
|
const isFillNameSpaces = computed(() => form.value.testResourceDTO.nameSpaces.trim() !== '');
|
||||||
|
// 是否填写了命名空间及Deploy Name
|
||||||
|
const isFillNameSpacesAndDeployName = computed(
|
||||||
|
() => isFillNameSpaces.value && form.value.testResourceDTO.deployName.trim() !== ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchFormRef = ref<MsBatchFormInstance | null>(null);
|
||||||
|
const batchFormModels: Ref<FormItemModel[]> = ref([
|
||||||
|
{
|
||||||
|
filed: 'ip',
|
||||||
|
type: 'input',
|
||||||
|
label: 'system.resourcePool.ip',
|
||||||
|
rules: [{ required: true, message: t('system.resourcePool.ipRequired') }],
|
||||||
|
placeholder: 'system.resourcePool.ipPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filed: 'port',
|
||||||
|
type: 'input',
|
||||||
|
label: 'system.resourcePool.port',
|
||||||
|
rules: [{ required: true, message: t('system.resourcePool.portRequired') }],
|
||||||
|
placeholder: 'system.resourcePool.portPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filed: 'monitor',
|
||||||
|
type: 'input',
|
||||||
|
label: 'system.resourcePool.monitor',
|
||||||
|
rules: [{ required: true, message: t('system.resourcePool.monitorRequired') }],
|
||||||
|
placeholder: 'system.resourcePool.monitorPlaceholder',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filed: 'concurrentNumber',
|
||||||
|
type: 'inputNumber',
|
||||||
|
label: 'system.resourcePool.concurrentNumber',
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: t('system.resourcePool.concurrentNumberRequired') },
|
||||||
|
{
|
||||||
|
validator: (val, cb) => {
|
||||||
|
if (val <= 0) {
|
||||||
|
cb(t('system.resourcePool.concurrentNumberMin'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'system.resourcePool.concurrentNumberPlaceholder',
|
||||||
|
min: 1,
|
||||||
|
max: 9999999,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 动态表单默认值
|
||||||
|
const defaultVals = computed(() => {
|
||||||
|
const { nodesList } = form.value.testResourceDTO;
|
||||||
|
return nodesList.map((node) => node);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 代码编辑器内容
|
||||||
|
const editorContent = ref('');
|
||||||
|
|
||||||
|
// 代码编辑器内容根据动态表单的内容拼接
|
||||||
|
watchEffect(() => {
|
||||||
|
const { nodesList } = form.value.testResourceDTO;
|
||||||
|
let res = '';
|
||||||
|
for (let i = 0; i < nodesList.length; i++) {
|
||||||
|
const node = nodesList[i];
|
||||||
|
// 按顺序拼接:ip、port、monitor、concurrentNumber
|
||||||
|
if (Object.values(node).every((e) => e !== '')) {
|
||||||
|
res += `${node.ip},${node.port},${node.monitor},${node.concurrentNumber}\r`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editorContent.value = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取动态表单项输入的内容
|
||||||
|
*/
|
||||||
|
function setBatchFormRes() {
|
||||||
|
const res = batchFormRef.value?.getFormResult<NodesListItem>();
|
||||||
|
if (res?.length) {
|
||||||
|
form.value.testResourceDTO.nodesList = res.map((e) => e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析代码编辑器内容
|
||||||
|
*/
|
||||||
|
function analyzeCode() {
|
||||||
|
const arr = editorContent.value.split('\r'); // 以回车符分割
|
||||||
|
// 将代码编辑器内写的内容抽取出来
|
||||||
|
arr.forEach((e, i) => {
|
||||||
|
e = e.replaceAll('\n', ''); // 去除换行符
|
||||||
|
if (e.trim() !== '') {
|
||||||
|
// 排除空串
|
||||||
|
const line = e.split(',');
|
||||||
|
if (line.every((s) => s.trim() !== '') && !Number.isNaN(Number(line[3]))) {
|
||||||
|
const item = {
|
||||||
|
ip: line[0],
|
||||||
|
port: line[1],
|
||||||
|
monitor: line[2],
|
||||||
|
concurrentNumber: Number(line[3]),
|
||||||
|
};
|
||||||
|
if (form.value.testResourceDTO.nodesList.length === 0) {
|
||||||
|
// 第四个是concurrentNumber,需要是数字
|
||||||
|
form.value.testResourceDTO.nodesList.push(item);
|
||||||
|
} else {
|
||||||
|
form.value.testResourceDTO.nodesList = [item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换资源添加类型
|
||||||
|
* @param val 切换的类型
|
||||||
|
*/
|
||||||
|
function handleTypeChange(val: string | number | boolean) {
|
||||||
|
if (val === 'single') {
|
||||||
|
// 从批量添加切换至单个添加,需要解析代码编辑器内容
|
||||||
|
analyzeCode();
|
||||||
|
} else if (val === 'multiple') {
|
||||||
|
// 从单个添加切换到批量添加,需要先提取组件的输入框内容
|
||||||
|
setBatchFormRes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeResourceType(val: string | number | boolean) {
|
||||||
|
if (val === 'Kubernetes') {
|
||||||
|
setBatchFormRes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiImageTag = ref('dev');
|
||||||
|
/**
|
||||||
|
* 下载 yaml 文件
|
||||||
|
* @param type 文件类型
|
||||||
|
*/
|
||||||
|
function downloadYaml(type: YamlType) {
|
||||||
|
let name = '';
|
||||||
|
let yamlStr = '';
|
||||||
|
const { nameSpaces, deployName, apiTestImage } = form.value.testResourceDTO;
|
||||||
|
let apiImage = `registry.cn-qingdao.aliyuncs.com/metersphere/node-controller:${apiImageTag.value}`;
|
||||||
|
if (apiTestImage) {
|
||||||
|
apiImage = apiTestImage;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'role':
|
||||||
|
name = 'Role.yml';
|
||||||
|
yamlStr = getYaml('role', '', nameSpaces, '');
|
||||||
|
break;
|
||||||
|
case 'Deployment':
|
||||||
|
name = 'Deployment.yml';
|
||||||
|
yamlStr = getYaml('Deployment', deployName, nameSpaces, apiImage);
|
||||||
|
break;
|
||||||
|
case 'DaemonSet':
|
||||||
|
name = 'Daemonset.yml';
|
||||||
|
yamlStr = getYaml('DaemonSet', deployName, nameSpaces, apiImage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('文件类型不在可选范围');
|
||||||
|
}
|
||||||
|
downloadStringFile('text/yaml', yamlStr, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showJobDrawer = ref(false);
|
||||||
|
const isContinueAdd = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置表单信息
|
||||||
|
*/
|
||||||
|
function resetForm() {
|
||||||
|
form.value = { ...defaultForm };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接添加资源池参数
|
||||||
|
*/
|
||||||
|
function makeResourcePoolParams(): AddResourcePoolParams {
|
||||||
|
const { type, testResourceDTO } = form.value;
|
||||||
|
const {
|
||||||
|
ip,
|
||||||
|
token, // k8s token
|
||||||
|
nameSpaces, // k8s 命名空间
|
||||||
|
concurrentNumber, // k8s 最大并发数
|
||||||
|
podThreads, // k8s 单pod最大线程数
|
||||||
|
jobDefinition, // k8s job自定义模板
|
||||||
|
apiTestImage, // k8s api测试镜像
|
||||||
|
deployName, // k8s api测试部署名称
|
||||||
|
nodesList,
|
||||||
|
loadTestImage,
|
||||||
|
loadTestHeap,
|
||||||
|
uiGrid,
|
||||||
|
girdConcurrentNumber,
|
||||||
|
} = testResourceDTO;
|
||||||
|
// Node资源
|
||||||
|
const nodeResourceDTO = {
|
||||||
|
nodesList: type === 'Node' ? nodesList : [],
|
||||||
|
};
|
||||||
|
// K8S资源
|
||||||
|
const k8sResourceDTO =
|
||||||
|
type === 'Kubernetes'
|
||||||
|
? {
|
||||||
|
ip,
|
||||||
|
token,
|
||||||
|
nameSpaces,
|
||||||
|
concurrentNumber,
|
||||||
|
podThreads,
|
||||||
|
jobDefinition,
|
||||||
|
apiTestImage,
|
||||||
|
deployName,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
// 性能测试资源
|
||||||
|
const performanceDTO = isCheckedPerformance.value
|
||||||
|
? {
|
||||||
|
loadTestImage,
|
||||||
|
loadTestHeap,
|
||||||
|
...nodeResourceDTO,
|
||||||
|
...k8sResourceDTO,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
// 接口测试资源
|
||||||
|
const apiDTO = isCheckedAPI.value
|
||||||
|
? {
|
||||||
|
...nodeResourceDTO,
|
||||||
|
...k8sResourceDTO,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
// ui 测试资源
|
||||||
|
const uiDTO = isCheckedUI.value
|
||||||
|
? {
|
||||||
|
uiGrid,
|
||||||
|
girdConcurrentNumber,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
...form.value,
|
||||||
|
type: isShowTypeItem.value ? form.value.type : '',
|
||||||
|
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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const params = makeResourcePoolParams();
|
||||||
|
await addPool(params);
|
||||||
|
Message.success(t('system.resourcePool.addSuccess'));
|
||||||
|
if (isContinueAdd.value) {
|
||||||
|
resetForm();
|
||||||
|
} else {
|
||||||
|
await sleep(300);
|
||||||
|
router.push({ name: 'settingSystemResourcePool' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验批量添加的资源信息
|
||||||
|
* @param cb 校验通过后的回调函数
|
||||||
|
*/
|
||||||
|
function validateBtachNodes(cb: () => void) {
|
||||||
|
if (
|
||||||
|
form.value.testResourceDTO.nodesList.some((e) => {
|
||||||
|
return Object.values(e).every((v) => v !== '') && e.concurrentNumber > 0;
|
||||||
|
}) &&
|
||||||
|
typeof cb === 'function'
|
||||||
|
) {
|
||||||
|
cb();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollIntoView(document.querySelector('.ms-code-editor'), { block: 'center' });
|
||||||
|
}, 0);
|
||||||
|
Message.error(t('system.resourcePool.nodeResourceRequired'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeSave(isContinue = false) {
|
||||||
|
isContinueAdd.value = isContinue;
|
||||||
|
formRef.value?.validate().then((res) => {
|
||||||
|
if (!res) {
|
||||||
|
// 整个表单校验,除了批量动态的表单项
|
||||||
|
if (isShowNodeResources.value) {
|
||||||
|
// 如果显示node 资源,需要对批量动态表单项进行校验
|
||||||
|
if (form.value.addType === 'single') {
|
||||||
|
// node 资源单个添加时,调用批量表单的校验
|
||||||
|
return batchFormRef.value?.formValidate((batchRes: any) => {
|
||||||
|
form.value.testResourceDTO.nodesList = batchRes;
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// node 资源批量添加时,先将代码编辑器的值解析到表单对象中,再校验
|
||||||
|
analyzeCode();
|
||||||
|
validateBtachNodes(save);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return save();
|
||||||
|
}
|
||||||
|
return scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-item {
|
||||||
|
width: 732px;
|
||||||
|
}
|
||||||
|
:deep(.hide-wrapper) {
|
||||||
|
.arco-form-item-wrapper-col {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
.arco-form-item-label-col {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</MsDrawer>
|
</MsDrawer>
|
||||||
|
<JobTemplateDrawer v-model:visible="showJobDrawer" :value="activePool?.testResourceDTO.jobDefinition || ''" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
|
||||||
|
|
||||||
import type { Description } from '@/components/pure/ms-description/index.vue';
|
import type { Description } from '@/components/pure/ms-description/index.vue';
|
||||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||||
|
@ -218,6 +220,7 @@
|
||||||
const showDetailDrawer = ref(false);
|
const showDetailDrawer = ref(false);
|
||||||
const activePoolDesc: Ref<Description[]> = ref([]);
|
const activePoolDesc: Ref<Description[]> = ref([]);
|
||||||
const activePool: Ref<ResourcePoolItem | null> = ref(null);
|
const activePool: Ref<ResourcePoolItem | null> = ref(null);
|
||||||
|
const showJobDrawer = ref(false);
|
||||||
/**
|
/**
|
||||||
* 查看资源池详情
|
* 查看资源池详情
|
||||||
* @param record
|
* @param record
|
||||||
|
@ -230,6 +233,101 @@
|
||||||
activePool.value.apiTest ? t('system.resourcePool.useAPI') : '',
|
activePool.value.apiTest ? t('system.resourcePool.useAPI') : '',
|
||||||
activePool.value.uiTest ? t('system.resourcePool.useUI') : '',
|
activePool.value.uiTest ? t('system.resourcePool.useUI') : '',
|
||||||
];
|
];
|
||||||
|
const { type, testResourceDTO, loadTest, apiTest, uiTest } = activePool.value;
|
||||||
|
const {
|
||||||
|
ip,
|
||||||
|
token, // k8s token
|
||||||
|
nameSpaces, // k8s 命名空间
|
||||||
|
concurrentNumber, // k8s 最大并发数
|
||||||
|
podThreads, // k8s 单pod最大线程数
|
||||||
|
apiTestImage, // k8s api测试镜像
|
||||||
|
deployName, // k8s api测试部署名称
|
||||||
|
nodesList,
|
||||||
|
loadTestImage,
|
||||||
|
loadTestHeap,
|
||||||
|
uiGrid,
|
||||||
|
} = testResourceDTO;
|
||||||
|
// Node
|
||||||
|
const nodeResourceDesc =
|
||||||
|
type === 'Node'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.detailResources'),
|
||||||
|
value: nodesList?.map((e) => `${e.ip},${e.port},${e.monitor},${e.concurrentNumber}`),
|
||||||
|
isTag: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
// K8S
|
||||||
|
const k8sResourceDesc =
|
||||||
|
type === 'Kubernetes'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.ip'),
|
||||||
|
value: ip,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.token'),
|
||||||
|
value: token,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.nameSpaces'),
|
||||||
|
value: nameSpaces,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.deployName'),
|
||||||
|
value: deployName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.apiTestImage'),
|
||||||
|
value: apiTestImage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.concurrentNumber'),
|
||||||
|
value: concurrentNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testResourceDTO.podThreads'),
|
||||||
|
value: podThreads,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.jobTemplate'),
|
||||||
|
value: t('system.resourcePool.customJobTemplate'),
|
||||||
|
isButton: true,
|
||||||
|
onClick: () => {
|
||||||
|
showJobDrawer.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
// 性能测试
|
||||||
|
const performanceDesc = loadTest
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.mirror'),
|
||||||
|
value: loadTestImage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.testHeap'),
|
||||||
|
value: loadTestHeap,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
// 接口测试/性能测试
|
||||||
|
const resourceDesc = apiTest || loadTest ? [...nodeResourceDesc, ...k8sResourceDesc] : [];
|
||||||
|
// ui 测试资源
|
||||||
|
const uiDesc = uiTest
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.uiGrid'),
|
||||||
|
value: uiGrid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('system.resourcePool.concurrentNumber'),
|
||||||
|
value: concurrentNumber,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
activePoolDesc.value = [
|
activePoolDesc.value = [
|
||||||
{
|
{
|
||||||
label: t('system.resourcePool.detailDesc'),
|
label: t('system.resourcePool.detailDesc'),
|
||||||
|
@ -241,31 +339,23 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('system.resourcePool.detailRange'),
|
label: t('system.resourcePool.detailRange'),
|
||||||
value: activePool.value.organizationList.map((e) => e.name).join(','),
|
value: activePool.value.allOrg
|
||||||
|
? [t('system.resourcePool.orgAll')]
|
||||||
|
: activePool.value.testResourceDTO.orgIds.join(','),
|
||||||
isTag: true,
|
isTag: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('system.resourcePool.detailUse'),
|
label: t('system.resourcePool.detailUse'),
|
||||||
value: poolUses.filter((e) => e !== '').join(','),
|
value: poolUses.filter((e) => e !== ''),
|
||||||
isTag: true,
|
isTag: true,
|
||||||
},
|
},
|
||||||
{
|
...performanceDesc,
|
||||||
label: t('system.resourcePool.detailMirror'),
|
...uiDesc,
|
||||||
value: activePool.value.configuration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('system.resourcePool.detailJMHeap'),
|
|
||||||
value: activePool.value.configuration,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: t('system.resourcePool.detailType'),
|
label: t('system.resourcePool.detailType'),
|
||||||
value: activePool.value.configuration,
|
value: activePool.value.type,
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('system.resourcePool.detailResources'),
|
|
||||||
value: activePool.value.resources.join(','),
|
|
||||||
isTag: true,
|
|
||||||
},
|
},
|
||||||
|
...resourceDesc,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,4 +36,86 @@ export default {
|
||||||
'system.resourcePool.usePerformance': 'Performance test',
|
'system.resourcePool.usePerformance': 'Performance test',
|
||||||
'system.resourcePool.useAPI': 'API test',
|
'system.resourcePool.useAPI': 'API test',
|
||||||
'system.resourcePool.useUI': ' UI test',
|
'system.resourcePool.useUI': ' UI test',
|
||||||
|
'system.resourcePool.name': 'Resource pool name',
|
||||||
|
'system.resourcePool.nameRequired': 'Please enter a resource pool name',
|
||||||
|
'system.resourcePool.namePlaceholder': 'Please enter a resource pool name',
|
||||||
|
'system.resourcePool.desc': 'Description',
|
||||||
|
'system.resourcePool.descPlaceholder': 'Please describe the resource pool',
|
||||||
|
'system.resourcePool.serverUrl': 'Current site URL',
|
||||||
|
'system.resourcePool.rootUrlPlaceholder': 'MS deployment address',
|
||||||
|
'system.resourcePool.orgRange': 'Application organization',
|
||||||
|
'system.resourcePool.orgAll': 'All organization',
|
||||||
|
'system.resourcePool.orgSetup': 'Specified organization',
|
||||||
|
'system.resourcePool.orgSelect': 'specified organization',
|
||||||
|
'system.resourcePool.orgRequired': 'Please select at least one organization',
|
||||||
|
'system.resourcePool.orgPlaceholder': 'Please select an organization',
|
||||||
|
'system.resourcePool.orgRangeTip': 'This rule is common to new organizations',
|
||||||
|
'system.resourcePool.use': 'Use',
|
||||||
|
'system.resourcePool.useRequired': 'Please select at least one purpose',
|
||||||
|
'system.resourcePool.mirror': 'Mirror',
|
||||||
|
'system.resourcePool.mirrorPlaceholder': 'Please enter mirror image',
|
||||||
|
'system.resourcePool.testHeap': 'JMeter HEAP',
|
||||||
|
'system.resourcePool.testHeapPlaceholder': 'Please enter',
|
||||||
|
'system.resourcePool.testHeapExample': 'For example:{heap}',
|
||||||
|
'system.resourcePool.uiGrid': 'Selenium-grid',
|
||||||
|
'system.resourcePool.uiGridPlaceholder': 'Please enter',
|
||||||
|
'system.resourcePool.uiGridExample': 'For example:{grid}',
|
||||||
|
'system.resourcePool.uiGridRequired': 'Please enter selenium-grid address',
|
||||||
|
'system.resourcePool.girdConcurrentNumber': 'grid maximum number of threads',
|
||||||
|
'system.resourcePool.type': 'Type',
|
||||||
|
'system.resourcePool.addResource': 'Add resources',
|
||||||
|
'system.resourcePool.singleAdd': 'Single add',
|
||||||
|
'system.resourcePool.batchAdd': 'Batch add',
|
||||||
|
'system.resourcePool.batchAddTipConfirm': 'Got it',
|
||||||
|
'system.resourcePool.batchAddResource': 'Batch add resources',
|
||||||
|
'system.resourcePool.changeAddTypeTip':
|
||||||
|
'After switching, the content of the added resources will continue to appear in yaml; the added resources can be modified in batches',
|
||||||
|
'system.resourcePool.changeAddTypePopTitle': 'Toggle add resource type?',
|
||||||
|
'system.resourcePool.ip': 'IP',
|
||||||
|
'system.resourcePool.ipRequired': 'Please enter an IP address',
|
||||||
|
'system.resourcePool.ipPlaceholder': 'Please enter an IP address',
|
||||||
|
'system.resourcePool.port': 'Port',
|
||||||
|
'system.resourcePool.portRequired': 'Please enter Port',
|
||||||
|
'system.resourcePool.portPlaceholder': 'Please enter Port',
|
||||||
|
'system.resourcePool.monitor': 'Monitor',
|
||||||
|
'system.resourcePool.monitorRequired': 'Please enter Monitor',
|
||||||
|
'system.resourcePool.monitorPlaceholder': 'Please enter Monitor',
|
||||||
|
'system.resourcePool.concurrentNumber': 'Maximum concurrency',
|
||||||
|
'system.resourcePool.concurrentNumberRequired': 'Please enter the maximum number of concurrency',
|
||||||
|
'system.resourcePool.concurrentNumberMin': 'The maximum concurrent number must be greater than or equal to 1',
|
||||||
|
'system.resourcePool.concurrentNumberPlaceholder': 'Please enter the maximum number of concurrency',
|
||||||
|
'system.resourcePool.nodeResourceRequired': 'Please fill in the Node resources added in batches correctly',
|
||||||
|
'system.resourcePool.nodeConfigEditorTip':
|
||||||
|
'Writing format: IP, Port, Monitor, maximum concurrent number; such as 192.168.1.52,8082,500,1',
|
||||||
|
'system.resourcePool.testResourceDTO.ip': 'IP Address/Domain Name',
|
||||||
|
'system.resourcePool.testResourceDTO.ipRequired': 'Please fill in IP address / domain name',
|
||||||
|
'system.resourcePool.testResourceDTO.ipPlaceholder': 'example.com',
|
||||||
|
'system.resourcePool.testResourceDTO.ipSubTip': 'Example: {ip} or {domain}',
|
||||||
|
'system.resourcePool.testResourceDTO.token': 'Token',
|
||||||
|
'system.resourcePool.testResourceDTO.tokenRequired': 'Please fill in the Token',
|
||||||
|
'system.resourcePool.testResourceDTO.tokenPlaceholder': 'Please fill in the Token',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpaces': 'NameSpaces',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpacesRequired': 'Please fill in the namespace',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpacesPlaceholder':
|
||||||
|
'To use the K8S resource pool, you need to deploy the Role.yaml file',
|
||||||
|
'system.resourcePool.testResourceDTO.deployName': 'Deploy Name',
|
||||||
|
'system.resourcePool.testResourceDTO.deployNameRequired': 'Please fill in Deploy Name',
|
||||||
|
'system.resourcePool.testResourceDTO.deployNamePlaceholder':
|
||||||
|
'To perform interface testing, a Daemonset.yaml or Deployment .yaml file is required',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImage': 'API mirroring',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImageRequired': 'Please fill in the API image',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImagePlaceholder': 'Please fill in the API image',
|
||||||
|
'system.resourcePool.testResourceDTO.concurrentNumber': 'Maximum concurrency',
|
||||||
|
'system.resourcePool.testResourceDTO.podThreads': 'Maximum number of threads per Pod',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadRoleYaml': 'Download YAML files',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadRoleYamlTip':
|
||||||
|
'Please fill in the namespace before downloading the YAML file',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDeployYamlTip':
|
||||||
|
'Please fill in the namespace and Deploy Name before downloading the YAML file',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDaemonsetYaml': 'Daemonset.yaml',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDeploymentYaml': 'Deployment.yaml',
|
||||||
|
'system.resourcePool.customJobTemplate': 'Custom Job Templates',
|
||||||
|
'system.resourcePool.jobTemplate': 'Job Templates',
|
||||||
|
'system.resourcePool.jobTemplateTip':
|
||||||
|
'A Kubernetes job template is a text in YAML format, which is used to define the running parameters of the job. You can edit the job template here.',
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,5 +34,84 @@ export default {
|
||||||
'system.resourcePool.detailResources': '已添加资源',
|
'system.resourcePool.detailResources': '已添加资源',
|
||||||
'system.resourcePool.usePerformance': '性能测试',
|
'system.resourcePool.usePerformance': '性能测试',
|
||||||
'system.resourcePool.useAPI': '接口测试',
|
'system.resourcePool.useAPI': '接口测试',
|
||||||
'system.resourcePool.useUI': ' UI 测试',
|
'system.resourcePool.useUI': 'UI 测试',
|
||||||
|
'system.resourcePool.name': '资源池名称',
|
||||||
|
'system.resourcePool.nameRequired': '请输入资源池名称',
|
||||||
|
'system.resourcePool.namePlaceholder': '请输入资源池名称',
|
||||||
|
'system.resourcePool.desc': '描述',
|
||||||
|
'system.resourcePool.descPlaceholder': '请对该资源池进行描述',
|
||||||
|
'system.resourcePool.serverUrl': '当前站点 URL',
|
||||||
|
'system.resourcePool.rootUrlPlaceholder': 'MS的部署地址',
|
||||||
|
'system.resourcePool.orgRange': '应用组织',
|
||||||
|
'system.resourcePool.orgAll': '全部组织',
|
||||||
|
'system.resourcePool.orgSetup': '指定组织',
|
||||||
|
'system.resourcePool.orgSelect': '指定组织',
|
||||||
|
'system.resourcePool.orgRequired': '请至少选择一个组织',
|
||||||
|
'system.resourcePool.orgPlaceholder': '请选择组织',
|
||||||
|
'system.resourcePool.orgRangeTip': '新建组织通用此规则',
|
||||||
|
'system.resourcePool.use': '用途',
|
||||||
|
'system.resourcePool.useRequired': '请至少选择一项用途',
|
||||||
|
'system.resourcePool.mirror': '镜像',
|
||||||
|
'system.resourcePool.mirrorPlaceholder': '请输入镜像',
|
||||||
|
'system.resourcePool.testHeap': 'JMeter HEAP',
|
||||||
|
'system.resourcePool.testHeapPlaceholder': '请输入',
|
||||||
|
'system.resourcePool.testHeapExample': '例如:{heap}',
|
||||||
|
'system.resourcePool.uiGrid': 'selenium-grid',
|
||||||
|
'system.resourcePool.uiGridPlaceholder': '请输入',
|
||||||
|
'system.resourcePool.uiGridExample': '例如:{grid}',
|
||||||
|
'system.resourcePool.uiGridRequired': 'selenium-grid 不能为空',
|
||||||
|
'system.resourcePool.girdConcurrentNumber': 'grid最大线程数',
|
||||||
|
'system.resourcePool.type': '类型',
|
||||||
|
'system.resourcePool.addResource': '添加资源',
|
||||||
|
'system.resourcePool.singleAdd': '单个添加',
|
||||||
|
'system.resourcePool.batchAdd': '批量添加',
|
||||||
|
'system.resourcePool.batchAddTipConfirm': '知道了',
|
||||||
|
'system.resourcePool.batchAddResource': '批量添加资源',
|
||||||
|
'system.resourcePool.changeAddTypeTip': '切换后,已添加资源内容将继续现在 yaml 内;可批量修改已添加资源',
|
||||||
|
'system.resourcePool.changeAddTypePopTitle': '切换添加资源类型?',
|
||||||
|
'system.resourcePool.ip': 'IP',
|
||||||
|
'system.resourcePool.ipRequired': 'IP 地址不能为空',
|
||||||
|
'system.resourcePool.ipPlaceholder': '请输入 IP 地址',
|
||||||
|
'system.resourcePool.port': 'Port',
|
||||||
|
'system.resourcePool.portRequired': 'Port 不能为空',
|
||||||
|
'system.resourcePool.portPlaceholder': '请输入 Port',
|
||||||
|
'system.resourcePool.monitor': 'Monitor',
|
||||||
|
'system.resourcePool.monitorRequired': 'Monitor 不能为空',
|
||||||
|
'system.resourcePool.monitorPlaceholder': '请输入 Monitor',
|
||||||
|
'system.resourcePool.concurrentNumber': '最大并发数',
|
||||||
|
'system.resourcePool.concurrentNumberRequired': '最大并发数不能为空',
|
||||||
|
'system.resourcePool.concurrentNumberMin': '最大并发数须大于等于 1',
|
||||||
|
'system.resourcePool.concurrentNumberPlaceholder': '请输入最大并发数',
|
||||||
|
'system.resourcePool.nodeResourceRequired': '请正确填写批量添加的 Node 资源',
|
||||||
|
'system.resourcePool.nodeConfigEditorTip': '书写格式:IP,Port,Monitor,最大并发数;如 192.168.1.52,8082,500,1',
|
||||||
|
'system.resourcePool.testResourceDTO.ip': 'IP 地址/域名',
|
||||||
|
'system.resourcePool.testResourceDTO.ipRequired': 'IP 地址/域名不能为空',
|
||||||
|
'system.resourcePool.testResourceDTO.ipPlaceholder': 'example.com',
|
||||||
|
'system.resourcePool.testResourceDTO.ipSubTip': '例如:{ip} 或 {domain}',
|
||||||
|
'system.resourcePool.testResourceDTO.token': 'Token',
|
||||||
|
'system.resourcePool.testResourceDTO.tokenRequired': 'Token 不能为空',
|
||||||
|
'system.resourcePool.testResourceDTO.tokenPlaceholder': '请输入 Token',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpaces': '命名空间',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpacesRequired': '命名空间不能为空',
|
||||||
|
'system.resourcePool.testResourceDTO.nameSpacesPlaceholder': '使用K8S资源池需要部署Role.yaml文件',
|
||||||
|
'system.resourcePool.testResourceDTO.deployName': 'Deploy Name',
|
||||||
|
'system.resourcePool.testResourceDTO.deployNameRequired': 'Deploy Name 不能为空',
|
||||||
|
'system.resourcePool.testResourceDTO.deployNamePlaceholder':
|
||||||
|
'执行接口测试需要部署 Daemonset.yaml 或 Deployment .yaml文件',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImage': 'API 镜像',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImageRequired': 'API 镜像不能为空',
|
||||||
|
'system.resourcePool.testResourceDTO.apiTestImagePlaceholder': '请输入 API 镜像',
|
||||||
|
'system.resourcePool.testResourceDTO.concurrentNumber': '最大并发数',
|
||||||
|
'system.resourcePool.testResourceDTO.podThreads': '单 Pod 最大线程数',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadRoleYaml': '下载 YAML 文件',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadRoleYamlTip': '请先填写命名空间再下载YAML文件',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDeployYamlTip': '请先填写命名空间 和 Deploy Name 再下载YAML文件',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDaemonsetYaml': 'Daemonset.yaml',
|
||||||
|
'system.resourcePool.testResourceDTO.downloadDeploymentYaml': 'Deployment.yaml',
|
||||||
|
'system.resourcePool.customJobTemplate': '自定义 Job 模版',
|
||||||
|
'system.resourcePool.jobTemplate': 'Job 模版',
|
||||||
|
'system.resourcePool.jobTemplateTip':
|
||||||
|
'Kubernetes Job 模版是一个YAML格式的文本,用于定义Job的运行参数,您可以在此处编辑Job模版。',
|
||||||
|
'system.resourcePool.addSuccess': '添加资源池成功',
|
||||||
|
'system.resourcePool.updateSuccess': '更新资源池成功',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
export const daemonSet = `apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ms-node-controller
|
||||||
|
name: {name}
|
||||||
|
namespace: {namespace}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ms-node-controller
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ms-node-controller
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: app
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ms-node-controller
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
weight: 100
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
image: {image}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
name: ms-node-controller
|
||||||
|
ports:
|
||||||
|
- containerPort: 8082
|
||||||
|
protocol: TCP
|
||||||
|
resources: {}
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /opt/metersphere/logs
|
||||||
|
name: metersphere-logs
|
||||||
|
restartPolicy: Always
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: metersphere-logs
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const deployment = `apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ms-node-controller
|
||||||
|
name: {name}
|
||||||
|
namespace: {namespace}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ms-node-controller
|
||||||
|
replicas: 2
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ms-node-controller
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: app
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- ms-node-controller
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
weight: 100
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
image: {image}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
name: ms-node-controller
|
||||||
|
ports:
|
||||||
|
- containerPort: 8082
|
||||||
|
protocol: TCP
|
||||||
|
resources: {}
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /opt/metersphere/logs
|
||||||
|
name: metersphere-logs
|
||||||
|
restartPolicy: Always
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: metersphere-logs
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const role = `apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: metersphere
|
||||||
|
namespace: {namespace}
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
- list
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- exec
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods/exec
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- create
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
- list
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
- deployments
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
- list
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- extensions
|
||||||
|
resources:
|
||||||
|
- deployments
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
- list
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
- apiGroups:
|
||||||
|
- batch
|
||||||
|
resources:
|
||||||
|
- jobs
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- watch
|
||||||
|
- list
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
- delete
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const job = `apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
test-id: \${TEST_ID}
|
||||||
|
name: \${JOB_NAME}
|
||||||
|
spec:
|
||||||
|
parallelism: 1
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
test-id: \${TEST_ID}
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: test-id
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- \${TEST_ID}
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
weight: 100
|
||||||
|
containers:
|
||||||
|
- command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- /run-test.sh
|
||||||
|
env:
|
||||||
|
- name: START_TIME
|
||||||
|
value: "\${START_TIME}"
|
||||||
|
- name: GRANULARITY
|
||||||
|
value: "\${GRANULARITY}"
|
||||||
|
- name: JMETER_REPORTS_TOPIC
|
||||||
|
value: \${JMETER_REPORTS_TOPIC}
|
||||||
|
- name: METERSPHERE_URL
|
||||||
|
value: \${METERSPHERE_URL}
|
||||||
|
- name: RESOURCE_ID
|
||||||
|
value: \${RESOURCE_ID}
|
||||||
|
- name: BACKEND_LISTENER
|
||||||
|
value: "\${BACKEND_LISTENER}"
|
||||||
|
- name: BOOTSTRAP_SERVERS
|
||||||
|
value: \${BOOTSTRAP_SERVERS}
|
||||||
|
- name: RATIO
|
||||||
|
value: "\${RATIO}"
|
||||||
|
- name: REPORT_FINAL
|
||||||
|
value: "\${REPORT_FINAL}"
|
||||||
|
- name: TEST_ID
|
||||||
|
value: \${TEST_ID}
|
||||||
|
- name: THREAD_NUM
|
||||||
|
value: "\${THREAD_NUM}"
|
||||||
|
- name: HEAP
|
||||||
|
value: \${HEAP}
|
||||||
|
- name: REPORT_ID
|
||||||
|
value: \${REPORT_ID}
|
||||||
|
- name: REPORT_REALTIME
|
||||||
|
value: "\${REPORT_REALTIME}"
|
||||||
|
- name: RESOURCE_INDEX
|
||||||
|
value: "\${RESOURCE_INDEX}"
|
||||||
|
- name: LOG_TOPIC
|
||||||
|
value: \${LOG_TOPIC}
|
||||||
|
- name: GC_ALGO
|
||||||
|
value: \${GC_ALGO}
|
||||||
|
image: \${JMETER_IMAGE}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
name: jmeter
|
||||||
|
ports:
|
||||||
|
- containerPort: 60000
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /test
|
||||||
|
name: test-files
|
||||||
|
- mountPath: /jmeter-log
|
||||||
|
name: log-files
|
||||||
|
- command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- /generate-report.sh
|
||||||
|
env:
|
||||||
|
- name: START_TIME
|
||||||
|
value: "\${START_TIME}"
|
||||||
|
- name: GRANULARITY
|
||||||
|
value: "\${GRANULARITY}"
|
||||||
|
- name: JMETER_REPORTS_TOPIC
|
||||||
|
value: \${JMETER_REPORTS_TOPIC}
|
||||||
|
- name: METERSPHERE_URL
|
||||||
|
value: \${METERSPHERE_URL}
|
||||||
|
- name: RESOURCE_ID
|
||||||
|
value: \${RESOURCE_ID}
|
||||||
|
- name: BACKEND_LISTENER
|
||||||
|
value: "\${BACKEND_LISTENER}"
|
||||||
|
- name: BOOTSTRAP_SERVERS
|
||||||
|
value: \${BOOTSTRAP_SERVERS}
|
||||||
|
- name: RATIO
|
||||||
|
value: "\${RATIO}"
|
||||||
|
- name: REPORT_FINAL
|
||||||
|
value: "\${REPORT_FINAL}"
|
||||||
|
- name: TEST_ID
|
||||||
|
value: \${TEST_ID}
|
||||||
|
- name: THREAD_NUM
|
||||||
|
value: "\${THREAD_NUM}"
|
||||||
|
- name: HEAP
|
||||||
|
value: \${HEAP}
|
||||||
|
- name: REPORT_ID
|
||||||
|
value: \${REPORT_ID}
|
||||||
|
- name: REPORT_REALTIME
|
||||||
|
value: "\${REPORT_REALTIME}"
|
||||||
|
- name: RESOURCE_INDEX
|
||||||
|
value: "\${RESOURCE_INDEX}"
|
||||||
|
- name: LOG_TOPIC
|
||||||
|
value: \${LOG_TOPIC}
|
||||||
|
- name: GC_ALGO
|
||||||
|
value: \${GC_ALGO}
|
||||||
|
image: \${JMETER_IMAGE}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
name: report
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /test
|
||||||
|
name: test-files
|
||||||
|
- mountPath: /jmeter-log
|
||||||
|
name: log-files
|
||||||
|
restartPolicy: Never
|
||||||
|
volumes:
|
||||||
|
- emptyDir: {}
|
||||||
|
name: test-files
|
||||||
|
- emptyDir: {}
|
||||||
|
name: log-files
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type YamlType = 'Deployment' | 'DaemonSet' | 'role';
|
||||||
|
|
||||||
|
export function getYaml(type: YamlType, name: string, namespace: string, image: string) {
|
||||||
|
if (type === 'Deployment') {
|
||||||
|
return deployment.replace('{name}', name).replace('{namespace}', namespace).replace('{image}', image);
|
||||||
|
}
|
||||||
|
if (type === 'DaemonSet') {
|
||||||
|
return daemonSet.replace('{name}', name).replace('{namespace}', namespace).replace('{image}', image);
|
||||||
|
}
|
||||||
|
if (type === 'role') {
|
||||||
|
return role.replace('{namespace}', namespace);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
Loading…
Reference in New Issue