From 39d2bbe0b320469a5349998f0b7bc8ad0413a3ed Mon Sep 17 00:00:00 2001 From: bskjon Date: Mon, 17 Mar 2025 18:05:25 +0100 Subject: [PATCH] Updated UI + Indexing --- .../services/UnattendedIndexing.kt | 17 +- .../mediaprocessing/ui/Configuration.kt | 43 ++++- .../no/iktdev/mediaprocessing/ui/UIEnv.kt | 1 + .../ui/socket/EventbasedTopic.kt | 60 ------ .../ui/socket/ExplorerTopic.kt | 23 --- .../ui/socket/FileRequestTopic.kt | 45 +++++ .../ui/socket/PersistentEventsTableTopic.kt | 19 -- .../ui/socket/ProcesserTasksTopic.kt | 56 ++++++ .../ui/socket/SocketListener.kt | 11 ++ .../ui/socket/UnprocessedFilesTopic.kt | 102 ++++++++--- .../ui/socket/a2a/ProcesserListenerService.kt | 97 ++++++++++ .../socket/internal/EncoderReaderService.kt | 77 -------- .../src/main/resources/application.properties | 1 + apps/ui/web/src/App.tsx | 43 ++++- .../features/table/multiListSortedTable.tsx | 156 ++++++++++++++++ .../features/table/sortableGroupedTable.tsx | 173 ++++++++++++++++++ .../web/src/app/page/ProcesserTasksPage.tsx | 98 ++++++++++ .../web/src/app/page/UnprocessedFilesPage.tsx | 108 +++++++++++ apps/ui/web/src/app/store.ts | 4 + apps/ui/web/src/app/store/tasks-slice.ts | 95 ++++++++++ .../src/app/store/unprocessed-files-slice.ts | 39 ++++ apps/ui/web/src/index.tsx | 6 +- 22 files changed, 1059 insertions(+), 215 deletions(-) delete mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventbasedTopic.kt create mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/FileRequestTopic.kt delete mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/PersistentEventsTableTopic.kt create mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt create mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/SocketListener.kt create mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt delete mode 100644 apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/internal/EncoderReaderService.kt create mode 100644 apps/ui/web/src/app/features/table/multiListSortedTable.tsx create mode 100644 apps/ui/web/src/app/features/table/sortableGroupedTable.tsx create mode 100644 apps/ui/web/src/app/page/ProcesserTasksPage.tsx create mode 100644 apps/ui/web/src/app/page/UnprocessedFilesPage.tsx create mode 100644 apps/ui/web/src/app/store/tasks-slice.ts create mode 100644 apps/ui/web/src/app/store/unprocessed-files-slice.ts diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/services/UnattendedIndexing.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/services/UnattendedIndexing.kt index eaf7f0b4..0b58d41e 100644 --- a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/services/UnattendedIndexing.kt +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/services/UnattendedIndexing.kt @@ -25,8 +25,21 @@ class UnattendedIndexing { @Scheduled(fixedDelay = 60_000*60) fun indexContent() { - logger.info { "Performing indexing of input root: ${SharedConfig.inputRoot.absolutePath}" } - val fileList = SharedConfig.inputRoot.walkTopDown().filter { it.isFile && it.isSupportedVideoFile() }.toList() + val allFiles = SharedConfig.incomingContent.flatMap { folder -> + logger.info { "Performing indexing of folder: ${folder.name}" } + folder.walkTopDown() + .filter { it.isFile && it.isSupportedVideoFile() } + .toMutableList() + } + val ignoredParents = allFiles + .asSequence() + .mapNotNull { it.parentFile } + .filter { parent -> parent.resolve(".ignore").exists() } + .toSet() + + val fileList = allFiles + .filter { file -> file.parentFile !in ignoredParents } + fileList.forEach { file -> withTransaction(eventDatabase.database) { files.insertIgnore { diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt index cfe2eaf0..bbb85507 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/Configuration.kt @@ -2,21 +2,27 @@ package no.iktdev.mediaprocessing.ui import no.iktdev.mediaprocessing.shared.common.Defaults import no.iktdev.mediaprocessing.shared.common.socket.SocketImplementation +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory import org.springframework.boot.web.server.WebServerFactoryCustomizer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import import org.springframework.core.io.Resource +import org.springframework.stereotype.Component +import org.springframework.stereotype.Service import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.RestTemplate import org.springframework.web.method.HandlerTypePredicate import org.springframework.web.servlet.config.annotation.* import org.springframework.web.servlet.resource.PathResourceResolver +import org.springframework.web.socket.CloseStatus +import org.springframework.web.socket.TextMessage +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.handler.TextWebSocketHandler import org.springframework.web.util.DefaultUriBuilderFactory -import org.springframework.web.util.UriTemplateHandler +import java.util.concurrent.ConcurrentHashMap @Configuration @@ -84,6 +90,39 @@ class ApiCommunicationConfig { @Configuration class SocketImplemented: SocketImplementation() { + override var additionalOrigins: List = UIEnv.wsAllowedOrigins.split(",") +} + +@Service +class WebSocketMonitoringService() { + private val clients = ConcurrentHashMap.newKeySet() + fun anyListening() = clients.isNotEmpty() + + fun addClient(session: WebSocketSession) { + clients.add(session) + } + fun removeClient(session: WebSocketSession) { + clients.remove(session) + } +} + +@Component +class WebSocketHandler(private val webSocketPollingService: WebSocketMonitoringService) : TextWebSocketHandler() { + + // Kalles når en WebSocket-klient kobler til + override fun afterConnectionEstablished(session: WebSocketSession) { + webSocketPollingService.addClient(session) // Legg til klienten i service + } + + // Kalles når en WebSocket-klient kobler fra + override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) { + webSocketPollingService.removeClient(session) // Fjern klienten fra service + } + + // Håndterer meldinger fra WebSocket-klientene hvis nødvendig + override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { + // Håndter meldinger fra klienten + } } @Configuration diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt index a75b6599..da4f9e65 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/UIEnv.kt @@ -5,4 +5,5 @@ import java.io.File object UIEnv { val socketEncoder: String = System.getenv("EncoderWs")?.takeIf { it.isNotBlank() } ?: "ws://encoder:8080" val coordinatorUrl: String = System.getenv("Coordinator")?.takeIf { it.isNotBlank() } ?: "http://coordinator" + val wsAllowedOrigins: String = System.getenv("AllowedOriginsWebsocket")?.takeIf { it.isNotBlank() } ?: "" } \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventbasedTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventbasedTopic.kt deleted file mode 100644 index 025ca1f5..00000000 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/EventbasedTopic.kt +++ /dev/null @@ -1,60 +0,0 @@ -package no.iktdev.mediaprocessing.ui.socket - -import mu.KotlinLogging -import no.iktdev.exfl.observable.ObservableList -import no.iktdev.exfl.observable.ObservableMap -import no.iktdev.exfl.observable.observableListOf -import no.iktdev.exfl.observable.observableMapOf -import no.iktdev.mediaprocessing.ui.dto.EventDataObject -import no.iktdev.mediaprocessing.ui.dto.EventSummary -import no.iktdev.mediaprocessing.ui.dto.SimpleEventDataObject -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.messaging.handler.annotation.MessageMapping -import org.springframework.messaging.simp.SimpMessagingTemplate -import org.springframework.stereotype.Controller - -@Controller -class EventbasedTopic( - @Autowired private val template: SimpMessagingTemplate? -) { - private val log = KotlinLogging.logger {} - val summaryList: ObservableList = observableListOf() - val memSimpleConvertedEventsMap: ObservableMap = observableMapOf() - val memActiveEventMap: ObservableMap = observableMapOf() - - init { - memActiveEventMap.addListener(object : ObservableMap.Listener { - override fun onMapUpdated(map: Map) { - super.onMapUpdated(map) - log.info { "Sending data to WS" } - template?.convertAndSend("/topic/event/items", map.values.reversed()) - if (template == null) { - log.error { "Template is null!" } - } - } - }) - memSimpleConvertedEventsMap.addListener(object : ObservableMap.Listener { - override fun onMapUpdated(map: Map) { - super.onMapUpdated(map) - log.info { "Sending data to WS" } - template?.convertAndSend("/topic/event/flat", map.values.reversed()) - if (template == null) { - log.error { "Template is null!" } - } - } - }) - summaryList.addListener(object: ObservableList.Listener { - override fun onListChanged(items: List) { - super.onListChanged(items) - template?.convertAndSend("/topic/summary", items) - } - }) - } - - @MessageMapping("/items") - fun sendItems() { - template?.convertAndSend("/topic/event/items", memActiveEventMap.values.reversed()) - template?.convertAndSend("/topic/event/flat", memSimpleConvertedEventsMap.values.reversed()) - } - -} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt index bb83ae2f..e978a33f 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ExplorerTopic.kt @@ -35,29 +35,6 @@ class ExplorerTopic( } } - @MessageMapping("/request/encode") - fun requestEncode(@Payload data: EventRequest) { - val req = coordinatorTemplate.postForEntity("/request/encode", data, String::class.java) - log.info { req } - } - @MessageMapping("/request/extract") - fun requestExtract(@Payload data: EventRequest) { - val req = coordinatorTemplate.postForEntity("/request/extract", data, String::class.java) - log.info { req } - } - - @MessageMapping("/request/convert") - fun requestConvert(@Payload data: EventRequest) { - val req = coordinatorTemplate.postForEntity("/request/convert", data, String::class.java) - log.info { req } - } - - @MessageMapping("/request/all") - fun requestAllAvailableActions(@Payload data: EventRequest) { - log.info { "Sending data to coordinator: ${Gson().toJson(data)}" } - val req = coordinatorTemplate.postForEntity("/request/all", data, String::class.java) - log.info { req } - } } \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/FileRequestTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/FileRequestTopic.kt new file mode 100644 index 00000000..bae80e7a --- /dev/null +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/FileRequestTopic.kt @@ -0,0 +1,45 @@ +package no.iktdev.mediaprocessing.ui.socket + +import com.google.gson.Gson +import mu.KotlinLogging +import no.iktdev.mediaprocessing.shared.common.contract.dto.EventRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.handler.annotation.Payload +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Controller +import org.springframework.web.client.RestTemplate + +@Controller +class FileRequestTopic( + @Autowired private val template: SimpMessagingTemplate?, + @Autowired private val coordinatorTemplate: RestTemplate, +) { + val log = KotlinLogging.logger {} + + @MessageMapping("/request/encode") + fun requestEncode(@Payload data: EventRequest) { + val req = coordinatorTemplate.postForEntity("/request/encode", data, String::class.java) + log.info { req } + } + + @MessageMapping("/request/extract") + fun requestExtract(@Payload data: EventRequest) { + val req = coordinatorTemplate.postForEntity("/request/extract", data, String::class.java) + log.info { req } + } + + @MessageMapping("/request/convert") + fun requestConvert(@Payload data: EventRequest) { + val req = coordinatorTemplate.postForEntity("/request/convert", data, String::class.java) + log.info { req } + } + + @MessageMapping("/request/all") + fun requestAllAvailableActions(@Payload data: EventRequest) { + log.info { "Sending data to coordinator: ${Gson().toJson(data)}" } + val req = coordinatorTemplate.postForEntity("/request/all", data, String::class.java) + log.info { req } + } + +} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/PersistentEventsTableTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/PersistentEventsTableTopic.kt deleted file mode 100644 index 8a267fff..00000000 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/PersistentEventsTableTopic.kt +++ /dev/null @@ -1,19 +0,0 @@ -package no.iktdev.mediaprocessing.ui.socket - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.messaging.handler.annotation.MessageMapping -import org.springframework.messaging.simp.SimpMessagingTemplate -import org.springframework.stereotype.Controller - -@Controller -class PersistentEventsTableTopic( - @Autowired private val template: SimpMessagingTemplate?, - //@Autowired private val persistentEventsTableService: PersistentEventsTableService -) { - - @MessageMapping("/persistent/events") - fun readbackEvents() { - //template?.convertAndSend("/topic/persistent/events", persistentEventsTableService.cachedEvents) - } - -} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt new file mode 100644 index 00000000..aef1d94a --- /dev/null +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/ProcesserTasksTopic.kt @@ -0,0 +1,56 @@ +package no.iktdev.mediaprocessing.ui.socket + +import no.iktdev.eventi.database.withTransaction +import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo +import no.iktdev.mediaprocessing.shared.common.database.cal.toTask +import no.iktdev.mediaprocessing.shared.common.database.tables.tasks +import no.iktdev.mediaprocessing.shared.common.task.Task +import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService +import no.iktdev.mediaprocessing.ui.eventDatabase +import no.iktdev.mediaprocessing.ui.socket.a2a.ProcesserListenerService +import org.jetbrains.exposed.sql.selectAll +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.handler.annotation.MessageMapping +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Service + +@Service +class ProcesserTasksTopic( + @Autowired a2AProcesserService: ProcesserListenerService, + @Autowired private val webSocketMonitoringService: WebSocketMonitoringService, + @Autowired override var template: SimpMessagingTemplate?, +): SocketListener(template) { + + final val a2a = object : ProcesserListenerService.A2AProcesserListener { + override fun onExtractProgress(info: ProcesserEventInfo) { + } + + override fun onEncodeProgress(info: ProcesserEventInfo) { + } + + override fun onEncodeAssigned() { + } + + override fun onExtractAssigned() { + } + } + + init { + a2AProcesserService.attachListener(a2a) + } + + data class TaskGroup( + val referenceId: String, + val tasks: List + ) + + @MessageMapping("/tasks/all") + fun pullAllTasks() { + val result = withTransaction(eventDatabase.database) { + tasks.selectAll().toTask() + .groupBy { it.referenceId }.map { g -> TaskGroup(g.key, g.value) } + } ?: emptyList() + template?.convertAndSend("/topic/tasks/all", result) + } + +} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/SocketListener.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/SocketListener.kt new file mode 100644 index 00000000..532421f9 --- /dev/null +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/SocketListener.kt @@ -0,0 +1,11 @@ +package no.iktdev.mediaprocessing.ui.socket + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.stereotype.Controller + +@Controller +class SocketListener( + @Autowired protected val template: SimpMessagingTemplate?, +) { +} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/UnprocessedFilesTopic.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/UnprocessedFilesTopic.kt index eb586794..c4cff844 100644 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/UnprocessedFilesTopic.kt +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/UnprocessedFilesTopic.kt @@ -1,58 +1,104 @@ package no.iktdev.mediaprocessing.ui.socket import no.iktdev.eventi.database.withTransaction -import no.iktdev.mediaprocessing.shared.common.contract.dto.EventRequest +import no.iktdev.exfl.observable.ObservableList +import no.iktdev.mediaprocessing.shared.common.contract.data.MediaProcessStartEvent +import no.iktdev.mediaprocessing.shared.common.contract.data.az +import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent +import no.iktdev.mediaprocessing.shared.common.database.tables.events import no.iktdev.mediaprocessing.shared.common.database.tables.files import no.iktdev.mediaprocessing.shared.common.database.tables.filesProcessed -import no.iktdev.mediaprocessing.ui.UIEnv +import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService import no.iktdev.mediaprocessing.ui.eventDatabase +import no.iktdev.mediaprocessing.ui.socket.UnprocessedFilesTopic.DatabaseData.filesInProcess +import no.iktdev.mediaprocessing.ui.socket.UnprocessedFilesTopic.DatabaseData.pullUnprocessedFiles +import no.iktdev.mediaprocessing.ui.socket.UnprocessedFilesTopic.DatabaseData.unprocessedFiles +import no.iktdev.mediaprocessing.ui.socket.UnprocessedFilesTopic.DatabaseData.update import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.springframework.beans.factory.annotation.Autowired import org.springframework.messaging.handler.annotation.MessageMapping -import org.springframework.messaging.handler.annotation.Payload import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Controller import org.springframework.web.client.RestTemplate -import java.io.File +import org.springframework.web.socket.WebSocketSession +import java.util.concurrent.ConcurrentHashMap @Controller @EnableScheduling class UnprocessedFilesTopic( @Autowired private val template: SimpMessagingTemplate?, @Autowired private val coordinatorTemplate: RestTemplate, + @Autowired private val webSocketMonitoringService: WebSocketMonitoringService ) { - fun pullUnprocessedFiles(): List = withTransaction(eventDatabase.database) { - files.select { - files.checksum notInSubQuery filesProcessed.slice(filesProcessed.checksum).selectAll() - }.mapNotNull { - FileInfo( - it[files.baseName], - it[files.fileName], - it[files.checksum] - ) - }.filter { File(it.fileName).exists() } - } ?: emptyList() - @MessageMapping("/files") - fun getUnprocessedFiles() { - refreshUnprocessedFiles() - } - @MessageMapping("/request/process") - fun requestProcess(@Payload data: EventRequest) { - val req = coordinatorTemplate.postForEntity("/request/all", data, String::class.java) - log.info { "RequestProcess report:\n\tStatus: ${req.statusCode}\n\tMessage: ${req.body}" } + object DatabaseData { + private const val PULL_MIN_INTERVAL = 10_000 // 10 sekunder + private var lastPoll: Long = 0 + var unprocessedFiles: List = emptyList() + private set + var filesInProcess: List = emptyList() + private set + + // Funksjon som oppdaterer dataen hvis det har gått mer enn PULL_MIN_INTERVAL siden forrige oppdatering + fun update() { + val currentTime = System.currentTimeMillis() + + // Sjekk om det har gått mer enn 10 sekunder (10000 ms) siden siste oppdatering + if (currentTime - lastPoll >= PULL_MIN_INTERVAL) { + // Oppdater tidspunktet for siste poll + lastPoll = currentTime + + // Oppdater dataene ved å hente nye verdier + val filesNotCompleted = pullUnprocessedFiles() + filesInProcess = pullUncompletedFiles() + unprocessedFiles = filesNotCompleted.filter { u -> !filesInProcess.any { p -> p.checksum == u.checksum } } + + + } + } + + private fun pullUnprocessedFiles(): List = withTransaction(eventDatabase.database) { + val found = files.select { + files.checksum notInSubQuery filesProcessed.slice(filesProcessed.checksum).selectAll() + }.mapNotNull { + FileInfo( + it[files.baseName], + it[files.fileName], + it[files.checksum] + ) + } + unprocessedFiles = found + found//.filter { File(it.fileName).exists() } + } ?: emptyList() + + private fun pullUncompletedFiles(): List = withTransaction(eventDatabase.database) { + val eventStartedFiles = events.select { + events.event eq "ProcessStarted" + }.mapNotNull { it[events.data].jsonToEvent(it[events.event]) } + .mapNotNull { it.az() } + .mapNotNull { it.data?.file } + unprocessedFiles.filter { it.fileName in eventStartedFiles }.also { + filesInProcess = it + } + } ?: emptyList() } + data class UnprocessedFiles( + val available: List, + val inProcess: List + ) - - @Scheduled(fixedDelay = 5_000) - fun refreshUnprocessedFiles() { - val unprocessedFiles = pullUnprocessedFiles() - template?.convertAndSend("/topic/files/unprocessed", unprocessedFiles) + @MessageMapping("/files/unprocessed") + fun postUnProcessedFiles() { + update() + template?.convertAndSend("/topic/files/unprocessed", UnprocessedFiles( + available = unprocessedFiles, + inProcess = filesInProcess + )) } } diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt new file mode 100644 index 00000000..a0fb3596 --- /dev/null +++ b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/a2a/ProcesserListenerService.kt @@ -0,0 +1,97 @@ +package no.iktdev.mediaprocessing.ui.socket.a2a + +import com.google.gson.Gson +import mu.KotlinLogging +import no.iktdev.mediaprocessing.shared.common.contract.dto.ProcesserEventInfo +import no.iktdev.mediaprocessing.ui.UIEnv +import no.iktdev.mediaprocessing.ui.WebSocketMonitoringService +import no.iktdev.mediaprocessing.ui.log +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.messaging.simp.SimpMessagingTemplate +import org.springframework.messaging.simp.stomp.* +import org.springframework.stereotype.Service +import org.springframework.web.socket.client.standard.StandardWebSocketClient +import org.springframework.web.socket.messaging.WebSocketStompClient +import java.lang.reflect.Type + +@Service +class ProcesserListenerService( + @Autowired private val webSocketMonitoringService: WebSocketMonitoringService, + @Autowired private val message: SimpMessagingTemplate?, +) { + private val logger = KotlinLogging.logger {} + private val listeners: MutableList = mutableListOf() + + fun attachListener(listener: A2AProcesserListener) { + listeners.add(listener) + } + + val gson = Gson() + + val client = WebSocketStompClient(StandardWebSocketClient()) + + init { + connectAndListen() + } + + private final fun connectAndListen() { + log.info { "EncoderWsUrl: ${UIEnv.socketEncoder}" } + client.connect(UIEnv.socketEncoder, object : StompSessionHandlerAdapter() { + override fun afterConnected(session: StompSession, connectedHeaders: StompHeaders) { + super.afterConnected(session, connectedHeaders) + logger.info { "Tilkoblet processer" } + subscribeToTopics(session) + } + + override fun handleException( + session: StompSession, + command: StompCommand?, + headers: StompHeaders, + payload: ByteArray, + exception: Throwable + ) { + super.handleException(session, command, headers, payload, exception) + logger.error { "Feil ved tilkobling: ${exception.message}" } + } + }) + } + + private fun subscribeToTopics(session: StompSession) { + session.subscribe("/topic/encode/progress", encodeProcessFrameHandler) + session.subscribe("/topic/extract/progress", extractProcessFrameHandler) + } + + private val encodeProcessFrameHandler = object : StompFrameHandler { + override fun getPayloadType(headers: StompHeaders): Type { + return ProcesserEventInfo::class.java + } + + override fun handleFrame(headers: StompHeaders, payload: Any?) { + val response = gson.fromJson(payload.toString(), ProcesserEventInfo::class.java) + if (webSocketMonitoringService.anyListening()) { + message?.convertAndSend("/topic/processer/encode/progress", response) + } + } + } + + private val extractProcessFrameHandler = object : StompFrameHandler { + override fun getPayloadType(headers: StompHeaders): Type { + return ProcesserEventInfo::class.java + } + + override fun handleFrame(headers: StompHeaders, payload: Any?) { + val response = gson.fromJson(payload.toString(), ProcesserEventInfo::class.java) + if (webSocketMonitoringService.anyListening()) { + message?.convertAndSend("/topic/processer/extract/progress", response) + } + } + } + + interface A2AProcesserListener { + fun onExtractProgress(info: ProcesserEventInfo) + fun onEncodeProgress(info: ProcesserEventInfo) + fun onEncodeAssigned() + fun onExtractAssigned() + } + +} \ No newline at end of file diff --git a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/internal/EncoderReaderService.kt b/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/internal/EncoderReaderService.kt deleted file mode 100644 index 6463e0bb..00000000 --- a/apps/ui/src/main/kotlin/no/iktdev/mediaprocessing/ui/socket/internal/EncoderReaderService.kt +++ /dev/null @@ -1,77 +0,0 @@ -package no.iktdev.mediaprocessing.ui.socket.internal - -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import mu.KotlinLogging - -import org.springframework.messaging.simp.stomp.StompFrameHandler -import org.springframework.messaging.simp.stomp.StompHeaders -import org.springframework.messaging.simp.stomp.StompSession -import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter -import org.springframework.stereotype.Service -import org.springframework.web.socket.client.standard.StandardWebSocketClient -import org.springframework.web.socket.messaging.WebSocketStompClient -import java.lang.reflect.Type - -@Service -class EncoderReaderService { - private val logger = KotlinLogging.logger {} - - - /*fun startSubscription(session: StompSession) { - session.subscribe("/topic/encoder/workorder", object : StompFrameHandler { - override fun getPayloadType(headers: StompHeaders): Type { - return object : TypeToken() {}.type - //return object : TypeToken?>() {}.type - } - - override fun handleFrame(headers: StompHeaders, payload: Any?) { - if (payload is String) { - Gson().fromJson(payload, WorkOrderItem::class.java)?.let { - val item: EventDataObject = memActiveEventMap[it.id] ?: return - item.encode?.progress = it.progress - item.encode?.timeLeft = it.remainingTime - memActiveEventMap[it.id] = item; - memSimpleConvertedEventsMap[it.id] = item.toSimple() - } - - } - - } - }) - session.subscribe("/topic/extractor/workorder", object : StompFrameHandler { - override fun getPayloadType(headers: StompHeaders): Type { - return object : TypeToken() {}.type - } - - override fun handleFrame(headers: StompHeaders?, payload: Any?) { - if (payload is String) { - val item = Gson().fromJson(payload, WorkOrderItem::class.java) - - - } - } - }) - - } - - - val client = WebSocketStompClient(StandardWebSocketClient()) - val sessionHandler = object : StompSessionHandlerAdapter() { - override fun afterConnected(session: StompSession, connectedHeaders: StompHeaders) { - super.afterConnected(session, connectedHeaders) - logger.info { "Connected to Encode Socket" } - startSubscription(session) - } - - override fun handleFrame(headers: StompHeaders, payload: Any?) { - super.handleFrame(headers, payload) - - } - } - - init { - client.connect(UIEnv.socketEncoder, sessionHandler) - }*/ - -} \ No newline at end of file diff --git a/apps/ui/src/main/resources/application.properties b/apps/ui/src/main/resources/application.properties index 6dd22f9b..054b3815 100644 --- a/apps/ui/src/main/resources/application.properties +++ b/apps/ui/src/main/resources/application.properties @@ -1,3 +1,4 @@ spring.output.ansi.enabled=always logging.level.org.apache.kafka=INFO logging.level.root=INFO +logging.level.org.springframework.web.socket.config.WebSocketMessageBrokerStats = INFO \ No newline at end of file diff --git a/apps/ui/web/src/App.tsx b/apps/ui/web/src/App.tsx index 481b2371..6caf972f 100644 --- a/apps/ui/web/src/App.tsx +++ b/apps/ui/web/src/App.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import logo from './logo.svg'; import './App.css'; -import { Box, CssBaseline } from '@mui/material'; +import { Box, CssBaseline, IconButton, SxProps, Theme } from '@mui/material'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Footer from './app/features/footer'; import LaunchPage from './app/page/LaunchPage'; @@ -15,6 +15,13 @@ import theme from './theme'; import { simpleEventsUpdate } from './app/store/kafka-items-flat-slice'; import { EventDataObject, SimpleEventDataObject } from './types'; import EventsChainPage from './app/page/EventsChainPage'; +import UnprocessedFilesPage from './app/page/UnprocessedFilesPage'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; +import FolderIcon from '@mui/icons-material/Folder'; +import QueueIcon from '@mui/icons-material/Queue'; +import AppsIcon from '@mui/icons-material/Apps'; +import ConstructionIcon from '@mui/icons-material/Construction'; +import ProcesserTasksPage from './app/page/ProcesserTasksPage'; function App() { const client = useStompClient(); @@ -44,14 +51,46 @@ function App() { }, [client, dispatch]); + const iconHeight: SxProps = { + height: 50, + width: 50 + } return ( + window.location.href = "/"} sx={{ + ...iconHeight + }}> + + + window.location.href = "/events"} sx={{ + ...iconHeight + }}> + + + window.location.href = "/files"} sx={{ + ...iconHeight + }}> + + + window.location.href = "/unprocessed"} sx={{ + ...iconHeight + }}> + + + window.location.href = "/tasks"} sx={{ + ...iconHeight + }}> + + + } /> + } /> } /> } /> } /> diff --git a/apps/ui/web/src/app/features/table/multiListSortedTable.tsx b/apps/ui/web/src/app/features/table/multiListSortedTable.tsx new file mode 100644 index 00000000..eab52f2c --- /dev/null +++ b/apps/ui/web/src/app/features/table/multiListSortedTable.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; +import IconArrowUp from '@mui/icons-material/ArrowUpward'; +import IconArrowDown from '@mui/icons-material/ArrowDownward'; +import { Table, TableHead, TableRow, TableCell, TableBody, Typography, Box, useTheme, TableContainer } from "@mui/material"; +import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table"; +import { title } from "process"; + +export interface TableItemGroup { + title: string + items: Array +} + +export default function MultiListSortedTable({ items, columns, customizer, onRowClickedEvent }: { items: Array>, columns: Array, customizer?: TableCellCustomizer, onRowClickedEvent?: TableRowActionEvents }) { + const muiTheme = useTheme(); + + const [order, setOrder] = useState<'asc' | 'desc'>('asc'); + const [orderBy, setOrderBy] = useState(''); + const [selectedRow, setSelectedRow] = useState(null); + + const tableRowSingleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.click(row); + } + } + const tableRowDoubleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.doubleClick(row); + } + } + + const tableRowContextMenu = (e: React.MouseEvent , row: T | null) => { + if (row && onRowClickedEvent && onRowClickedEvent.contextMenu) { + e.preventDefault() + onRowClickedEvent.contextMenu(row, e.pageX, e.pageY) + } + } + + const handleSort = (property: string) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const compareValues = (a: any, b: any, orderBy: string) => { + if (typeof a[orderBy] === 'string') { + return a[orderBy].localeCompare(b[orderBy]); + } else if (typeof a[orderBy] === 'number') { + return a[orderBy] - b[orderBy]; + } + return 0; + }; + + const sortedData: Array> = items.map((item: TableItemGroup) => { + return { + title: item.title, + items: item.items.slice().sort((a, b) => { + if (order === 'asc') { + return compareValues(a, b, orderBy); + } else { + return compareValues(b, a, orderBy); + } + }) + } + }); + + + + useEffect(() => { + handleSort(columns[0].accessor) + }, []) + + + return ( + + + + + + {columns.map((column) => ( + handleSort(column.accessor)} sx={{ cursor: "pointer" }}> + + {orderBy === column.accessor ? + (order === "asc" ? () : ()) : ( + + ) + } + {column.label} + + + ))} + + + + {sortedData.map((item, index) => ( + <> + + + + {item.title} + + + + + {item.items?.map((row: T, rowIndex: number) => ( + tableRowSingleClicked(row)} + onDoubleClick={() => tableRowDoubleClicked(row)} + onContextMenu={(e) => { + tableRowContextMenu(e, row); + tableRowSingleClicked(row); + }} + style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }} + > + {columns.map((column) => ( + + {customizer && customizer(column.accessor, row) !== null + ? customizer(column.accessor, row) + : {(row as any)[column.accessor]}} + + ))} + + ))} + + + ))} + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/features/table/sortableGroupedTable.tsx b/apps/ui/web/src/app/features/table/sortableGroupedTable.tsx new file mode 100644 index 00000000..297207bb --- /dev/null +++ b/apps/ui/web/src/app/features/table/sortableGroupedTable.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; +import IconArrowUp from '@mui/icons-material/ArrowUpward'; +import IconArrowDown from '@mui/icons-material/ArrowDownward'; +import { Table, TableHead, TableRow, TableCell, TableBody, Typography, Box, useTheme, TableContainer, IconButton } from "@mui/material"; +import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; + +export interface TableItemGroup { + title: string + items: Array +} + +export default function SortableGroupedTable({ items, columns, customizer, onRowClickedEvent }: { items: Array>, columns: Array, customizer?: TableCellCustomizer, onRowClickedEvent?: TableRowActionEvents }) { + const muiTheme = useTheme(); + + const [order, setOrder] = useState<'asc' | 'desc'>('asc'); + const [orderBy, setOrderBy] = useState(''); + const [selectedRow, setSelectedRow] = useState(null); + const [expandedRows, setExpandedRows] = useState>(new Set()); + const toggleExpand = (index: number) => { + setExpandedRows((prev) => { + const newSet = new Set(prev); + if (newSet.has(index)) { + newSet.delete(index); // Collapse if already expanded + } else { + newSet.add(index); // Expand if not already expanded + } + return newSet; + }); + }; + + const tableRowSingleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.click(row); + } + } + const tableRowDoubleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.doubleClick(row); + } + } + + const tableRowContextMenu = (e: React.MouseEvent , row: T | null) => { + if (row && onRowClickedEvent && onRowClickedEvent.contextMenu) { + e.preventDefault() + onRowClickedEvent.contextMenu(row, e.pageX, e.pageY) + } + } + + const handleSort = (property: string) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const compareValues = (a: any, b: any, orderBy: string) => { + if (typeof a[orderBy] === 'string') { + return a[orderBy].localeCompare(b[orderBy]); + } else if (typeof a[orderBy] === 'number') { + return a[orderBy] - b[orderBy]; + } + return 0; + }; + + const sortedData: Array> = items.map((item: TableItemGroup) => { + return { + title: item.title, + items: item.items.slice().sort((a, b) => { + if (order === 'asc') { + return compareValues(a, b, orderBy); + } else { + return compareValues(b, a, orderBy); + } + }) + } + }); + + + + useEffect(() => { + handleSort(columns[0].accessor) + }, []) + + + return ( + + + + + + {columns.map((column) => ( + handleSort(column.accessor)} sx={{ cursor: "pointer" }}> + + {orderBy === column.accessor ? + (order === "asc" ? () : ()) : ( + + ) + } + {column.label} + + + ))} + + + + {sortedData.map((item, index) => ( + <> + + + + toggleExpand(index)} sx={{ display: "flex", justifyContent: "space-between" }}> + {item.title} + toggleExpand(index)}> + {expandedRows.has(index) ? : } + + + + + + + {item.items?.map((row: T, rowIndex: number) => ( + tableRowSingleClicked(row)} + onDoubleClick={() => tableRowDoubleClicked(row)} + onContextMenu={(e) => { + tableRowContextMenu(e, row); + tableRowSingleClicked(row); + }} + style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }} + > + {columns.map((column) => ( + + {customizer && customizer(column.accessor, row) !== null + ? customizer(column.accessor, row) + : {(row as any)[column.accessor]}} + + ))} + + ))} + + + ))} + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/page/ProcesserTasksPage.tsx b/apps/ui/web/src/app/page/ProcesserTasksPage.tsx new file mode 100644 index 00000000..93b63404 --- /dev/null +++ b/apps/ui/web/src/app/page/ProcesserTasksPage.tsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useStompClient } from "react-stomp-hooks"; +import { Box, Button, Grid, TextField, Typography, useTheme } from '@mui/material'; +import { ExplorerItem } from "../../types"; +import { ContextMenuItem } from "../features/ContextMenu"; +import { RootState } from "../store"; +import { useWsSubscription } from "../ws/subscriptions"; +import SimpleTable from "../features/table/sortableTable"; +import { TableCellCustomizer, TablePropetyConfig } from "../features/table/table"; +import MultiListSortedTable from "../features/table/multiListSortedTable"; +import { TableTaskGroup, Task, TaskGroup, update } from "../store/tasks-slice"; +import SortableGroupedTable from "../features/table/sortableGroupedTable"; +import { UnixTimestamp } from "../features/UxTc"; + +const columns: Array = [ + { label: "Name", accessor: "data.inputFile" }, + { label: "Task", accessor: "task" }, + { label: "Status", accessor: "status" }, + { label: "Created", accessor: "created" }, +]; + +const createTableCell: TableCellCustomizer = (accessor, data) => { + switch (accessor) { + case "created": { + if (typeof data[accessor] === "string") { + return UnixTimestamp({ timestamp: Date.parse(data[accessor]) }); + } + return null; + } + case "data.inputFile": { + const parts = data.data?.inputFile.split("/") ?? []; + return {parts[parts?.length - 1]} + } + default: + return null; + } + }; + + +export default function ProcesserTasksPage() { + const muiTheme = useTheme(); + const dispatch = useDispatch(); + const client = useStompClient(); + const taskGroups = useSelector((state: RootState) => state.tasks); + + useWsSubscription>("/topic/tasks/all", (response) => { + console.log(response) + dispatch(update(response)) + }); + + useEffect(() => { + client?.publish({ + destination: "/app/tasks/all" + }); + }, [client, dispatch]); + + return ( + <> + + + + Tasks + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/page/UnprocessedFilesPage.tsx b/apps/ui/web/src/app/page/UnprocessedFilesPage.tsx new file mode 100644 index 00000000..76453349 --- /dev/null +++ b/apps/ui/web/src/app/page/UnprocessedFilesPage.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useStompClient } from "react-stomp-hooks"; +import { Box, Button, Grid, TextField, Typography, useTheme } from '@mui/material'; +import { ExplorerItem } from "../../types"; +import { ContextMenuItem } from "../features/ContextMenu"; +import { RootState } from "../store"; +import { useWsSubscription } from "../ws/subscriptions"; +import { FileInfo, FileInfoGroup, IncomingUnprocessedFiles, update, } from "../store/unprocessed-files-slice"; +import SimpleTable from "../features/table/sortableTable"; +import { TablePropetyConfig } from "../features/table/table"; +import MultiListSortedTable from "../features/table/multiListSortedTable"; + + +const columns: Array = [ + { label: "Name", accessor: "name" }, + { label: "Checksum", accessor: "checksum" }, +]; + +export default function UnprocessedFilesPage() { + const muiTheme = useTheme(); + const dispatch = useDispatch(); + const client = useStompClient(); + const files = useSelector((state: RootState) => state.unprocessedFiles); + const [tableItems, setTableItems] = useState>([]); + + const [selectedRow, setSelectedRow] = useState(null); + const [actionableItems, setActionableItems] = useState>([]); + + useWsSubscription("/topic/files/unprocessed", (response) => { + dispatch(update(response)) + }); + + + const pullData = () => { + client?.publish({ + destination: "/app/files/unprocessed" + }); + } + + useEffect(() => { + client?.publish({ + destination: "/app/files/unprocessed" + }); + + const intervalId = setInterval(pullData, 20000); + return () => { + clearInterval(intervalId); // Fjern intervallet når komponenten fjernes fra DOM + }; + }, [client, dispatch]); + + useEffect(() => { + const entries = [ + { + title: "In Process", + items: files.inProcess + }, + { + title: "Available", + items: files.available + } + ]; + setTableItems(entries); + console.log(entries) + + }, [files]) + + return ( + <> + + + + Unprocessed Files + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/store.ts b/apps/ui/web/src/app/store.ts index 3d3137de..430db45f 100644 --- a/apps/ui/web/src/app/store.ts +++ b/apps/ui/web/src/app/store.ts @@ -5,6 +5,8 @@ import kafkaItemsFlatSlice from './store/kafka-items-flat-slice'; import contextMenuSlice from './store/context-menu-slice'; import persistentEventsSlice from './store/persistent-events-slice'; import chainedEventsSlice from './store/chained-events-slice'; +import unprocessedFilesSlice from './store/unprocessed-files-slice'; +import tasksSlice from './store/tasks-slice'; export const store = configureStore({ @@ -15,6 +17,8 @@ export const store = configureStore({ contextMenu: contextMenuSlice, persistentEvents: persistentEventsSlice, chained: chainedEventsSlice, + unprocessedFiles: unprocessedFilesSlice, + tasks: tasksSlice, }, }); diff --git a/apps/ui/web/src/app/store/tasks-slice.ts b/apps/ui/web/src/app/store/tasks-slice.ts new file mode 100644 index 00000000..2df36d11 --- /dev/null +++ b/apps/ui/web/src/app/store/tasks-slice.ts @@ -0,0 +1,95 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { TableItemGroup } from "../features/table/multiListSortedTable"; + +export enum TaskType { + Encode = 'Encode', + Extract = 'Extract', + Convert = 'Convert' +} + +export interface TaskData { + inputFile: string; +} + +export enum SubtitleFormats { + SRT = 'SRT', + VTT = 'VTT', + ASS = 'ASS', + SUB = 'SUB' +} + + +export interface EncodeArgumentData extends TaskData { + arguments: string[]; + outputFileName: string; +} + + export interface ExtractArgumentData extends TaskData { + arguments: string[]; + language: string; + storeFileName: string; + outputFileName: string; +} + + export interface ConvertData extends TaskData { + language: string; + outputDirectory: string; + outputFileName: string; + storeFileName: string; + formats: SubtitleFormats[]; + allowOverwrite: boolean; +} + + +export interface Task { + referenceId: string; + status?: string | null; + claimed: boolean; + claimedBy?: string | null; + consumed: boolean; + task: TaskType; + eventId: string; + derivedFromEventId?: string | null; + data?: EncodeArgumentData | ExtractArgumentData | ConvertData | null; + created: string; // Bruk ISO-dato som string + lastCheckIn?: string | null; + } + + +export interface TaskGroup{ + referenceId: string + tasks: Array +} + +export interface TaskGroupList { + items: Array +} + +export interface TableTaskGroup extends TableItemGroup { + title: string + items: Array +} + +export interface TableTaskGroupList { + items: Array +} + +const initialState: TableTaskGroupList = { + items: [] +} + +const tasksSlice = createSlice({ + name: "Tasks", + initialState, + reducers: { + update(state, action: PayloadAction>) { + state.items = action.payload.map((value) => ({ + title: value.referenceId, + items: value.tasks + })) ?? [] + }, + } +}); + +export const { update } = tasksSlice.actions; +export default tasksSlice.reducer; diff --git a/apps/ui/web/src/app/store/unprocessed-files-slice.ts b/apps/ui/web/src/app/store/unprocessed-files-slice.ts new file mode 100644 index 00000000..b69051b7 --- /dev/null +++ b/apps/ui/web/src/app/store/unprocessed-files-slice.ts @@ -0,0 +1,39 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" +import { ExplorerItem, ExplorerCursor } from "../../types" +import { TableItemGroup } from "../features/table/multiListSortedTable" +import exp from "constants" + +export interface FileInfo { + name: string + fileName: string + checksum: string +} + +export interface FileInfoGroup extends TableItemGroup { + title: string + items: Array +} + +export interface IncomingUnprocessedFiles { + available: Array + inProcess: Array +} + +const initialState: IncomingUnprocessedFiles = { + available: [], + inProcess: [] +} + +const unprocessedFilesSlice = createSlice({ + name: "UnprocessedFiles", + initialState, + reducers: { + update(state, action: PayloadAction) { + state.available = action.payload.available ?? [] + state.inProcess = action.payload.inProcess ?? [] + }, + } +}) + +export const { update } = unprocessedFilesSlice.actions; +export default unprocessedFilesSlice.reducer; \ No newline at end of file diff --git a/apps/ui/web/src/index.tsx b/apps/ui/web/src/index.tsx index 839f11f2..a09c4bd9 100644 --- a/apps/ui/web/src/index.tsx +++ b/apps/ui/web/src/index.tsx @@ -13,10 +13,10 @@ const root = ReactDOM.createRoot( const wsUrl = () => { - const protocol = window.location.protocol; + const protocol = "ws" // window.location.protocol; const host = window.location.host; if (window.location.href.startsWith("http://localhost:3000")) { - return "http://localhost:8080/ws"; + return "ws://localhost:8080/ws"; } else { return `${protocol}//${host}/ws`; } @@ -31,7 +31,7 @@ root.render( console.log(str); }} onUnhandledMessage={(val) => { - console.log(val) + console.log("Unhandled message", val) }} onStompError={(val) => { console.log(val)