Updated UI + Indexing
This commit is contained in:
parent
b36f0c5e1d
commit
39d2bbe0b3
@ -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 {
|
||||
|
||||
@ -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<String> = UIEnv.wsAllowedOrigins.split(",")
|
||||
}
|
||||
|
||||
@Service
|
||||
class WebSocketMonitoringService() {
|
||||
private val clients = ConcurrentHashMap.newKeySet<WebSocketSession>()
|
||||
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
|
||||
|
||||
@ -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() } ?: ""
|
||||
}
|
||||
@ -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<EventSummary> = observableListOf()
|
||||
val memSimpleConvertedEventsMap: ObservableMap<String, SimpleEventDataObject> = observableMapOf()
|
||||
val memActiveEventMap: ObservableMap<String, EventDataObject> = observableMapOf()
|
||||
|
||||
init {
|
||||
memActiveEventMap.addListener(object : ObservableMap.Listener<String, EventDataObject> {
|
||||
override fun onMapUpdated(map: Map<String, EventDataObject>) {
|
||||
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<String, SimpleEventDataObject> {
|
||||
override fun onMapUpdated(map: Map<String, SimpleEventDataObject>) {
|
||||
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<EventSummary> {
|
||||
override fun onListChanged(items: List<EventSummary>) {
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<Task>
|
||||
)
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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?,
|
||||
) {
|
||||
}
|
||||
@ -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<FileInfo> = 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<FileInfo> = emptyList()
|
||||
private set
|
||||
var filesInProcess: List<FileInfo> = 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<FileInfo> = 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<FileInfo> = withTransaction(eventDatabase.database) {
|
||||
val eventStartedFiles = events.select {
|
||||
events.event eq "ProcessStarted"
|
||||
}.mapNotNull { it[events.data].jsonToEvent(it[events.event]) }
|
||||
.mapNotNull { it.az<MediaProcessStartEvent>() }
|
||||
.mapNotNull { it.data?.file }
|
||||
unprocessedFiles.filter { it.fileName in eventStartedFiles }.also {
|
||||
filesInProcess = it
|
||||
}
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
data class UnprocessedFiles(
|
||||
val available: List<FileInfo>,
|
||||
val inProcess: List<FileInfo>
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<A2AProcesserListener> = 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()
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<WorkOrderItem>() {}.type
|
||||
//return object : TypeToken<List<WorkOrderItem?>?>() {}.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<WorkOrderItem>() {}.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)
|
||||
}*/
|
||||
|
||||
}
|
||||
@ -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
|
||||
@ -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<Theme> = {
|
||||
height: 50,
|
||||
width: 50
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{
|
||||
height: 70,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingLeft: 1,
|
||||
backgroundColor: theme.palette.action.selected
|
||||
}}>
|
||||
<IconButton onClick={() => window.location.href = "/"} sx={{
|
||||
...iconHeight
|
||||
}}>
|
||||
<AppsIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => window.location.href = "/events"} sx={{
|
||||
...iconHeight
|
||||
}}>
|
||||
<AccountTreeIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => window.location.href = "/files"} sx={{
|
||||
...iconHeight
|
||||
}}>
|
||||
<FolderIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => window.location.href = "/unprocessed"} sx={{
|
||||
...iconHeight
|
||||
}}>
|
||||
<QueueIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => window.location.href = "/tasks"} sx={{
|
||||
...iconHeight
|
||||
}}>
|
||||
<ConstructionIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: "block",
|
||||
@ -62,6 +101,8 @@ function App() {
|
||||
}}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path='/tasks' element={<ProcesserTasksPage />} />
|
||||
<Route path='/unprocessed' element={<UnprocessedFilesPage />} />
|
||||
<Route path='/files' element={<ExplorePage />} />
|
||||
<Route path='/events' element={<EventsChainPage />} />
|
||||
<Route path='/' element={<LaunchPage />} />
|
||||
|
||||
156
apps/ui/web/src/app/features/table/multiListSortedTable.tsx
Normal file
156
apps/ui/web/src/app/features/table/multiListSortedTable.tsx
Normal file
@ -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<T> {
|
||||
title: string
|
||||
items: Array<T>
|
||||
}
|
||||
|
||||
export default function MultiListSortedTable<T>({ items, columns, customizer, onRowClickedEvent }: { items: Array<TableItemGroup<T>>, columns: Array<TablePropetyConfig>, customizer?: TableCellCustomizer<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
|
||||
const muiTheme = useTheme();
|
||||
|
||||
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
||||
const [orderBy, setOrderBy] = useState<string>('');
|
||||
const [selectedRow, setSelectedRow] = useState<T | null>(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<HTMLTableRowElement, 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<TableItemGroup<T>> = items.map((item: TableItemGroup<T>) => {
|
||||
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 (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column", // Bruk column-fleksretning
|
||||
height: "100%",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<TableContainer sx={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
position: "relative", // Legg til denne linjen for å justere layout
|
||||
maxHeight: "100%" // Legg til denne linjen for å begrense høyden
|
||||
}}>
|
||||
<Table>
|
||||
<TableHead sx={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
backgroundColor: muiTheme.palette.background.paper,
|
||||
}}>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
|
||||
<Box display="flex">
|
||||
{orderBy === column.accessor ?
|
||||
(order === "asc" ? (<IconArrowDown />) : (<IconArrowUp />)) : (
|
||||
<IconArrowDown sx={{ color: "transparent" }} />
|
||||
)
|
||||
}
|
||||
<Typography>{column.label}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
{sortedData.map((item, index) => (
|
||||
<>
|
||||
<TableHead
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: 55,
|
||||
backgroundColor: muiTheme.palette.primary.dark,
|
||||
}}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length}>
|
||||
<Typography variant='h6'>{item.title}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody sx={{
|
||||
overflowY: "scroll"
|
||||
}}>
|
||||
{item.items?.map((row: T, rowIndex: number) => (
|
||||
<TableRow key={rowIndex}
|
||||
onClick={() => 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) => (
|
||||
<TableCell key={column.accessor}>
|
||||
{customizer && customizer(column.accessor, row) !== null
|
||||
? customizer(column.accessor, row)
|
||||
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</>
|
||||
))}
|
||||
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
173
apps/ui/web/src/app/features/table/sortableGroupedTable.tsx
Normal file
173
apps/ui/web/src/app/features/table/sortableGroupedTable.tsx
Normal file
@ -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<T> {
|
||||
title: string
|
||||
items: Array<T>
|
||||
}
|
||||
|
||||
export default function SortableGroupedTable<T>({ items, columns, customizer, onRowClickedEvent }: { items: Array<TableItemGroup<T>>, columns: Array<TablePropetyConfig>, customizer?: TableCellCustomizer<T>, onRowClickedEvent?: TableRowActionEvents<T> }) {
|
||||
const muiTheme = useTheme();
|
||||
|
||||
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
|
||||
const [orderBy, setOrderBy] = useState<string>('');
|
||||
const [selectedRow, setSelectedRow] = useState<T | null>(null);
|
||||
const [expandedRows, setExpandedRows] = useState<Set<number>>(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<HTMLTableRowElement, 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<TableItemGroup<T>> = items.map((item: TableItemGroup<T>) => {
|
||||
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 (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column", // Bruk column-fleksretning
|
||||
height: "100%",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<TableContainer sx={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
position: "relative", // Legg til denne linjen for å justere layout
|
||||
maxHeight: "100%" // Legg til denne linjen for å begrense høyden
|
||||
}}>
|
||||
<Table>
|
||||
<TableHead sx={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
backgroundColor: muiTheme.palette.background.paper,
|
||||
}}>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.accessor} onClick={() => handleSort(column.accessor)} sx={{ cursor: "pointer" }}>
|
||||
<Box display="flex">
|
||||
{orderBy === column.accessor ?
|
||||
(order === "asc" ? (<IconArrowDown />) : (<IconArrowUp />)) : (
|
||||
<IconArrowDown sx={{ color: "transparent" }} />
|
||||
)
|
||||
}
|
||||
<Typography>{column.label}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
{sortedData.map((item, index) => (
|
||||
<>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: muiTheme.palette.primary.dark,
|
||||
}}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length}>
|
||||
<Box onClick={() => toggleExpand(index)} sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography variant='h6'>{item.title}</Typography>
|
||||
<IconButton onClick={() => toggleExpand(index)}>
|
||||
{expandedRows.has(index) ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody sx={{
|
||||
display: expandedRows.has(index) ? 'table-row-group' : 'none',
|
||||
overflowY: "scroll"
|
||||
}}>
|
||||
{item.items?.map((row: T, rowIndex: number) => (
|
||||
<TableRow key={rowIndex}
|
||||
onClick={() => 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) => (
|
||||
<TableCell key={column.accessor}>
|
||||
{customizer && customizer(column.accessor, row) !== null
|
||||
? customizer(column.accessor, row)
|
||||
: <Typography variant='body1'>{(row as any)[column.accessor]}</Typography>}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</>
|
||||
))}
|
||||
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
98
apps/ui/web/src/app/page/ProcesserTasksPage.tsx
Normal file
98
apps/ui/web/src/app/page/ProcesserTasksPage.tsx
Normal file
@ -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<TablePropetyConfig> = [
|
||||
{ label: "Name", accessor: "data.inputFile" },
|
||||
{ label: "Task", accessor: "task" },
|
||||
{ label: "Status", accessor: "status" },
|
||||
{ label: "Created", accessor: "created" },
|
||||
];
|
||||
|
||||
const createTableCell: TableCellCustomizer<Task> = (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 <Typography>{parts[parts?.length - 1]}</Typography>
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default function ProcesserTasksPage() {
|
||||
const muiTheme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const client = useStompClient();
|
||||
const taskGroups = useSelector((state: RootState) => state.tasks);
|
||||
|
||||
useWsSubscription<Array<TaskGroup>>("/topic/tasks/all", (response) => {
|
||||
console.log(response)
|
||||
dispatch(update(response))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
client?.publish({
|
||||
destination: "/app/tasks/all"
|
||||
});
|
||||
}, [client, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="block">
|
||||
<Grid container sx={{
|
||||
height: 50,
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: muiTheme.palette.background.paper
|
||||
}}>
|
||||
<Grid item xs={2}>
|
||||
<Typography variant="h6">Tasks</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
placeholder="Search"
|
||||
fullWidth={true}
|
||||
id="search-field"
|
||||
variant="filled"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: "block",
|
||||
height: "calc(100% - 120px)",
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
width: "100%"
|
||||
}}>
|
||||
<SortableGroupedTable items={taskGroups.items ?? []} columns={columns} customizer={createTableCell} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
108
apps/ui/web/src/app/page/UnprocessedFilesPage.tsx
Normal file
108
apps/ui/web/src/app/page/UnprocessedFilesPage.tsx
Normal file
@ -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<TablePropetyConfig> = [
|
||||
{ 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<Array<FileInfoGroup>>([]);
|
||||
|
||||
const [selectedRow, setSelectedRow] = useState<ExplorerItem|null>(null);
|
||||
const [actionableItems, setActionableItems] = useState<Array<ContextMenuItem>>([]);
|
||||
|
||||
useWsSubscription<IncomingUnprocessedFiles>("/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 (
|
||||
<>
|
||||
<Box display="block">
|
||||
<Grid container sx={{
|
||||
height: 50,
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: muiTheme.palette.background.paper
|
||||
}}>
|
||||
<Grid item xs={2}>
|
||||
<Typography variant="h6">Unprocessed Files</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
placeholder="Search"
|
||||
fullWidth={true}
|
||||
id="search-field"
|
||||
variant="filled"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: "block",
|
||||
height: "calc(100% - 120px)",
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
width: "100%"
|
||||
}}>
|
||||
<MultiListSortedTable items={tableItems ?? []} columns={columns} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
95
apps/ui/web/src/app/store/tasks-slice.ts
Normal file
95
apps/ui/web/src/app/store/tasks-slice.ts
Normal file
@ -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<Task>
|
||||
}
|
||||
|
||||
export interface TaskGroupList {
|
||||
items: Array<TaskGroup>
|
||||
}
|
||||
|
||||
export interface TableTaskGroup extends TableItemGroup<Task> {
|
||||
title: string
|
||||
items: Array<Task>
|
||||
}
|
||||
|
||||
export interface TableTaskGroupList {
|
||||
items: Array<TableTaskGroup>
|
||||
}
|
||||
|
||||
const initialState: TableTaskGroupList = {
|
||||
items: []
|
||||
}
|
||||
|
||||
const tasksSlice = createSlice({
|
||||
name: "Tasks",
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<Array<TaskGroup>>) {
|
||||
state.items = action.payload.map((value) => ({
|
||||
title: value.referenceId,
|
||||
items: value.tasks
|
||||
})) ?? []
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const { update } = tasksSlice.actions;
|
||||
export default tasksSlice.reducer;
|
||||
39
apps/ui/web/src/app/store/unprocessed-files-slice.ts
Normal file
39
apps/ui/web/src/app/store/unprocessed-files-slice.ts
Normal file
@ -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<FileInfo> {
|
||||
title: string
|
||||
items: Array<FileInfo>
|
||||
}
|
||||
|
||||
export interface IncomingUnprocessedFiles {
|
||||
available: Array<FileInfo>
|
||||
inProcess: Array<FileInfo>
|
||||
}
|
||||
|
||||
const initialState: IncomingUnprocessedFiles = {
|
||||
available: [],
|
||||
inProcess: []
|
||||
}
|
||||
|
||||
const unprocessedFilesSlice = createSlice({
|
||||
name: "UnprocessedFiles",
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<IncomingUnprocessedFiles>) {
|
||||
state.available = action.payload.available ?? []
|
||||
state.inProcess = action.payload.inProcess ?? []
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const { update } = unprocessedFilesSlice.actions;
|
||||
export default unprocessedFilesSlice.reducer;
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user