mqttdevicemanager/console/src/components/dashboard/CommandsWidget.vue

264 lines
6.6 KiB
Vue

<script setup>
import { ref, reactive, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
import Accordion from 'primevue/accordion';
import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
import Dialog from 'primevue/dialog'
import { JsonForms } from "@jsonforms/vue";
import { createAjv } from '@jsonforms/core'
import { primeVueRenderers } from 'jsonforms-primevue'
import { useToast } from 'primevue/usetoast'
const toast = useToast()
const renderers = Object.freeze([
...primeVueRenderers,
// here you can add custom renderers
]);
const props = defineProps({
deviceId: String
})
const mqtt2 = new MQTTService()
const filters = ref({})
const commands = ref([])
const commandMap = reactive({})
const commandByResponseId = reactive({})
const defaultsAjv = createAjv({ useDefaults: true })
// Suppress Ajv unknown format warning by registering a no-op validator for "textarea"
defaultsAjv.addFormat('textarea', true)
function rebuildList() {
commands.value = Object.values(commandMap)
}
function updateCommand(device, name, field, value) {
if (!commandMap[name]) {
commandMap[name] = {
name,
description: "No description provided.",
schema: {},
input: {},
response: null
}
}
commandMap[name][field] = value
rebuildList()
}
function sendCommand(cmd) {
cmd.responseId = null
cmd.response = {}
const topic = `device/${props.deviceId}/command/${cmd.name}`
var payload = JSON.stringify(cmd.input, null, 2)
var responseId = generateId()
commandByResponseId[responseId] = cmd
mqtt2.publish(topic, payload || '{}', {
qos: 1,
properties: {
responseTopic: `client/${mqtt2.clientId}/responses`,
correlationData: new TextEncoder().encode(responseId)
}
})
}
function generateId() {
return Math.random().toString(16).substr(2, 8)
}
const schemaDialog = reactive({
visible: false,
content: ''
})
function showSchema(cmd) {
try {
schemaDialog.content = JSON.stringify(cmd.schema, null, 2)
} catch {
schemaDialog.content = cmd.schema || 'No schema available'
}
schemaDialog.visible = true
}
onMounted(() => {
mqtt2.subscribe('device/+/command/#', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
if (device !== props.deviceId) return
const command = parts[3]
const field = parts[4]
if (field === '$description') {
updateCommand(device, command, 'description', payload)
}
if (field === '$schema') {
const schema = JSON.parse(payload)
updateCommand(device, command, 'schema', schema)
}
})
mqtt2.subscribe(`client/${mqtt2.clientId}/responses`, (payload, topic) => {
let response = JSON.parse(payload)
const responseId = response.correlation
const cmd = commandByResponseId[responseId]
if (cmd) {
cmd.response = response
toast.add({
severity: response.success ? 'success' : 'error',
summary: cmd.name,
detail: response.message !== 'None' ? response.message : (response.success ? 'Success' : 'Failed'),
life: 4000
})
delete commandByResponseId[responseId]
}
})
})
const jsonFormsConfig = {
validationMode: 'ValidateOnTouched',
showAllErrors: false,
showErrorsOnTouched: true,
}
</script>
<template>
<Dialog
v-model:visible="schemaDialog.visible"
header="Command Schema"
:modal="true"
:closable="true"
:style="{width: '700px'}"
>
<pre>{{ schemaDialog.content }}</pre>
</Dialog>
<div class="layout-card col-item-2">
<DataView :value="commands">
<template #header>
<div class="flex products-header">
<span class="products-title">Commands</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText v-model="filters['global']" placeholder="Filter by..." />
</IconField>
</div>
</template>
<template #empty> No commands available.</template>
<template #list="slotProps">
<Accordion >
<AccordionPanel
v-for="cmd in slotProps.items"
:key="cmd.name"
:value="cmd.name"
>
<AccordionHeader>
<div class="command-header">
<span class="command-name">{{ cmd.name }}</span>
<span class="command-desc">{{ cmd.description }}</span>
</div>
</AccordionHeader>
<AccordionContent>
<div class="command-content">
<!-- Left: Code editor -->
<JsonForms
:data="cmd.input"
:schema="cmd.schema"
:renderers="renderers"
:config="jsonFormsConfig"
:onChange="({ data, errors }) => cmd.input = data"
:ajv="defaultsAjv"
/>
<!-- Right: Buttons -->
<div class="button-container">
<Button
v-if="cmd.response"
:icon="cmd.response.success ? 'pi pi-check' : 'pi pi-times'"
class="p-button-rounded p-button-text"
:class="cmd.response.success ? 'p-button-success' : 'p-button-danger'"
v-tooltip="cmd.response.message !== 'None' ? cmd.response.message : null"
/>
<Button
icon="pi pi-question-circle"
class="p-mb-2"
label="Help"
severity="secondary"
@click="showSchema(cmd)"
tooltip="Show schema"
/>
<Button
icon="pi pi-send"
label="Send"
@click="sendCommand(cmd)"
tooltip="Send command"
/>
</div>
</div>
</AccordionContent>
</AccordionPanel>
</Accordion>
</template>
</DataView>
</div>
</template>
<style scoped>
.command-header {
display: flex;
gap: 10px;
align-items: center;
}
.command-name {
font-weight: 600;
}
.command-desc {
font-size: 0.8rem;
}
.command-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.editor-container {
flex: 1;
}
.button-container {
display: flex;
flex-direction: row;
gap: 0.5rem;
justify-content: flex-end;
}
</style>