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
parent f021225cb0
commit 74cb8d1479
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(
"/account/**",
"/type/**",
"/device/**",
)

View File

@@ -1,9 +1,11 @@
package ltd.hlaeja.service
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.property.DeviceRegistryProperty
import ltd.hlaeja.util.deviceRegistryDevices
import ltd.hlaeja.util.deviceRegistryType
import ltd.hlaeja.util.deviceRegistryTypes
import ltd.hlaeja.util.deviceRegistryTypesCreate
@@ -41,4 +43,9 @@ class DeviceRegistryService(
request: Type.Request,
): Mono<Type.Response> = webClient.deviceRegistryTypesUpdate(type, request, property)
.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.NotFoundException
import ltd.hlaeja.exception.TypeNameDuplicateException
import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.property.DeviceRegistryProperty
@@ -59,3 +60,12 @@ fun WebClient.deviceRegistryTypesUpdate(
.onStatus(NOT_FOUND::equals) { throw NotFoundException("Remote service returned 404") }
.onStatus(CONFLICT::equals) { throw TypeNameDuplicateException("Remote service returned 409") }
.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>
<tbody>
<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 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>

View File

@@ -34,6 +34,7 @@
<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="/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>
<hr class="dropdown-divider">
</th:block>