Updated UI + Indexing
This commit is contained in:
parent
b36f0c5e1d
commit
39d2bbe0b3
@ -25,8 +25,21 @@ class UnattendedIndexing {
|
|||||||
|
|
||||||
@Scheduled(fixedDelay = 60_000*60)
|
@Scheduled(fixedDelay = 60_000*60)
|
||||||
fun indexContent() {
|
fun indexContent() {
|
||||||
logger.info { "Performing indexing of input root: ${SharedConfig.inputRoot.absolutePath}" }
|
val allFiles = SharedConfig.incomingContent.flatMap { folder ->
|
||||||
val fileList = SharedConfig.inputRoot.walkTopDown().filter { it.isFile && it.isSupportedVideoFile() }.toList()
|
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 ->
|
fileList.forEach { file ->
|
||||||
withTransaction(eventDatabase.database) {
|
withTransaction(eventDatabase.database) {
|
||||||
files.insertIgnore {
|
files.insertIgnore {
|
||||||
|
|||||||
@ -2,21 +2,27 @@ package no.iktdev.mediaprocessing.ui
|
|||||||
|
|
||||||
import no.iktdev.mediaprocessing.shared.common.Defaults
|
import no.iktdev.mediaprocessing.shared.common.Defaults
|
||||||
import no.iktdev.mediaprocessing.shared.common.socket.SocketImplementation
|
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.beans.factory.annotation.Value
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder
|
import org.springframework.boot.web.client.RestTemplateBuilder
|
||||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
|
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
|
||||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer
|
import org.springframework.boot.web.server.WebServerFactoryCustomizer
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.context.annotation.Import
|
|
||||||
import org.springframework.core.io.Resource
|
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.bind.annotation.RestController
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import org.springframework.web.method.HandlerTypePredicate
|
import org.springframework.web.method.HandlerTypePredicate
|
||||||
import org.springframework.web.servlet.config.annotation.*
|
import org.springframework.web.servlet.config.annotation.*
|
||||||
import org.springframework.web.servlet.resource.PathResourceResolver
|
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.DefaultUriBuilderFactory
|
||||||
import org.springframework.web.util.UriTemplateHandler
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ -84,6 +90,39 @@ class ApiCommunicationConfig {
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
class SocketImplemented: SocketImplementation() {
|
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
|
@Configuration
|
||||||
|
|||||||
@ -5,4 +5,5 @@ import java.io.File
|
|||||||
object UIEnv {
|
object UIEnv {
|
||||||
val socketEncoder: String = System.getenv("EncoderWs")?.takeIf { it.isNotBlank() } ?: "ws://encoder:8080"
|
val socketEncoder: String = System.getenv("EncoderWs")?.takeIf { it.isNotBlank() } ?: "ws://encoder:8080"
|
||||||
val coordinatorUrl: String = System.getenv("Coordinator")?.takeIf { it.isNotBlank() } ?: "http://coordinator"
|
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
|
package no.iktdev.mediaprocessing.ui.socket
|
||||||
|
|
||||||
import no.iktdev.eventi.database.withTransaction
|
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.files
|
||||||
import no.iktdev.mediaprocessing.shared.common.database.tables.filesProcessed
|
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.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.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping
|
import org.springframework.messaging.handler.annotation.MessageMapping
|
||||||
import org.springframework.messaging.handler.annotation.Payload
|
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate
|
import org.springframework.messaging.simp.SimpMessagingTemplate
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Controller
|
import org.springframework.stereotype.Controller
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import java.io.File
|
import org.springframework.web.socket.WebSocketSession
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
class UnprocessedFilesTopic(
|
class UnprocessedFilesTopic(
|
||||||
@Autowired private val template: SimpMessagingTemplate?,
|
@Autowired private val template: SimpMessagingTemplate?,
|
||||||
@Autowired private val coordinatorTemplate: RestTemplate,
|
@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")
|
object DatabaseData {
|
||||||
fun getUnprocessedFiles() {
|
private const val PULL_MIN_INTERVAL = 10_000 // 10 sekunder
|
||||||
refreshUnprocessedFiles()
|
private var lastPoll: Long = 0
|
||||||
}
|
var unprocessedFiles: List<FileInfo> = emptyList()
|
||||||
@MessageMapping("/request/process")
|
private set
|
||||||
fun requestProcess(@Payload data: EventRequest) {
|
var filesInProcess: List<FileInfo> = emptyList()
|
||||||
val req = coordinatorTemplate.postForEntity("/request/all", data, String::class.java)
|
private set
|
||||||
log.info { "RequestProcess report:\n\tStatus: ${req.statusCode}\n\tMessage: ${req.body}" }
|
|
||||||
|
// 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
@MessageMapping("/files/unprocessed")
|
||||||
@Scheduled(fixedDelay = 5_000)
|
fun postUnProcessedFiles() {
|
||||||
fun refreshUnprocessedFiles() {
|
update()
|
||||||
val unprocessedFiles = pullUnprocessedFiles()
|
template?.convertAndSend("/topic/files/unprocessed", UnprocessedFiles(
|
||||||
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
|
spring.output.ansi.enabled=always
|
||||||
logging.level.org.apache.kafka=INFO
|
logging.level.org.apache.kafka=INFO
|
||||||
logging.level.root=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 React, { useEffect, useState } from 'react';
|
||||||
import logo from './logo.svg';
|
import logo from './logo.svg';
|
||||||
import './App.css';
|
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 { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import Footer from './app/features/footer';
|
import Footer from './app/features/footer';
|
||||||
import LaunchPage from './app/page/LaunchPage';
|
import LaunchPage from './app/page/LaunchPage';
|
||||||
@ -15,6 +15,13 @@ import theme from './theme';
|
|||||||
import { simpleEventsUpdate } from './app/store/kafka-items-flat-slice';
|
import { simpleEventsUpdate } from './app/store/kafka-items-flat-slice';
|
||||||
import { EventDataObject, SimpleEventDataObject } from './types';
|
import { EventDataObject, SimpleEventDataObject } from './types';
|
||||||
import EventsChainPage from './app/page/EventsChainPage';
|
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() {
|
function App() {
|
||||||
const client = useStompClient();
|
const client = useStompClient();
|
||||||
@ -44,14 +51,46 @@ function App() {
|
|||||||
|
|
||||||
}, [client, dispatch]);
|
}, [client, dispatch]);
|
||||||
|
|
||||||
|
const iconHeight: SxProps<Theme> = {
|
||||||
|
height: 50,
|
||||||
|
width: 50
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
height: 70,
|
height: 70,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: 1,
|
||||||
backgroundColor: theme.palette.action.selected
|
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>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "block",
|
display: "block",
|
||||||
@ -62,6 +101,8 @@ function App() {
|
|||||||
}}>
|
}}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path='/tasks' element={<ProcesserTasksPage />} />
|
||||||
|
<Route path='/unprocessed' element={<UnprocessedFilesPage />} />
|
||||||
<Route path='/files' element={<ExplorePage />} />
|
<Route path='/files' element={<ExplorePage />} />
|
||||||
<Route path='/events' element={<EventsChainPage />} />
|
<Route path='/events' element={<EventsChainPage />} />
|
||||||
<Route path='/' element={<LaunchPage />} />
|
<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 contextMenuSlice from './store/context-menu-slice';
|
||||||
import persistentEventsSlice from './store/persistent-events-slice';
|
import persistentEventsSlice from './store/persistent-events-slice';
|
||||||
import chainedEventsSlice from './store/chained-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({
|
export const store = configureStore({
|
||||||
@ -15,6 +17,8 @@ export const store = configureStore({
|
|||||||
contextMenu: contextMenuSlice,
|
contextMenu: contextMenuSlice,
|
||||||
persistentEvents: persistentEventsSlice,
|
persistentEvents: persistentEventsSlice,
|
||||||
chained: chainedEventsSlice,
|
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 wsUrl = () => {
|
||||||
const protocol = window.location.protocol;
|
const protocol = "ws" // window.location.protocol;
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
if (window.location.href.startsWith("http://localhost:3000")) {
|
if (window.location.href.startsWith("http://localhost:3000")) {
|
||||||
return "http://localhost:8080/ws";
|
return "ws://localhost:8080/ws";
|
||||||
} else {
|
} else {
|
||||||
return `${protocol}//${host}/ws`;
|
return `${protocol}//${host}/ws`;
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ root.render(
|
|||||||
console.log(str);
|
console.log(str);
|
||||||
}}
|
}}
|
||||||
onUnhandledMessage={(val) => {
|
onUnhandledMessage={(val) => {
|
||||||
console.log(val)
|
console.log("Unhandled message", val)
|
||||||
}}
|
}}
|
||||||
onStompError={(val) => {
|
onStompError={(val) => {
|
||||||
console.log(val)
|
console.log(val)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user