Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(template): Added template management function. #396

Merged
merged 5 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/api/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import http from '@/utils/http'
import type { AxiosResponse } from 'axios'
import type { RawTemplateData, TemplateFormData } from '@/types/config'

export const queryTemplateList = async (): Promise<Array<RawTemplateData>> => {
const { data }: AxiosResponse<{ error: number; templates: Array<RawTemplateData> }> = await http.get('/template')
return Promise.resolve(data?.templates || [])
}

// without name,delete all
export const deleteTemplate = async (templateName?: string): Promise<AxiosResponse> => {
return http.delete('/template', {
params: { name: templateName },
})
}

export const createTemplate = async (data: TemplateFormData): Promise<AxiosResponse> => {
return http.post('/template', data)
}

export const updateTemplate = async (data: TemplateFormData): Promise<AxiosResponse> => {
return http.post('/template', data)
}

export const getTemplateDetailByName = async (templateName: string): Promise<AxiosResponse> => {
return http.get('/template', { params: { name: templateName } })
}
4 changes: 4 additions & 0 deletions src/components/SideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ const navList = computed(() => {
to: '/configuration/north-driver',
label: 'config.northAppSetup',
},
{
to: '/configuration/template',
label: 'config.templateManagement',
},
{
to: '/configuration/plugin',
label: 'config.plugin',
Expand Down
106 changes: 106 additions & 0 deletions src/composables/config/useTemplateDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ref, computed, watch, nextTick } from 'vue'
import type { Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { TemplateFormData } from '@/types/config'
import { createTemplate, updateTemplate } from '@/api/template'
import { EmqxMessage } from '@emqx/emqx-ui'
import { cloneDeep } from 'lodash'

export const useTemplateForm = () => {
const createTemplateForm = (): TemplateFormData => ({
name: '',
plugin: '',
groups: [],
})
return {
createTemplateForm,
}
}

interface TemplateProps {
modelValue: boolean
isEdit?: boolean
isImport?: boolean
templateData: TemplateFormData
}
export default (props: TemplateProps) => {
const { t } = useI18n()

const { createTemplateForm } = useTemplateForm()

const formRef = ref()
const templateForm: Ref<TemplateFormData> = ref(createTemplateForm())
const isSubmitting = ref(false)

const rules = {
name: [{ required: true, message: t('config.nameRequired') }],
plugin: [{ required: true, message: t('config.pluginRequired') }],
}

const dialogTitle = computed(() => {
let title = ''
if (props.isImport) {
title = t('template.importTemplate')
} else {
title = !props.isEdit ? t('template.addTemplate') : t('template.editTemplate')
}
return title
})

const initForm = async () => {
formRef.value.resetField()
templateForm.value = createTemplateForm()
}

const submitData = async () => {
try {
await formRef.value.validate()
isSubmitting.value = true

const data = templateForm.value
if (!props.isEdit || props.isImport) {
await createTemplate(data)

const message = !props.isImport ? t('common.createSuccess') : t('common.importSuccess')
EmqxMessage.success(message)
} else {
// TODO: edit
await updateTemplate(data)
EmqxMessage.success(t('common.updateSuccess'))
}
return Promise.resolve()
} catch (error) {
return Promise.reject()
} finally {
isSubmitting.value = false
}
}

const initTemplateData = () => {
if (props.isImport) {
templateForm.value = cloneDeep(props.templateData)
}
}

watch(
() => props.templateData.name,
(newV) => {
if (newV) {
nextTick(() => {
initTemplateData()
})
}
},
{ immediate: true },
)
return {
createTemplateForm,
formRef,
templateForm,
dialogTitle,
isSubmitting,
rules,
initForm,
submitData,
}
}
122 changes: 122 additions & 0 deletions src/composables/config/useTemplateList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ref } from 'vue'
import type { Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RawTemplateData, TemplateFormData } from '@/types/config'
import { queryTemplateList, deleteTemplate, getTemplateDetailByName } from '@/api/template'
import { EmqxMessage } from '@emqx/emqx-ui'
import { MessageBoxConfirm } from '@/utils/element'
import { useTemplateForm } from '@/composables/config/useTemplateDialog'
import { useDownload } from '@/composables/useDownload'
import { dataType, isJSONData } from '@/utils/utils'

export default () => {
const { t } = useI18n()
const { createTemplateForm } = useTemplateForm()

const templateList: Ref<Array<RawTemplateData>> = ref([])
const isListLoading = ref(false)
const templateDialogVisible = ref(false)
const isEditTemplate = ref(false)
const isImportTemplate = ref(false)
const editTemplateData: Ref<TemplateFormData> = ref(createTemplateForm())

const getTemplateList = async () => {
try {
isListLoading.value = true
templateList.value = await queryTemplateList()
} finally {
isListLoading.value = false
}
}

const showTemplateDialog = () => {
templateDialogVisible.value = true
}

const goGroupPage = (rowData: RawTemplateData) => {
// TODO
console.log('row', rowData)
}
const editTemplate = (rowData: RawTemplateData) => {
// TODO
isEditTemplate.value = true
console.log('row', rowData)
}
const removeTemplate = async (rowData: RawTemplateData) => {
try {
await MessageBoxConfirm() // Phase 2 support: t('template.deleteTemplateTip')
const { name } = rowData
await deleteTemplate(name)
EmqxMessage.success(t('common.operateSuccessfully'))
getTemplateList()
} catch (error) {
console.error(error)
}
}

const { downloadFile, readTextFile } = useDownload()
const exportTemplate = async (rowData: RawTemplateData) => {
try {
const { name } = rowData
const { data } = await getTemplateDetailByName(name)
const dataJSON = JSON.stringify(data, null, 2)
const blobData = new Blob([dataJSON])
downloadFile({ 'content-type': 'application/octet-stream', 'content-disposition': `filename=${name}` }, blobData)
} catch (error) {
console.error(error)
}
}

// import file
const importFile = async (file: File) => {
if (dataType(file) !== 'file') {
EmqxMessage.error(t('common.notJSONData'))
}

isImportTemplate.value = true
const fileData: unknown = await readTextFile(file)

if (isJSONData(String(fileData))) {
const jsonData = JSON.parse(String(fileData)) || createTemplateForm()

const { name, plugin } = jsonData
if (!name) {
EmqxMessage.error(t('template.missingNameInFile'))
}
if (!plugin) {
EmqxMessage.error(t('template.missingPluginInFile'))
}
if (name && plugin) {
editTemplateData.value = jsonData
templateDialogVisible.value = true
}
}

// Capture uploader action
return Promise.reject()
}

const cancelOperateTemplate = () => {
isEditTemplate.value = false
isImportTemplate.value = false
editTemplateData.value = createTemplateForm()
}

getTemplateList()
return {
templateList,
isListLoading,
getTemplateList,
templateDialogVisible,
showTemplateDialog,
goGroupPage,
removeTemplate,
editTemplate,
exportTemplate,
importFile,
isImportTemplate,
editTemplateData,
isEditTemplate,
cancelOperateTemplate,
}
}
31 changes: 30 additions & 1 deletion src/composables/useDownload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { useI18n } from 'vue-i18n'
import { dataType } from '@/utils/utils'
import { ElMessage } from 'element-plus'

export const useDownload = () => {
const { t } = useI18n()

// must take the `responseType: 'blob'` when api request,such as `downloadLogs` api in `@/api/admin`
const downloadFile = (responseHeaders: any, blobData: Blob, fileNameKey?: string) => {
if (!responseHeaders) return
Expand All @@ -17,7 +23,7 @@ export const useDownload = () => {
// download
if ('download' in document.createElement('a')) {
// not IE
const url = window.URL.createObjectURL(new Blob([blobData]))
const url = window.URL.createObjectURL(new Blob([blobData], { type: 'application/json' }))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', name)
Expand All @@ -31,7 +37,30 @@ export const useDownload = () => {
}
}

const readTextFile = (file: File) => {
return new Promise((resolve, reject) => {
if (dataType(file) !== 'file') {
ElMessage.error(t('common.isNotFile'))
reject(t('common.isNotFile'))
} else {
let fileContent: string | ArrayBuffer | null = ''
const reader = new FileReader()
reader.readAsText(file, 'utf-8')
reader.onload = () => {
fileContent = reader?.result || ''
resolve(fileContent)
}
reader.onerror = () => {
ElMessage.error(t('common.readFileError'))
console.error(reader.error)
reject(reader.error)
}
}
})
}

return {
downloadFile,
readTextFile,
}
}
22 changes: 21 additions & 1 deletion src/i18n/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,16 @@ export default {
},
submitSuccess: {
zh: '提交成功',
en: 'Submit success!',
en: 'Submit success',
},
createSuccess: {
zh: '创建成功',
en: 'Create success',
},
updateSuccess: {
zh: '更新成功',
en: 'Update success',
},
operateConfirm: {
zh: '操作确认',
en: 'Operation Confirmation',
Expand Down Expand Up @@ -375,4 +379,20 @@ export default {
zh: '卡片',
en: 'Card',
},
importSuccess: {
zh: '导入成功',
en: 'Import success',
},
isNotFile: {
zh: '不支持非文件类型',
en: 'Non-file types are not supported',
},
notJSONData: {
zh: '非 JSON 数据',
en: 'Non-JSON data',
},
jsonFormatError: {
zh: '数据格式发生错误,请检查数据',
en: 'Data format error, please check the data',
},
}
8 changes: 8 additions & 0 deletions src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ export default {
zh: '南向设备',
en: 'South device',
},
templateManagement: {
zh: '模板管理',
en: 'Template',
},
plugin: {
zh: '插件',
en: 'Plugin',
Expand Down Expand Up @@ -469,4 +473,8 @@ export default {
zh: '间隔',
en: 'Interval',
},
selectPlugin: {
zh: '请选择插件',
en: 'Select plugin',
},
}
2 changes: 1 addition & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createI18n } from 'vue-i18n'
import store from '@/store'
import { setLang } from '@/composables/useLang'

const LangModules = ['data', 'common', 'config', 'admin', 'error', 'ekuiper', 'plugin', 'schema']
const LangModules = ['data', 'common', 'config', 'admin', 'error', 'ekuiper', 'plugin', 'schema', 'template']

type LangModel = {
[key in Language]: $TSFixed
Expand Down
Loading