optimize subscription model bug fixes
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,6 +14,7 @@ node_modules
|
|||||||
/src-cordova/www
|
/src-cordova/www
|
||||||
|
|
||||||
# Capacitor related directories and files
|
# Capacitor related directories and files
|
||||||
|
/src-capacitor
|
||||||
/src-capacitor/www
|
/src-capacitor/www
|
||||||
/src-capacitor/node_modules
|
/src-capacitor/node_modules
|
||||||
|
|
||||||
@@ -38,3 +39,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# local .log files
|
# local .log files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# golang quasar websever executable
|
||||||
|
backend/server-linux-arm64
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lightcontrol",
|
"name": "lightcontrol",
|
||||||
"version": "0.0.17",
|
"version": "0.0.18",
|
||||||
"description": "A Tecamino App",
|
"description": "A Tecamino App",
|
||||||
"productName": "Light Control",
|
"productName": "Light Control",
|
||||||
"author": "A. Zuercher",
|
"author": "A. Zuercher",
|
||||||
|
@@ -16,101 +16,56 @@
|
|||||||
<template v-slot:[`default-header`]="props">
|
<template v-slot:[`default-header`]="props">
|
||||||
<div
|
<div
|
||||||
class="row items-center text-blue"
|
class="row items-center text-blue"
|
||||||
@contextmenu.prevent.stop="openContextMenu($event, props.node)"
|
@contextmenu.prevent.stop="openSubMenu($event, props.node.key)"
|
||||||
>
|
>
|
||||||
<div class="row items-center text-blue"></div>
|
<div class="row items-center text-blue"></div>
|
||||||
<div>{{ props.node.path }}</div>
|
<div>{{ props.node.path }}</div>
|
||||||
</div>
|
</div>
|
||||||
<q-popup-edit
|
|
||||||
v-if="props.node.value !== undefined && props.node.value !== ''"
|
|
||||||
v-model="props.node.value"
|
|
||||||
class="q-ml-xl bg-grey text-white"
|
|
||||||
@save="(val) => onValueEdit(val, props.node)"
|
|
||||||
>
|
|
||||||
<template v-slot="scope">
|
|
||||||
<q-input
|
|
||||||
dark
|
|
||||||
color="white"
|
|
||||||
v-model="scope.value"
|
|
||||||
dense
|
|
||||||
autofocus
|
|
||||||
counter
|
|
||||||
@keyup.enter="scope.set"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon name="edit" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</template>
|
|
||||||
</q-popup-edit>
|
|
||||||
</template>
|
</template>
|
||||||
</q-tree>
|
</q-tree>
|
||||||
<sub-menu :node="selectedNode"></sub-menu>
|
<!-- not implemented yet <sub-menu ref="subMenuRef"></sub-menu> -->
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<DataTable :rows="Subscriptions" class="col-8" />
|
<dataTable class="col-8" :rows="getRows()" />
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import DataTable from './dataTable.vue';
|
|
||||||
import type { TreeNode } from 'src/composables/dbm/dbmTree';
|
|
||||||
import {
|
import {
|
||||||
dbmData,
|
type TreeNode,
|
||||||
buildTree,
|
buildTree,
|
||||||
getSubscriptionsByUuid,
|
dbmData,
|
||||||
addChildrentoTree,
|
removeSubtreeByParentKey,
|
||||||
getAllSubscriptions,
|
|
||||||
} from 'src/composables/dbm/dbmTree';
|
} from 'src/composables/dbm/dbmTree';
|
||||||
|
import DataTable from './DataTable.vue';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import { openContextMenu } from 'src/composables/dbm/useContextMenu';
|
|
||||||
import { NotifyResponse } from 'src/composables/notify';
|
import { NotifyResponse } from 'src/composables/notify';
|
||||||
import SubMenu from 'src/components/dbm/SubMenu.vue';
|
|
||||||
import { QCard } from 'quasar';
|
import { QCard } from 'quasar';
|
||||||
import { subscribe, unsubscribe, setValues } from 'src/services/websocket';
|
import { subscribe, unsubscribe } from 'src/services/websocket';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
import { api } from 'boot/axios';
|
import { api } from 'boot/axios';
|
||||||
import type { Subs } from 'src/models/Subscribe';
|
// not implemented yet import SubMenu from './SubMenu.vue';
|
||||||
import { reactive } from 'vue';
|
import { type Subs, type RawSubs, convertToSubscribes } from 'src/models/Subscribe';
|
||||||
|
import { addSubscriptions, getRows, removeAllSubscriptions } from 'src/models/Subscriptions';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const expanded = ref<string[]>([]);
|
const expanded = ref<string[]>([]);
|
||||||
const selectedNode = ref<TreeNode | null>(null);
|
|
||||||
const ZERO_UUID = '00000000-0000-0000-0000-000000000000';
|
const ZERO_UUID = '00000000-0000-0000-0000-000000000000';
|
||||||
const Subscriptions = reactive<Subs>([]);
|
|
||||||
let lastExpanded: string[] = [];
|
let lastExpanded: string[] = [];
|
||||||
|
const postQuery = '/json_data';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const payload = {
|
|
||||||
get: [
|
|
||||||
{
|
|
||||||
path: '.*',
|
|
||||||
query: { depth: 1 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
api
|
api
|
||||||
.post('/json_data', payload)
|
.post(postQuery, { get: [{ path: '.*', query: { depth: 1 } }] })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.get) {
|
if (res.data.get) buildTree(convertToSubscribes(res.data.get as RawSubs));
|
||||||
dbmData.splice(0, dbmData.length, ...buildTree(res.data.get));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
unsubscribe([
|
unsubscribe([{ path: '.*', depth: 0 }]).catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
{
|
|
||||||
path: '.*',
|
|
||||||
depth: 0,
|
|
||||||
},
|
|
||||||
]).catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onLazyLoad({
|
function onLazyLoad({
|
||||||
@@ -122,47 +77,11 @@ function onLazyLoad({
|
|||||||
done: (children: TreeNode[]) => void;
|
done: (children: TreeNode[]) => void;
|
||||||
fail: () => void;
|
fail: () => void;
|
||||||
}): void {
|
}): void {
|
||||||
//first unsubsrice nodes
|
api
|
||||||
unsubscribe([
|
.post(postQuery, { get: [{ uuid: node.key ?? ZERO_UUID, path: '', query: { depth: 2 } }] })
|
||||||
{
|
|
||||||
path: '.*',
|
|
||||||
depth: 0,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then(() => {
|
|
||||||
Subscriptions.length = 0;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
|
|
||||||
// now subscribe nodes
|
|
||||||
subscribe([
|
|
||||||
{
|
|
||||||
uuid: node.key ?? ZERO_UUID,
|
|
||||||
path: '',
|
|
||||||
depth: 2,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp?.subscribe) {
|
if (resp?.data.get) done(buildTree(convertToSubscribes(resp?.data.get as RawSubs)));
|
||||||
// Optional: update your internal store too
|
else done([]); // no children returned
|
||||||
addChildrentoTree(resp?.subscribe);
|
|
||||||
|
|
||||||
const toRemove = new Set(
|
|
||||||
resp.subscribe.filter((sub) => sub.uuid !== ZERO_UUID).map((sub) => sub.uuid),
|
|
||||||
);
|
|
||||||
|
|
||||||
Subscriptions.splice(
|
|
||||||
0,
|
|
||||||
Subscriptions.length,
|
|
||||||
...getAllSubscriptions().filter((sub) => toRemove.has(sub.uuid)),
|
|
||||||
);
|
|
||||||
|
|
||||||
done(dbmData);
|
|
||||||
} else {
|
|
||||||
done([]); // no children returned
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
NotifyResponse($q, err, 'error');
|
NotifyResponse($q, err, 'error');
|
||||||
@@ -170,87 +89,61 @@ function onLazyLoad({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onValueEdit(newValue: undefined, node: TreeNode) {
|
async function onExpandedChange(newExpanded: readonly string[]) {
|
||||||
console.log(node.value, node.value === undefined);
|
|
||||||
if (!node.key) return;
|
|
||||||
const sub = getSubscriptionsByUuid(node.key);
|
|
||||||
if (sub) {
|
|
||||||
setValues([
|
|
||||||
{
|
|
||||||
path: sub.value?.path ?? '',
|
|
||||||
value: newValue,
|
|
||||||
},
|
|
||||||
]).catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExpandedChange(newExpanded: readonly string[]) {
|
|
||||||
const collapsed = lastExpanded.filter((k) => !newExpanded.includes(k));
|
const collapsed = lastExpanded.filter((k) => !newExpanded.includes(k));
|
||||||
const newlyExpanded = newExpanded.filter((k) => !lastExpanded.includes(k));
|
const newlyExpanded = newExpanded.filter((k) => !lastExpanded.includes(k));
|
||||||
|
|
||||||
if (collapsed.length) {
|
try {
|
||||||
collapsed.forEach((key: string) => {
|
await unsubscribe([{ path: '.*', depth: 0 }])
|
||||||
subscribe([
|
.then(() => {
|
||||||
{
|
removeAllSubscriptions();
|
||||||
uuid: key,
|
})
|
||||||
path: '',
|
.catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
depth: 2,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp?.subscribe) {
|
|
||||||
// Optional: update your internal store too
|
|
||||||
addChildrentoTree(resp?.subscribe);
|
|
||||||
|
|
||||||
const toRemove = new Set(
|
if (collapsed.length) {
|
||||||
resp.subscribe.filter((sub) => sub.uuid !== ZERO_UUID).map((sub) => sub.uuid),
|
collapsed.forEach((key) => {
|
||||||
);
|
removeSubtreeByParentKey(key);
|
||||||
|
api
|
||||||
|
.post(postQuery, { get: [{ uuid: key, path: '', query: { depth: 2 } }] })
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp?.data.get) {
|
||||||
|
buildTree(resp?.data.get as Subs);
|
||||||
|
subscribe([{ uuid: key, path: '', depth: 2 }]).catch((err) =>
|
||||||
|
NotifyResponse($q, err, 'error'),
|
||||||
|
);
|
||||||
|
addSubscriptions(resp?.data.get as RawSubs);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (newlyExpanded.length) {
|
||||||
|
newlyExpanded.forEach((key) => {
|
||||||
|
api
|
||||||
|
.post(postQuery, { get: [{ uuid: key, path: '', query: { depth: 2 } }] })
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp?.data.get) {
|
||||||
|
buildTree(resp?.data.get as Subs);
|
||||||
|
subscribe([{ uuid: key, path: '', depth: 2 }]).catch((err) =>
|
||||||
|
NotifyResponse($q, err, 'error'),
|
||||||
|
);
|
||||||
|
addSubscriptions(resp?.data.get as RawSubs);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Subscriptions.splice(
|
lastExpanded = [...newExpanded];
|
||||||
0,
|
} catch {
|
||||||
Subscriptions.length,
|
console.error('error in expand function');
|
||||||
...getAllSubscriptions().filter((sub) => toRemove.has(sub.uuid)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (newlyExpanded.length) {
|
|
||||||
newlyExpanded.forEach((key: string) => {
|
|
||||||
subscribe([
|
|
||||||
{
|
|
||||||
uuid: key,
|
|
||||||
path: '',
|
|
||||||
depth: 2,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then((resp) => {
|
|
||||||
if (resp?.subscribe) {
|
|
||||||
// Optional: update your internal store too
|
|
||||||
addChildrentoTree(resp?.subscribe);
|
|
||||||
|
|
||||||
const toRemove = new Set(
|
|
||||||
resp.subscribe.filter((sub) => sub.uuid !== ZERO_UUID).map((sub) => sub.uuid),
|
|
||||||
);
|
|
||||||
|
|
||||||
Subscriptions.splice(
|
|
||||||
0,
|
|
||||||
Subscriptions.length,
|
|
||||||
...getAllSubscriptions().filter((sub) => toRemove.has(sub.uuid)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lastExpanded = [...newExpanded];
|
const subMenuRef = ref();
|
||||||
|
|
||||||
|
function openSubMenu(event: MouseEvent, uuid: string) {
|
||||||
|
subMenuRef.value?.open(event, uuid);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -4,50 +4,49 @@
|
|||||||
<q-item clickable v-close-popup @click="handleAction('Add')">
|
<q-item clickable v-close-popup @click="handleAction('Add')">
|
||||||
<q-item-section>Add Datapoint</q-item-section>
|
<q-item-section>Add Datapoint</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item clickable v-close-popup @click="handleAction('Delete')">
|
<q-item
|
||||||
|
:class="disable ? 'text-grey-5' : ''"
|
||||||
|
:clickable="!disable"
|
||||||
|
v-close-popup
|
||||||
|
@click="handleAction('Delete')"
|
||||||
|
>
|
||||||
<q-item-section>Delete Datapoint</q-item-section>
|
<q-item-section>Delete Datapoint</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-menu>
|
</q-menu>
|
||||||
|
<AddDialog :dialogLabel="label" width="700px" button-ok-label="Add" ref="addDialog" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
//import { useQuasar } from 'quasar';
|
import AddDialog from 'src/components/dialog/AddDatapoint.vue';
|
||||||
//import { NotifyResponse, NotifyError } from 'src/composables/notify';
|
import { ref } from 'vue';
|
||||||
import { contextMenuState, contextMenuRef } from 'src/composables/dbm/useContextMenu';
|
|
||||||
//import AddDatapoint from 'src/components/dbm/AddDatapoint.vue';
|
|
||||||
//import { send } from 'src/services/websocket';
|
|
||||||
|
|
||||||
//const $q = useQuasar();
|
const ZERO_UUID = '00000000-0000-0000-0000-000000000000';
|
||||||
|
const addDialog = ref();
|
||||||
|
const datapointUuid = ref('');
|
||||||
|
const contextMenuRef = ref();
|
||||||
|
const label = ref('');
|
||||||
|
const disable = ref(false);
|
||||||
|
|
||||||
function handleAction(action: string) {
|
function handleAction(action: string) {
|
||||||
console.log(`Action '${action}' on node:`, contextMenuState.value);
|
|
||||||
|
|
||||||
// Add your actual logic here
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'Add':
|
case 'Add':
|
||||||
// send({
|
label.value = 'Add New Datapoint';
|
||||||
// set: [
|
addDialog.value?.open(datapointUuid.value);
|
||||||
// {
|
break;
|
||||||
// uuid: contextMenuState.value?.key,
|
case 'Delete':
|
||||||
// path: 'New',
|
label.value = 'Remove Datapoint';
|
||||||
// type: 'BIT',
|
addDialog.value?.open(datapointUuid.value);
|
||||||
// value: true,
|
break;
|
||||||
// create: true,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// if (response?.set) {
|
|
||||||
// console.log(response);
|
|
||||||
// } else {
|
|
||||||
// NotifyResponse($q, response);
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .catch((err) => {
|
|
||||||
// NotifyError($q, err);
|
|
||||||
// });
|
|
||||||
console.log(4);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const open = (event: MouseEvent, uuid: string) => {
|
||||||
|
if (uuid === ZERO_UUID) disable.value = true;
|
||||||
|
event.preventDefault();
|
||||||
|
datapointUuid.value = uuid;
|
||||||
|
contextMenuRef.value?.show(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<QCard v-if="props.display"> Test </QCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
display: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
@@ -14,21 +14,42 @@
|
|||||||
>
|
>
|
||||||
<template v-slot:body-cell-value="props">
|
<template v-slot:body-cell-value="props">
|
||||||
<q-td :props="props" @click="openDialog(props.row)">
|
<q-td :props="props" @click="openDialog(props.row)">
|
||||||
<span :class="['cursor-pointer', open ? 'text-green' : '']"> {{ props.row.value }}</span>
|
<div :class="['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-icon size="sm" name="cell_tower" :color="props.row.drivers ? 'blue-5' : 'grey-4'" />
|
||||||
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
<Dialog dialogLabel="Update Value" :show-dialog="open" />
|
<UpdateDialog
|
||||||
|
dialogLabel="Update Value"
|
||||||
|
width="400px"
|
||||||
|
button-ok-label="Write"
|
||||||
|
ref="updateDialog"
|
||||||
|
v-model:datapoint="dialogValue"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Dialog from 'src/components/dialog/UpdateValueDialog.vue';
|
import UpdateDialog from 'src/components/dialog/UpdateValueDialog.vue';
|
||||||
import type { QTableProps } from 'quasar';
|
import type { QTableProps } from 'quasar';
|
||||||
import type { Subs, Subscribe } from 'src/models/Subscribe';
|
import type { Subscribe } from 'src/models/Subscribe';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import type { Subs } from 'src/models/Subscribe';
|
||||||
|
|
||||||
const open = ref(false);
|
const updateDialog = ref();
|
||||||
|
const dialogValue = ref();
|
||||||
|
|
||||||
|
const openDialog = (sub: Subscribe, type?: string) => {
|
||||||
|
updateDialog.value?.open(sub, type);
|
||||||
|
};
|
||||||
|
|
||||||
// we generate lots of rows here
|
// we generate lots of rows here
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -39,22 +60,23 @@ const tableRows = computed(() => [...props.rows]);
|
|||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'path', label: 'Path', field: 'path', align: 'left' },
|
{ name: 'path', label: 'Path', field: 'path', align: 'left' },
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
label: 'Type',
|
||||||
|
field: 'type',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'value',
|
name: 'value',
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
field: 'value',
|
field: 'value',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
{
|
// {. not implemented yet
|
||||||
name: 'test',
|
// name: 'drivers',
|
||||||
label: '',
|
// label: 'Drivers',
|
||||||
field: 'test',
|
// field: 'drivers',
|
||||||
align: 'left',
|
// align: 'center',
|
||||||
},
|
// },
|
||||||
] as QTableProps['columns'];
|
] as QTableProps['columns'];
|
||||||
|
|
||||||
function openDialog(item: Subscribe) {
|
|
||||||
console.log(77, item);
|
|
||||||
open.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
101
src/components/dialog/AddDatapoint.vue
Normal file
101
src/components/dialog/AddDatapoint.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame ref="Dialog" :width="props.width">
|
||||||
|
<q-card-section
|
||||||
|
v-if="props.dialogLabel"
|
||||||
|
class="text-bold text-left q-mb-none q-pb-none"
|
||||||
|
:class="'text-' + props.labelColor"
|
||||||
|
>{{ props.dialogLabel }}
|
||||||
|
</q-card-section>
|
||||||
|
<q-form ref="addForm" @submit="onSubmit" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
class="q-pa-md"
|
||||||
|
filled
|
||||||
|
v-model="path"
|
||||||
|
label=""
|
||||||
|
:rules="[(val) => !!val || 'Path is required']"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<div class="column">
|
||||||
|
<span class="text-caption text-primary non-editable-prefix">Path *</span>
|
||||||
|
<span class="text-body2 text-grey-6 non-editable-prefix">{{ staticPrefix }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<DataTypes class="q-ma-md" flat></DataTypes>
|
||||||
|
<q-btn no-caps class="q-ma-lg" type="submit" color="primary">{{ props.buttonOkLabel }}</q-btn>
|
||||||
|
</q-form>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { NotifyResponse } from 'src/composables/notify';
|
||||||
|
import DataTypes from 'src/vueLib/buttons/DataTypes.vue';
|
||||||
|
//import datatype from 'src/vueLib/buttons/DataType.vue';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const Dialog = ref();
|
||||||
|
const path = ref('');
|
||||||
|
const staticPrefix = ref('');
|
||||||
|
//const radio = ref('bool');
|
||||||
|
const addForm = ref();
|
||||||
|
|
||||||
|
const open = (uuid: string) => {
|
||||||
|
Dialog.value?.open();
|
||||||
|
getDatapoint(uuid);
|
||||||
|
};
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
addForm.value.validate().then((success: undefined) => {
|
||||||
|
if (success) {
|
||||||
|
console.log(909);
|
||||||
|
// yay, models are correct
|
||||||
|
} else {
|
||||||
|
console.log(910);
|
||||||
|
// oh no, user has filled in
|
||||||
|
// at least one invalid value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
buttonOkLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'OK',
|
||||||
|
},
|
||||||
|
labelColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
|
dialogLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '300px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
validate();
|
||||||
|
console.log('submit', props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDatapoint(uuid: string) {
|
||||||
|
api
|
||||||
|
.post('/json_data', { get: [{ uuid: uuid, query: { depth: 1 } }] })
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.data.get) {
|
||||||
|
staticPrefix.value = resp.data.get[0].path;
|
||||||
|
if (staticPrefix.value !== '') staticPrefix.value += ':';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse($q, err, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
@@ -1,42 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="internalShowDialog">
|
<DialogFrame ref="Dialog" :width="props.width">
|
||||||
<q-card :style="'width:' + props.width">
|
<q-card-section
|
||||||
<q-card-section
|
v-if="props.dialogLabel"
|
||||||
v-if="props.dialogLabel"
|
class="text-bold text-left q-mb-none q-pb-none"
|
||||||
class="text-h6 text-center"
|
:class="'text-' + props.labelColor"
|
||||||
:class="'text-' + props.labelColor"
|
>{{ props.dialogLabel + ': ' + props.datapoint?.path }}</q-card-section
|
||||||
>{{ props.dialogLabel }}</q-card-section
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input
|
||||||
|
class="q-px-md q-ma-sm"
|
||||||
|
label="current value"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
readonly
|
||||||
|
v-model="inputValue as string | number"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-px-md q-mx-sm"
|
||||||
|
label="new value"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
@keyup.enter="write"
|
||||||
|
v-model="writeValue as string | number"
|
||||||
|
></q-input>
|
||||||
|
</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"
|
||||||
|
:label="props.buttonOkLabel"
|
||||||
|
@click="write"
|
||||||
>
|
>
|
||||||
<q-card-section>
|
</q-btn>
|
||||||
<q-input v-model="inputValue"></q-input>
|
</q-card-actions>
|
||||||
</q-card-section>
|
</DialogFrame>
|
||||||
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
|
|
||||||
props.text
|
|
||||||
}}</q-card-section>
|
|
||||||
<q-card-actions align="right" class="text-primary">
|
|
||||||
<q-btn v-if="props.buttonCancelLabel" flat :label="props.buttonCancelLabel" v-close-popup>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-if="props.buttonOkLabel"
|
|
||||||
flat
|
|
||||||
:label="props.buttonOkLabel"
|
|
||||||
v-close-popup
|
|
||||||
@click="closeDialog"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import type { Subscribe } from 'src/models/Subscribe';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { setValues } from 'src/services/websocket';
|
||||||
|
import { NotifyResponse } from 'src/composables/notify';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const Dialog = ref();
|
||||||
|
const writeValue = ref();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showDialog: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
buttonOkLabel: {
|
buttonOkLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'OK',
|
default: 'OK',
|
||||||
@@ -61,41 +80,47 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '300px',
|
default: '300px',
|
||||||
},
|
},
|
||||||
value: {
|
datapoint: Object as PropType<Subscribe>,
|
||||||
type: [String, Number],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputValue = ref(props.value);
|
const inputValue = ref(props.datapoint?.value);
|
||||||
|
|
||||||
const emit = defineEmits(['update:showDialog', 'update:value', 'confirmed', 'cancel']);
|
const open = (sub: Subscribe, type?: string) => {
|
||||||
const internalShowDialog = ref(props.showDialog);
|
Dialog.value?.open();
|
||||||
|
switch (type) {
|
||||||
watch(inputValue, (val) => {
|
case 'driver':
|
||||||
emit('update:value', val);
|
console.log(9, sub.drivers);
|
||||||
});
|
writeValue.value = sub.drivers;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
writeValue.value = sub.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.showDialog,
|
() => props.datapoint?.value,
|
||||||
(newValue) => {
|
(newVal) => {
|
||||||
console.log('watch showDialog', newValue);
|
inputValue.value = newVal;
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function closeDialog() {
|
function write() {
|
||||||
internalShowDialog.value = false;
|
setValues([{ uuid: props.datapoint?.uuid ?? '', value: writeValue.value ?? undefined }])
|
||||||
emit('update:showDialog', false);
|
.then((resp) => {
|
||||||
|
if (resp?.set) {
|
||||||
|
resp.set.forEach((set) => {
|
||||||
|
inputValue.value = set.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse($q, err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.outercard {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -69,43 +69,30 @@
|
|||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import LightSlider from './LightSlider.vue';
|
import LightSlider from './LightSlider.vue';
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { subscribe, unsubscribe } from 'src/services/websocket';
|
import { unsubscribe, subscribeToPath } from 'src/services/websocket';
|
||||||
import SettingDialog from 'src/components/lights/SettingDomeLight.vue';
|
import SettingDialog from 'src/components/lights/SettingDomeLight.vue';
|
||||||
import { NotifyResponse } from 'src/composables/notify';
|
import { NotifyResponse } from 'src/composables/notify';
|
||||||
import { updateValue, buildTree, dbmData } from 'src/composables/dbm/dbmTree';
|
import { updateValue } from 'src/composables/dbm/dbmTree';
|
||||||
|
import { removeAllSubscriptions } from 'src/models/Subscriptions';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const settings = ref(false);
|
const settings = ref(false);
|
||||||
const brightness = updateValue('LightBar:Brightness', $q);
|
const brightness = updateValue('LightBar:Brightness', $q);
|
||||||
const state = updateValue('LightBar:State', $q);
|
const state = updateValue('LightBar:State', $q);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
subscribe([
|
subscribeToPath($q, 'LightBar:.*');
|
||||||
{
|
|
||||||
path: 'LightBar:.*',
|
|
||||||
depth: 0,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then((response) => {
|
|
||||||
if (response?.subscribe) {
|
|
||||||
dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe));
|
|
||||||
} else {
|
|
||||||
NotifyResponse($q, response);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
NotifyResponse($q, err, 'error');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsubscribe([
|
unsubscribe([
|
||||||
{
|
{
|
||||||
path: '.*',
|
path: 'LightBar',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
},
|
},
|
||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
NotifyResponse($q, err, 'error');
|
NotifyResponse($q, err, 'error');
|
||||||
});
|
});
|
||||||
|
removeAllSubscriptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
function changeState() {
|
function changeState() {
|
||||||
|
@@ -107,10 +107,11 @@ import { NotifyResponse } from 'src/composables/notify';
|
|||||||
import { onBeforeUpdate, computed, onMounted, onUnmounted, ref } from 'vue';
|
import { onBeforeUpdate, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { subscribeToPath, unsubscribe, setValues } from 'src/services/websocket';
|
import { subscribeToPath, unsubscribe, setValues } from 'src/services/websocket';
|
||||||
import { LocalStorage } from 'quasar';
|
import { LocalStorage } from 'quasar';
|
||||||
import { getSubscriptionsByPath, updateValue } from 'src/composables/dbm/dbmTree';
|
import { updateValue } from 'src/composables/dbm/dbmTree';
|
||||||
import DragPad from 'src/components/lights/DragPad.vue';
|
import DragPad from 'src/components/lights/DragPad.vue';
|
||||||
import SettingDialog from './SettingMovingHead.vue';
|
import SettingDialog from './SettingMovingHead.vue';
|
||||||
import type { Settings } from 'src/models/MovingHead';
|
import type { Settings } from 'src/models/MovingHead';
|
||||||
|
import { findSubscriptionByPath, removeAllSubscriptions } from 'src/models/Subscriptions';
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const brightness = updateBrightnessValue('MovingHead:Brightness');
|
const brightness = updateBrightnessValue('MovingHead:Brightness');
|
||||||
@@ -141,31 +142,29 @@ onUnmounted(() => {
|
|||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
NotifyResponse($q, err, 'error');
|
NotifyResponse($q, err, 'error');
|
||||||
});
|
});
|
||||||
|
removeAllSubscriptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
function changeState() {
|
function changeState() {
|
||||||
console.log(55, brightness.value);
|
|
||||||
console.log(56, state.value);
|
|
||||||
if (brightness.value === 0) {
|
if (brightness.value === 0) {
|
||||||
if (state.value === 0) {
|
if (state.value === 0) {
|
||||||
brightness.value = 255;
|
brightness.value = 255;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
brightness.value = state.value;
|
brightness.value = state.value;
|
||||||
console.log(57, brightness.value);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.value = brightness.value;
|
state.value = brightness.value;
|
||||||
console.log(58, state.value);
|
|
||||||
brightness.value = 0;
|
brightness.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBrightnessValue(path: string) {
|
function updateBrightnessValue(path: string) {
|
||||||
return computed({
|
return computed({
|
||||||
get() {
|
get() {
|
||||||
const sub = getSubscriptionsByPath(path);
|
const sub = findSubscriptionByPath(path);
|
||||||
|
if (!sub) return 0;
|
||||||
if (!sub.value) return 0;
|
if (!sub.value) return 0;
|
||||||
return Number(sub.value.value);
|
return Number(sub.value);
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
const setPaths = [{ path, value: val }];
|
const setPaths = [{ path, value: val }];
|
||||||
|
@@ -1,110 +1,76 @@
|
|||||||
import type { Subs, Subscribe } from 'src/models/Subscribe';
|
import { reactive, computed, type Ref } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { nextTick, computed, reactive, ref } from 'vue';
|
|
||||||
import { setValues } from 'src/services/websocket';
|
|
||||||
import { NotifyResponse } from 'src/composables/notify';
|
|
||||||
import type { QVueGlobals } from 'quasar';
|
import type { QVueGlobals } from 'quasar';
|
||||||
|
import type { Subs } from 'src/models/Subscribe';
|
||||||
const Subscriptions = reactive<Record<string, Subscribe>>({});
|
import { NotifyResponse } from '../notify';
|
||||||
|
import { setValues } from 'src/services/websocket';
|
||||||
|
import { findSubscriptionByPath } from 'src/models/Subscriptions';
|
||||||
|
|
||||||
export const dbmData = reactive<TreeNode[]>([]);
|
export const dbmData = reactive<TreeNode[]>([]);
|
||||||
|
//export const reactiveValues = new Map<string, Ref<string | number | boolean | null>>();
|
||||||
|
|
||||||
export interface TreeNode {
|
export type TreeNode = {
|
||||||
path: string | undefined;
|
path: string | undefined;
|
||||||
key?: string; // optional: useful for QTree's node-key
|
key?: string; // optional: useful for QTree's node-key
|
||||||
value?: string | number | boolean | undefined;
|
|
||||||
lazy: boolean;
|
lazy: boolean;
|
||||||
children?: TreeNode[];
|
children?: TreeNode[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type TreeMap = {
|
||||||
|
[key: string]: {
|
||||||
|
__children: TreeMap;
|
||||||
|
uuid?: string;
|
||||||
|
value?: string | number | boolean | null;
|
||||||
|
lazy: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const root: TreeMap = {};
|
||||||
|
|
||||||
export function buildTree(subs: Subs): TreeNode[] {
|
export function buildTree(subs: Subs): TreeNode[] {
|
||||||
type TreeMap = {
|
for (const { path, uuid, value, hasChild } of subs) {
|
||||||
[key: string]: {
|
if (!path) continue;
|
||||||
__children: TreeMap;
|
|
||||||
uuid?: string;
|
|
||||||
value?: string | undefined;
|
|
||||||
lazy: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const root: TreeMap = {};
|
const parts = path.split(':');
|
||||||
|
|
||||||
for (const item of subs) {
|
|
||||||
if (item.path) {
|
|
||||||
addNewSubscription(item);
|
|
||||||
}
|
|
||||||
const pathParts = item.path?.split(':') ?? [];
|
|
||||||
let current = root;
|
let current = root;
|
||||||
|
|
||||||
for (let i = 0; i < pathParts.length; i++) {
|
parts.forEach((part, idx) => {
|
||||||
const part = pathParts[i];
|
if (!part) return;
|
||||||
|
|
||||||
if (!part) continue;
|
|
||||||
|
|
||||||
if (!current[part]) {
|
if (!current[part]) {
|
||||||
current[part] = { __children: {}, lazy: true };
|
current[part] = { __children: {}, lazy: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally attach uuid only at the final part
|
if (idx === parts.length - 1 && uuid) {
|
||||||
if (i === pathParts.length - 1 && item.uuid) {
|
if (current[part].uuid === uuid) return;
|
||||||
current[part].uuid = item.uuid;
|
|
||||||
current[part].value = item.value !== undefined ? String(item.value) : '';
|
current[part].uuid = uuid;
|
||||||
current[part].lazy = item.hasChild ?? false;
|
current[part].value = value?.value ?? null;
|
||||||
|
current[part].lazy = hasChild ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current[part].__children;
|
current = current[part].__children;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convert(map: TreeMap): TreeNode[] {
|
function mapToTree(map: TreeMap): TreeNode[] {
|
||||||
return reactive(
|
return Object.entries(map).map(([key, node]) => ({
|
||||||
Object.entries(map).map(([path, node]) => ({
|
path: key,
|
||||||
path,
|
key: node.uuid ?? key,
|
||||||
key: node.uuid ?? path, // `key` is used by QTree
|
value: node.value,
|
||||||
value: node.value,
|
lazy: node.lazy,
|
||||||
lazy: node.lazy,
|
children: mapToTree(node.__children),
|
||||||
children: convert(node.__children),
|
}));
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
const newTree = [
|
||||||
{
|
{
|
||||||
path: 'DBM',
|
path: 'DBM',
|
||||||
key: '00000000-0000-0000-0000-000000000000',
|
key: '00000000-0000-0000-0000-000000000000',
|
||||||
lazy: true,
|
lazy: true,
|
||||||
children: convert(root),
|
children: mapToTree(root),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
return dbmData.splice(0, dbmData.length, ...newTree);
|
||||||
|
|
||||||
export function getTreeElementByPath(path: string) {
|
|
||||||
const sub = dbmData.find((s) => s.path === path);
|
|
||||||
return ref(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSubscriptionsByUuid(uid: string) {
|
|
||||||
const sub = Object.values(Subscriptions).find((sub) => sub.uuid === uid);
|
|
||||||
return ref(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addChildrentoTree(subs: Subs) {
|
|
||||||
const ZERO_UUID = '00000000-0000-0000-0000-000000000000';
|
|
||||||
const existingIds = new Set(Object.values(Subscriptions).map((sub) => sub.uuid));
|
|
||||||
const newSubs = subs
|
|
||||||
.filter((sub) => sub.uuid !== ZERO_UUID) // Skip UUIDs with all zeroes
|
|
||||||
.filter((sub) => !existingIds.has(sub.uuid));
|
|
||||||
|
|
||||||
for (const sub of newSubs) {
|
|
||||||
if (sub.path !== undefined) {
|
|
||||||
Subscriptions[sub.path] = sub;
|
|
||||||
} else {
|
|
||||||
console.warn('Skipping sub with undefined path', sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void nextTick(() => {
|
|
||||||
dbmData.splice(0, dbmData.length, ...buildTree(Object.values(Subscriptions)));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeSubtreeByParentKey(parentKey: string) {
|
export function removeSubtreeByParentKey(parentKey: string) {
|
||||||
@@ -122,23 +88,9 @@ export function removeSubtreeByParentKey(parentKey: string) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeChildrenAndMarkLazy(dbmData, parentKey);
|
removeChildrenAndMarkLazy(dbmData, parentKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSubscriptionsByPath(path: string) {
|
|
||||||
return ref(Subscriptions[path]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addNewSubscription(sub: Subscribe) {
|
|
||||||
if (!sub.path) return;
|
|
||||||
Subscriptions[sub.path] = sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllSubscriptions() {
|
|
||||||
return Object.values(Subscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateValue(
|
export function updateValue(
|
||||||
path1: string,
|
path1: string,
|
||||||
$q: QVueGlobals,
|
$q: QVueGlobals,
|
||||||
@@ -149,8 +101,8 @@ export function updateValue(
|
|||||||
) {
|
) {
|
||||||
return computed({
|
return computed({
|
||||||
get() {
|
get() {
|
||||||
const sub = getSubscriptionsByPath(toggle?.value && path2 ? path2 : path1);
|
const sub = findSubscriptionByPath(toggle?.value && path2 ? path2 : path1);
|
||||||
return sub?.value ? Number(sub.value.value ?? 0) : 0;
|
return sub?.value ? Number(sub.value ?? 0) : 0;
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
const baseValue = val;
|
const baseValue = val;
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
import { ref } from 'vue';
|
|
||||||
import type { TreeNode } from './dbmTree';
|
|
||||||
|
|
||||||
export const contextMenuRef = ref();
|
|
||||||
|
|
||||||
export const contextMenuState = ref<TreeNode | undefined>();
|
|
||||||
|
|
||||||
export function openContextMenu(event: MouseEvent, node: undefined) {
|
|
||||||
event.preventDefault();
|
|
||||||
contextMenuState.value = node;
|
|
||||||
contextMenuRef.value?.show(event);
|
|
||||||
}
|
|
@@ -3,6 +3,14 @@ export type Publish = {
|
|||||||
uuid: string;
|
uuid: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: undefined;
|
value: string | number | boolean | null;
|
||||||
};
|
};
|
||||||
export type Pubs = Publish[];
|
export type Pubs = Publish[];
|
||||||
|
|
||||||
|
import { updateSubscriptionValue } from './Subscriptions';
|
||||||
|
|
||||||
|
export function publishToSubscriptions(pubs: Pubs) {
|
||||||
|
pubs.forEach((pub) => {
|
||||||
|
updateSubscriptionValue(pub.uuid, pub.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import type { Gets } from './Get';
|
import type { Gets } from './Get';
|
||||||
import type { Sets } from './Set';
|
import type { Sets } from './Set';
|
||||||
import type { Subs } from './Subscribe';
|
import type { RawSubs } from './Subscribe';
|
||||||
import type { Pubs } from './Publish';
|
import type { Pubs } from './Publish';
|
||||||
|
|
||||||
export type Response = {
|
export type Response = {
|
||||||
get?: Gets;
|
get?: Gets;
|
||||||
set?: Sets;
|
set?: Sets;
|
||||||
subscribe?: Subs;
|
subscribe?: RawSubs;
|
||||||
unsubscribe?: Subs;
|
unsubscribe?: RawSubs;
|
||||||
publish?: Pubs;
|
publish?: Pubs;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
export type Set = {
|
export type Set = {
|
||||||
uuid?: string | undefined;
|
uuid?: string | undefined;
|
||||||
path: string;
|
path?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
value: string | number | boolean | undefined;
|
value: string | number | boolean | undefined;
|
||||||
create?: boolean;
|
create?: boolean;
|
||||||
|
@@ -1,10 +1,46 @@
|
|||||||
// API type (from backend)
|
import { ref } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
export type Subscribe = {
|
export type Subscribe = {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
value?: string | number | boolean;
|
type?: string;
|
||||||
|
drivers?: object | undefined;
|
||||||
|
value?: Ref<string | number | boolean | null | undefined>;
|
||||||
hasChild?: boolean;
|
hasChild?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Subs = Subscribe[];
|
export type Subs = Subscribe[];
|
||||||
|
|
||||||
|
export type RawSubscribe = {
|
||||||
|
uuid?: string;
|
||||||
|
path?: string;
|
||||||
|
depth?: number;
|
||||||
|
value?: string | number | boolean | null;
|
||||||
|
hasChild?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RawSubs = RawSubscribe[];
|
||||||
|
|
||||||
|
export function convertToSubscribe(raw: RawSubscribe): Subscribe {
|
||||||
|
return {
|
||||||
|
...raw,
|
||||||
|
value: ref(raw.value ?? null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToSubscribes(rawList: RawSubs): Subs {
|
||||||
|
const subs = rawList.map(convertToSubscribe);
|
||||||
|
return subs as Subs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToRaw(sub: Subscribe): RawSubscribe {
|
||||||
|
return {
|
||||||
|
...(sub.uuid !== undefined ? { uuid: sub.uuid } : {}),
|
||||||
|
...(sub.path !== undefined ? { path: sub.path } : {}),
|
||||||
|
...(sub.depth !== undefined ? { depth: sub.depth } : {}),
|
||||||
|
...(sub.value?.value !== undefined ? { value: sub.value.value } : {}),
|
||||||
|
...(sub.hasChild !== undefined ? { hasChild: sub.hasChild } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
67
src/models/Subscriptions.ts
Normal file
67
src/models/Subscriptions.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { convertToSubscribe } from 'src/models/Subscribe';
|
||||||
|
import type { Subs, Subscribe, RawSubs } from 'src/models/Subscribe';
|
||||||
|
|
||||||
|
const EMPTYUUID = '00000000-0000-0000-0000-000000000000';
|
||||||
|
export const Subscriptions = reactive<Record<string, Subscribe>>({});
|
||||||
|
|
||||||
|
export type TableSubscription = {
|
||||||
|
path: string;
|
||||||
|
value: Ref<string | number | boolean | null | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getRows(): Subs {
|
||||||
|
return Object.values(Subscriptions).map((sub) => {
|
||||||
|
sub.path = sub.path?.split(':').pop() ?? '';
|
||||||
|
if (!sub.type) sub.type = 'None';
|
||||||
|
else sub.type = sub.type.toLowerCase();
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSubscriptions(subs: RawSubs) {
|
||||||
|
subs.forEach((sub) => addSubscription(convertToSubscribe(sub)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSubscription(sub: Subscribe) {
|
||||||
|
if (EMPTYUUID === sub.uuid) {
|
||||||
|
sub.path = 'DBM';
|
||||||
|
}
|
||||||
|
if (!sub.uuid) return;
|
||||||
|
Subscriptions[sub.uuid] = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSubscription(sub: Subscribe) {
|
||||||
|
if (!sub.uuid) return;
|
||||||
|
Subscriptions[sub.uuid] = sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSubscriptionValue(
|
||||||
|
uuid: string,
|
||||||
|
value: string | number | boolean | null | undefined,
|
||||||
|
) {
|
||||||
|
if (!uuid) return;
|
||||||
|
if (!Subscriptions[uuid]) return;
|
||||||
|
Subscriptions[uuid].value = ref(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSubscriptions(subs: RawSubs) {
|
||||||
|
subs.forEach((sub) => {
|
||||||
|
removeSubscription(sub.path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAllSubscriptions() {
|
||||||
|
Object.keys(Subscriptions).forEach((key) => delete Subscriptions[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSubscription(uuid: string | undefined) {
|
||||||
|
if (uuid === undefined) return;
|
||||||
|
if (!Subscriptions || Subscriptions[uuid] === undefined) return;
|
||||||
|
delete Subscriptions[uuid];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findSubscriptionByPath(path: string): Subscribe | undefined {
|
||||||
|
return Object.values(Subscriptions).find((sub) => sub.path === path);
|
||||||
|
}
|
@@ -1,19 +1,13 @@
|
|||||||
import type { Response } from 'src/models/Response';
|
import type { Response } from 'src/models/Response';
|
||||||
import type { Publish } from 'src/models/Publish';
|
import { publishToSubscriptions } from 'src/models/Publish';
|
||||||
import type { Request } from 'src/models/Request';
|
import type { Request } from 'src/models/Request';
|
||||||
import type { QVueGlobals } from 'quasar';
|
import type { QVueGlobals } from 'quasar';
|
||||||
import {
|
|
||||||
buildTree,
|
|
||||||
dbmData,
|
|
||||||
getSubscriptionsByPath,
|
|
||||||
getAllSubscriptions,
|
|
||||||
} from 'src/composables/dbm/dbmTree';
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { Subs } from 'src/models/Subscribe';
|
import { NotifyResponse } from 'src/composables/notify';
|
||||||
|
import { type Subs } from 'src/models/Subscribe';
|
||||||
import type { Sets } from 'src/models/Set';
|
import type { Sets } from 'src/models/Set';
|
||||||
import type { PongMessage } from 'src/models/Pong';
|
import type { PongMessage } from 'src/models/Pong';
|
||||||
import { NotifyResponse } from 'src/composables/notify';
|
import { addSubscriptions } from 'src/models/Subscriptions';
|
||||||
|
|
||||||
const pendingResponses = new Map<string, (data: Response | undefined) => void>();
|
const pendingResponses = new Map<string, (data: Response | undefined) => void>();
|
||||||
//const lastKnownValues: Record<string, string> = reactive({});
|
//const lastKnownValues: Record<string, string> = reactive({});
|
||||||
|
|
||||||
@@ -91,13 +85,7 @@ export function initWebSocket(url: string, $q?: QVueGlobals) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.publish) {
|
if (message.publish) {
|
||||||
(message.publish as Publish[]).forEach((pub) => {
|
publishToSubscriptions(message.publish);
|
||||||
const sub = getSubscriptionsByPath(pub.path);
|
|
||||||
if (sub.value && pub.value) {
|
|
||||||
sub.value.value = pub.value;
|
|
||||||
}
|
|
||||||
dbmData.splice(0, dbmData.length, ...buildTree(getAllSubscriptions())); // rebuild reactive tree
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -139,10 +127,6 @@ function waitForSocketConnection(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribe(data: Subs): Promise<Response | undefined> {
|
|
||||||
return send({ subscribe: data });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function subscribeToPath(q: QVueGlobals, path: string) {
|
export function subscribeToPath(q: QVueGlobals, path: string) {
|
||||||
subscribe([
|
subscribe([
|
||||||
{
|
{
|
||||||
@@ -151,9 +135,8 @@ export function subscribeToPath(q: QVueGlobals, path: string) {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(response);
|
|
||||||
if (response?.subscribe) {
|
if (response?.subscribe) {
|
||||||
dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe));
|
addSubscriptions(response.subscribe);
|
||||||
} else {
|
} else {
|
||||||
NotifyResponse(q, response);
|
NotifyResponse(q, response);
|
||||||
}
|
}
|
||||||
@@ -163,6 +146,10 @@ export function subscribeToPath(q: QVueGlobals, path: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscribe(data: Subs): Promise<Response | undefined> {
|
||||||
|
return send({ subscribe: data });
|
||||||
|
}
|
||||||
|
|
||||||
export function unsubscribe(data: Subs): Promise<Response | undefined> {
|
export function unsubscribe(data: Subs): Promise<Response | undefined> {
|
||||||
return send({ unsubscribe: data });
|
return send({ unsubscribe: data });
|
||||||
}
|
}
|
||||||
|
32
src/vueLib/buttons/DataTypes.vue
Normal file
32
src/vueLib/buttons/DataTypes.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<q-card>
|
||||||
|
<div class="text-primary q-ma-md">Datatypes *</div>
|
||||||
|
<div>
|
||||||
|
<div class="row q-gutter-sm q-ml-sm">
|
||||||
|
<q-card-section class="items-between">
|
||||||
|
<RadioButton class="q-ma-xs" v-model:opt="datatype" text="None" hint="none">
|
||||||
|
</RadioButton>
|
||||||
|
<RadioButton class="q-ma-xs" v-model:opt="datatype" text="String" hint="Text">
|
||||||
|
</RadioButton>
|
||||||
|
<RadioButton class="q-ma-xs" v-model:opt="datatype" text="Bool" hint="On / Off">
|
||||||
|
</RadioButton>
|
||||||
|
</q-card-section>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Uint8" hint="0 - 255"> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Uint16" hint="0 - 65535"> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Uint32" hint="0 - 429496..."> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Int8" hint="-128 - 127"> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Int16" hint="-32768 - 3..."> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Int32" hint="-21474836..."> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="Int32" hint="-2^63 - (2^...)"> </RadioButton>
|
||||||
|
<RadioButton v-model:opt="datatype" text="double" hint="1.7E 1/- 3..."> </RadioButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import RadioButton from './RadioButton.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const datatype = ref('none');
|
||||||
|
</script>
|
31
src/vueLib/buttons/RadioButton.vue
Normal file
31
src/vueLib/buttons/RadioButton.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<q-btn style="border-radius: 8px" no-caps
|
||||||
|
><q-radio v-model="opt" :val="props.text"></q-radio>
|
||||||
|
<div class="column items-start q-mx-md">
|
||||||
|
<div class="text-body1 text-black">{{ props.text }}</div>
|
||||||
|
<div class="text-caption text-grey">{{ props.hint }}</div>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch, ref } from 'vue';
|
||||||
|
|
||||||
|
const opt = defineModel('opt');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const localOption = ref('');
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:option']);
|
||||||
|
|
||||||
|
watch(localOption, (val) => emit('update:option', val));
|
||||||
|
</script>
|
65
src/vueLib/dialog/DialogFrame.vue
Normal file
65
src/vueLib/dialog/DialogFrame.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
:maximized="minMaxState"
|
||||||
|
:full-width="minMaxState"
|
||||||
|
:no-focus="!minMaxState"
|
||||||
|
:no-refocus="!minMaxState"
|
||||||
|
:seamless="!minMaxState"
|
||||||
|
>
|
||||||
|
<q-card class="layout" :style="cardStyle" v-touch-pan.mouse.prevent.stop="handlePan">
|
||||||
|
<div class="row justify-end q-mx-sm q-mt-xs">
|
||||||
|
<q-btn dense flat :icon="minMaxIcon" size="md" @click="minMax()"></q-btn>
|
||||||
|
<q-btn dense flat icon="close" size="md" v-close-popup></q-btn>
|
||||||
|
</div>
|
||||||
|
<q-separator color="black" class="q-my-none" />
|
||||||
|
<slot />
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const dialogRef = ref();
|
||||||
|
const open = () => dialogRef.value?.show();
|
||||||
|
|
||||||
|
const minMaxIcon = ref('fullscreen');
|
||||||
|
const minMaxState = ref(false);
|
||||||
|
function minMax() {
|
||||||
|
if (minMaxState.value) {
|
||||||
|
minMaxIcon.value = 'fullscreen';
|
||||||
|
} else {
|
||||||
|
minMaxIcon.value = 'fullscreen_exit';
|
||||||
|
}
|
||||||
|
minMaxState.value = !minMaxState.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '300px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const position = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// This makes the dialog draggable
|
||||||
|
const handlePan = (details: { delta: { x: number; y: number } }) => {
|
||||||
|
position.value.x += details.delta.x;
|
||||||
|
position.value.y += details.delta.y;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardStyle = computed(() => ({
|
||||||
|
width: props.width,
|
||||||
|
transform: `translate(${position.value.x}px, ${position.value.y}px)`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user