add services with statsu server state and remove stage lights

This commit is contained in:
Adrian Zuercher
2025-09-07 14:24:38 +02:00
parent a648be3bcb
commit 9585fb1b7a
35 changed files with 2387 additions and 328 deletions

View File

@@ -62,14 +62,14 @@
</q-table>
<RenameDialog
dialogLabel="Rename Datapoint"
width="400px"
:width="400"
button-ok-label="Rename"
ref="renameDialog"
/>
<UpdateDialog width="400px" button-ok-label="Write" ref="updateDialog" />
<UpdateDriver width="400px" ref="updateDriverDialog" />
<UpdateDialog :width="400" button-ok-label="Write" ref="updateDialog" />
<UpdateDriver :width="400" ref="updateDriverDialog" />
<UpdateDatatype
width="400px"
:width="400"
button-ok-label="Update"
ref="updateDatatype"
dialog-label="Update Datatype"

View File

@@ -101,13 +101,13 @@
</q-item>
</q-list>
</q-menu>
<RenameDatapoint :dialogLabel="label" width="700px" button-ok-label="Rename" ref="renameDialog" />
<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" />
<RenameDatapoint :dialogLabel="label" :width="700" button-ok-label="Rename" ref="renameDialog" />
<AddDialog :dialogLabel="label" :width="750" button-ok-label="Add" ref="addDialog" />
<RemoveDialog :dialogLabel="label" :width="350" button-ok-label="Remove" ref="removeDialog" />
<CopyDialog :dialogLabel="label" :width="300" button-ok-label="Copy" ref="copyDialog" />
<UpdateDatapoint
:dialogLabel="label"
width="300px"
:width="300"
button-ok-label="Update"
ref="datatypeDialog"
/>

View File

@@ -136,12 +136,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '650px',
type: Number,
default: 650,
},
});

View File

@@ -121,12 +121,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '400px',
type: Number,
default: 400,
},
});

View File

@@ -130,12 +130,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '480px',
type: Number,
default: 480,
},
});

View File

@@ -86,8 +86,8 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
});

View File

@@ -128,12 +128,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '400px',
type: Number,
default: 400,
},
});

View File

@@ -119,12 +119,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '340px',
type: Number,
default: 340,
},
});

View File

@@ -126,12 +126,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '500px',
type: Number,
default: 500,
},
});

View File

@@ -106,12 +106,12 @@ const props = defineProps({
default: '',
},
width: {
type: String,
default: '300px',
type: Number,
default: 300,
},
height: {
type: String,
default: '350px',
type: Number,
default: 350,
},
});

View File

