add nodes list

- add NodeController
- add getNodes to DeviceRegistryService
- add WebClient deviceRegistryNodes to DeviceRegisterWebClientCalls.kt
- add node list.html
- add node to main menu and cleanup in layout.html
- add node to AdminPaths.kt
This commit is contained in:
2025-08-18 10:17:50 +02:00
parent 40bc9f073e
commit 570981d5ac
6 changed files with 116 additions and 3 deletions

View File

@@ -0,0 +1,37 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import ltd.hlaeja.service.DeviceRegistryService
import ltd.hlaeja.util.Pagination
import ltd.hlaeja.util.Pagination.Companion.DEFAULT_PAGE
import ltd.hlaeja.util.Pagination.Companion.DEFAULT_SIZE
import ltd.hlaeja.util.Pagination.Companion.MAX
import ltd.hlaeja.util.Pagination.Companion.MIN
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import reactor.core.publisher.Mono
@Controller
class NodeController(
private val deviceRegistryService: DeviceRegistryService,
) {
@GetMapping(
"/node",
"/node/page-{page}",
"/node/page-{page}/show-{show}",
)
fun getNodes(
@PathVariable(required = false) @Min(MIN) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(MIN) @Max(MAX) show: Int = DEFAULT_SIZE,
model: Model,
) = deviceRegistryService.getNodes(page, show)
.collectList()
.doOnNext { items ->
model.addAttribute("items", items)
model.addAttribute("pagination", Pagination(page, show, items.size, DEFAULT_SIZE))
}
.then(Mono.just("node/list"))
}

View File

@@ -6,4 +6,5 @@ fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatch
"/account/**", "/account/**",
"/type/**", "/type/**",
"/device/**", "/device/**",
"/node/**",
) )

View File

@@ -2,10 +2,12 @@ package ltd.hlaeja.service
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Devices import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
import ltd.hlaeja.util.deviceRegistryDevices import ltd.hlaeja.util.deviceRegistryDevices
import ltd.hlaeja.util.deviceRegistryNodes
import ltd.hlaeja.util.deviceRegistryType import ltd.hlaeja.util.deviceRegistryType
import ltd.hlaeja.util.deviceRegistryTypes import ltd.hlaeja.util.deviceRegistryTypes
import ltd.hlaeja.util.deviceRegistryTypesCreate import ltd.hlaeja.util.deviceRegistryTypesCreate
@@ -48,4 +50,9 @@ class DeviceRegistryService(
page: Int, page: Int,
show: Int, show: Int,
): Flux<Devices.Response> = webClient.deviceRegistryDevices(page, show, property) ): Flux<Devices.Response> = webClient.deviceRegistryDevices(page, show, property)
fun getNodes(
page: Int,
show: Int,
): Flux<Nodes.Response> = webClient.deviceRegistryNodes(page, show, property)
} }

View File

@@ -6,6 +6,7 @@ import ltd.hlaeja.exception.NoChangeException
import ltd.hlaeja.exception.NotFoundException import ltd.hlaeja.exception.NotFoundException
import ltd.hlaeja.exception.TypeNameDuplicateException import ltd.hlaeja.exception.TypeNameDuplicateException
import ltd.hlaeja.library.deviceRegistry.Devices import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
@@ -69,3 +70,12 @@ fun WebClient.deviceRegistryDevices(
.uri("${property.url}/devices/page-$page/show-$size".also(::logCall)) .uri("${property.url}/devices/page-$page/show-$size".also(::logCall))
.retrieve() .retrieve()
.bodyToFlux(Devices.Response::class.java) .bodyToFlux(Devices.Response::class.java)
fun WebClient.deviceRegistryNodes(
page: Int,
size: Int,
property: DeviceRegistryProperty,
): Flux<Nodes.Response> = get()
.uri("${property.url}/nodes/page-$page/show-$size".also(::logCall))
.retrieve()
.bodyToFlux(Nodes.Response::class.java)

View File

@@ -33,13 +33,14 @@
<div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-48 bg-gray-800 border border-green-900 shadow-lg z-10"> <div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-48 bg-gray-800 border border-green-900 shadow-lg z-10">
<th:block th:if="${remoteUser.hasRole('admin')}"> <th:block th:if="${remoteUser.hasRole('admin')}">
<a href="/account" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Account</a> <a href="/account" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Account</a>
<a href="/type" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Device Type</a> <hr class="dropdown-divider">
<a href="/type" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Type</a>
<a href="/device" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Device</a> <a href="/device" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Device</a>
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ Device</a> <a href="/node" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Node</a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</th:block> </th:block>
<th:block th:if="${remoteUser.authenticated}"> <th:block th:if="${remoteUser.authenticated}">
<a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ Node</a> <a href="#" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">$ User Node</a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</th:block> </th:block>
<th:block th:if="${remoteUser.authenticated}"> <th:block th:if="${remoteUser.authenticated}">

View File

@@ -0,0 +1,57 @@
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hlæja Management</title>
<th:block th:replace="~{layout.html :: metadata}"/>
</head>
<body class="bg-gray-900 text-green-400 min-h-screen flex flex-col">
<th:block th:replace="~{layout.html :: header}"/>
<main class="container mx-auto p-4 flex-grow">
<div class="bg-gray-800 p-6 rounded-lg border border-green-900">
<h1 class="text-lg sm:text-xl mb-4 terminal-glow">Nodes</h1>
<hr class="border-green-900 mb-4">
<div class="flex justify-between items-center mb-4">
<div th:if="${pagination.start > pagination.size}" class="text-sm">
Show page <span th:text="${pagination.page}"/> items 0 - 0
</div>
<div th:unless="${pagination.start > pagination.size}" class="text-sm">
Show page <span th:text="${pagination.page}"/>
items <span th:text="${pagination.start}"/> -
<span th:text="${pagination.size}"/>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-green-900">
<th class="py-2 px-4 text-left">Name</th>
<th class="py-2 px-4 text-left">Time</th>
<th class="py-2 px-4 text-left">ID <span></span></th>
<th class="py-2 px-4 text-left">Client ID</th>
<th class="py-2 px-4 text-left">Device ID</th>
</tr>
</thead>
<tbody>
<tr th:if="${items.isEmpty()}">
<td colspan="4" class="py-2 px-4 text-center text-green-600">No nodes found</td>
</tr>
<tr th:each="item : ${items}" class="border-b border-gray-700 hover:bg-gray-700">
<td th:text="${item.name}" class="py-2 px-4"></td>
<td th:data-timestamp="${item.timestamp}" class="py-2 px-4 utcTimestamp"></td>
<td th:text="${item.id}" class="py-2 px-4"></td>
<td th:text="${item.client}" class="py-2 px-4"></td>
<td th:text="${item.device}" class="py-2 px-4"></td>
</tr>
</tbody>
</table>
</div>
<th:block th:replace="~{pagination :: pagination('/node', ${pagination})}"/>
</div>
</main>
<th:block th:replace="~{layout.html :: footer}"/>
<th:block th:replace="~{layout.html :: script}"/>
<script>
document.addEventListener('DOMContentLoaded', () => makeLocalTime(document.querySelectorAll('.utcTimestamp')));
</script>
</body>
</html>