diff --git a/src/components/DynamicTable.vue b/src/components/DynamicTable.vue new file mode 100644 index 00000000..9d44f731 --- /dev/null +++ b/src/components/DynamicTable.vue @@ -0,0 +1,244 @@ + + + + + diff --git a/src/composables/config/useNodeConfig.ts b/src/composables/config/useNodeConfig.ts index 80771e73..ed214ccc 100644 --- a/src/composables/config/useNodeConfig.ts +++ b/src/composables/config/useNodeConfig.ts @@ -3,7 +3,6 @@ import { useNodeMsgMap } from '@/composables/config/useNodeList' import useNodeConfigParamCommon from '@/composables/config/useNodeConfigParamCommon' import type { ParamInfo, PluginInfo } from '@/types/config' import type { DriverDirection } from '@/types/enums' -import { TypeOfPluginParam } from '@/types/enums' import type { Ref } from 'vue' import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' @@ -28,7 +27,7 @@ export default (props: Props) => { const { initMap, getNodeMsgById } = useNodeMsgMap(props.direction, false) const { pluginMsgIdMap, initMsgIdMap } = useGetPluginMsgIdMap() - const { isParamHexadecimalBase, transToHexadecimal, transToDecimal } = useNodeConfigParamCommon() + const { isParamHexadecimalBase, transToHexadecimal, initParamDefaultValueByType } = useNodeConfigParamCommon() const configForm: Ref> = ref({}) const defaultConfigData: Ref> = ref({}) @@ -36,8 +35,13 @@ export default (props: Props) => { const fieldList: Ref> = ref([]) const isLoading = ref(false) const formCom = ref() + const formComParmasRef: any = ref([]) const isSubmitting = ref(false) + const setParamRef = (el: any) => { + formComParmasRef.value.push(el) + } + const node = computed(() => route.params.node.toString()) /** @@ -100,21 +104,14 @@ export default (props: Props) => { return newDefaultValue } - const initValueMap = { - [TypeOfPluginParam.Int]: null, - [TypeOfPluginParam.String]: '', - [TypeOfPluginParam.Boolean]: null, - [TypeOfPluginParam.Enum]: '', - [TypeOfPluginParam.Map]: '', - [TypeOfPluginParam.File]: '', - } - return initValueMap[param.type as TypeOfPluginParam] || '' + return initParamDefaultValueByType(param.type) } // init plugin default data const initFormFromPluginInfo = (info: PluginInfo) => { // TODO: delete params after api changed const { tag_type, params, ...fields } = info + const keys = Object.keys(fields) return keys.reduce((obj, currentKey) => { obj[currentKey] = createInitValue(info[currentKey]) @@ -126,6 +123,7 @@ export default (props: Props) => { const createFieldListFormPluginInfo = (info: PluginInfo) => { // TODO: delete params after api changed const { params, tag_type, ...fields } = info + return Object.keys(fields).map((key) => { return { key, @@ -143,6 +141,7 @@ export default (props: Props) => { const schemName = pluginMsgIdMap[pluginName]?.schema || nodePluginToLower const { data } = await queryPluginConfigInfo(schemName) const pluginInfo: PluginInfo = data + if (!pluginInfo) { defaultConfigData.value = {} fieldList.value = [] @@ -174,65 +173,61 @@ export default (props: Props) => { } } - const shouldFieldShow = (fieldData: Field) => { - const { key } = fieldData - const whiteList = ['tag_regex', 'group_interval'] - if (whiteList.includes(key)) return false - - if (!fieldData.info.condition) { - return true - } + const cancel = () => { + router.back() + } - const { field, regex, value } = fieldData.info.condition - const fieldValue = configForm.value[field] - if (regex) { - const regexExpression = new RegExp(regex) - return regexExpression.test(fieldValue) - } - return value !== undefined || value !== null ? fieldValue === value : true + const validateFileds = async (fields: Array) => { + const { form } = formCom.value + await form.validateField(fields) } - const cancel = () => { - router.back() + const validateAll = async () => { + const validateList = [formCom.value.validate()] + + // counts array parmas + formComParmasRef.value.forEach((item: { arrayRef: any; validateArrayParam: any }) => { + const { arrayRef, validateArrayParam } = item + if (arrayRef && validateArrayParam) { + validateList.push(validateArrayParam()) + } + }) + + const valids = await Promise.allSettled(validateList) + const valid = valids.every((item: any) => item?.value || false) + return Promise.resolve(valid) } const submit = async () => { try { - await formCom.value.validate() - isSubmitting.value = true - // delete `tag_regex` - const bodyData = cloneDeep(configForm.value) - const { tag_regex } = bodyData - if (tag_regex !== undefined) { - delete bodyData.tag_regex - } - - const dataKeys = Object.keys(bodyData) - dataKeys.forEach((key) => { - const value = bodyData[key] - const field = fieldList.value.find((item: { key: string; info: any }) => item.key === key) + const valid = await validateAll() + if (valid) { + // // delete `tag_regex` + const { tag_regex } = configForm.value + if (tag_regex !== undefined) { + delete configForm.value.tag_regex + } // if configForm value is '' or 'undefined' or null, change its value to `default` value or delete it. - if (value === '' || value === undefined || value == null) { - const isOptional = field?.info?.attribute - const isDefaultValue = field?.info?.default !== undefined - if (isOptional && isDefaultValue) { - bodyData[key] = defaultConfigData.value[key] - } else { - delete bodyData[key] + const dataKeys = Object.keys(configForm.value) + dataKeys.forEach((key) => { + const value = configForm.value[key] + if (value === '' || value === undefined || value == null) { + const field = fieldList.value.find((item: { key: string; info: any }) => item.key === key) + const isOptional = field?.info?.attribute + const isDefaultValue = field?.info?.default !== undefined + if (isOptional && isDefaultValue) { + configForm.value[key] = defaultConfigData.value[key] + } else { + delete configForm.value[key] + } } - } - - // if base = 16, transefer value to 10 - if (field && field.info && isParamHexadecimalBase(field.info)) { - bodyData[key] = transToDecimal(value) - } - }) - - await submitNodeConfig(node.value, bodyData) - EmqxMessage.success(t('common.submitSuccess')) - router.back() + }) + await submitNodeConfig(node.value, configForm.value) + EmqxMessage.success(t('common.submitSuccess')) + router.back() + } } catch (error) { console.error(error) } finally { @@ -260,10 +255,11 @@ export default (props: Props) => { fieldList, isLoading, formCom, + setParamRef, isSubmitting, - shouldFieldShow, submit, cancel, reset, + validateFileds, } } diff --git a/src/composables/config/useNodeConfigParamCommon.ts b/src/composables/config/useNodeConfigParamCommon.ts index f9d60c5d..15318453 100644 --- a/src/composables/config/useNodeConfigParamCommon.ts +++ b/src/composables/config/useNodeConfigParamCommon.ts @@ -1,9 +1,18 @@ import type { ParamInfo } from '@/types/config' import { TypeOfPluginParam, SchameBase } from '@/types/enums' +import { dataType } from '@/utils/utils' import { HEXADECIMAL_PREFIX } from '@/utils/constants' import { transIntHexToDecimalNum, transPositiveIntegerToHex } from '@/composables/data/convert' +import useLang from '@/composables/useLang' export default () => { + const { i18nContent } = useLang() + + interface Field { + key: string + info: ParamInfo + } + const isParamHexadecimalBase = (param: ParamInfo) => { return param.type === TypeOfPluginParam.Int && param?.base === SchameBase.hexadecimal } @@ -35,10 +44,68 @@ export default () => { return transIntHexToDecimalNum(hexStr) } + const shouldFieldShow = (fieldData: Field, formData: Record) => { + const { key } = fieldData + + const whiteList = ['tag_regex', 'group_interval'] + if (whiteList.includes(key)) return false + + if (!fieldData?.info?.condition) { + return true + } + + const { field, regex, value, values = [] } = fieldData.info.condition + const fieldValue = formData[field] + + if (regex) { + const regexExpression = new RegExp(regex) + return regexExpression.test(fieldValue) + } + + let res = true + const emptyEnums = ['undefined', 'null'] + const emptyValue = emptyEnums.includes(String(dataType(value))) + const emptyValues = emptyEnums.includes(String(dataType(values))) + if (!emptyValue) { + res = fieldValue === value + } else if (!emptyValues) { + res = values.includes(fieldValue) + } + return res + } + const initParamDefaultValueByType = (type: TypeOfPluginParam) => { + const initValueMap = { + [TypeOfPluginParam.Int]: null, + [TypeOfPluginParam.String]: '', + [TypeOfPluginParam.Boolean]: null, + [TypeOfPluginParam.Enum]: '', + [TypeOfPluginParam.Map]: '', + [TypeOfPluginParam.Array]: [], + [TypeOfPluginParam.File]: '', + } + return initValueMap[type] === undefined ? '' : initValueMap[type] + } + + const upperFirstLetter = (str: string) => { + if (/^[a-z]/.test(str)) { + return str.slice(0, 1).toUpperCase() + str.slice(1) + } + return str + } + + const showLabel = (field: ParamInfo) => { + const label = i18nContent(field, 'name') + return upperFirstLetter(label) + } + return { checkHexadecimalValue, isParamHexadecimalBase, transToDecimal, transToHexadecimal, + shouldFieldShow, + initParamDefaultValueByType, + upperFirstLetter, + showLabel, } } diff --git a/src/composables/config/useNodeConfigParamItem.ts b/src/composables/config/useNodeConfigParamItem.ts index 13fea8ff..6a5c322f 100644 --- a/src/composables/config/useNodeConfigParamItem.ts +++ b/src/composables/config/useNodeConfigParamItem.ts @@ -1,9 +1,8 @@ -import type { NumberParamInfo, ParamInfo, StringParamInfo } from '@/types/config' -import { ParamRequired, TypeOfPluginParam, SchameBase } from '@/types/enums' -import { createCommonErrorMessage } from '@/utils/utils' import { computed } from 'vue' import { useI18n } from 'vue-i18n' -import useNodeConfigParamCommon from '@/composables/config/useNodeConfigParamCommon' +import type { NumberParamInfo, ParamInfo, StringParamInfo, ArrayParamInfo } from '@/types/config' +import { ParamRequired, TypeOfPluginParam } from '@/types/enums' +import { createCommonErrorMessage, dataType } from '@/utils/utils' import useLang from '@/composables/useLang' type Props = Readonly<{ @@ -16,48 +15,75 @@ export default (props: Props) => { const { t } = useI18n() const { i18nContent } = useLang() - const { isParamHexadecimalBase, checkHexadecimalValue, transToDecimal } = useNodeConfigParamCommon() + enum RangeErrorEnums { + All = 'all', + Max = 'max', + Min = 'min', + Default = '', + } + + const comparisonRange = ( + val: number, + range: { min: number; max: number }, + ): { errorMsgType: string; inRange: boolean } => { + const value = Number(val) + const { min, max } = range + const emptyNumbers = ['undefined', 'null'] + const minNumberType = String(dataType(min)) + const maxNumberType = String(dataType(max)) + const isMinEmpty = emptyNumbers.includes(minNumberType) + const isMaxEmpty = emptyNumbers.includes(maxNumberType) + + let inRange = true // default with all undefined ro all null + let errorMsgType = RangeErrorEnums.Default + + if (!isMinEmpty && !isMaxEmpty) { + // all not undefined or all not null + errorMsgType = RangeErrorEnums.All + inRange = value >= min && value <= max + } + if (!isMinEmpty && isMaxEmpty) { + errorMsgType = RangeErrorEnums.Min + inRange = value >= min + } + if (isMinEmpty && !isMaxEmpty) { + errorMsgType = RangeErrorEnums.Max + inRange = value >= 0 && value <= max + } + return { + errorMsgType, + inRange, + } + } // valid number limit const checkNumberParamLimit = async (rule: unknown, value: string, callback: any) => { - const trueValue = isParamHexadecimalBase(props.paramInfo) ? transToDecimal(value) : value const { valid, attribute } = props.paramInfo as NumberParamInfo const { max, min } = valid - if (max === undefined || max === null || min === undefined || min === null) { - callback() - } + const isInRange = comparisonRange(Number(value), { min, max }) - const isNumber = Number.isNaN(Number(trueValue)) || Number(trueValue) > valid.max || Number(trueValue) < valid.min - if (attribute === 'required') { - if (isNumber) { - callback(new Error(`${t('config.numberErrorPrefix') + valid.min}-${valid.max}${t('config.numberErrorSuffix')}`)) - } - } else if (trueValue !== '' && isNumber) { - callback(new Error(`${t('config.numberErrorPrefix') + valid.min}-${valid.max}${t('config.numberErrorSuffix')}`)) - } - callback() - } + const { inRange, errorMsgType } = isInRange - // check number hexadecimal | decimal - const checkNumberParamHexadecimal = async (rule: unknown, value: string, callback: any) => { - const { base } = props.paramInfo as NumberParamInfo + const errorMsgMap = new Map([ + [String(RangeErrorEnums.All), t('config.numberRangeErrorMsg', { min, max })], + [String(RangeErrorEnums.Min), t('config.numberMinimumErrorMsg', { min })], + [String(RangeErrorEnums.Max), t('config.numberMaximumErrorMsg', { max })], + [String(RangeErrorEnums.Default), ''], + ]) - const trueVlue = String(value).replace(/\s/g, '') - if (base === SchameBase.hexadecimal) { - if (!checkHexadecimalValue(trueVlue)) { - callback(new Error(t('config.hexadecimalFormatError'))) - } else { - callback() - } - } else { - const isDecimalValue = /^[0-9]\d*$/.test(trueVlue) - if (!isDecimalValue) { - callback(new Error(t('config.decimalFormatError'))) + if (attribute === 'required') { + if (!inRange) { + const errorMsg = errorMsgMap.get(errorMsgType) + callback(new Error(errorMsg)) } else { callback() } + } else if (value !== '' && !inRange) { + const errorMsg = errorMsgMap.get(errorMsgType) + callback(new Error(errorMsg)) } + callback() } // valid string length @@ -88,6 +114,28 @@ export default (props: Props) => { callback() } + const checkArrayParamLength = (rule: unknown, value: string, callback: any) => { + const { valid } = props.paramInfo as ArrayParamInfo + const { min_length, max_length } = valid + + const isInRange = comparisonRange(value.length, { min: min_length, max: max_length }) + const { inRange, errorMsgType } = isInRange + + const errorMsgMap = new Map([ + [String(RangeErrorEnums.All), t('config.lengthRangeErrorMsg', { min: min_length, max: max_length })], + [String(RangeErrorEnums.Min), t('config.lengthMinimumErrorMsg', { min: min_length })], + [String(RangeErrorEnums.Max), t('config.lengthMaximumErrorMsg', { max: max_length })], + [String(RangeErrorEnums.Default), ''], + ]) + + if (!inRange) { + const errorMsg = errorMsgMap.get(errorMsgType) + callback(new Error(errorMsg)) + } else { + callback() + } + } + const createNumberParamRules = () => [ { // required: !!props.paramInfo.default, @@ -95,17 +143,15 @@ export default (props: Props) => { message: createCommonErrorMessage('input', i18nContent(props.paramInfo, 'name')), }, { - type: isParamHexadecimalBase(props.paramInfo) ? 'string' : 'number', - message: isParamHexadecimalBase(props.paramInfo) - ? t('config.hexadecimalFormatError') - : t('config.numberFormatError'), + type: 'number', + message: t('config.numberFormatError'), }, - { validator: checkNumberParamHexadecimal, trigger: 'blur' }, { validator: checkNumberParamLimit, trigger: 'blur' }, ] const createStringParamRules = () => [ { + type: 'string', // required: !!props.paramInfo.default, required: props.paramInfo.attribute === ParamRequired.True, message: createCommonErrorMessage('input', i18nContent(props.paramInfo, 'name')), @@ -128,6 +174,14 @@ export default (props: Props) => { }, ] + const createArrayParamRules = () => [ + { + required: props.paramInfo.attribute === ParamRequired.True, + message: createCommonErrorMessage('input', props.paramInfo.name), + }, + { validator: checkArrayParamLength, trigger: ['blur', 'change'] }, + ] + const rules = computed(() => { const createMap = { [TypeOfPluginParam.Int]: createNumberParamRules, @@ -136,6 +190,7 @@ export default (props: Props) => { [TypeOfPluginParam.Enum]: createSelectParamRules, [TypeOfPluginParam.Map]: createSelectParamRules, [TypeOfPluginParam.File]: createFileParamRules, + [TypeOfPluginParam.Array]: createArrayParamRules, } return (createMap[props.paramInfo.type] && createMap[props.paramInfo.type]()) || [] }) diff --git a/src/i18n/config.ts b/src/i18n/config.ts index f840cf69..108ff3b8 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -344,21 +344,30 @@ export default { zh: '请输入数字', en: 'Please enter a number', }, - hexadecimalFormatError: { - zh: '请输入格式正确的 16 进制数', - en: 'Please enter a hexadecimal number in the correct format', - }, - decimalFormatError: { - zh: '请输入格式正确的 10 进制数', - en: 'Please enter a decimal number in the correct format', - }, - numberErrorPrefix: { - zh: '请输入', - en: 'Please enter a number between', - }, - numberErrorSuffix: { - zh: '之间的数字', - en: '', + numberRangeErrorMsg: { + zh: '请输入 {min} - {max} 之间的数字', + en: 'Please enter a number between {min} - {max}', + }, + numberMinimumErrorMsg: { + zh: '请输入不小于 {min} 的数字', + en: 'Please enter a number not less than {min}', + }, + numberMaximumErrorMsg: { + zh: '请输入不大于 {max} 的数字', + en: 'Please enter a number no greater than {max}', + }, + + lengthRangeErrorMsg: { + zh: '请输入长度在 {min} - {max} 之间的数据', + en: 'Please enter data between {min} - {max} in length', + }, + lengthMinimumErrorMsg: { + zh: '请输入长度不小于 {length} 的数据', + en: 'Please enter data with a length not less than {length}', + }, + lengthMaximumErrorMsg: { + zh: '请输入长度不大于 {length} 的数据', + en: 'Please enter data with a length no greater than {length}', }, stringLengthErrorPrefix: { zh: '请输入', diff --git a/src/types/config.d.ts b/src/types/config.d.ts index e9151ef5..c3d49079 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -83,12 +83,15 @@ export interface ResponseDriverListData { interface ParamBaseInfo { description: string + description_zh?: string name: string + name_zh: string attribute: ParamRequired base?: SchameBase condition?: { field: string value: boolean | number | string + values?: Array regex: string } } @@ -134,8 +137,23 @@ interface MapParamInfo extends ParamBaseInfo { map: Array } } +interface ArrayParamInfo extends ParamBaseInfo { + type: TypeOfPluginParam.Array + default?: Array + fields: Array + valid: { + min_length: number + max_length: number + } +} -export type ParamInfo = NumberParamInfo | StringParamInfo | BoolParamInfo | MapParamInfo | FileParamInfo +export type ParamInfo = + | NumberParamInfo + | StringParamInfo + | BoolParamInfo + | MapParamInfo + | FileParamInfo + | ArrayParamInfo interface TagRegex { type: number diff --git a/src/types/enums.ts b/src/types/enums.ts index f20b3d20..3f65b8c7 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -34,6 +34,7 @@ export enum TypeOfPluginParam { Enum = 'enum', Map = 'map', File = 'file', + Array = 'array', } export enum TagType { diff --git a/src/views/config/NodeConfig.vue b/src/views/config/NodeConfig.vue index 8d15f01e..8d3234b7 100644 --- a/src/views/config/NodeConfig.vue +++ b/src/views/config/NodeConfig.vue @@ -8,21 +8,23 @@ {{ node }}

- - - - - - - + + + + +