@@ -13,7 +13,7 @@
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">
<div class="text-left text-bold text-caption q-mx-sm">
{{ headerTitle }}
</div>
<div class="row justify-end q-mx-sm">
@@ -45,8 +45,8 @@ defineExpose({ open, close });
const props = defineProps({
headerTitle: { type: String, default: '' },
width: { type: String, default: '400' },
height: { type: String, default: '250' },
width: { type: Number, default: 400 },
height: { type: Number, default: 250 },
});
// Fullscreen toggle
@@ -59,8 +59,8 @@ function minMax() {
// Position and Size
const position = ref({ x: 0, y: 0 });
const width = ref(parseInt(props.width));
const height = ref(parseInt(props.height));
const width = ref(props.width);
const height = ref(props.height);
// Dragging (only from header)
const handlePan = (details: { delta: { x: number; y: number } }) => {

View File

@@ -1,5 +1,5 @@
<template>
<DialogFrame ref="Dialog" width="300px" height="380px" header-title="Login">
<DialogFrame ref="Dialog" :width="300" :height="380" header-title="Login">
<div class="text-black"></div>
<q-form ref="refForm">
<q-item-section class="q-gutter-md q-pa-md">

View File

@@ -0,0 +1,64 @@
import { appApi } from 'src/boot/axios';
//import { ref } from 'vue';
export interface Service {
name: string;
workingDirectory: string;
executablePath: string;
description: string;
arguments: string[];
state: State;
stateColor: string;
stateTextColor: string;
}
export type Services = Service[];
export type State = 'Stopped' | 'Stopping' | 'Starting' | 'Running' | 'Error';
export function useServices() {
async function initServices() {
return appApi
.get('services/load')
.then((resp) => {
const normalized = resp.data.services.map((item: Service) => {
if (!item.state) {
item.state = 'Stopped';
}
let stateColor = 'grey-4';
let stateTextColor = 'black';
switch (item.state) {
case 'Running':
stateColor = 'green';
stateTextColor = 'white';
break;
case 'Starting':
case 'Stopping':
stateColor = 'green';
stateTextColor = 'white';
break;
case 'Stopped':
default:
stateColor = 'grey-4';
stateTextColor = 'black';
break;
}
return {
...item,
stateColor,
stateTextColor,
};
});
return normalized;
})
.catch((err) => {
throw err;
});
}
return {
initServices,
};
}

View File

@@ -1,9 +1,14 @@
<template>
<DialogFrame ref="refDialog" width="500px" header-title="Add new Service">
<DialogFrame ref="refDialog" :width="250" header-title="Add new Service">
<div class="row justify-center">
<q-select class="col-4" :options="opts" v-model="option"></q-select>
<div>
<div class="q-ma-md text-primary text-bold">Choose new service</div>
<q-select dense filled class="col-4" :options="opts" v-model="option"></q-select>
<div class="row justify-end">
<q-btn class="q-mt-md" no-caps color="primary">Add Service</q-btn>
</div>
</div>
</div>
<!-- <q-table :rows="driverRows"> </q-table> -->
</DialogFrame>
</template>
@@ -18,17 +23,17 @@ const { NotifyResponse } = useNotify();
const refDialog = ref();
const driverRows = ref([]);
const opts = ref();
const option = ref('Choose new service');
const option = ref('');
interface conf {
name: string;
}
function open() {
appApi
.get('/allDrivers')
.get('services/all')
.then((resp) => {
driverRows.value = resp.data;
opts.value = resp.data.map((item: conf) => item.name);
opts.value = resp.data.services.map((item: conf) => item.name);
})
.catch((err) => NotifyResponse(err, 'error'));
refDialog.value.open();

View File

@@ -0,0 +1,92 @@
<template>
<q-menu ref="refMenu" context-menu>
<q-list>
<q-item :disable="!startEnabled" clickable @click="start">
<q-item-section>
<q-icon :color="startEnabled ? 'green' : 'green-2'" name="play_arrow" size="sm" />
</q-item-section>
</q-item>
<q-item :disable="!stopEnabled" clickable @click="stop">
<q-item-section>
<q-icon :color="stopEnabled ? 'red' : 'red-2'" name="stop" size="sm" />
</q-item-section>
</q-item>
<q-item :disable="!restartEnabled" clickable>
<q-item-section>
<q-icon
:color="restartEnabled ? 'orange' : 'orange-3'"
name="restart_alt"
size="sm"
@click="console.log('restart')"
/>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</template>
<script lang="ts" setup>
import { appApi } from 'src/boot/axios';
import type { Service } from 'src/vueLib/models/Services';
import { computed, ref } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
const { NotifyResponse } = useNotify();
const refMenu = ref();
const startEnabled = computed(() => localService.value?.state === 'Stopped' || 'Error');
const stopEnabled = computed(() => localService.value?.state === 'Running');
const restartEnabled = computed(() => localService.value?.state === 'Running');
const localService = ref<Service>();
const open = (event: MouseEvent, service: Service) => {
localService.value = service;
refMenu.value?.show(event);
};
function start() {
if (!localService.value) {
return;
}
localService.value.state = 'Starting';
localService.value.stateColor = 'yellow';
localService.value.stateTextColor = 'black';
appApi
.get(`/services/${localService.value.name}/start`)
.then(() => {
if (!localService.value) {
return;
}
localService.value.state = 'Running';
localService.value.stateColor = 'green';
localService.value.stateTextColor = 'white';
})
.catch((err) => {
console.error(err);
NotifyResponse(err, 'error');
});
}
function stop() {
if (!localService.value) {
return;
}
localService.value.state = 'Stopping';
localService.value.stateColor = 'yellow';
localService.value.stateTextColor = 'black';
appApi
.get(`/services/${localService.value.name}/stop`)
.then(() => {
if (!localService.value) {
return;
}
localService.value.state = 'Stopped';
localService.value.stateColor = 'grey-4';
localService.value.stateTextColor = 'black';
})
.catch((err) => NotifyResponse(err, 'error'));
}
defineExpose({ open });
</script>

View File

@@ -0,0 +1,7 @@
export type Payload = {
action?: Action;
topic: string;
data: { state?: string; message?: string };
};
export type Action = 'subscribe' | 'publish';

View File

@@ -0,0 +1,115 @@
import type { Response } from '../../models/Response';
import type { QVueGlobals } from 'quasar';
import { ref } from 'vue';
import type { Payload } from './models';
let socket: WebSocket | null = null;
const isConnected = ref(false);
export function useStatusServer(
host: string,
port: number = 9500,
path: string,
id: string,
callback: (payload: Payload) => void,
$q?: QVueGlobals,
) {
const connect = () => {
socket = new WebSocket(`ws://${host}:${port}/${path}?id=${id}`);
socket.onopen = () => {
console.log('WebSocket connected');
isConnected.value = true;
};
socket.onclose = () => {
isConnected.value = false;
console.log('WebSocket disconnected');
$q?.notify({
message: 'WebSocket disconnected',
color: 'orange',
position: 'bottom-right',
icon: 'warning',
timeout: 10000,
});
};
socket.onerror = (err) => {
console.log(`WebSocket error: ${err.type}`);
isConnected.value = false;
$q?.notify({
message: `WebSocket error: ${err.type}`,
color: 'red',
position: 'bottom-right',
icon: 'error',
timeout: 10000,
});
};
socket.onmessage = (event) => {
callback(JSON.parse(event.data));
};
};
function subscribe(topic: string): Promise<Response | undefined> {
return new Promise((resolve) => {
waitForSocketConnection()
.then(() => {
socket?.send('{"action":"subscribe", "topic":"' + topic + '"}');
})
.catch((err) => {
console.warn('WebSocket send failed:', err);
resolve(undefined); // or reject(err) if strict failure is desired
});
});
}
function publish(topic: string, data: unknown): Promise<Response | undefined> {
return new Promise((resolve) => {
waitForSocketConnection()
.then(() => {
socket?.send(
'{"action":"publish", "topic":"' + topic + '", "data":' + JSON.stringify(data) + '}',
);
})
.catch((err) => {
console.warn('WebSocket send failed:', err);
resolve(undefined); // or reject(err) if strict failure is desired
});
});
}
const close = () => {
if (socket) {
socket.close();
}
};
return {
connect,
close,
subscribe,
publish,
};
}
function waitForSocketConnection(): Promise<void> {
return new Promise((resolve, reject) => {
const maxWait = 5000; // timeout after 5 seconds
const interval = 50;
let waited = 0;
const check = () => {
if (socket && socket.readyState === WebSocket.OPEN) {
resolve();
} else {
waited += interval;
if (waited >= maxWait) {
reject(new Error('WebSocket connection timeout'));
} else {
setTimeout(check, interval);
}
}
};
check();
});
}