holy shit it works

This commit is contained in:
Jono Targett 2026-03-17 19:18:04 +10:30
parent 7ced5a1662
commit 451b639395
3 changed files with 1382 additions and 125 deletions

1295
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,10 +14,16 @@
"@codemirror/state": "^6.6.0", "@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0", "@codemirror/view": "^6.40.0",
"@jsonforms/core": "^3.7.0",
"@jsonforms/material-renderers": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@jsonforms/vue": "^3.7.0",
"@jsonforms/vue-vanilla": "^3.7.0",
"@primeuix/themes": "^1.0.0", "@primeuix/themes": "^1.0.0",
"@primevue/core": "^4.2.5", "@primevue/core": "^4.2.5",
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"jsonforms-primevue": "github:kobbejager/jsonforms-primevue",
"mqtt": "^5.15.0", "mqtt": "^5.15.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.2.5", "primevue": "^4.2.5",

View File

@ -7,24 +7,16 @@ import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader'; import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent'; import AccordionContent from 'primevue/accordioncontent';
import { nextTick } from 'vue'
import {basicSetup} from "codemirror" import { JsonForms } from "@jsonforms/vue";
import { oneDark } from "@codemirror/theme-one-dark" import { defaultStyles, mergeStyles, vanillaRenderers } from "@jsonforms/vue-vanilla";
import { EditorView } from "@codemirror/view" import { primeVueRenderers } from 'jsonforms-primevue'
import { EditorState, StateEffect } from "@codemirror/state"
import { useLayout } from "../../composables/useLayout";
const { isDarkMode } = useLayout(); const renderers = Object.freeze([
...primeVueRenderers,
// here you can add custom renderers
]);
const lightTheme = EditorView.theme({}, { dark: false })
const darkTheme = oneDark //EditorView.theme({}, { dark: true })
import { json } from "@codemirror/lang-json"
const editorContainers = reactive({})
const editors = reactive({})
const openTabs = ref([]) const openTabs = ref([])
const props = defineProps({ const props = defineProps({
@ -40,6 +32,8 @@ const commands = ref([])
const commandMap = reactive({}) const commandMap = reactive({})
function rebuildList() { function rebuildList() {
commands.value = Object.values(commandMap) commands.value = Object.values(commandMap)
} }
@ -48,9 +42,9 @@ function updateCommand(device, name, field, value) {
if (!commandMap[name]) { if (!commandMap[name]) {
commandMap[name] = { commandMap[name] = {
name, name,
description: '', description: "No description provided.",
schema: '', schema: {},
input: '' input: {}
} }
} }
@ -62,78 +56,11 @@ function sendCommand(cmd) {
const topic = `device/${props.deviceId}/command/${cmd.name}` const topic = `device/${props.deviceId}/command/${cmd.name}`
mqtt2.publish(topic, cmd.input || '{}') var payload = JSON.stringify(cmd.input, null, 2)
}
function setEditorRef(name, el) { console.log("Command input", payload)
if (el) {
editorContainers[name] = el
}
}
function onAccordionChange(value) {
const opened = Array.isArray(value) ? value : [value]
opened.forEach(name => {
if (editors[name]) return
const cmd = commands.value.find(c => c.name === name)
if (!cmd) return
const el = editorContainers[name]
if (!el) return
nextTick(() => createEditor(cmd, el))
})
}
function createEditor(cmd, el) {
if (editors[cmd.name]) return
const initialDoc =
cmd.input ||
''
const state = EditorState.create({
doc: initialDoc,
extensions: baseExtensions(cmd, isDarkMode.value ? darkTheme : lightTheme)
})
const view = new EditorView({
state,
parent: el
})
editors[cmd.name] = view
}
function baseExtensions(cmd, theme) {
return [
basicSetup,
json(),
theme,
EditorView.updateListener.of(update => {
if (update.docChanged) {
cmd.input = update.state.doc.toString()
}
})
]
}
function updateEditorTheme() {
Object.entries(editors).forEach(([cmd, view]) => {
console.log("changing theme of ", view)
view.dispatch({
effects: StateEffect.reconfigure.of(baseExtensions(cmd, isDarkMode.value ? darkTheme : lightTheme))
})
})
mqtt2.publish(topic, payload || '{}')
} }
import Dialog from 'primevue/dialog' import Dialog from 'primevue/dialog'
@ -145,24 +72,13 @@ const schemaDialog = reactive({
function showSchema(cmd) { function showSchema(cmd) {
try { try {
schemaDialog.content = JSON.stringify(JSON.parse(cmd.schema), null, 2) schemaDialog.content = JSON.stringify(cmd.schema, null, 2)
} catch { } catch {
schemaDialog.content = cmd.schema || 'No schema available' schemaDialog.content = cmd.schema || 'No schema available'
} }
schemaDialog.visible = true schemaDialog.visible = true
} }
watch(() => props.deviceId, () => {
Object.keys(commandMap).forEach(k => delete commandMap[k])
commands.value = []
})
watch(isDarkMode, () => {
console.log("Editor mode changed")
updateEditorTheme()
})
onMounted(() => { onMounted(() => {
mqtt2.subscribe('device/+/command/#', (payload, topic) => { mqtt2.subscribe('device/+/command/#', (payload, topic) => {
@ -179,8 +95,8 @@ onMounted(() => {
updateCommand(device, command, 'description', payload) updateCommand(device, command, 'description', payload)
} }
if (field === 'schema') { if (field === 'schema') {
const pretty = JSON.stringify(JSON.parse(payload), null, 2) const schema = JSON.parse(payload)
updateCommand(device, command, 'schema', pretty) updateCommand(device, command, 'schema', schema)
} }
}) })
@ -189,24 +105,62 @@ onMounted(() => {
updateEditorTheme() updateEditorTheme()
}) })
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
}) })
const schema = {
properties: {
measRate: {
type: "integer",
minimum: 50,
maximum: 60000,
description: "Measurement period in milliseconds"
},
navRate: {
type: "integer",
minimum: 1,
maximum: 127,
description: "Number of measurement cycles per navigation solution"
},
timeRef: {
type: "integer",
enum: [
0,
1
],
description: "Time reference (0=UTC, 1=GPS)"
}
},
required: [
"measRate",
"navRate",
"timeRef"
],
additionalProperties: false
}
const jsonFormsConfig = {
validationMode: 'ValidateOnTouched',
showAllErrors: false,
showErrorsOnTouched: true,
}
const onChange = (event) => {
console.log(event.data)
// state.errors = event.errors || []
}
</script> </script>
<template> <template>
<Dialog <Dialog
v-model:visible="schemaDialog.visible" v-model:visible="schemaDialog.visible"
header="Command Schema" header="Command Schema"
:modal="true" :modal="true"
:closable="true" :closable="true"
:style="{width: '400px'}" :style="{width: '400px'}"
> >
<pre>{{ schemaDialog.content }}</pre> <pre>{{ schemaDialog.content }}</pre>
</Dialog> </Dialog>
<div class="layout-card"> <div class="layout-card">
<DataView :value="commands"> <DataView :value="commands">
<template #header> <template #header>
@ -220,7 +174,7 @@ onMounted(() => {
</template> </template>
<template #empty> No commands available.</template> <template #empty> No commands available.</template>
<template #list="slotProps"> <template #list="slotProps">
<Accordion v-model:value="openTabs" @update:value="onAccordionChange"> <Accordion >
<AccordionPanel <AccordionPanel
v-for="cmd in slotProps.items" v-for="cmd in slotProps.items"
@ -237,22 +191,29 @@ onMounted(() => {
<AccordionContent> <AccordionContent>
<div class="command-content"> <div class="command-content">
<!-- Left: Code editor --> <!-- Left: Code editor -->
<div class="editor-container" :ref="el => setEditorRef(cmd.name, el)"></div> <JsonForms
:data="cmd.input"
:schema="cmd.schema"
:renderers="renderers"
:config="jsonFormsConfig"
:onChange="({ data, errors }) => cmd.input = data"
/>
<!-- Right: Buttons --> <!-- Right: Buttons -->
<div class="button-container"> <div class="button-container">
<Button <Button
icon="pi pi-question-circle" icon="pi pi-question-circle"
class="p-mb-2" class="p-mb-2"
label="Help"
severity="secondary"
@click="showSchema(cmd)" @click="showSchema(cmd)"
tooltip="Show schema" tooltip="Show schema"
tooltipOptions="{position:'top'}"
/> />
<Button <Button
icon="pi pi-send" icon="pi pi-send"
label="Send"
@click="sendCommand(cmd)" @click="sendCommand(cmd)"
tooltip="Show schema" tooltip="Send command"
tooltipOptions="{position:'top'}"
/> />
</div> </div>
</div> </div>
@ -338,6 +299,7 @@ onMounted(() => {
.command-content { .command-content {
display: flex; display: flex;
flex-direction: column;
gap: 1rem; gap: 1rem;
} }
@ -347,9 +309,9 @@ onMounted(() => {
.button-container { .button-container {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 0.5rem; gap: 0.5rem;
justify-content: flex-start; /* aligns buttons to the top */ justify-content: flex-end; /* aligns buttons to the top */
} }
</style> </style>