Updated web interface with axios requests, additonal functions and styling
This commit is contained in:
parent
e7436622f8
commit
abdc45a799
|
@ -3,29 +3,9 @@
|
|||
<v-toolbar color="main" dark prominent>
|
||||
<img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" />
|
||||
<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 -->
|
||||
<v-btn icon variant="outlined" @click="handlelogout">
|
||||
<v-btn @click="handlelogout">
|
||||
<LogOut />
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
@ -39,20 +19,6 @@
|
|||
import { store } from "@/store";
|
||||
import { ref } from "vue";
|
||||
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() {
|
||||
store.setToken(null);
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="mr-3" v-if="node.status === 200">
|
||||
Name: {{ node.name }} (Status: online)
|
||||
<div class="mr-3" v-if="sensorData?.voltage">
|
||||
Name: {{ node.id }} (Status: online)
|
||||
</div>
|
||||
<div class="mr-3" v-else>
|
||||
Name: {{ node.name }} (Status: offline)
|
||||
Name: {{ node.id }} (Status: offline)
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
|
@ -20,18 +20,18 @@
|
|||
<span class="mr-2">Coordinates:</span>
|
||||
<span>La: {{ node.coordla }}, Long: {{ node.coordlong }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="mr-2">Temperature:</span>
|
||||
<span>{{ node.temperature }}°C</span>
|
||||
<span>{{ sensorData?.temperature }}°C</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="mr-2">Battery:</span>
|
||||
<span>{{ node.battery }}%</span>
|
||||
<span class="mr-2">Battery Voltage:</span>
|
||||
<span>{{ sensorData?.voltage || 'N/A' }}V</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="mr-2">Runtime:</span>
|
||||
<span>{{ node.runtime }} hours</span>
|
||||
<span>{{ sensorData?.uptime }} hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
@ -41,19 +41,33 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Node } from "@/types";
|
||||
import { ComputedRef, inject, ref, watch } from "vue";
|
||||
import { Node, SensorData } from "@/types";
|
||||
import { ComputedRef, inject, ref, watch, onMounted } from "vue";
|
||||
import { key } from "@/store";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps<{ node: Node }>();
|
||||
const visible = ref(true);
|
||||
const sensorData = ref<SensorData | null>(null);
|
||||
|
||||
const { searching, visibleIds } = inject(key) as {
|
||||
searching: ComputedRef<boolean>;
|
||||
visibleIds: ComputedRef<string[]>;
|
||||
};
|
||||
|
||||
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>
|
||||
|
|
|
@ -5,26 +5,32 @@
|
|||
<tr>
|
||||
<th>Node/Name</th>
|
||||
<th>Status</th>
|
||||
<th>Latitude</th> <!-- Separate column for Latitude -->
|
||||
<th>Longitude</th> <!-- Separate column for Longitude -->
|
||||
<th>Latitude</th>
|
||||
<th>Longitude</th>
|
||||
<th>Battery</th>
|
||||
<th>Gemessene - Temperatur</th>
|
||||
<th>Laufzeit</th>
|
||||
<th>Temperature</th>
|
||||
<th>Runtime</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(node, index) in tableData" :key="index">
|
||||
<td>{{ node.name }}</td>
|
||||
<td>
|
||||
<span :class="node.status === 'ONLINE' ? 'status-online' : 'status-offline'">
|
||||
{{ node.status }}
|
||||
<span :class="node.sensorData.voltage ? 'status-online' : 'status-offline'">
|
||||
{{ node.sensorData.voltage ? 'ONLINE' : 'OFFLINE' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ node.position.lat }}</td>
|
||||
<td>{{ node.position.lng }}</td>
|
||||
<td>{{ node.battery }}%</td>
|
||||
<td>{{ node.temperature }}°C</td>
|
||||
<td>{{ node.runtime }}</td>
|
||||
<td
|
||||
contenteditable="true"
|
||||
@blur="validateAndUpdateLatLng(node, 'coordla', $event)"
|
||||
>{{ node.coordla }}</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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -32,92 +38,103 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
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",
|
||||
},
|
||||
],
|
||||
tableData: [],
|
||||
};
|
||||
},
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
/* Styling remains the same */
|
||||
.table-container {
|
||||
background-color: white;
|
||||
padding: 0.5rem 1rem; /* Reduce top padding to 0.5rem */
|
||||
margin: 0 auto; /* Remove margin at the top, keep it centered horizontally */
|
||||
width: 100%; /* Full width of parent */
|
||||
overflow-x: auto; /* Allow horizontal scroll if necessary */
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
width: 100%; /* Full width */
|
||||
border-collapse: collapse; /* Ensure tight borders */
|
||||
margin: 0; /* Remove margin at the top of the table */
|
||||
table-layout: auto; /* Auto layout for slimmer columns */
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.responsive-table th,
|
||||
.responsive-table td {
|
||||
padding: 0.5rem; /* Maintain slim padding */
|
||||
text-align: center; /* Center text */
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
@ -126,7 +143,7 @@ export default {
|
|||
}
|
||||
|
||||
.responsive-table tr:nth-child(even) {
|
||||
background-color: #f9f9f9; /* Striped rows */
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
export interface Node {
|
||||
uuid: string;
|
||||
name: string;
|
||||
status: number;
|
||||
id: string;
|
||||
coordla: number;
|
||||
coordlong: number;
|
||||
temperature: number;
|
||||
battery: number;
|
||||
runtime: number;
|
||||
batteryMinimum: number;
|
||||
batteryMaximum: number;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export interface NodeGroup {
|
||||
groupId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
nodes: Node[];
|
||||
}
|
||||
|
||||
export interface SensorData {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
temperature: number;
|
||||
voltage: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
uuid: string;
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
admin: boolean;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface CreateUserDto {
|
||||
|
|
Loading…
Reference in a new issue