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 @@
+
+
+
+
+
+
+
+ {{ showLabel(field) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ False
+
+
+
+
+
+
+ {{ upperFirstLetter(key) }}
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ {{ $t('common.delete') }}
+
+
+
+
+
+
+ {{ $t('common.add') }}
+
+
+
+
+
+
+
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 }}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -45,6 +47,7 @@ import useNodeConfig from '@/composables/config/useNodeConfig'
import { DriverDirection } from '@/types/enums'
import NodeConfigParamItem from './components/NodeConfigParamItem.vue'
import { useI18n } from 'vue-i18n'
+import useNodeConfigParamCommon from '@/composables/config/useNodeConfigParamCommon'
const props = defineProps({
direction: {
@@ -52,6 +55,7 @@ const props = defineProps({
required: true,
},
})
+
const { t } = useI18n()
const cardTitle = computed(() =>
t(props.direction === DriverDirection.North ? 'config.appConfig' : 'config.deviceConfig'),
@@ -60,6 +64,8 @@ const labelForNodeName = computed(() =>
t(props.direction === DriverDirection.North ? 'config.appName' : 'config.deviceName'),
)
+const { shouldFieldShow } = useNodeConfigParamCommon()
+
const {
node,
configForm,
@@ -67,11 +73,13 @@ const {
fieldList,
isLoading,
formCom,
+ setParamRef,
isSubmitting,
- shouldFieldShow,
+ // shouldFieldShow,
submit,
cancel,
reset,
+ validateFileds,
} = useNodeConfig(props)
diff --git a/src/views/config/components/NodeConfigParamItem.vue b/src/views/config/components/NodeConfigParamItem.vue
index 91b6e62d..369e27ac 100644
--- a/src/views/config/components/NodeConfigParamItem.vue
+++ b/src/views/config/components/NodeConfigParamItem.vue
@@ -1,77 +1,85 @@
-
-
- {{ showLabel() }}
-
-
-
-
-
-
-
-
+
+
+
+ {{ showLabel(paramInfo) }}
+
+
+
+
+
+
+
-
-
-
-
-
-
- True
- False
-
-
-
-
- {{ t('common.uploadFile') }}
-
-
- {{ t('config.clearUploadedFile') }}
-
-
-
- {{ fileContentPreview(inputValue) }}
-
-
-
-
-
-
-
-
- {{ upperFirstLetter(key) }}
-
+
+
+
+
+ True
+ False
-
-
+
+
+ {{ t('common.uploadFile') }}
+
+
+ {{ t('config.clearUploadedFile') }}
+
+
+
+ {{ fileContentPreview(inputValue) }}
+
+
+
+
+
+
+
+
+ {{ upperFirstLetter(key) }}
+
+
+
+
+
+
+
+
-
-
-
+
+
+