5 Commits

Author SHA1 Message Date
Adrian Zuercher
7d2ab814da increment app version 2025-07-31 12:30:18 +02:00
Adrian Zuercher
d50bf9c058 implement driver add update remove and improve rename tree experience 2025-07-31 12:29:16 +02:00
Adrian Zuercher
dac7130544 remove dead file from pull 2025-07-25 18:47:01 +02:00
Adrian Zuercher
7434f02c30 move file 2025-07-25 18:41:13 +02:00
zuadi
8c506b9af3 Rename dataTable.vue to DataTable.vue
bug fix of file name
2025-07-12 23:33:59 +02:00
19 changed files with 664 additions and 113 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "lightcontrol",
"version": "0.0.19",
"version": "0.0.21",
"description": "A Tecamino App",
"productName": "Light Control",
"author": "A. Zuercher",

View File

@@ -3,10 +3,9 @@ import axios from 'axios';
const host = window.location.hostname;
const port = 8100;
const baseURL = `http://${host}:${port}`;
const api = axios.create({
baseURL: baseURL,
baseURL: `http://${host}:${port}`,
timeout: 30000,
headers: {
'Content-Type': 'application/json',

View File

@@ -4,11 +4,8 @@ import { initWebSocket } from '../vueLib/services/websocket';
export default boot(({ app }) => {
const $q = app.config.globalProperties.$q as QVueGlobals;
const host = window.location.hostname;
const port = 8100;
const randomId = Math.floor(Math.random() * 10001); // random number from 0 to 10000
const ws = initWebSocket(`ws://${host}:${port}/ws?id=q${randomId}`, $q);
const ws = initWebSocket(window.location.hostname, 8100, $q);
app.config.globalProperties.$socket = ws;
ws.connect();

View File

@@ -66,18 +66,14 @@ const internalShowDialog = ref(props.showDialog);
watch(
() => props.showDialog,
(newValue) => {
console.log('watch showDialog', newValue);
internalShowDialog.value = newValue;
},
);
watch(internalShowDialog, (newValue) => {
console.log('watch internalShowDialog', newValue);
emit('update:showDialog', newValue);
if (!newValue) {
console.log('emit cancel');
emit('cancel');
} else {
console.log('emit confirmed');
emit('confirmed');
}
});

View File

@@ -17,7 +17,7 @@
<div
:class="[
'text-left',
!props.row.path.includes('System') && props.row.path !== 'DBM'
props.row.path?.split(':')[0] !== 'System' && props.row.path !== 'DBM'
? 'cursor-pointer'
: '',
'q-mx-sm',
@@ -32,7 +32,7 @@
<div
:class="[
'text-center',
!props.row.path.includes('System') && props.row.path !== 'DBM'
props.row.path?.split(':')[0] !== 'System' && props.row.path !== 'DBM'
? 'cursor-pointer'
: '',
'q-mx-sm',
@@ -43,15 +43,18 @@
</q-td>
</template>
<template v-slot:body-cell-value="props">
<q-td :props="props" @click="openDialog(props.row)">
<q-td :props="props" @click="openDialog(props.row, 'value')">
<div :class="['text-center', 'cursor-pointer', 'q-mx-sm']">
{{ props.row.value }}
</div>
</q-td>
</template>
<template v-slot:body-cell-drivers="props">
<q-td :props="props" @click="openDialog(props.row, 'driver')">
<div v-if="props.row.type !== 'none'" :class="['cursor-pointer']">
<q-td :props="props" @click="openDialog(props.row, 'drivers')">
<div
v-if="props.row.type !== 'NONE' || props.row.path?.split(':')[0] !== 'System'"
:class="['cursor-pointer']"
>
<q-icon size="sm" name="cell_tower" :color="props.row.drivers ? 'blue-5' : 'grey-4'" />
</div>
</q-td>
@@ -59,6 +62,7 @@
</q-table>
<RenameDialog width="400px" button-ok-label="Rename" ref="renameDialog" />
<UpdateDialog width="400px" button-ok-label="Write" ref="updateDialog" />
<UpdateDriver width="400px" ref="updateDriverDialog" />
<UpdateDatatype
width="400px"
button-ok-label="Update"
@@ -72,6 +76,7 @@
import UpdateDialog from './dialog/UpdateValueDialog.vue';
import RenameDialog from './dialog/RenameDatapoint.vue';
import UpdateDatatype from './dialog/UpdateDatatype.vue';
import UpdateDriver from './dialog/UpdateDriverDialog.vue';
import type { QTableProps } from 'quasar';
import type { Subscribe } from '../models/Subscribe';
import { computed, ref } from 'vue';
@@ -80,10 +85,12 @@ import { convertFromType } from './Datapoint';
const renameDialog = ref();
const updateDialog = ref();
const updateDriverDialog = ref();
const updateDatatype = ref();
const openDialog = (sub: Subscribe, type?: string) => {
if (sub.path?.includes('System') || sub.path === 'DBM') return;
console.log(11, sub);
if (sub.path?.split(':')[0] === 'System' && sub.path !== 'DBM') return;
switch (type) {
case 'type':
updateDatatype.value.open(sub.uuid);
@@ -91,10 +98,14 @@ const openDialog = (sub: Subscribe, type?: string) => {
case 'rename':
renameDialog.value.open(sub.uuid);
break;
default:
if (sub.type === 'none') return;
case 'value':
if (sub.type === 'NONE') return;
updateDialog.value?.open(ref(sub), type);
break;
case 'drivers':
if (sub.type === 'NONE') return;
updateDriverDialog.value?.open(sub);
break;
}
};

View File

@@ -10,7 +10,7 @@
<q-input
class="q-mt-lg q-mb-none q-pl-lg q-pr-xl"
filled
v-model="path"
v-model="addingForm.path"
label=""
:rules="[(val) => !!val || 'Path is required']"
>
@@ -18,7 +18,7 @@
<div class="column">
<span class="text-caption text-primary non-editable-prefix">Path *</span>
<span class="text-body2 text-grey-6 non-editable-prefix"
>{{ prefix }}{{ staticPrefix }}</span
>{{ addingForm.prefix }}{{ addingForm.staticPrefix }}</span
>
</div>
</template>
@@ -26,8 +26,8 @@
<DataTypes class="q-mt-lg q-pl-md q-pr-xl" flat v-model:datatype="datatype"></DataTypes>
<div class="q-pl-lg">
<div class="text-grey text-bold">Read Write Access</div>
<q-checkbox v-model="read">Read</q-checkbox>
<q-checkbox v-model="write">Write</q-checkbox>
<q-checkbox v-model="addingForm.read">Read</q-checkbox>
<q-checkbox v-model="addingForm.write">Write</q-checkbox>
</div>
<q-input
:type="valueType"
@@ -35,7 +35,7 @@
label="Value"
class="q-pl-md q-pr-xl"
filled
v-model="value"
v-model="addingForm.value"
></q-input>
<q-btn no-caps class="q-mb-xl q-mx-xl q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
@@ -45,7 +45,7 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { reactive, ref, watch } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import { useNotify } from '../../general/useNotify';
import DataTypes from '../../buttons/DataTypes.vue';
@@ -56,16 +56,21 @@ import { convertToType } from '../Datapoint';
import { catchError } from 'src/vueLib/models/error';
const { NotifyResponse } = useNotify();
const addingForm = reactive({
path: '',
value: '',
staticPrefix: '',
read: true,
write: true,
prefix: 'DBM:',
});
const Dialog = ref();
const path = ref('');
const staticPrefix = ref('');
const value = ref('');
const valueType = ref<'text' | 'number'>('text');
const read = ref(true);
const write = ref(true);
const datatype = ref('None');
const addForm = ref();
const prefix = 'DBM:';
const open = (uuid: string) => {
Dialog.value?.open();
@@ -84,15 +89,15 @@ function onSubmit() {
if (success) {
type = convertToType(datatype.value);
if (read.value) access = 'R';
if (write.value) access += 'W';
if (addingForm.read) access = 'R';
if (addingForm.write) access += 'W';
if (access == '') access = 'R';
setRequest(staticPrefix.value + path.value, type, value.value, access)
setRequest(addingForm.staticPrefix + addingForm.path, type, addingForm.value, access)
.then((respond) => {
if (respond) {
respond.forEach((set) => {
NotifyResponse("Datapoint '" + prefix + set.path + "' added");
NotifyResponse("Datapoint '" + addingForm.prefix + set.path + "' added");
});
addRawSubscription(respond[0]);
UpdateTable();
@@ -102,7 +107,7 @@ function onSubmit() {
NotifyResponse(catchError(err), 'error');
});
} else {
if (path.value === '') {
if (addingForm.path === '') {
NotifyResponse("Field 'Path' is requierd", 'error');
return;
} else NotifyResponse('Form not validated', 'error');
@@ -133,8 +138,8 @@ function getDatapoint(uuid: string) {
getRequest(uuid, '', 1)
.then((resp) => {
if (resp[0]) {
staticPrefix.value = resp[0].path ?? '';
if (staticPrefix.value !== '') staticPrefix.value += ':';
addingForm.staticPrefix = resp[0].path ?? '';
if (addingForm.staticPrefix !== '') addingForm.staticPrefix += ':';
}
})
.catch((err) => NotifyResponse(catchError(err), 'error'));

View File

@@ -10,7 +10,7 @@
<q-input
class="q-mt-lg q-mb-none q-pl-md q-mx-lg"
filled
v-model="path"
v-model="copyData.path"
label="Current Path"
label-color="primary"
readonly
@@ -19,7 +19,7 @@
<q-input
class="q-mt-lg q-mt-none q-pl-md q-mx-lg"
filled
v-model="copyPath"
v-model="copyData.copyPath"
label="New Path *"
label-color="primary"
@keyup.enter="onSubmit"
@@ -36,7 +36,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { reactive, ref } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import { useNotify } from '../../general/useNotify';
import { getRequest, setsRequest } from 'src/vueLib/models/Request';
@@ -44,11 +44,14 @@ import { datapointRequestForCopy } from '../Datapoint';
import { catchError } from 'src/vueLib/models/error';
const { NotifyResponse } = useNotify();
const copyData = reactive({
path: '',
copyPath: '',
prefix: 'DBM:',
});
const Dialog = ref();
const path = ref('');
const copyPath = ref('');
const copyForm = ref();
const prefix = 'DBM:';
const open = (uuid: string) => {
Dialog.value?.open();
@@ -58,18 +61,18 @@ const open = (uuid: string) => {
function onSubmit() {
copyForm.value.validate().then((success: undefined) => {
if (success) {
if (copyPath.value === path.value) {
if (copyData.copyPath === copyData.path) {
NotifyResponse('copy path can not be the same as current path', 'error');
return;
}
const absolutePath = path.value.slice(prefix.length);
const absolutecopyPath = copyPath.value.slice(prefix.length);
const absolutePath = copyData.path.slice(copyData.prefix.length);
const absolutecopyPath = copyData.copyPath.slice(copyData.prefix.length);
getRequest('', absolutecopyPath, 1)
.then((response) => {
if (response?.length > 0) {
NotifyResponse("path '" + copyPath.value + "' already exists", 'warning');
NotifyResponse("path '" + copyData.copyPath + "' already exists", 'warning');
return;
}
})
@@ -80,7 +83,7 @@ function onSubmit() {
setsRequest(
datapointRequestForCopy(response, absolutePath, absolutecopyPath),
).catch((err) => console.error(err));
NotifyResponse(copyPath.value + ' copied');
NotifyResponse(copyData.copyPath + ' copied');
})
.catch((err) => NotifyResponse(catchError(err), 'error'));
} else {
@@ -89,7 +92,7 @@ function onSubmit() {
return;
});
} else {
if (copyPath.value === '') {
if (copyData.copyPath === '') {
NotifyResponse("Field 'New Path' is requierd", 'error');
return;
} else NotifyResponse('Form not validated', 'error');
@@ -120,8 +123,8 @@ function getDatapoint(uuid: string) {
getRequest(uuid)
.then((resp) => {
if (resp[0]) {
path.value = prefix + resp[0].path;
copyPath.value = prefix + resp[0].path;
copyData.path = copyData.prefix + resp[0].path;
copyData.copyPath = copyData.prefix + resp[0].path;
}
})
.catch((err) => NotifyResponse(catchError(err), 'error'));

View File

@@ -0,0 +1,226 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="localDialogLabel">
<q-card-section
v-if="props.dialogLabel || localDialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
:class="'text-' + props.labelColor"
>{{ props.dialogLabel || localDialogLabel }}</q-card-section
>
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
props.text
}}</q-card-section>
<q-form ref="form">
<q-card-section class="q-gutter-xs row q-col-gutter-xs">
<div>
<q-select
class="col-8"
filled
label="Driver Name"
type="text"
name="Type"
dense
:options="options"
:rules="[(val) => !!val || 'Name is required']"
v-model="driverForm.type"
/>
<q-input
class="col-8"
filled
label="Bus"
type="text"
name="Bus"
dense
:rules="[(val) => !!val || 'Bus is required']"
v-model="driverForm.bus"
/>
<q-input
v-if="driverForm.isAddress"
class="col-8"
filled
dense
label="Address"
type="number"
name="Address"
v-model.number="driverForm.address"
/>
</div>
<div v-if="!driverForm.isAddress" class="q-gutter-xs row q-col-gutter-xs">
<q-input
class="col-8"
filled
dense
label="Subscribe"
type="text"
name="Address"
v-model="driverForm.subscribe"
/>
<q-input
class="col-8"
filled
dense
label="Publish"
type="text"
name="Address"
v-model="driverForm.publish"
/>
</div>
</q-card-section>
</q-form>
<q-card-actions class="text-primary">
<q-btn v-if="props.buttonCancelLabel" flat :label="props.buttonCancelLabel" v-close-popup />
<q-btn
class="q-mb-xl q-ml-lg q-px-lg"
v-if="props.buttonOkLabel"
color="primary"
no-caps
:label="props.buttonOkLabel"
@click="updateDriver"
>
</q-btn>
</q-card-actions>
</DialogFrame>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import type { Driver } from '../../models/Drivers';
import { setRequest } from 'src/vueLib/models/Request';
import { addRawSubscriptions } from 'src/vueLib/models/Subscriptions';
import { convertToSubscribe, type RawSubs } from 'src/vueLib/models/Subscribe';
import { useNotify } from 'src/vueLib/general/useNotify';
import { UpdateTable } from '../updateTable';
import { updateDriverTable, type DriverTableRow } from 'src/vueLib/models/driverTable';
const { NotifyResponse } = useNotify();
const Dialog = ref();
const options = ['ArtNetDriver', 'OSCDriver'];
const driverForm = reactive({
type: 'ArtNetDriver',
bus: '',
isAddress: true,
address: 0,
subscribe: '',
publish: '',
});
const props = defineProps({
buttonOkLabel: {
type: String,
default: 'OK',
},
labelColor: {
type: String,
default: 'primary',
},
dialogLabel: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
buttonCancelLabel: {
type: String,
default: '',
},
width: {
type: String,
default: '300px',
},
});
const localDialogLabel = ref('');
const driver = ref<Driver>();
let dpUuid = '';
const form = ref();
const address = ref();
const topic = ref();
watch(
() => driverForm.type,
(val) => {
driverForm.isAddress = val === options[0];
if (driverForm.isAddress) {
driverForm.subscribe = '';
driverForm.publish = '';
topic.value = undefined;
} else {
driverForm.address = -1;
address.value = undefined;
}
},
);
const open = (uuid: string, drvs: DriverTableRow, type: 'add' | 'edit') => {
switch (type) {
case 'add':
localDialogLabel.value = 'Add Driver';
driverForm.type = 'ArtNetDriver';
driverForm.isAddress = true;
break;
case 'edit':
localDialogLabel.value = 'Edit Driver';
driverForm.type = drvs.type;
driverForm.bus = drvs.bus;
}
dpUuid = uuid;
driver.value = drvs;
if (drvs.address) driverForm.address = drvs.address;
if (drvs.subscribe) driverForm.subscribe = drvs.subscribe;
if (drvs.publish) driverForm.publish = drvs.publish;
Dialog.value?.open();
};
function updateDriver() {
form.value?.validate();
if (!driverForm.type || !driverForm.bus) {
NotifyResponse('Please fill in all required fields', 'warning');
return;
}
if (driverForm.address > -1) {
address.value = [driverForm.address];
}
if (driverForm.subscribe !== '' || driverForm.publish !== '') {
topic.value = { subscribe: [driverForm.subscribe], publish: [driverForm.publish] };
}
setRequest('', undefined, undefined, undefined, dpUuid, {
type: driverForm.type,
buses: [
{
name: driverForm.bus,
address: address.value,
topic: topic.value,
},
],
})
.then((resp) => {
addRawSubscriptions(resp as RawSubs);
resp.forEach((set) => {
updateDriverTable(convertToSubscribe(set));
});
UpdateTable();
Dialog.value.close();
})
.catch((err) => {
NotifyResponse(err, 'error');
});
}
defineExpose({ open });
</script>
<style scoped>
.outercard {
border-radius: 10px;
}
</style>

View File

@@ -10,7 +10,7 @@
<q-input
class="q-mt-lg q-mb-none q-pl-md q-mx-lg"
filled
v-model="path"
v-model="removeData.path"
label="Current Path"
label-color="primary"
readonly
@@ -19,7 +19,7 @@
<q-input
class="q-mt-lg q-mt-none q-pl-md q-mx-lg"
filled
v-model="newPath"
v-model="removeData.newPath"
label="New Path *"
label-color="primary"
@keyup.enter="onSubmit"
@@ -36,7 +36,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { reactive, ref } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import { useNotify } from '../../general/useNotify';
import { getRequest, setRequest } from 'src/vueLib/models/Request';
@@ -49,10 +49,13 @@ import { buildTree } from '../dbmTree';
const { NotifyResponse } = useNotify();
const Dialog = ref();
const datapoint = ref();
const path = ref('');
const newPath = ref('');
const removeData = reactive({
path: '',
newPath: '',
prefix: 'DBM:',
});
const copyForm = ref();
const prefix = 'DBM:';
const open = (uuid: string) => {
Dialog.value?.open();
@@ -62,14 +65,13 @@ const open = (uuid: string) => {
function onSubmit() {
copyForm.value.validate().then((success: undefined) => {
if (success) {
if (newPath.value === path.value) {
if (removeData.newPath === removeData.path) {
NotifyResponse('same name', 'warning');
return;
}
getRequest('', newPath.value.slice(prefix.length), 1)
getRequest('', removeData.newPath.slice(removeData.prefix.length), 1)
.then((response) => {
console.log(10, response);
if (response?.length > 0) {
NotifyResponse("path '" + response[0]?.path + "' already exists", 'warning');
return;
@@ -83,22 +85,24 @@ function onSubmit() {
}
setRequest(
newPath.value.slice(prefix.length),
removeData.newPath.slice(removeData.prefix.length),
datapoint.value.type,
datapoint.value.value,
datapoint.value.rights,
datapoint.value.uuid,
undefined,
true,
)
.then((res) => {
addRawSubscriptions(res as RawSubs);
console.log(80, res);
buildTree(convertToSubscribes(res as RawSubs));
UpdateTable();
})
.catch((err) => NotifyResponse(err, 'error'));
});
} else {
if (newPath.value === '') {
if (removeData.newPath === '') {
NotifyResponse("Field 'New Path' is requierd", 'error');
return;
} else NotifyResponse('Form not validated', 'error');
@@ -130,8 +134,8 @@ function getDatapoint(uuid: string) {
.then((resp) => {
if (resp[0]) {
datapoint.value = resp[0];
path.value = prefix + resp[0].path;
newPath.value = prefix + resp[0].path;
removeData.path = removeData.prefix + resp[0].path;
removeData.newPath = removeData.prefix + resp[0].path;
}
})
.catch((err) => NotifyResponse(catchError(err), 'error'));

View File

@@ -0,0 +1,199 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="datapoint?.path">
<q-card-section
v-if="props.dialogLabel || localDialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
:class="'text-' + props.labelColor"
>{{ props.dialogLabel || localDialogLabel }}</q-card-section
>
<q-card-section>
<q-table
flat
dense
virtual-scroll
:rows-per-page-options="[0]"
:rows="drivers"
:columns="columns"
row-key="type"
>
<!-- add symbol on top right of table-->
<template v-slot:top-right>
<q-btn
size="sm"
ripple
rounded
color="primary"
icon="add"
round
dense
@click="handleRow(driver, 'add')"
/>
</template>
<template v-slot:body-cell-settings="props">
<q-td :props="props" class="cursor-pointer">
<q-btn
dense
flat
size="sm"
icon="more_vert"
@click="(evt) => openSubMenu(evt, props.row)"
></q-btn>
</q-td>
</template>
</q-table>
<q-menu ref="contextMenuRef" context-menu>
<q-list>
<q-item>
<q-item-section class="cursor-pointer" @click="handleRow(driver, 'edit')">
Edit
</q-item-section>
</q-item>
<q-item>
<q-item-section class="text-negative cursor-pointer" @click="deleteDriver(driver)">
Delete
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-card-section>
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
props.text
}}</q-card-section>
<q-card-actions align="left" class="text-primary">
<q-btn v-if="props.buttonCancelLabel" flat :label="props.buttonCancelLabel" v-close-popup>
</q-btn>
<q-btn
class="q-mb-xl q-ml-lg q-mt-none"
v-if="props.buttonOkLabel"
color="primary"
no-caps
:label="props.buttonOkLabel"
v-close-popup
>
</q-btn>
</q-card-actions>
</DialogFrame>
<DriverDialog :button-ok-label="driverOkLabel" ref="driverDialog"></DriverDialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import DriverDialog from './DriverDialog.vue';
import { convertToSubscribe, type Subscribe } from '../../models/Subscribe';
import type { Driver } from '../../models/Drivers';
import { driverDefault } from '../../models/Drivers';
import type { DriverTableRow } from 'src/vueLib/models/driverTable';
import { updateDriverTable, useDriverTable } from 'src/vueLib/models/driverTable';
import { deleteRequest } from 'src/vueLib/models/Request';
import { useNotify } from 'src/vueLib/general/useNotify';
import type { Bus } from 'src/vueLib/models/Bus';
const { NotifyResponse } = useNotify();
const Dialog = ref();
const driverDialog = ref();
const writeValue = ref();
const onlyRead = ref(false);
const props = defineProps({
buttonOkLabel: {
type: String,
default: 'OK',
},
labelColor: {
type: String,
default: 'primary',
},
dialogLabel: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
buttonCancelLabel: {
type: String,
default: '',
},
width: {
type: String,
default: '300px',
},
});
const datapoint = ref();
const localDialogLabel = ref('');
const contextMenuRef = ref();
const table = useDriverTable();
const drivers = table.driverTable;
const driver: Driver = { type: '' };
const driverOkLabel = ref('');
const columns = table.columns;
const open = (sub: Subscribe) => {
datapoint.value = sub;
if (datapoint.value.rights == 'R') onlyRead.value = true;
table.emptyTable();
localDialogLabel.value = 'Update Drivers';
updateDriverTable(sub);
writeValue.value = sub.drivers;
Dialog.value?.open();
};
function openSubMenu(evt: Event, d: DriverTableRow) {
if (d) {
const bus: Bus = {
name: d.bus,
};
bus.name = d.bus;
if (d.address) bus.address = d.address !== undefined ? [d.address] : [];
if (d.subscribe || d.publish)
bus.topic = {
subscribe:
typeof d.subscribe === 'string' ? d.subscribe.split(',').map((s) => s.trim()) : [],
publish: typeof d.publish === 'string' ? d.publish.split(',').map((s) => s.trim()) : [],
};
driver.type = d.type;
driver.buses = [bus];
}
const mouseEvent = evt as MouseEvent;
contextMenuRef.value?.show(mouseEvent);
}
function handleRow(driver: Driver | undefined, type: 'add' | 'edit') {
driverOkLabel.value = 'Add';
switch (type) {
case 'add':
driver = driverDefault;
break;
case 'edit':
driverOkLabel.value = 'Update';
break;
}
driverDialog.value?.open(datapoint.value.uuid, driver, type);
}
function deleteDriver(driver: Driver | undefined) {
deleteRequest(datapoint.value.uuid, '', driver)
.then((resp) => {
resp.forEach((set) => {
updateDriverTable(convertToSubscribe(set));
});
})
.catch((err) => {
NotifyResponse(err, 'error');
});
}
defineExpose({ open });
</script>
<style scoped>
.outercard {
border-radius: 10px;
}
</style>

View File

@@ -6,7 +6,7 @@
:class="'text-' + props.labelColor"
>{{ props.dialogLabel ? props.dialogLabel : localDialogLabel }}</q-card-section
>
<q-card-section v-if="drivers && drivers.length == 0">
<q-card-section v-if="datapoint.type !== 'BIT'">
<q-input
class="q-px-md q-ma-sm"
label="current value"
@@ -27,15 +27,20 @@
></q-input>
</q-card-section>
<q-card-section v-else>
<q-table
flat
dense
virtual-scroll
:rows-per-page-options="[0]"
:rows="drivers"
:columns="columns"
>
</q-table>
<div class="column q-pr-xs q-ma-sm">
<div class="row items-center q-gutter-sm">
<div>current value</div>
<div class="row items-left">
<q-toggle class="readonly-toggle" left-label v-model="inputValue"></q-toggle>
</div>
</div>
<div class="row items-center q-gutter-lg">
<div>new value</div>
<div class="row items-left">
<q-toggle left-label v-model="writeValue"></q-toggle>
</div>
</div>
</div>
</q-card-section>
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
props.text
@@ -63,15 +68,16 @@ import type { Subscribe } from '../../models/Subscribe';
import type { Ref } from 'vue';
import { setValues } from '../../services/websocket';
import { useNotify } from '../../general/useNotify';
import type { QTableProps } from 'quasar';
import type { Driver } from '../../models/Drivers';
import { catchError } from 'src/vueLib/models/error';
const { NotifyResponse } = useNotify();
const Dialog = ref();
const localDialogLabel = ref('');
const writeValue = ref();
const onlyRead = ref(false);
const writeType = ref<'text' | 'number'>('text');
const datapoint = ref();
const inputValue = ref(datapoint?.value);
const props = defineProps({
buttonOkLabel: {
@@ -100,33 +106,14 @@ const props = defineProps({
},
});
const datapoint = ref();
const inputValue = ref(datapoint?.value);
const localDialogLabel = ref('');
const drivers = ref<Driver[]>([]);
const columns = [
{ name: 'type', label: 'Driver Name', field: 'type', align: 'left' },
{ name: 'bus', label: 'Bus Name', field: 'bus', align: 'center' },
{ name: 'address', label: 'Address', field: 'address', align: 'center' },
] as QTableProps['columns'];
const open = (sub: Ref<Subscribe>, type?: string) => {
const open = (sub: Ref<Subscribe>) => {
datapoint.value = sub.value;
if (datapoint.value.rights == 'R') onlyRead.value = true;
drivers.value = [];
switch (type) {
case 'driver':
localDialogLabel.value = 'Update Drivers';
if (sub.value.drivers) drivers.value = Object.values(sub.value.drivers);
writeValue.value = sub.value.drivers;
break;
default:
localDialogLabel.value = 'Update Value';
if (sub.value.type === 'STR') writeType.value = 'text';
else if (sub.value.type === 'BIT') writeType.value = 'text';
else writeType.value = 'number';
writeValue.value = sub.value.value;
}
localDialogLabel.value = 'Update Value';
if (sub.value.type === 'STR') writeType.value = 'text';
else writeType.value = 'number';
writeValue.value = sub.value.value;
Dialog.value?.open();
};
@@ -156,4 +143,8 @@ defineExpose({ open });
.outercard {
border-radius: 10px;
}
.readonly-toggle {
pointer-events: none;
opacity: 0.7;
}
</style>

7
src/vueLib/models/Bus.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { Topic } from './Topic';
export interface Bus {
name: string;
address?: number[];
topic?: Topic;
}

View File

@@ -1,5 +1,9 @@
import type { Bus } from './Bus';
export interface Driver {
type: string;
addess: number;
value: number;
buses?: Bus[];
}
export const driverDefault = <Driver>{
type: '',
};

View File

@@ -1,14 +1,21 @@
import type { Driver } from './Drivers';
export type Publish = {
event: string;
uuid: string;
path: string;
type: string;
drivers?: Record<string, Driver>;
value: string | number | boolean | null;
hasChild: boolean;
};
export type Pubs = Publish[];
import { updateSubscriptionValue, removeRawSubscriptions } from './Subscriptions';
import {
updateSubscriptionValue,
removeRawSubscriptions,
addRawSubscription,
removeRawSubscription,
} from './Subscriptions';
import { buildTree, buildTreeWithRawSubs, removeNodes } from '../dbm/dbmTree';
import type { RawSubs, RawSubscribe } from '../models/Subscribe';
import { ref } from 'vue';
@@ -33,6 +40,11 @@ export function publishToSubscriptions(pubs: Pubs) {
rawSubs.value.push(pub as RawSubscribe);
break;
}
if (pub.drivers) {
removeRawSubscription(pub as RawSubscribe);
addRawSubscription(pub as RawSubscribe);
UpdateTable();
}
updateSubscriptionValue(pub.uuid, pub.value);
});

View File

@@ -1,3 +1,4 @@
import type { Driver } from './Drivers';
import type { Gets } from './Get';
import type { Sets } from './Set';
import type { Subs } from './Subscribe';
@@ -61,10 +62,11 @@ export async function rawSetsRequest(sets: Sets): Promise<Sets> {
export async function setRequest(
path: string,
type: string,
value: string | number | boolean,
type?: string,
value?: string | number | boolean,
rights?: string,
uuid?: string,
driver?: Driver,
rename?: boolean,
): Promise<Sets> {
const payload = {
@@ -73,6 +75,7 @@ export async function setRequest(
value: value,
rights: rights,
uuid: uuid,
driver: driver,
rename: rename,
};
@@ -99,14 +102,18 @@ export async function setsRequest(sets: Sets): Promise<Sets> {
}
}
export async function deleteRequest(uuid?: string, path?: string, rename?: boolean): Promise<Sets> {
export async function deleteRequest(
uuid?: string,
path?: string,
driver?: Driver,
rename?: boolean,
): Promise<Sets> {
let payload = {};
if (uuid) {
payload = { uuid: uuid, rename: rename };
payload = { uuid: uuid, driver: driver, rename: rename };
} else if (path) {
payload = { path: path };
payload = { path: path, driver: driver };
}
const resp = await api.delete('/json_data', {
data: {
set: [payload],

View File

@@ -20,6 +20,7 @@ export type RawSubscribe = {
path?: string;
depth?: number;
value?: string | number | boolean | null;
drivers?: Record<string, Driver>;
rights?: string;
hasChild?: boolean;
};

View File

@@ -0,0 +1,4 @@
export interface Topic {
subscribe: string[];
publish: string[];
}

View File

@@ -0,0 +1,83 @@
import { reactive, ref } from 'vue';
import type { QTableColumn } from 'quasar';
import type { Subscribe } from './Subscribe';
export type DriverTableRow = {
type: string;
bus: string;
address?: number | undefined;
subscribe?: string;
publish?: string;
};
const driverTable = reactive<DriverTableRow[]>([]);
const columns = ref<QTableColumn[]>([]);
const baseColumns: QTableColumn[] = [
{ name: 'type', label: 'Driver Name', field: 'type', align: 'left' },
{ name: 'bus', label: 'Bus Name', field: 'bus', align: 'center' },
{ name: 'address', label: 'Address', field: 'address', align: 'center' },
{ name: 'settings', label: '', field: 'settings', align: 'center' },
];
export function updateDriverTable(sub: Subscribe) {
driverTable.length = 0;
let hasSubs = false;
let hasPubs = false;
if (sub.drivers)
Object.entries(sub.drivers).forEach(([driverName, driverData]) => {
driverData.buses?.forEach((bus) => {
hasSubs = bus.topic?.subscribe !== undefined || hasSubs;
hasPubs = bus.topic?.publish !== undefined || hasPubs;
const subscribeList = bus.topic?.subscribe ?? [];
const publishList = bus.topic?.publish ?? [];
const addresses = bus.address?.length ? bus.address : [undefined];
addresses.forEach((addr) => {
driverTable.push({
type: driverName,
bus: bus.name,
address: addr,
subscribe: subscribeList.join(', '),
publish: publishList.join(', '),
});
});
});
});
reloadColumns(hasSubs, hasPubs);
}
export function useDriverTable() {
function emptyTable() {
driverTable.length = 0;
}
return {
driverTable,
emptyTable,
columns,
};
}
function reloadColumns(hasSubs: boolean, hasPubs: boolean) {
columns.value = [...baseColumns];
const settingsIndex = columns?.value.findIndex((col) => col.name === 'settings');
if (hasSubs) {
columns.value?.splice(settingsIndex ?? -1, 0, {
name: 'subscribe',
label: 'subscribe',
field: 'subscribe',
align: 'left',
});
}
if (hasPubs) {
columns.value?.splice(settingsIndex ?? -1, 0, {
name: 'publish',
label: 'publish',
field: 'publish',
align: 'left',
});
}
}

View File

@@ -12,9 +12,11 @@ export let socket: WebSocket | null = null;
const isConnected = ref(false);
export function initWebSocket(url: string, $q?: QVueGlobals) {
export function initWebSocket(host: string, port: number = 8100, $q?: QVueGlobals) {
const randomId = Math.floor(Math.random() * 10001); // random number from 0 to 10000
const connect = () => {
socket = new WebSocket(url);
socket = new WebSocket(`ws://${host}:${port}/ws?id=q${randomId}`);
socket.onopen = () => {
console.log('WebSocket connected');