feat: 资源池页面

This commit is contained in:
baiqi 2023-07-19 11:31:58 +08:00 committed by 刘瑞斌
parent cd57074414
commit 025965b4ee
11 changed files with 1457 additions and 86 deletions

View File

@ -1,6 +1,7 @@
import MSR from '@/api/http/index';
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 { 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 });
}

View File

@ -3,32 +3,32 @@
<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 }">
<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]">
<a-form-item
v-for="item of props.models"
:key="`${item.filed}${order}`"
:field="`${item.filed}${order}`"
v-for="model of props.models"
:key="`${model.filed}${i}`"
:field="`list[${i}].${model.filed}`"
:class="i > 0 ? 'hidden-item' : 'mb-0 flex-1'"
:label="i === 0 && item.label ? t(item.label) : ''"
:rules="item.rules"
:label="i === 0 && model.label ? t(model.label) : ''"
:rules="model.rules"
asterisk-position="end"
>
<a-input
v-if="item.type === 'input'"
v-model="form[`${item.filed}${order}`]"
v-if="model.type === 'input'"
v-model="item[model.filed]"
class="mb-[4px] flex-1"
:placeholder="t(item.placeholder || '')"
:max-length="item.maxLength || 250"
:placeholder="t(model.placeholder || '')"
:max-length="model.maxLength || 250"
allow-clear
/>
<a-input-number
v-if="item.type === 'inputNumber'"
v-model="form[`${item.filed}${order}`]"
v-if="model.type === 'inputNumber'"
v-model="item[model.filed]"
class="mb-[4px] flex-1"
:placeholder="t(item.placeholder || '')"
:min="item.min"
:max="item.max || 9999999"
:placeholder="t(model.placeholder || '')"
:min="model.min"
:max="model.max || 9999999"
allow-clear
/>
</a-form-item>
@ -44,7 +44,7 @@
'text-[var(--color-text-brand)]',
i === 0 ? 'mt-[36px]' : 'mt-[5px]',
]"
@click="removeField(order, i)"
@click="removeField(i)"
>
<icon-minus-circle />
</div>
@ -65,8 +65,9 @@
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { ref, watchEffect, unref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { scrollIntoView } from '@/utils/dom';
import type { ValidatedError, FormInstance } from '@arco-design/web-vue';
import type { FormItemModel, FormMode, ValueType } from './types';
@ -81,7 +82,7 @@
maxHeight?: string;
valueType?: ValueType;
delimiter?: string; // valueType string ,
defaultVals?: Record<string, string | string[] | number[]>; //
defaultVals?: any[]; //
}>(),
{
valueType: 'Array',
@ -91,10 +92,11 @@
);
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 formItem: Record<string, any> = {};
/**
* 监测defaultVals和models的变化
@ -103,42 +105,17 @@
*/
watchEffect(() => {
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: [0] };
form.value.list = [{ ...formItem }];
if (props.defaultVals?.length) {
// defaultVals filed
const arr = Object.keys(props.defaultVals);
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);
}
});
}
form.value.list = props.defaultVals.map((e) => e);
}
});
function getFormResult() {
const res: Record<string, any> = {};
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;
return unref<Record<string, any>[]>(form.value.list);
}
/**
@ -146,15 +123,15 @@
* @param cb 校验通过后执行回调
* @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>) => {
if (errors) {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
return;
}
if (typeof cb === 'function') {
if (isSubmit) {
const res = getFormResult();
cb(props.valueType === 'Array' ? res : res.join(','));
cb(getFormResult());
return;
}
cb();
@ -167,24 +144,15 @@
*/
function addField() {
formValidate(() => {
const lastIndex = form.value.list.length - 1;
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 : '';
});
form.value.list.push({ ...formItem }); //
}, false);
}
/**
* 移除表单项
* @param index 表单项的序号
* @param i 表单项对应 list 的下标
*/
function removeField(index: number, i: number) {
props.models.forEach((e) => {
delete form.value[`${e.filed}${index}`];
});
function removeField(i: number) {
form.value.list.splice(i, 1);
}

View File

@ -29,7 +29,8 @@ declare const _default: import('vue').DefineComponent<
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[];
}
>;

View File

@ -20,6 +20,7 @@ export interface TestResourceDTO {
apiTestImage: string; // k8s api测试镜像
deployName: string; // k8s api测试部署名称
uiGrid: string; // ui测试selenium-grid
girdConcurrentNumber: number; // ui测试selenium-grid最大并发数
orgIds: string[]; // 应用范围选择指定组织时的id集合
}

View File

@ -88,3 +88,14 @@ export const downloadStringFile = (type: string, content: string, fileName: stri
link.click();
document.body.removeChild(link);
};
/**
*
* @param ms
* @returns
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}

View File

@ -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>

View File

@ -1,7 +1,789 @@
<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>
</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>
<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];
// ipportmonitorconcurrentNumber
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>

View File

@ -48,6 +48,7 @@
</a-button>
</template>
</MsDrawer>
<JobTemplateDrawer v-model:visible="showJobDrawer" :value="activePool?.testResourceDTO.jobDefinition || ''" />
</template>
<script setup lang="ts">
@ -62,6 +63,7 @@
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 type { Description } from '@/components/pure/ms-description/index.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
@ -218,6 +220,7 @@
const showDetailDrawer = ref(false);
const activePoolDesc: Ref<Description[]> = ref([]);
const activePool: Ref<ResourcePoolItem | null> = ref(null);
const showJobDrawer = ref(false);
/**
* 查看资源池详情
* @param record
@ -230,6 +233,101 @@
activePool.value.apiTest ? t('system.resourcePool.useAPI') : '',
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 = [
{
label: t('system.resourcePool.detailDesc'),
@ -241,31 +339,23 @@
},
{
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,
},
{
label: t('system.resourcePool.detailUse'),
value: poolUses.filter((e) => e !== '').join(','),
value: poolUses.filter((e) => e !== ''),
isTag: true,
},
{
label: t('system.resourcePool.detailMirror'),
value: activePool.value.configuration,
},
{
label: t('system.resourcePool.detailJMHeap'),
value: activePool.value.configuration,
},
...performanceDesc,
...uiDesc,
{
label: t('system.resourcePool.detailType'),
value: activePool.value.configuration,
},
{
label: t('system.resourcePool.detailResources'),
value: activePool.value.resources.join(','),
isTag: true,
value: activePool.value.type,
},
...resourceDesc,
];
}

View File

@ -36,4 +36,86 @@ export default {
'system.resourcePool.usePerformance': 'Performance test',
'system.resourcePool.useAPI': 'API 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.',
};

View File

@ -34,5 +34,84 @@ export default {
'system.resourcePool.detailResources': '已添加资源',
'system.resourcePool.usePerformance': '性能测试',
'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': '更新资源池成功',
};

View File

@ -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 '';
}