add new lights, add login and role to it, add services page

This commit is contained in:
Adrian Zuercher
2025-08-09 18:00:36 +02:00
parent 7d2ab814da
commit 1697a4dcfd
56 changed files with 1337 additions and 290 deletions

View File

@@ -60,7 +60,12 @@
</q-td>
</template>
</q-table>
<RenameDialog width="400px" button-ok-label="Rename" ref="renameDialog" />
<RenameDialog
dialogLabel="Rename Datapoint"
width="400px"
button-ok-label="Rename"
ref="renameDialog"
/>
<UpdateDialog width="400px" button-ok-label="Write" ref="updateDialog" />
<UpdateDriver width="400px" ref="updateDriverDialog" />
<UpdateDatatype
@@ -89,7 +94,6 @@ const updateDriverDialog = ref();
const updateDatatype = ref();
const openDialog = (sub: Subscribe, type?: string) => {
console.log(11, sub);
if (sub.path?.split(':')[0] === 'System' && sub.path !== 'DBM') return;
switch (type) {
case 'type':
@@ -103,7 +107,7 @@ const openDialog = (sub: Subscribe, type?: string) => {
updateDialog.value?.open(ref(sub), type);
break;
case 'drivers':
if (sub.type === 'NONE') return;
if (sub.path === 'DBM' || sub.type === 'NONE') return;
updateDriverDialog.value?.open(sub);
break;
}

View File

@@ -102,7 +102,7 @@
</q-list>
</q-menu>
<RenameDatapoint :dialogLabel="label" width="700px" button-ok-label="Rename" ref="renameDialog" />
<AddDialog :dialogLabel="label" width="700px" button-ok-label="Add" ref="addDialog" />
<AddDialog :dialogLabel="label" width="750px" button-ok-label="Add" ref="addDialog" />
<RemoveDialog :dialogLabel="label" width="350px" button-ok-label="Remove" ref="removeDialog" />
<CopyDialog :dialogLabel="label" width="300px" button-ok-label="Copy" ref="copyDialog" />
<UpdateDatapoint

View File

@@ -1,5 +1,10 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="props.dialogLabel">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="props.dialogLabel"
>
<q-card-section
v-if="props.dialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
@@ -37,9 +42,11 @@
filled
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
}}</q-btn>
<div class="row justify-end">
<q-btn no-caps class="q-mb-xl q-mx-xl q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
}}</q-btn>
</div>
</q-form>
</DialogFrame>
</template>
@@ -132,6 +139,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '650px',
},
});
function getDatapoint(uuid: string) {

View File

@@ -1,5 +1,10 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="props.dialogLabel">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="props.dialogLabel"
>
<q-card-section
v-if="props.dialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
@@ -27,9 +32,11 @@
>
</q-input>
<div class="q-mx-sm">
<q-btn no-caps class="q-mb-xl q-ml-lg q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
}}</q-btn>
<div class="row justify-end">
<q-btn no-caps class="q-mb-xl q-mr-md q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
}}</q-btn>
</div>
</div>
</q-form>
</DialogFrame>
@@ -117,6 +124,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '400px',
},
});
function getDatapoint(uuid: string) {

View File

@@ -1,19 +1,23 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="localDialogLabel">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="localDialogLabel"
>
<q-card-section
v-if="props.dialogLabel || localDialogLabel"
v-if="dialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
:class="'text-' + props.labelColor"
>{{ props.dialogLabel || localDialogLabel }}</q-card-section
>
>{{ dialogLabel }}
</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">
<q-card-section class="q-gutter-xs row q-col-gutter-xs q-ml-sm">
<div>
<q-select
class="col-8"
filled
label="Driver Name"
type="text"
@@ -24,7 +28,6 @@
v-model="driverForm.type"
/>
<q-input
class="col-8"
filled
label="Bus"
type="text"
@@ -35,18 +38,17 @@
/>
<q-input
v-if="driverForm.isAddress"
class="col-8"
filled
dense
label="Address"
type="number"
name="Address"
@keyup.enter="updateDriver"
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"
@@ -55,21 +57,21 @@
v-model="driverForm.subscribe"
/>
<q-input
class="col-8"
filled
dense
label="Publish"
type="text"
name="Address"
@keyup.enter="updateDriver"
v-model="driverForm.publish"
/>
</div>
</q-card-section>
</q-form>
<q-card-actions class="text-primary">
<q-card-actions align="right" 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"
class="q-mb-xl q-mr-lg"
v-if="props.buttonOkLabel"
color="primary"
no-caps
@@ -82,10 +84,10 @@
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import { reactive, ref, watch, computed } from 'vue';
import DialogFrame from '../../dialog/DialogFrame.vue';
import type { Driver } from '../../models/Drivers';
import { setRequest } from 'src/vueLib/models/Request';
import { deleteRequest, 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';
@@ -95,6 +97,7 @@ import { updateDriverTable, type DriverTableRow } from 'src/vueLib/models/driver
const { NotifyResponse } = useNotify();
const Dialog = ref();
const dialogLabel = computed(() => props.dialogLabel || localDialogLabel.value);
const options = ['ArtNetDriver', 'OSCDriver'];
const driverForm = reactive({
type: 'ArtNetDriver',
@@ -130,6 +133,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '480px',
},
});
const localDialogLabel = ref('');
@@ -138,6 +145,7 @@ let dpUuid = '';
const form = ref();
const address = ref();
const topic = ref();
const edit = ref(false);
watch(
() => driverForm.type,
@@ -158,13 +166,20 @@ const open = (uuid: string, drvs: DriverTableRow, type: 'add' | 'edit') => {
switch (type) {
case 'add':
localDialogLabel.value = 'Add Driver';
driverForm.type = 'ArtNetDriver';
driverForm.isAddress = true;
edit.value = false;
Object.assign(driverForm, {
type: 'ArtNetDriver',
bus: '',
isAddress: true,
address: 0,
subscribe: '',
publish: '',
});
break;
case 'edit':
localDialogLabel.value = 'Edit Driver';
driverForm.type = drvs.type;
driverForm.bus = drvs.bus;
edit.value = true;
fillDriverFormFromRow(drvs);
}
dpUuid = uuid;
@@ -177,9 +192,9 @@ const open = (uuid: string, drvs: DriverTableRow, type: 'add' | 'edit') => {
Dialog.value?.open();
};
function updateDriver() {
form.value?.validate();
if (!driverForm.type || !driverForm.bus) {
async function updateDriver() {
const valid = await form.value?.validate();
if (!valid) {
NotifyResponse('Please fill in all required fields', 'warning');
return;
}
@@ -191,6 +206,15 @@ function updateDriver() {
topic.value = { subscribe: [driverForm.subscribe], publish: [driverForm.publish] };
}
if (edit.value) {
deleteRequest(dpUuid, '', driver.value)
.then((resp) => {
resp.forEach((set) => {
updateDriverTable(convertToSubscribe(set));
});
})
.catch((err) => NotifyResponse(err, 'error'));
}
setRequest('', undefined, undefined, undefined, dpUuid, {
type: driverForm.type,
buses: [
@@ -216,6 +240,14 @@ function updateDriver() {
});
}
function fillDriverFormFromRow(drvs: DriverTableRow) {
driverForm.type = drvs.type;
driverForm.bus = drvs.buses?.[0]?.name ?? '';
driverForm.address = drvs.buses?.[0]?.address?.[0] ?? -1;
driverForm.subscribe = drvs.topic?.subscribe?.[0] ?? '';
driverForm.publish = drvs.topic?.publish?.[0] ?? '';
}
defineExpose({ open });
</script>

View File

@@ -8,12 +8,14 @@
</q-card-section>
<div class="text-center text-bold text-primary">
Do you want to remove Datapoint
<br />
<br /><br />
'{{ datapoint.path ?? '' }}'
</div>
<q-btn no-caps class="q-ma-md" filled color="negative" @click="remove">{{
props.buttonOkLabel
}}</q-btn>
<div class="row justify-end">
<q-btn no-caps class="q-ma-lg q-mr-xl" filled color="negative" @click="remove">{{
props.buttonOkLabel
}}</q-btn>
</div>
</DialogFrame>
</template>

View File

@@ -1,5 +1,10 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="props.dialogLabel">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="props.dialogLabel"
>
<q-card-section
v-if="props.dialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
@@ -26,7 +31,7 @@
:rules="[(val) => !!val || 'Path is required']"
>
</q-input>
<div class="q-mx-sm">
<div class="row justify-end q-mr-lg">
<q-btn no-caps class="q-mb-xl q-ml-lg q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
}}</q-btn>
@@ -95,7 +100,6 @@ function onSubmit() {
)
.then((res) => {
addRawSubscriptions(res as RawSubs);
console.log(80, res);
buildTree(convertToSubscribes(res as RawSubs));
UpdateTable();
})
@@ -127,6 +131,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '400px',
},
});
function getDatapoint(uuid: string) {

View File

@@ -1,10 +1,17 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="props.dialogLabel">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="props.dialogLabel"
>
<q-card-section
v-if="props.dialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
:class="'text-' + props.labelColor"
>DBM:{{ datapoint.path }}
>{{
datapoint.uuid === '00000000-0000-0000-0000-000000000000' ? 'DBM' : 'DBM:' + datapoint.path
}}
</q-card-section>
<q-form ref="datatypeForm" class="q-gutter-md">
<q-input
@@ -12,7 +19,7 @@
filled
dense
v-model="currentDatatype"
label="Current Path"
label="Current Datatype"
label-color="primary"
readonly
>
@@ -20,6 +27,7 @@
<q-select
class="q-mt-lg q-mt-none q-pl-md q-mx-lg"
popup-content-class="small-dropdown"
label="New Datatype"
filled
dense
v-model="selectedDatatype"
@@ -27,7 +35,7 @@
option-label="label"
>
</q-select>
<div class="q-mx-sm">
<div class="row justify-end q-mr-lg">
<q-btn no-caps class="q-mb-xl q-ml-lg q-px-lg" @click="onSubmit" color="primary">{{
props.buttonOkLabel
}}</q-btn>
@@ -114,6 +122,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '340px',
},
});
async function getDatapoint(uuid: string) {

View File

@@ -1,5 +1,10 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="datapoint?.path">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="'DBM:' + datapoint?.path"
>
<q-card-section
v-if="props.dialogLabel || localDialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
@@ -44,12 +49,16 @@
<q-menu ref="contextMenuRef" context-menu>
<q-list>
<q-item>
<q-item-section class="cursor-pointer" @click="handleRow(driver, 'edit')">
<q-item-section v-close-popup 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)">
<q-item-section
v-close-popup
class="text-negative cursor-pointer"
@click="deleteDriver(driver)"
>
Delete
</q-item-section>
</q-item>
@@ -59,7 +68,7 @@
<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-card-actions align="right" class="text-primary">
<q-btn v-if="props.buttonCancelLabel" flat :label="props.buttonCancelLabel" v-close-popup>
</q-btn>
<q-btn
@@ -120,6 +129,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '500px',
},
});
const datapoint = ref();

