Adjusted process check. Now checking if there is a produced derived event and if it can create more.

This commit is contained in:
bskjon 2025-02-27 00:46:46 +01:00
parent e4e972f36b
commit 82cb245639
19 changed files with 2032 additions and 989 deletions

View File

@ -0,0 +1,43 @@
package no.iktdev.mediaprocessing
import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.Event
import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent
import org.json.JSONArray
enum class Files(val fileName: String) {
MultipleLanguageBased("Events.json")
}
fun Files.getContent(): String? {
return this.javaClass.classLoader.getResource(this.fileName)?.readText()
}
fun Files.databaseJsonToEvents(): List<Event> {
val content = this.getContent();
try {
val jarr = JSONArray(content)
for (i in 0 until jarr.length()) {
val o = jarr.getJSONObject(i)
if (o.has("type") && o.getString("type") == "table") {
val dataArray = o.getJSONArray("data")
val events: MutableList<Event> = mutableListOf()
for (x in 0 until dataArray.length()) {
val obj = dataArray.getJSONObject(x)
val eventType = obj.getString("event")
val dataString = obj.getString("data")
dataString.jsonToEvent(eventType).also {
events.add(it)
}
}
return events
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return emptyList()
}

View File

@ -0,0 +1,29 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.listeners
import no.iktdev.eventi.data.referenceId
import no.iktdev.mediaprocessing.Files
import no.iktdev.mediaprocessing.databaseJsonToEvents
import no.iktdev.mediaprocessing.shared.common.contract.Events
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class ConvertWorkTaskListenerTest {
@Test
fun validateParsingOfEvents() {
val content = Files.MultipleLanguageBased.databaseJsonToEvents()
assertThat(content).isNotEmpty()
val referenceId = content.firstOrNull()?.referenceId()
assertThat(referenceId).isNotNull()
}
@Test
fun validateCreationOfConvertTasks() {
val listener: ConvertWorkTaskListener = ConvertWorkTaskListener()
val content = Files.MultipleLanguageBased.databaseJsonToEvents().filter { it.eventType in listOf( Events.WorkExtractPerformed, Events.ProcessStarted, Events.WorkConvertCreated, Events.WorkConvertPerformed) }
assertThat(listener).isNotNull()
val success = content.map { listener.shouldIProcessAndHandleEvent(it, content) to it }
assertThat(success.filter { it.first }.size).isGreaterThan(2)
}
}

View File

@ -3,7 +3,7 @@ package no.iktdev.mediaprocessing.coordinator.tasksV2.listeners
import com.google.gson.JsonObject import com.google.gson.JsonObject
import no.iktdev.eventi.data.dataAs import no.iktdev.eventi.data.dataAs
import no.iktdev.mediaprocessing.shared.common.contract.Events import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.fromJsonWithDeserializer import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -11,7 +11,7 @@ class ParseMediaFileStreamsTaskListenerTest {
@Test @Test
fun testParse() { fun testParse() {
val event = data.fromJsonWithDeserializer(Events.ReadStreamPerformed).dataAs<JsonObject>() val event = data.jsonToEvent(Events.ReadStreamPerformed.event).dataAs<JsonObject>()
val parser = ParseMediaFileStreamsTaskListener() val parser = ParseMediaFileStreamsTaskListener()
val result = parser.parseStreams(event) val result = parser.parseStreams(event)

View File

@ -6,14 +6,14 @@ import no.iktdev.mediaprocessing.shared.common.contract.data.az
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioPreference import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioPreference
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.EncodingPreference import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.EncodingPreference
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference
import no.iktdev.mediaprocessing.shared.common.contract.fromJsonWithDeserializer import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class EncodeWorkArgumentsMappingTest { class EncodeWorkArgumentsMappingTest {
@Test @Test
fun parse() { fun parse() {
val event = data.fromJsonWithDeserializer(Events.ParseStreamPerformed) val event = data.jsonToEvent(Events.ParseStreamPerformed.event)
val parser = EncodeWorkArgumentsMapping( val parser = EncodeWorkArgumentsMapping(
"potato.mkv", "potato.mkv",
"potato.mp4", "potato.mp4",

View File

@ -0,0 +1,23 @@
[
{"type":"header","version":"5.2.1","comment":"Export to JSON plugin for PHPMyAdmin"},
{"type":"database","name":"eventsV4"},
{"type":"table","name":"events","database":"eventsV4","data":
[
{"id":"15349","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"8ca447c5-aad9-40cc-a561-6669e1248d77","event":"event:media-process:started","data":"{\"metadata\":{\"eventId\":\"8ca447c5-aad9-40cc-a561-6669e1248d77\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:46.029447375\",\"source\":\"Coordinator\"},\"data\":{\"type\":\"FLOW\",\"operations\":[\"ENCODE\",\"EXTRACT\",\"CONVERT\"],\"file\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"},\"eventType\":\"ProcessStarted\"}","created":"2025-02-26 22:26:46.047704"},
{"id":"15359","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"fc1d6871-b5e3-47ad-96ef-6e63e611dec2","event":"event:work-extract:created","data":"{\"metadata\":{\"derivedFromEventId\":\"17f5ba56-7155-4fac-b787-3371480a08ac\",\"eventId\":\"fc1d6871-b5e3-47ad-96ef-6e63e611dec2\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.668593271\",\"source\":\"ExtractWorkTaskListener\"},\"eventType\":\"WorkExtractCreated\",\"data\":{\"arguments\":[\"-c:s\",\"copy\",\"-map\",\"0:s:0\"],\"language\":\"eng\",\"storeFileName\":\"Potetmos\",\"outputFileName\":\"Potetmos.eng.srt\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.695234"},
{"id":"15360","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"11abadee-73f5-4b7a-ad28-5ec76ae5e095","event":"event:work-extract:created","data":"{\"metadata\":{\"derivedFromEventId\":\"17f5ba56-7155-4fac-b787-3371480a08ac\",\"eventId\":\"11abadee-73f5-4b7a-ad28-5ec76ae5e095\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.668599001\",\"source\":\"ExtractWorkTaskListener\"},\"eventType\":\"WorkExtractCreated\",\"data\":{\"arguments\":[\"-c:s\",\"copy\",\"-map\",\"0:s:1\"],\"language\":\"dan\",\"storeFileName\":\"Potetmos\",\"outputFileName\":\"Potetmos.dan.srt\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.727437"},
{"id":"15361","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"9f929171-91be-460f-b6db-0243603eb222","event":"event:work-extract:created","data":"{\"metadata\":{\"derivedFromEventId\":\"17f5ba56-7155-4fac-b787-3371480a08ac\",\"eventId\":\"9f929171-91be-460f-b6db-0243603eb222\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.668600721\",\"source\":\"ExtractWorkTaskListener\"},\"eventType\":\"WorkExtractCreated\",\"data\":{\"arguments\":[\"-c:s\",\"copy\",\"-map\",\"0:s:2\"],\"language\":\"swe\",\"storeFileName\":\"Potetmos\",\"outputFileName\":\"Potetmos.swe.srt\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.762233"},
{"id":"15362","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"abbba9e0-0491-48a1-bf48-dd0f201cef7f","event":"event:work-extract:created","data":"{\"metadata\":{\"derivedFromEventId\":\"17f5ba56-7155-4fac-b787-3371480a08ac\",\"eventId\":\"abbba9e0-0491-48a1-bf48-dd0f201cef7f\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.668602091\",\"source\":\"ExtractWorkTaskListener\"},\"eventType\":\"WorkExtractCreated\",\"data\":{\"arguments\":[\"-c:s\",\"copy\",\"-map\",\"0:s:3\"],\"language\":\"nob\",\"storeFileName\":\"Potetmos\",\"outputFileName\":\"Potetmos.nob.srt\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.792813"},
{"id":"15363","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"fe594289-b2c5-4e3e-8827-4ba2988184b6","event":"event:work-extract:created","data":"{\"metadata\":{\"derivedFromEventId\":\"17f5ba56-7155-4fac-b787-3371480a08ac\",\"eventId\":\"fe594289-b2c5-4e3e-8827-4ba2988184b6\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.668603331\",\"source\":\"ExtractWorkTaskListener\"},\"eventType\":\"WorkExtractCreated\",\"data\":{\"arguments\":[\"-c:s\",\"copy\",\"-map\",\"0:s:4\"],\"language\":\"fin\",\"storeFileName\":\"Potetmos\",\"outputFileName\":\"Potetmos.fin.srt\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.827601"},
{"id":"15364","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"99692869-ba77-4976-8544-02fdc841a06a","event":"event:work-encode:created","data":"{\"metadata\":{\"derivedFromEventId\":\"fb06297e-e5ab-4e2f-bf20-b624d37c2f8b\",\"eventId\":\"99692869-ba77-4976-8544-02fdc841a06a\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:26:57.842844507\",\"source\":\"EncodeWorkTaskListener\"},\"eventType\":\"WorkEncodeCreated\",\"data\":{\"arguments\":[\"-c:v\",\"copy\",\"-vbsf\",\"hevc_mp4toannexb\",\"-acodec\",\"copy\",\"-map\",\"0:v:0\",\"-map\",\"0:a:0\"],\"outputFileName\":\"Potetmos.mp4\",\"inputFile\":\"\/src\/input\/completed\/standalone\/Potetmos\/Potetmos.mkv\"}}","created":"2025-02-26 22:26:57.871043"},
{"id":"15365","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"59d1bcda-0a34-48fc-83e3-34547d63abb5","event":"event:work-extract:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"fc1d6871-b5e3-47ad-96ef-6e63e611dec2\",\"eventId\":\"59d1bcda-0a34-48fc-83e3-34547d63abb5\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:15.577829445\",\"source\":\"ExtractService\"},\"eventType\":\"WorkExtractPerformed\",\"data\":{\"language\":\"eng\",\"storeFileName\":\"Potetmos\",\"outputFile\":\"\/src\/cache\/Potetmos.eng.srt\"}}","created":"2025-02-26 22:27:15.581941"},
{"id":"15366","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"5a0419e6-572f-4b73-95b8-11af31e6002e","event":"event:work-encode:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"99692869-ba77-4976-8544-02fdc841a06a\",\"eventId\":\"5a0419e6-572f-4b73-95b8-11af31e6002e\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:15.598866765\",\"source\":\"EncodeService\"},\"eventType\":\"WorkEncodePerformed\",\"data\":{\"outputFile\":\"\/src\/cache\/Potetmos.mp4\"}}","created":"2025-02-26 22:27:15.603074"},
{"id":"15367","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"4191b02e-1d85-49f3-8cd3-2ca3a424e645","event":"event:work-convert:created","data":"{\"metadata\":{\"derivedFromEventId\":\"59d1bcda-0a34-48fc-83e3-34547d63abb5\",\"eventId\":\"4191b02e-1d85-49f3-8cd3-2ca3a424e645\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:16.058203558\",\"source\":\"ConvertWorkTaskListener\"},\"eventType\":\"WorkConvertCreated\",\"data\":{\"inputFile\":\"\/src\/cache\/Potetmos.eng.srt\",\"language\":\"eng\",\"outputDirectory\":\"\/src\/cache\",\"outputFileName\":\"Potetmos.eng\",\"storeFileName\":\"Potetmos\",\"formats\":[],\"allowOverwrite\":true}}","created":"2025-02-26 22:27:16.091175"},
{"id":"15368","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"79f23190-8696-4b4f-b037-98c0a9cd6314","event":"event:work-extract:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"11abadee-73f5-4b7a-ad28-5ec76ae5e095\",\"eventId\":\"79f23190-8696-4b4f-b037-98c0a9cd6314\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:21.73082975\",\"source\":\"ExtractService\"},\"eventType\":\"WorkExtractPerformed\",\"data\":{\"language\":\"dan\",\"storeFileName\":\"Potetmos\",\"outputFile\":\"\/src\/cache\/Potetmos.dan.srt\"}}","created":"2025-02-26 22:27:21.734862"},
{"id":"15369","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"aa66dda3-dbb0-4738-9b38-0f10ec6e3c07","event":"event:work-convert:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"4191b02e-1d85-49f3-8cd3-2ca3a424e645\",\"eventId\":\"aa66dda3-dbb0-4738-9b38-0f10ec6e3c07\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:22.029604937\",\"source\":\"ConvertService\"},\"eventType\":\"WorkConvertPerformed\",\"data\":{\"language\":\"eng\",\"baseName\":\"Potetmos\",\"outputFiles\":[\"\/src\/cache\/Potetmos.eng.vtt\",\"\/src\/cache\/Potetmos.eng.smi\"]}}","created":"2025-02-26 22:27:22.033352"},
{"id":"15370","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"ec07bb50-1cc1-45b2-a5d7-73b84bfd29d4","event":"event:work-extract:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"9f929171-91be-460f-b6db-0243603eb222\",\"eventId\":\"ec07bb50-1cc1-45b2-a5d7-73b84bfd29d4\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:26.803119861\",\"source\":\"ExtractService\"},\"eventType\":\"WorkExtractPerformed\",\"data\":{\"language\":\"swe\",\"storeFileName\":\"Potetmos\",\"outputFile\":\"\/src\/cache\/Potetmos.swe.srt\"}}","created":"2025-02-26 22:27:26.807279"},
{"id":"15371","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"7b6663e4-cc38-4b5e-b0af-32bd1ed3039d","event":"event:work-extract:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"abbba9e0-0491-48a1-bf48-dd0f201cef7f\",\"eventId\":\"7b6663e4-cc38-4b5e-b0af-32bd1ed3039d\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:31.834864505\",\"source\":\"ExtractService\"},\"eventType\":\"WorkExtractPerformed\",\"data\":{\"language\":\"nob\",\"storeFileName\":\"Potetmos\",\"outputFile\":\"\/src\/cache\/Potetmos.nob.srt\"}}","created":"2025-02-26 22:27:31.839143"},
{"id":"15372","referenceId":"7aed4397-e286-4787-966e-7d375f43b1e5","eventId":"4b3c17b8-c624-40fb-94ca-33e3657bc6d3","event":"event:work-extract:performed","data":"{\"metadata\":{\"derivedFromEventId\":\"fe594289-b2c5-4e3e-8827-4ba2988184b6\",\"eventId\":\"4b3c17b8-c624-40fb-94ca-33e3657bc6d3\",\"referenceId\":\"7aed4397-e286-4787-966e-7d375f43b1e5\",\"status\":\"Success\",\"created\":\"2025-02-26T22:27:37.017421899\",\"source\":\"ExtractService\"},\"eventType\":\"WorkExtractPerformed\",\"data\":{\"language\":\"fin\",\"storeFileName\":\"Potetmos\",\"outputFile\":\"\/src\/cache\/Potetmos.fin.srt\"}}","created":"2025-02-26 22:27:37.021717"}
]
}
]

View File

@ -1,7 +1,18 @@
package no.iktdev.mediaprocessing.ui.dto package no.iktdev.mediaprocessing.ui.dto
data class EventHolder(
val referenceId: String,
val fileName: String?,
val events: List<EventChain>,
val created: Long
)
data class EventChain( data class EventChain(
val eventId: String, val eventId: String,
val eventName: String, val eventName: String,
val elements: MutableList<EventChain> = mutableListOf() val created: Long,
val success: Boolean,
val failure: Boolean,
val skipped: Boolean,
val events: MutableList<EventChain> = mutableListOf()
) )

View File

@ -1,49 +0,0 @@
package no.iktdev.mediaprocessing.ui.service
import no.iktdev.eventi.data.derivedFromEventId
import no.iktdev.eventi.data.eventId
import no.iktdev.eventi.data.referenceId
import no.iktdev.mediaprocessing.shared.common.contract.data.Event
import no.iktdev.mediaprocessing.shared.common.database.cal.EventsManager
import no.iktdev.mediaprocessing.ui.dto.EventChain
import no.iktdev.mediaprocessing.ui.eventsManager
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
//@Service
@EnableScheduling
class EventExecutionOrderService(
@Autowired eventsManager: EventsManager
) {
val collections: MutableMap<String, List<EventChain>> = mutableMapOf()
@Scheduled(fixedDelay = 5_000)
fun pullAvailableEvents() {
eventsManager.getAllEvents().onEach { events ->
collections[events.first().referenceId()] = events.chained()
}
}
fun List<Event>.chained(): List<EventChain> {
val eventMap = this.associateBy { it.eventId() }
val chains = mutableMapOf<String, EventChain>()
this.forEach { event ->
val chain = EventChain(eventId = event.eventId(), eventName = event.eventType.name)
chains[event.eventId()] = chain
if (event.derivedFromEventId() != null && eventMap.containsKey(event.derivedFromEventId())) {
val parentChain = chains[event.derivedFromEventId()]
parentChain?.elements?.add(chain)
}
}
return chains.values.filter { it.elements.isNotEmpty() }.toList()
}
}

View File

@ -1,27 +1,34 @@
package no.iktdev.mediaprocessing.ui.socket package no.iktdev.mediaprocessing.ui.socket
import no.iktdev.eventi.data.derivedFromEventId import no.iktdev.eventi.data.*
import no.iktdev.eventi.data.eventId import no.iktdev.eventi.database.toEpochSeconds
import no.iktdev.eventi.data.referenceId import no.iktdev.mediaprocessing.shared.common.contract.Events
import no.iktdev.mediaprocessing.shared.common.contract.data.Event import no.iktdev.mediaprocessing.shared.common.contract.data.*
import no.iktdev.mediaprocessing.ui.dto.EventChain import no.iktdev.mediaprocessing.ui.dto.EventChain
import no.iktdev.mediaprocessing.ui.dto.EventHolder
import no.iktdev.mediaprocessing.ui.eventsManager import no.iktdev.mediaprocessing.ui.eventsManager
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.simp.SimpMessagingTemplate import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import java.io.File
@Controller @Controller
class ChainedEventsTopic( class ChainedEventsTopic(
@Autowired private val template: SimpMessagingTemplate? @Autowired private val template: SimpMessagingTemplate?,
) { ) {
@MessageMapping("/chained/all") @MessageMapping("/chained/all")
fun sendAllChainedEvents() { fun sendAllChainedEvents() {
val collections: MutableMap<String, List<EventChain>> = mutableMapOf() val holders: MutableList<EventHolder> = mutableListOf()
eventsManager.getAllEvents().onEach { events -> eventsManager.getAllEvents().onEach { events ->
collections[events.first().referenceId()] = events.chained() holders.add(EventHolder(
referenceId = events.first().referenceId(),
fileName = events.findFirstOf(Events.ProcessStarted)?.dataAs<StartEventData>()?.file?.let { File(it).name },
events = events.chained(),
created = events.chained().firstOrNull()?.created ?: 0
))
} }
template?.convertAndSend("/topic/chained/all",collections) template?.convertAndSend("/topic/chained/all", holders)
} }
@ -33,17 +40,23 @@ class ChainedEventsTopic(
this.forEach { event -> this.forEach { event ->
val eventId = event.metadata.eventId val eventId = event.metadata.eventId
val derivedFromEventId = event.metadata.derivedFromEventId val derivedFromEventId = event.metadata.derivedFromEventId
val chain = chains.getOrPut(eventId) { EventChain(eventId, event.eventType.toString()) } val created = event.metadata.created.toEpochSeconds() * 1000L
val chain = chains.getOrPut(eventId) {
EventChain(eventId, event.eventType.toString(), created, success = event.isSuccessful(), skipped = event.isSkipped(), failure = event.isFailed())
}
if (derivedFromEventId != null && eventMap.containsKey(derivedFromEventId)) { if (derivedFromEventId != null && eventMap.containsKey(derivedFromEventId)) {
val parentChain = chains.getOrPut(derivedFromEventId) { val parentChain = chains.getOrPut(derivedFromEventId) {
EventChain(derivedFromEventId, eventMap[derivedFromEventId]!!.eventType.toString()) EventChain(derivedFromEventId, eventMap[derivedFromEventId]!!.eventType.toString(), created, success = event.isSuccessful(), skipped = event.isSkipped(), failure = event.isFailed())
} }
parentChain.elements.add(chain) parentChain.events.add(chain)
children.add(eventId) children.add(eventId)
} }
} }
chains.values.forEach { chain -> chain.events.sortBy { it.created }}
return chains.values.filter { it.eventId !in children } return chains.values.filter { it.eventId !in children }
.sortedBy { it.created }
} }
} }

View File

@ -6,7 +6,7 @@ import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
@Controller @Controller
class EventsTableTopic( class PersistentEventsTableTopic(
@Autowired private val template: SimpMessagingTemplate?, @Autowired private val template: SimpMessagingTemplate?,
//@Autowired private val persistentEventsTableService: PersistentEventsTableService //@Autowired private val persistentEventsTableService: PersistentEventsTableService
) { ) {

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/styled-components": "^5.1.27", "@types/styled-components": "^5.1.27",
"react": "^18.2.0", "react": "^18.2.0",
"react-d3-tree": "^3.6.5",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",

View File

@ -1,6 +1,19 @@
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
export function toUnixTimestamp({ timestamp }: { timestamp?: number }) {
if (!timestamp) {
return null;
}
const date = new Date(timestamp);
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'short' });
const year = date.getFullYear();
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${day}.${month}.${year} ${hours}.${minutes}`;
}
export function UnixTimestamp({ timestamp }: { timestamp?: number }) { export function UnixTimestamp({ timestamp }: { timestamp?: number }) {
if (!timestamp) { if (!timestamp) {
return null; return null;

View File

@ -1,141 +0,0 @@
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";
export interface TablePropetyConfig {
label: string
accessor: string
}
export interface TableCellCustomizer<T> {
(accessor: string, data: T): JSX.Element | null
}
type NullableTableRowActionEvents<T> = TableRowActionEvents<T> | null;
export interface TableRowActionEvents<T> {
click: (row: T) => void;
doubleClick: (row: T) => void;
contextMenu?: (row: T, x: number, y: number) => void;
}
export default function SimpleTable<T>({ items, columns, customizer, onRowClickedEvent }: { items: Array<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 = 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>
<TableBody sx={{
overflowY: "scroll"
}}>
{sortedData?.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>
)
}

View File

@ -1,41 +1,68 @@
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useStompClient } from 'react-stomp-hooks'; import { useStompClient } from 'react-stomp-hooks';
import { RootState } from "../store"; import { RootState } from "../store";
import { useWsSubscription } from "../ws/subscriptions"; import { useWsSubscription } from "../ws/subscriptions";
import { set } from "../store/persistent-events-slice";
import { EventsObjectListResponse } from "../../types"; import { EventsObjectListResponse } from "../../types";
import IconRefresh from '@mui/icons-material/Refresh' import IconRefresh from '@mui/icons-material/Refresh'
import { Button } from "@mui/material"; import { Box, Button, Typography, useTheme } from "@mui/material";
import { Tree } from "react-d3-tree";
import { EventChain, EventGroup, EventGroups, set } from "../store/chained-events-slice";
import { toUnixTimestamp, UnixTimestamp } from "../features/UxTc";
import { TableCellCustomizer, TablePropetyConfig } from "../features/table/table";
import ExpandableTable, { ExpandableItem } from "../features/table/expandableTable";
import { CustomNodeElementProps } from 'react-d3-tree';
export default function EventsChainPage() { interface RawNodeDatum {
const dispatch = useDispatch(); name: string;
const client = useStompClient(); attributes?: Record<string, string | number | boolean>;
const cursor = useSelector((state: RootState) => state.persistentEvents) children?: RawNodeDatum[];
fill?: string;
function log(data: any) {
console.log(data)
} }
useWsSubscription("/topic/chained/all", (response) => { // Transformasjonsfunksjon for EventChain
console.log(response) const transformEventChain = (eventChain: EventChain): RawNodeDatum => ({
name: eventChain.eventName,
attributes: {
//eventId: eventChain.eventId,
created: toUnixTimestamp({ timestamp: eventChain.created }) ?? "",
success: eventChain.success,
skipped: eventChain.skipped,
failure: eventChain.failure,
childrens: eventChain.events.length
},
fill: "white",
children: eventChain.events.map(transformEventChain),
}); });
// Transformasjonsfunksjon for EventGroups
const transformEventGroups = (group: EventGroup): RawNodeDatum[] => {
return group.events.map(transformEventChain);
}
interface EventGroupToTreeView extends EventGroup {
underView: JSX.Element
}
export default function EventsChainPage() {
const muiTheme = useTheme();
const dispatch = useDispatch();
const client = useStompClient();
const cursor = useSelector((state: RootState) => state.chained)
const [useReferenceId, setUseReferenceId] = useState<string | null>(null);
const [treeData, setTreeData] = useState<RawNodeDatum[] | null>(null);
useWsSubscription<Array<EventGroup>>("/topic/chained/all", (response) => {
dispatch(set(response))
});
useEffect(() => { useEffect(() => {
if (Object.keys(cursor).length === 0) {
// Kjør din funksjon her når komponenten lastes inn for første gang
// Sjekk om cursor er null
if (cursor.items === null && client !== null) {
console.log(cursor)
// Kjør din funksjon her når cursor er null og client ikke er null
client?.publish({ client?.publish({
destination: "/app/chained/all" destination: "/app/chained/all"
}); });
// Alternativt, du kan dispatche en Redux handling her
// dispatch(fetchDataAction()); // Eksempel på å dispatche en handling
} }
}, [cursor, client, dispatch]); }, [cursor, client, dispatch]);
@ -45,6 +72,104 @@ export default function EventsChainPage() {
"body": "Potato" "body": "Potato"
}) })
} }
useEffect(() => {
if (useReferenceId) {
const eventGroup = cursor.groups.find((group) => group.referenceId === useReferenceId);
if (eventGroup) {
const data = transformEventGroups(eventGroup);
console.log({
"info": "Tree data",
"data": data
})
setTreeData(data);
}
}
}, [useReferenceId, cursor])
const createTableCell: TableCellCustomizer<EventGroup> = (accessor, data) => {
switch (accessor) {
case "created": {
if (typeof data[accessor] === "number") {
const timestampObject = { timestamp: data[accessor] as number }; // Opprett et objekt med riktig struktur
return UnixTimestamp(timestampObject);
} else {
return null;
}
}
default: return null;
}
};
const columns: Array<TablePropetyConfig> = [
{ label: "ReferenceId", accessor: "referenceId" },
{ label: "File", accessor: "fileName" },
{ label: "Created", accessor: "created" },
];
function renderCustomNodeElement(nodeData: CustomNodeElementProps): JSX.Element {
const attr = nodeData.nodeDatum.attributes;
const nodeColor = attr?.success ? "green" : attr?.failure ? "red" : "yellow";
return (<>
<g>
<circle r="15" fill={nodeColor} onClick={nodeData.toggleNode} />
<text fill="white" stroke="white" strokeWidth="0" x="20">
{nodeData.nodeDatum.name}
</text>
{attr?.created && (
<text fill="lightgray" x="20" dy="20" strokeWidth="0">
Created: {attr?.created}
</text>
)}
{attr?.success && (
<text fill="lightgray" x="20" dy="40" strokeWidth="0">
Success
</text>
)}
{attr?.failure && (
<text fill="lightgray" x="20" dy="40" strokeWidth="0">
Failure
</text>
)}
{attr?.skipped && (
<text fill="lightgray" x="20" dy="40" strokeWidth="0">
Skipped
</text>
)}
{attr?.childrens && (
<text fill="lightgray" x="20" dy="60" strokeWidth="0">
{attr?.childrens} derived {Number(attr?.childrens) > 1 ? "events" : "event"}
</text>
)}
</g>
</>);
}
function renderExpandableItem(item: EventGroup): ExpandableItem<EventGroup> | null {
return {
tag: item.referenceId,
expandElement: (() => {
const data = transformEventGroups(item);
return (
<>
{(data) ? (
<div id="treeWrapper" style={{ width: '100%', height: '60vh', }}>
<Tree data={data} orientation="vertical" separation={{
nonSiblings: 3,
siblings: 2
}}
renderCustomNodeElement={renderCustomNodeElement}
/>
</div>
) : <Typography>Tree data not available</Typography>}
</>
);
})()
};
}
return ( return (
<> <>
<Button <Button
@ -52,7 +177,19 @@ export default function EventsChainPage() {
onClick={onRefresh} sx={{ onClick={onRefresh} sx={{
borderRadius: 5, borderRadius: 5,
textTransform: 'none' textTransform: 'none'
}}>Refresh</Button > }}>Refresh
</Button>
<Box display="block">
<Box sx={{
display: "block",
height: "calc(100% - 120px)",
overflow: "hidden",
position: "absolute",
width: "100%"
}}>
<ExpandableTable items={cursor?.groups ?? []} columns={columns} cellCustomizer={createTableCell} expandableRender={renderExpandableItem} />
</Box>
</Box>
</> </>
) )
} }

View File

@ -3,7 +3,7 @@ import { UnixTimestamp } from '../features/UxTc';
import { Box, Button, Typography, useTheme } from '@mui/material'; import { Box, Button, Typography, useTheme } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../store'; import { RootState } from '../store';
import SimpleTable, { TableCellCustomizer, TablePropetyConfig, TableRowActionEvents } from '../features/table'; import { TableCellCustomizer, TablePropetyConfig, TableRowActionEvents } from '../features/table/table';
import { useStompClient } from 'react-stomp-hooks'; import { useStompClient } from 'react-stomp-hooks';
import { useWsSubscription } from '../ws/subscriptions'; import { useWsSubscription } from '../ws/subscriptions';
import { updateItems } from '../store/explorer-slice'; import { updateItems } from '../store/explorer-slice';
@ -14,6 +14,7 @@ import IconHome from '@mui/icons-material/Home';
import { ExplorerItem, ExplorerCursor, ExplorerItemType } from '../../types'; import { ExplorerItem, ExplorerCursor, ExplorerItemType } from '../../types';
import ContextMenu, { ContextMenuActionEvent, ContextMenuItem } from '../features/ContextMenu'; import ContextMenu, { ContextMenuActionEvent, ContextMenuItem } from '../features/ContextMenu';
import { canConvert, canEncode, canExtract } from '../../fileUtil'; import { canConvert, canEncode, canExtract } from '../../fileUtil';
import SimpleTable from '../features/table/sortableTable';
const createTableCell: TableCellCustomizer<ExplorerItem> = (accessor, data) => { const createTableCell: TableCellCustomizer<ExplorerItem> = (accessor, data) => {

View File

@ -1,5 +1,4 @@
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import SimpleTable, { TableCellCustomizer, TablePropetyConfig } from "../features/table"
import { RootState } from "../store"; import { RootState } from "../store";
import { useEffect } from "react"; import { useEffect } from "react";
import { useStompClient } from "react-stomp-hooks"; import { useStompClient } from "react-stomp-hooks";
@ -7,6 +6,8 @@ import { Box, Button, IconButton, Typography, useTheme } from "@mui/material";
import IconRefresh from '@mui/icons-material/Refresh' import IconRefresh from '@mui/icons-material/Refresh'
import IconCompleted from '@mui/icons-material/Check' import IconCompleted from '@mui/icons-material/Check'
import IconWorking from '@mui/icons-material/Engineering'; import IconWorking from '@mui/icons-material/Engineering';
import { TablePropetyConfig } from "../features/table/table";
import SimpleTable from "../features/table/sortableTable";
const columns: Array<TablePropetyConfig> = [ const columns: Array<TablePropetyConfig> = [
{ label: "Title", accessor: "givenTitle" }, { label: "Title", accessor: "givenTitle" },

View File

@ -4,6 +4,7 @@ import explorerSlice from './store/explorer-slice';
import kafkaItemsFlatSlice from './store/kafka-items-flat-slice'; 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';
export const store = configureStore({ export const store = configureStore({
@ -12,7 +13,8 @@ export const store = configureStore({
explorer: explorerSlice, explorer: explorerSlice,
kafkaComposedFlat: kafkaItemsFlatSlice, kafkaComposedFlat: kafkaItemsFlatSlice,
contextMenu: contextMenuSlice, contextMenu: contextMenuSlice,
persistentEvents: persistentEventsSlice persistentEvents: persistentEventsSlice,
chained: chainedEventsSlice,
}, },
}); });

View File

@ -13,7 +13,7 @@ const root = ReactDOM.createRoot(
const protocol = window.location.protocol; const protocol = window.location.protocol;
const host = window.location.host; const host = window.location.host;
const wsUrl = `${protocol}//${host}/ws`; const wsUrl = "http://localhost:8080/ws" //`${protocol}//${host}/ws`;
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>

View File

@ -21,6 +21,10 @@ inline fun <reified T: Event> List<Event>.findFirstEventOf(): T? {
} else null } else null
} }
inline fun List<Event>.findFirstOf(events: Events): Event? {
return this.firstOrNull { it.eventType == events }
}
inline fun <reified T: Event> List<Event>.findEventsOf(): List<T> { inline fun <reified T: Event> List<Event>.findEventsOf(): List<T> {
return this.filterIsInstance<T>().map { it } return this.filterIsInstance<T>().map { it }
} }