add devices list

- add DeviceController
- add getDevices to DeviceRegistryService
- add WebClient deviceRegistryDevices to DeviceRegisterWebClientCalls.kt
- add device list.html
- fix no items found for device type list.html
- add device to main menu in layout.html
- add device to AdminPaths.kt
This commit is contained in:
2025-08-16 15:08:33 +02:00
committed by swordsteel
parent eeed35d93e
commit a7bb013a9d
8 changed files with 114 additions and 1078 deletions

View File

@@ -0,0 +1,40 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min
import ltd.hlaeja.dto.Pagination
import ltd.hlaeja.service.DeviceRegistryService
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 DeviceController(
private val deviceRegistryService: DeviceRegistryService,
) {
companion object {
const val DEFAULT_PAGE: Int = 1
const val DEFAULT_SIZE: Int = 25
const val MIN: Long = 1
const val MAX: Long = 100
}
@GetMapping(
"/device",
"/device/page-{page}",
"/device/page-{page}/show-{show}",
)
fun getDevice(
@PathVariable(required = false) @Min(MIN) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(MIN) @Max(MAX) show: Int = DEFAULT_SIZE,
model: Model,
) = deviceRegistryService.getDevices(page, show)
.collectList()
.doOnNext { items ->
model.addAttribute("items", items)
model.addAttribute("pagination", Pagination(page, show, items.size, DEFAULT_SIZE))
}
.then(Mono.just("device/list"))
}

View File

@@ -5,4 +5,5 @@ import org.springframework.security.config.web.server.ServerHttpSecurity.Authori
fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers( fun AuthorizeExchangeSpec.adminPaths(): AuthorizeExchangeSpec.Access = pathMatchers(
"/account/**", "/account/**",
"/type/**", "/type/**",
"/device/**",
) )

View File

@@ -1,9 +1,11 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Devices
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.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
@@ -41,4 +43,9 @@ class DeviceRegistryService(
request: Type.Request, request: Type.Request,
): Mono<Type.Response> = webClient.deviceRegistryTypesUpdate(type, request, property) ): Mono<Type.Response> = webClient.deviceRegistryTypesUpdate(type, request, property)
.onErrorResume(::hlaejaErrorHandler) .onErrorResume(::hlaejaErrorHandler)
fun getDevices(
page: Int,
show: Int,
): Flux<Devices.Response> = webClient.deviceRegistryDevices(page, show, property)
} }

View File

@@ -5,6 +5,7 @@ import ltd.hlaeja.exception.DeviceRegistryException
import ltd.hlaeja.exception.NoChangeException 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.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
@@ -59,3 +60,12 @@ fun WebClient.deviceRegistryTypesUpdate(
.onStatus(NOT_FOUND::equals) { throw NotFoundException("Remote service returned 404") } .onStatus(NOT_FOUND::equals) { throw NotFoundException("Remote service returned 404") }
.onStatus(CONFLICT::equals) { throw TypeNameDuplicateException("Remote service returned 409") } .onStatus(CONFLICT::equals) { throw TypeNameDuplicateException("Remote service returned 409") }
.bodyToMono(Type.Response::class.java) .bodyToMono(Type.Response::class.java)
fun WebClient.deviceRegistryDevices(
page: Int,
size: Int,
property: DeviceRegistryProperty,
): Flux<Devices.Response> = get()
.uri("${property.url}/devices/page-$page/show-$size".also(::logCall))
.retrieve()
.bodyToFlux(Devices.Response::class.java)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,53 @@
<!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">Device</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">ID</th>
<th class="py-2 px-4 text-left">Time</th>
<th class="py-2 px-4 text-left">Actions</th>
</tr>
</thead>
<tbody>
<tr th:if="${items.isEmpty()}">
<td colspan="4" class="py-2 px-4 text-center text-green-600">No devices found</td>
</tr>
<tr th:each="item : ${items}" class="border-b border-gray-700 hover:bg-gray-700">
<td th:text="${item.id}" class="py-2 px-4"></td>
<td th:data-timestamp="${item.timestamp}" class="py-2 px-4 utcTimestamp"></td>
<td th:text="${item.type}" class="py-2 px-4"></td>
</tr>
</tbody>
</table>
</div>
<th:block th:replace="~{pagination :: pagination('/device', ${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>

View File

@@ -35,7 +35,7 @@
</thead> </thead>
<tbody> <tbody>
<tr th:if="${items.isEmpty()}"> <tr th:if="${items.isEmpty()}">
<td colspan="4" class="py-2 px-4 text-center text-green-600">No accounts found</td> <td colspan="4" class="py-2 px-4 text-center text-green-600">No device types found</td>
</tr> </tr>
<tr th:each="item : ${items}" class="border-b border-gray-700 hover:bg-gray-700"> <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:text="${item.name}" class="py-2 px-4"></td>

View File

@@ -34,6 +34,7 @@
<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> <a href="/type" class="block px-4 py-2 text-sm hover:bg-gray-700 hover:text-green-300 transition-colors">Device 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="#" 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>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</th:block> </th:block>