View File

@@ -1,5 +1,10 @@
<template>
<DialogFrame ref="Dialog" :width="props.width" :header-title="datapoint?.path">
<DialogFrame
ref="Dialog"
:width="props.width"
:height="props.height"
:header-title="datapoint?.path"
>
<q-card-section
v-if="props.dialogLabel || localDialogLabel"
class="text-bold text-left q-mb-none q-pb-none"
@@ -45,11 +50,11 @@
<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-card-actions align="right" 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"
class="q-mb-xl q-mr-lg q-mt-none"
v-if="props.buttonOkLabel"
color="primary"
no-caps
@@ -104,6 +109,10 @@ const props = defineProps({
type: String,
default: '300px',
},
height: {
type: String,
default: '350px',
},
});
const open = (sub: Ref<Subscribe>) => {

View File

@@ -7,18 +7,30 @@
:no-refocus="!minMaxState"
:seamless="!minMaxState"
>
<q-card class="layout" :style="cardStyle" v-touch-pan.mouse.prevent.stop="handlePan">
<div :class="props.headerTitle ? 'row items-center justify-between' : ''">
<div v-if="headerTitle" class="q-mx-sm q-mt-xs text-left text-bold text-caption">
{{ props.headerTitle }}
<q-card class="layout" :style="cardStyle">
<!-- Draggable Header -->
<div
class="dialog-header row items-center justify-between bg-grey-1"
v-touch-pan.mouse.prevent.stop="handlePan"
>
<div v-if="headerTitle" class="text-left text-bold text-caption q-mx-sm">
{{ headerTitle }}
</div>
<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 class="row justify-end q-mx-sm">
<q-btn dense flat :icon="minMaxIcon" size="md" @click="minMax" />
<q-btn dense flat icon="close" size="md" v-close-popup />
</div>
</div>
<q-separator color="black" class="q-my-none" />
<div class="scrollArea"><slot /></div>
<q-separator color="black" />
<!-- Content Slot -->
<div class="scrollArea">
<slot />
</div>
<!-- Resize Handle -->
<div v-if="!minMaxState" class="resize-handle" @mousedown.prevent="startResizing" />
</q-card>
</q-dialog>
</template>
@@ -29,54 +41,107 @@ import { ref, computed } from 'vue';
const dialogRef = ref();
const open = () => dialogRef.value?.show();
const close = () => dialogRef.value?.hide();
defineExpose({ open, close });
const props = defineProps({
headerTitle: { type: String, default: '' },
width: { type: String, default: '400' },
height: { type: String, default: '250' },
});
// Fullscreen toggle
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;
minMaxIcon.value = minMaxState.value ? 'fullscreen_exit' : 'fullscreen';
}
const props = defineProps({
headerTitle: {
type: String,
default: '',
},
width: {
type: String,
default: '300px',
},
});
// Position and Size
const position = ref({ x: 0, y: 0 });
const width = ref(parseInt(props.width));
const height = ref(parseInt(props.height));
// This makes the dialog draggable
// Dragging (only from header)
const handlePan = (details: { delta: { x: number; y: number } }) => {
position.value.x += details.delta.x;
position.value.y += details.delta.y;
if (!minMaxState.value) {
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)`,
}));
// Resizing
const isResizing = ref(false);
function startResizing(e: MouseEvent) {
isResizing.value = true;
const startX = e.clientX;
const startY = e.clientY;
const startWidth = width.value;
const startHeight = height.value;
defineExpose({ open, close });
function onMouseMove(e: MouseEvent) {
width.value = Math.max(200, startWidth + e.clientX - startX);
height.value = Math.max(200, startHeight + e.clientY - startY);
}
function onMouseUp() {
isResizing.value = false;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
// Styles
const cardStyle = computed(() => {
if (minMaxState.value) {
return {};
}
return {
width: `${width.value}px`,
height: `${height.value}px`,
transform: `translate(${position.value.x}px, ${position.value.y}px)`,
};
});
</script>
<style scoped>
.layout {
border-radius: 10px;
position: relative;
display: flex;
flex-direction: column;
border-radius: 10px;
background-color: white;
}
/* Draggable header */
.dialog-header {
padding: 8px 0;
background: #f5f5f5;
cursor: move;
user-select: none;
}
/* Scrollable content */
.scrollArea {
overflow-y: auto;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 16px;
}
/* Resize handle in bottom right */
.resize-handle {
position: absolute;
width: 16px;
height: 16px;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.1);
cursor: nwse-resize;
z-index: 10;
}
</style>

View File

@@ -22,6 +22,19 @@ export function useNotify() {
break;
}
if (response instanceof Error) {
const resp = response as Response;
if (resp.response?.data?.error) {
$q?.notify({
message: resp.response.data.message as string,
color: color,
position: 'bottom-right',
icon: icon,
timeout: timeout,
});
return;
}
}
if (response) {
const message = typeof response === 'string' ? response : (response.message ?? '');
if (message === '') {

View File

@@ -0,0 +1,109 @@
<template>
<DialogFrame ref="Dialog" width="300px" height="380px" header-title="Login">
<div class="text-black"></div>
<q-form ref="refForm">
<q-item-section class="q-gutter-md q-pa-md">
<q-card :class="['q-gutter-xs q-items-center q-pa-lg', { shake: shake }]">
<div class="text-h5 text-primary text-center">{{ productName }}</div>
<q-input
ref="refUserInput"
dense
filled
type="text"
label="User"
v-model="user"
:rules="[(val) => !!val || 'User is required']"
></q-input>
<q-input
dense
filled
type="password"
label="Password"
v-model="password"
@keyup.enter="onSubmit"
:rules="[(val) => !!val || 'Password is required']"
></q-input>
<div class="q-pt-sm q-mr-md row justify-end">
<q-btn color="primary" label="Login" @click="onSubmit"></q-btn>
</div>
</q-card>
</q-item-section>
</q-form>
</DialogFrame>
</template>
<script setup lang="ts">
import { productName } from '../../../package.json';
import { ref, nextTick } from 'vue';
import DialogFrame from '../dialog/DialogFrame.vue';
import { useNotify } from '../general/useNotify';
import { useLogin } from './login';
const { NotifyResponse } = useNotify();
const Dialog = ref();
const refForm = ref();
const refUserInput = ref();
const user = ref('');
const password = ref('');
const { login } = useLogin();
const shake = ref(false);
const open = () => {
Dialog.value?.open();
nextTick(() => {
refUserInput.value?.focus();
}).catch((err) => console.error(err));
};
const onSubmit = () => {
refForm.value?.validate().then((success: boolean) => {
if (success) {
login(user.value, password.value)
.then(() => {
NotifyResponse("logged in as '" + user.value + "'");
Dialog.value.close();
})
.catch((err) => {
NotifyResponse(err, 'error');
shake.value = true;
setTimeout(() => {
shake.value = false;
}, 500);
});
} else {
NotifyResponse('error submitting login form', 'error');
}
});
};
defineExpose({ open });
</script>
<style scoped>
@keyframes shake {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-8px);
}
40% {
transform: translateX(8px);
}
60% {
transform: translateX(-6px);
}
80% {
transform: translateX(6px);
}
100% {
transform: translateX(0);
}
}
.shake {
animation: shake 0.4s ease;
border: 2px solid #f44336;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="q-gutter-md">
<q-btn dense flat round icon="person" :color="userStore.user ? 'green' : ''">
<q-menu ref="refLoginMenu">
<q-list style="min-width: 100px">
<q-item v-if="userStore.user" class="text-primary">{{ userStore.user?.username }}</q-item>
<q-item clickable v-close-popup @click="openLogin">
<q-item-section class="text-primary">{{ loginText }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
<LoginForm ref="refLoginForm"></LoginForm>
</template>
<script setup lang="ts">
import LoginForm from './LoginForm.vue';
import { computed, ref } from 'vue';
import { useLogin } from './login';
import { useUserStore } from './userStore';
import { useNotify } from '../general/useNotify';
const userStore = useUserStore();
const userLogin = useLogin();
const { NotifyResponse } = useNotify();
const loginText = computed(() => {
return userStore.user ? 'Logout' : 'Login';
});
const refLoginForm = ref();
function openLogin() {
if (userStore.user) {
const username = userStore.user.username;
userLogin.logout();
NotifyResponse("user '" + username + "' logged out", 'warning');
return;
}
refLoginForm.value?.open();
}
</script>

31
src/vueLib/login/login.ts Normal file
View File

@@ -0,0 +1,31 @@
import { appApi } from 'src/boot/axios';
import { useUserStore } from './userStore';
const useStore = useUserStore();
export function useLogin() {
async function login(user: string, password: string) {
await appApi
.post('/login', { user: user, password: password })
.then((resp) => {
useStore.setToken(resp.data.token);
})
.catch((err) => {
throw err;
});
}
function logout() {
useStore.logout();
}
function isTokenValid() {
const token = localStorage.getItem('token');
if (token === null) return false;
const payload = JSON.parse(atob(token.split('.')[1] ?? ''));
const currentTime = Math.floor(Date.now() / 1000);
return payload.exp > currentTime;
}
return { login, logout, isTokenValid };
}

View File

@@ -0,0 +1,80 @@
import { defineStore } from 'pinia';
import { jwtDecode } from 'jwt-decode';
import type { QVueGlobals } from 'quasar';
interface JwtPayload {
id: string;
role: string;
username: string;
exp?: number;
iat?: number;
}
interface UserState {
token: string | null;
user: JwtPayload | null;
}
let $q = <QVueGlobals>{};
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: null,
user: null,
}),
getters: {
isAuthenticated: (state): boolean => !!state.token && !!state.user,
},
actions: {
isAuthorizedAs(roles: string[]) {
return !!this.token && !!this.user && roles.includes(this.user.role);
},
setToken(token: string) {
try {
const decoded = jwtDecode<JwtPayload>(token);
this.token = token;
this.user = decoded;
localStorage.setItem('token', token);
if (decoded.exp) {
const timeUntilExpiry = decoded.exp * 1000 - Date.now();
if (timeUntilExpiry > 0) {
setTimeout(() => {
this.logout();
}, timeUntilExpiry);
} else {
this.logout();
}
}
} catch (err) {
console.error('Invalid token:', err);
this.logout();
}
},
logout() {
$q?.notify({
message: "user '" + this.user?.username + "' logged out",
color: 'orange',
position: 'bottom-right',
icon: 'warning',
timeout: 5000,
});
this.token = null;
this.user = null;
localStorage.removeItem('token');
setTimeout(() => {
window.location.href = '/';
}, 5000);
},
loadFromStorage() {
const token = localStorage.getItem('token');
if (token) {
this.setToken(token);
}
},
initStore(q: QVueGlobals) {
$q = q;
},
},
});

View File

@@ -2,7 +2,7 @@ import type { Driver } from './Drivers';
import type { Gets } from './Get';
import type { Sets } from './Set';
import type { Subs } from './Subscribe';
import { api } from 'src/boot/axios';
import { dbmApi } from 'src/boot/axios';
export type Request = {
get?: Gets;
@@ -25,7 +25,7 @@ export async function getRequest(
payload = { path: path, query: { depth: depth } };
}
const resp = await api.post(query, {
const resp = await dbmApi.post(query, {
get: [payload],
});
@@ -37,7 +37,7 @@ export async function getRequest(
}
export async function getRequests(gets: Gets): Promise<Gets> {
const resp = await api.post(query, {
const resp = await dbmApi.post(query, {
get: gets,
});
@@ -49,7 +49,7 @@ export async function getRequests(gets: Gets): Promise<Gets> {
}
export async function rawSetsRequest(sets: Sets): Promise<Sets> {
const resp = await api.post(query, {
const resp = await dbmApi.post(query, {
set: sets,
});
@@ -79,7 +79,7 @@ export async function setRequest(
rename: rename,
};
const resp = await api.post(query, {
const resp = await dbmApi.post(query, {
set: [payload],
});
@@ -91,7 +91,7 @@ export async function setRequest(
}
export async function setsRequest(sets: Sets): Promise<Sets> {
const resp = await api.post(query, {
const resp = await dbmApi.post(query, {
set: sets,
});
@@ -114,7 +114,7 @@ export async function deleteRequest(
} else if (path) {
payload = { path: path, driver: driver };
}
const resp = await api.delete('/json_data', {
const resp = await dbmApi.delete('/json_data', {
data: {
set: [payload],
},

View File

@@ -11,4 +11,10 @@ export type Response = {
publish?: Pubs;
error?: boolean;
message?: string;
response?: {
data?: {
error?: boolean;
message?: string;
};
};
};

View File

@@ -1,9 +1,13 @@
import { reactive, ref } from 'vue';
import type { QTableColumn } from 'quasar';
import type { Subscribe } from './Subscribe';
import type { Bus } from './Bus';
import type { Topic } from './Topic';
export type DriverTableRow = {
type: string;
buses?: Bus[];
topic?: Topic;
bus: string;
address?: number | undefined;
subscribe?: string;

View File

@@ -0,0 +1,38 @@
<template>
<DialogFrame ref="refDialog" width="500px" header-title="Add new Service">
<div class="row justify-center">
<q-select class="col-4" :options="opts" v-model="option"></q-select>
</div>
<!-- <q-table :rows="driverRows"> </q-table> -->
</DialogFrame>
</template>
<script setup lang="ts">
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { ref } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { appApi } from 'src/boot/axios';
const { NotifyResponse } = useNotify();
const refDialog = ref();
const driverRows = ref([]);
const opts = ref();
const option = ref('Choose new service');
interface conf {
name: string;
}
function open() {
appApi
.get('/allDrivers')
.then((resp) => {
driverRows.value = resp.data;
opts.value = resp.data.map((item: conf) => item.name);
})
.catch((err) => NotifyResponse(err, 'error'));
refDialog.value.open();
}
defineExpose({ open });
</script>