Updated web interface with axios requests, additonal functions and styling

This commit is contained in:
Mikail Killi 2024-10-14 17:23:30 +02:00
parent e7436622f8
commit abdc45a799
4 changed files with 138 additions and 136 deletions

View file

@ -3,29 +3,9 @@
<v-toolbar color="main" dark prominent> <v-toolbar color="main" dark prominent>
<img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" /> <img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" />
<v-spacer></v-spacer> <v-spacer></v-spacer>
<!-- Users, Groups, Licenses -->
<div>
<!-- USER MANAGEMENT -->
<template v-if="!user" />
<UsersDialog v-else-if="user.admin" />
<UsersEditActions v-else :admin-menu="false" :user="user!" />
</div>
<!-- Search -->
<v-text-field
class="compact-form mr-2"
label="Search"
variant="solo"
density="compact"
prepend-inner-icon="mdi-magnify"
hide-details
single-line
clearable
v-model="search"
rounded="pill"
></v-text-field>
<!-- Logout --> <!-- Logout -->
<v-btn icon variant="outlined" @click="handlelogout"> <v-btn @click="handlelogout">
<LogOut /> <LogOut />
</v-btn> </v-btn>
</v-toolbar> </v-toolbar>
@ -39,20 +19,6 @@
import { store } from "@/store"; import { store } from "@/store";
import { ref } from "vue"; import { ref } from "vue";
import { LogOut } from "lucide-vue-next"; import { LogOut } from "lucide-vue-next";
import { useQuery } from "@tanstack/vue-query";
import UsersDialog from "./NavBarIcons/UsersDialog.vue";
import UsersEditActions from "./NavBarIcons/UsersEditActions.vue";
import { axiosInstance } from "@/client";
import { User } from "@/types";
import { search } from "@/store";
const { data: user } = useQuery({
queryKey: ["user"],
queryFn: async () => {
const res = await axiosInstance.get<User>("/users/me");
return res.data;
},
});
function handlelogout() { function handlelogout() {
store.setToken(null); store.setToken(null);

View file

@ -5,11 +5,11 @@
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-title> <v-expansion-panel-title>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="mr-3" v-if="node.status === 200"> <div class="mr-3" v-if="sensorData?.voltage">
Name: {{ node.name }} (Status: online) Name: {{ node.id }} (Status: online)
</div> </div>
<div class="mr-3" v-else> <div class="mr-3" v-else>
Name: {{ node.name }} (Status: offline) Name: {{ node.id }} (Status: offline)
</div> </div>
</div> </div>
</v-expansion-panel-title> </v-expansion-panel-title>
@ -19,19 +19,19 @@
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Coordinates:</span> <span class="mr-2">Coordinates:</span>
<span>La: {{ node.coordla }}, Long: {{ node.coordlong }}</span> <span>La: {{ node.coordla }}, Long: {{ node.coordlong }}</span>
</div>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Temperature:</span> <span class="mr-2">Temperature:</span>
<span>{{ node.temperature }}°C</span> <span>{{ sensorData?.temperature }}°C</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Battery:</span> <span class="mr-2">Battery Voltage:</span>
<span>{{ node.battery }}%</span> <span>{{ sensorData?.voltage || 'N/A' }}V</span>
</div> </div>
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<span class="mr-2">Runtime:</span> <span class="mr-2">Runtime:</span>
<span>{{ node.runtime }} hours</span> <span>{{ sensorData?.uptime }} hours</span>
</div>
</div> </div>
</v-expansion-panel-text> </v-expansion-panel-text>
</v-expansion-panel> </v-expansion-panel>
@ -41,19 +41,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Node } from "@/types"; import { Node, SensorData } from "@/types";
import { ComputedRef, inject, ref, watch } from "vue"; import { ComputedRef, inject, ref, watch, onMounted } from "vue";
import { key } from "@/store"; import { key } from "@/store";
import axios from "axios";
const props = defineProps<{ node: Node }>(); const props = defineProps<{ node: Node }>();
const visible = ref(true); const visible = ref(true);
const sensorData = ref<SensorData | null>(null);
const { searching, visibleIds } = inject(key) as { const { searching, visibleIds } = inject(key) as {
searching: ComputedRef<boolean>; searching: ComputedRef<boolean>;
visibleIds: ComputedRef<string[]>; visibleIds: ComputedRef<string[]>;
}; };
watch([searching, visibleIds], ([searching, visibleIds]) => { watch([searching, visibleIds], ([searching, visibleIds]) => {
visible.value = searching ? visibleIds.includes(props.node.uuid) : true; visible.value = searching ? visibleIds.includes(props.node.id) : true;
});
onMounted(async () => {
try {
const { data } = await axios.get<SensorData>(
`http://localhost:8080/api/v1/data?nodeId=${props.node.id}`
);
sensorData.value = data;
} catch (error) {
console.error("Error fetching sensor data:", error);
}
}); });
</script> </script>

View file

@ -5,26 +5,32 @@
<tr> <tr>
<th>Node/Name</th> <th>Node/Name</th>
<th>Status</th> <th>Status</th>
<th>Latitude</th> <!-- Separate column for Latitude --> <th>Latitude</th>
<th>Longitude</th> <!-- Separate column for Longitude --> <th>Longitude</th>
<th>Battery</th> <th>Battery</th>
<th>Gemessene - Temperatur</th> <th>Temperature</th>
<th>Laufzeit</th> <th>Runtime</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(node, index) in tableData" :key="index"> <tr v-for="(node, index) in tableData" :key="index">
<td>{{ node.name }}</td> <td>{{ node.name }}</td>
<td> <td>
<span :class="node.status === 'ONLINE' ? 'status-online' : 'status-offline'"> <span :class="node.sensorData.voltage ? 'status-online' : 'status-offline'">
{{ node.status }} {{ node.sensorData.voltage ? 'ONLINE' : 'OFFLINE' }}
</span> </span>
</td> </td>
<td>{{ node.position.lat }}</td> <td
<td>{{ node.position.lng }}</td> contenteditable="true"
<td>{{ node.battery }}%</td> @blur="validateAndUpdateLatLng(node, 'coordla', $event)"
<td>{{ node.temperature }}°C</td> >{{ node.coordla }}</td>
<td>{{ node.runtime }}</td> <td
contenteditable="true"
@blur="validateAndUpdateLatLng(node, 'coordlong', $event)"
>{{ node.coordlong }}</td>
<td>{{ calculateBatteryPercentage(node.sensorData.voltage, node.batteryMinimum, node.batteryMaximum) }}%</td>
<td>{{ node.sensorData.temperature }}°C</td>
<td>{{ formatRuntime(node.sensorData.uptime) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -32,92 +38,103 @@
</template> </template>
<script> <script>
import axios from 'axios';
export default { export default {
data() { data() {
return { return {
tableData: [ tableData: [],
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 98,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 19,
temperature: 100,
runtime: "30m",
},
{
name: "Localnode-3",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 66,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 79,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 10,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "OFFLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 0,
temperature: 100,
runtime: "12h 30m 12s",
},
{
name: "XXXX-XXXX-XXXX-XXXX",
status: "ONLINE",
position: { lat: 40.7128, lng: 74.0012 },
battery: 56,
temperature: 100,
runtime: "12h 30m 12s",
},
],
}; };
}, },
mounted() {
this.fetchNodesAndData();
},
methods: {
async fetchNodesAndData() {
try {
const nodesResponse = await axios.get('http://localhost:8080/api/v1/nodes');
const nodes = nodesResponse.data;
const sensorDataResponse = await axios.get('http://localhost:8080/api/v1/data');
const sensorData = sensorDataResponse.data;
this.tableData = nodes.map((node) => {
const nodeSensorData = sensorData.find((data) => data.id === node.id);
return {
...node,
sensorData: nodeSensorData || {
temperature: 'N/A',
voltage: 'N/A',
uptime: 'N/A',
},
};
});
} catch (error) {
console.error('Error fetching node or sensor data:', error);
}
},
calculateBatteryPercentage(voltage, batteryMinimum, batteryMaximum) {
if (voltage <= batteryMinimum) {
return 0;
} else if (voltage >= batteryMaximum) {
return 100;
} else {
return ((voltage - batteryMinimum) / (batteryMaximum - batteryMinimum) * 100).toFixed(2);
}
},
formatRuntime(uptime) {
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = uptime % 60;
return `${hours}h ${minutes}m ${seconds}s`;
},
validateAndUpdateLatLng(node, field, event) {
const originalValue = node[field];
let newValue = event.target.innerText;
// Normalize separated values
newValue = newValue.replace(',', '.');
// Check if it's a valid float value
const validNumberRegex = /^-?\d+(\.\d+)?$/;
if (validNumberRegex.test(newValue)) {
const parsedValue = parseFloat(newValue);
// Update if valid
node[field] = parsedValue;
console.log(`Updated ${field} of ${node.name}: ${parsedValue}`);
} else {
// Reset to original value if invalid
event.target.innerText = originalValue;
console.log(`Failed to set ${field} of ${node.name}: Invalid input "${newValue}"`);
}
},
},
}; };
</script> </script>
<style scoped> <style scoped>
/* Styling remains the same */
.table-container { .table-container {
background-color: white; background-color: white;
padding: 0.5rem 1rem; /* Reduce top padding to 0.5rem */ padding: 0.5rem 1rem;
margin: 0 auto; /* Remove margin at the top, keep it centered horizontally */ margin: 0 auto;
width: 100%; /* Full width of parent */ width: 100%;
overflow-x: auto; /* Allow horizontal scroll if necessary */ overflow-x: auto;
} }
.responsive-table { .responsive-table {
width: 100%; /* Full width */ width: 100%;
border-collapse: collapse; /* Ensure tight borders */ border-collapse: collapse;
margin: 0; /* Remove margin at the top of the table */ margin: 0;
table-layout: auto; /* Auto layout for slimmer columns */ table-layout: auto;
} }
.responsive-table th, .responsive-table th,
.responsive-table td { .responsive-table td {
padding: 0.5rem; /* Maintain slim padding */ padding: 0.5rem;
text-align: center; /* Center text */ text-align: center;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
@ -126,7 +143,7 @@ export default {
} }
.responsive-table tr:nth-child(even) { .responsive-table tr:nth-child(even) {
background-color: #f9f9f9; /* Striped rows */ background-color: #f9f9f9;
} }
.status-online { .status-online {

View file

@ -1,25 +1,30 @@
export interface Node { export interface Node {
uuid: string; id: string;
name: string;
status: number;
coordla: number; coordla: number;
coordlong: number; coordlong: number;
temperature: number; batteryMinimum: number;
battery: number; batteryMaximum: number;
runtime: number; group: string;
} }
export interface NodeGroup { export interface NodeGroup {
groupId: string; id: string;
name: string; name: string;
nodes: Node[]; }
export interface SensorData {
id: string;
timestamp: number;
temperature: number;
voltage: number;
uptime: number;
} }
export interface User { export interface User {
uuid: string; id: string;
name: string; name: string;
email: string; email: string;
admin: boolean; hash: string;
} }
export interface CreateUserDto { export interface CreateUserDto {