Update
This commit is contained in:
parent
6ae15bdec9
commit
c5b6582c64
@ -20,7 +20,7 @@ dependencies {
|
||||
implementation("com.github.pgreze:kotlin-process:1.3.1")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:2.0.11")
|
||||
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha80")
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha84")
|
||||
implementation("no.iktdev:exfl:0.0.13-SNAPSHOT")
|
||||
|
||||
implementation("com.google.code.gson:gson:2.8.9")
|
||||
|
||||
@ -8,8 +8,9 @@ import no.iktdev.streamit.library.kafka.dto.Status
|
||||
import no.iktdev.streamit.library.kafka.dto.StatusType
|
||||
import no.iktdev.streamit.library.kafka.listener.deserializer.IMessageDataDeserialization
|
||||
import no.iktdev.streamit.library.kafka.producer.DefaultProducer
|
||||
import java.util.*
|
||||
|
||||
abstract class DefaultKafkaReader(val subId: String) {
|
||||
abstract class DefaultKafkaReader(val subId: String = UUID.randomUUID().toString()) {
|
||||
val messageProducer = DefaultProducer(CommonConfig.kafkaTopic)
|
||||
val defaultConsumer = DefaultConsumer(subId = subId)
|
||||
|
||||
|
||||
@ -14,6 +14,9 @@ class DeserializerRegistry {
|
||||
KafkaEvents.EVENT_READER_DETERMINED_FILENAME to ContentOutNameDeserializer(),
|
||||
|
||||
KafkaEvents.EVENT_READER_ENCODE_GENERATED_VIDEO to EncodeWorkDeserializer(),
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_QUEUED to EncodeWorkDeserializer(),
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_STARTED to EncodeWorkDeserializer(),
|
||||
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_ENDED to EncodeWorkDeserializer(),
|
||||
KafkaEvents.EVENT_READER_ENCODE_GENERATED_SUBTITLE to ExtractWorkDeserializer(),
|
||||
KafkaEvents.EVENT_ENCODER_SUBTITLE_FILE_ENDED to ExtractWorkDeserializer(),
|
||||
@ -29,6 +32,16 @@ class DeserializerRegistry {
|
||||
}
|
||||
return getRegistry().filter { keys.contains(it.key) }.map { it.key.event to it.value }.toMap()
|
||||
}
|
||||
|
||||
private fun toEvent(event: String): KafkaEvents? {
|
||||
return KafkaEvents.values().find { it.event == event }
|
||||
}
|
||||
|
||||
fun getDeserializerForEvent(event: String): IMessageDataDeserialization<*>? {
|
||||
val deszEvent = toEvent(event) ?: return null
|
||||
return getEventToDeserializer(deszEvent).values.first()
|
||||
}
|
||||
|
||||
fun addDeserializer(key: KafkaEvents, deserializer: IMessageDataDeserialization<*>) {
|
||||
_registry[key] = deserializer
|
||||
}
|
||||
|
||||
@ -3,9 +3,9 @@ package no.iktdev.streamit.content.common.dto.reader.work
|
||||
import java.util.*
|
||||
|
||||
data class ConvertWork(
|
||||
override val workId: String = UUID.randomUUID().toString(),
|
||||
override val collection: String,
|
||||
val workId: String = UUID.randomUUID().toString(),
|
||||
val collection: String,
|
||||
val language: String,
|
||||
override val inFile: String,
|
||||
override val outFile: String,
|
||||
) : WorkBase(collection = collection, inFile = inFile, outFile = outFile)
|
||||
val inFile: String,
|
||||
val outFiles: List<String>
|
||||
)
|
||||
@ -23,9 +23,9 @@ repositories {
|
||||
dependencies {
|
||||
implementation(project(":CommonCode"))
|
||||
|
||||
implementation("no.iktdev.library:subtitle:1.7.4-SNAPSHOT")
|
||||
implementation("no.iktdev.library:subtitle:1.7.5-SNAPSHOT")
|
||||
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha80")
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha84")
|
||||
implementation("no.iktdev:exfl:0.0.13-SNAPSHOT")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||
@ -38,6 +38,8 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter:2.7.0")
|
||||
implementation("org.springframework.kafka:spring-kafka:2.8.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket:2.6.3")
|
||||
|
||||
|
||||
|
||||
testImplementation(platform("org.junit:junit-bom:5.9.1"))
|
||||
|
||||
@ -55,20 +55,19 @@ class ConvertRunner(val referenceId: String, val listener: IConvertListener) {
|
||||
|
||||
val filtered = dialogs.filter { !it.ignore && it.type !in listOf(DialogType.SIGN_SONG, DialogType.CAPTION) }
|
||||
|
||||
val syncedDialogs = Syncro().sync(dialogs)
|
||||
val syncedDialogs = Syncro().sync(filtered)
|
||||
|
||||
try {
|
||||
val converted = Export(inFile, syncedDialogs, ConvertEnv.allowOverwrite).write()
|
||||
converted.forEach {
|
||||
val item = ConvertWork(
|
||||
inFile = inFile.absolutePath,
|
||||
collection = subtitleInfo.collection,
|
||||
language = subtitleInfo.language,
|
||||
outFile = it.absolutePath
|
||||
)
|
||||
withContext(Dispatchers.Default) {
|
||||
listener.onEnded(referenceId, subtitleInfo, work = item)
|
||||
}
|
||||
val item = ConvertWork(
|
||||
inFile = inFile.absolutePath,
|
||||
collection = subtitleInfo.collection,
|
||||
language = subtitleInfo.language,
|
||||
outFiles = converted.map { it.absolutePath }
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
listener.onEnded(referenceId, subtitleInfo, work = item)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
spring.output.ansi.enabled=always
|
||||
logging.level.org.apache.kafka=WARN
|
||||
logging.level.org.apache.kafka=INFO
|
||||
#logging.level.root=DEBUG
|
||||
|
||||
@ -23,7 +23,7 @@ repositories {
|
||||
dependencies {
|
||||
implementation(project(":CommonCode"))
|
||||
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha80")
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha84")
|
||||
implementation("no.iktdev:exfl:0.0.13-SNAPSHOT")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||
|
||||
@ -24,7 +24,7 @@ repositories {
|
||||
|
||||
val exposedVersion = "0.38.2"
|
||||
dependencies {
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha80")
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha84")
|
||||
implementation("no.iktdev:exfl:0.0.13-SNAPSHOT")
|
||||
|
||||
implementation("no.iktdev.streamit.library:streamit-library-db:0.0.6-alpha14")
|
||||
@ -47,6 +47,8 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter:2.7.0")
|
||||
implementation("org.springframework.kafka:spring-kafka:2.8.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket:2.6.3")
|
||||
|
||||
|
||||
implementation(project(":CommonCode"))
|
||||
|
||||
|
||||
@ -87,18 +87,27 @@ class SubtitleConsumer : DefaultKafkaReader("collectorConsumerExtractedSubtitle"
|
||||
}
|
||||
|
||||
fun storeConvertWork(referenceId: String, work: ConvertWork) {
|
||||
val of = File(work.outFile)
|
||||
|
||||
val status = transaction {
|
||||
SubtitleQuery(
|
||||
associatedWithVideo = of.nameWithoutExtension,
|
||||
language = work.language,
|
||||
collection = work.collection,
|
||||
format = of.extension.uppercase(),
|
||||
file = File(work.outFile).name
|
||||
)
|
||||
.insertAndGetStatus()
|
||||
work.outFiles.map {
|
||||
val of = File(it)
|
||||
transaction {
|
||||
SubtitleQuery(
|
||||
associatedWithVideo = of.nameWithoutExtension,
|
||||
language = work.language,
|
||||
collection = work.collection,
|
||||
format = of.extension.uppercase(),
|
||||
file = of.name
|
||||
)
|
||||
.insertAndGetStatus()
|
||||
} to it
|
||||
}
|
||||
}
|
||||
produceMessage(referenceId, work.outFile, if (status) StatusType.SUCCESS else StatusType.ERROR, "Store Converted: $status")
|
||||
val failed = status.filter { !it.first }.map { it.second }
|
||||
val success = status.filter { it.first }.map { it.second }
|
||||
|
||||
produceSuccessMessage(KafkaEvents.EVENT_COLLECTOR_STORED_SUBTITLE, referenceId, success)
|
||||
produceErrorMessage(KafkaEvents.EVENT_COLLECTOR_STORED_SUBTITLE, Message(referenceId, Status(StatusType.ERROR), failed), "See log")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package no.iktdev.streamit.content.reader.dto
|
||||
|
||||
data class CompletedItem(
|
||||
val name: String,
|
||||
val fullName: String,
|
||||
val time: String,
|
||||
val operations: List<CompletedTypes>
|
||||
)
|
||||
|
||||
enum class CompletedTypes {
|
||||
ENCODE,
|
||||
EXTRACT,
|
||||
CONVERT
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
spring.output.ansi.enabled=always
|
||||
logging.level.org.apache.kafka=WARN
|
||||
logging.level.org.apache.kafka=INFO
|
||||
#logging.level.root=DEBUG
|
||||
|
||||
1
UI/.gitignore
vendored
1
UI/.gitignore
vendored
@ -4,6 +4,7 @@ build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.7.4"
|
||||
id("io.spring.dependency-management") version "1.0.14.RELEASE"
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("jvm") version "1.8.21"
|
||||
|
||||
kotlin("plugin.spring") version "1.6.21"
|
||||
}
|
||||
|
||||
@ -12,19 +15,60 @@ version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io")
|
||||
maven {
|
||||
url = uri("https://reposilite.iktdev.no/releases")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://reposilite.iktdev.no/snapshots")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-graphql:2.7.4")
|
||||
implementation("no.iktdev.streamit.library:streamit-library-kafka:0.0.2-alpha85")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:3.0.4")
|
||||
implementation("org.springframework.kafka:spring-kafka:2.8.5")
|
||||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
implementation("com.google.code.gson:gson:2.9.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-websocket:2.6.3")
|
||||
implementation("io.github.microutils:kotlin-logging-jvm:2.0.11")
|
||||
implementation("com.github.vishna:watchservice-ktx:master-SNAPSHOT")
|
||||
|
||||
|
||||
implementation("no.iktdev:exfl:0.0.13-SNAPSHOT")
|
||||
|
||||
|
||||
|
||||
|
||||
implementation(project(":CommonCode"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<org.springframework.boot.gradle.tasks.run.BootRun> {
|
||||
dependsOn(":buildFrontend")
|
||||
}
|
||||
|
||||
|
||||
tasks.register<Exec>("buildFrontend") {
|
||||
workingDir = file("web") // Stien til frontend-mappen
|
||||
commandLine("npm", "install") // Installer frontend-avhengigheter
|
||||
commandLine("npm", "run", "build") // Bygg frontend
|
||||
|
||||
doLast {
|
||||
copy {
|
||||
from(file("web/build")) // Byggresultatet fra React-appen
|
||||
into(file("src/main/resources/static/")) // Mappen der du vil plassere det i Spring Boot-prosjektet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kjør frontendbygget før backendbygget
|
||||
//tasks.getByName("bootJar").dependsOn("buildFrontend")
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
rootProject.name = "UI"
|
||||
|
||||
include(":CommonCode")
|
||||
project(":CommonCode").projectDir = File("../CommonCode")
|
||||
@ -1,21 +0,0 @@
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
|
||||
|
||||
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
|
||||
registry.addEndpoint("/ws")
|
||||
// .setAllowedOrigins("*")
|
||||
.withSockJS()
|
||||
}
|
||||
|
||||
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
|
||||
registry.enableSimpleBroker("/topic")
|
||||
registry.setApplicationDestinationPrefixes("/app")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package no.iktdev.streamit.content.ui
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.method.HandlerTypePredicate
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer
|
||||
|
||||
|
||||
@Configuration
|
||||
class WebConfig: WebMvcConfigurer {
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowCredentials(false)
|
||||
}
|
||||
|
||||
override fun configurePathMatch(configurer: PathMatchConfigurer) {
|
||||
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
|
||||
// configurer.addPathPrefix("/ws", HandlerTypePredicate.forAnnotation(Controller::class.java))
|
||||
}
|
||||
|
||||
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.setCachePeriod(0)
|
||||
}
|
||||
|
||||
@Value("\${APP_DEPLOYMENT_PORT:8080}")
|
||||
private val deploymentPort = 8080
|
||||
|
||||
@Bean
|
||||
fun webServerFactoryCustomizer(): WebServerFactoryCustomizer<TomcatServletWebServerFactory>? {
|
||||
return WebServerFactoryCustomizer { factory: TomcatServletWebServerFactory ->
|
||||
factory.port = deploymentPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
class WebSocketConfig : WebSocketMessageBrokerConfigurer {
|
||||
|
||||
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
|
||||
registry.addEndpoint("/ws")
|
||||
.setAllowedOrigins("*://localhost:*/*", "http://localhost:3000/")
|
||||
.withSockJS()
|
||||
}
|
||||
|
||||
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
|
||||
registry.enableSimpleBroker("/topic")
|
||||
registry.setApplicationDestinationPrefixes("/app")
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
package no.iktdev.streamit.content.ui
|
||||
|
||||
|
||||
import mu.KotlinLogging
|
||||
import no.iktdev.exfl.coroutines.Coroutines
|
||||
import no.iktdev.exfl.observable.ObservableMap
|
||||
import no.iktdev.exfl.observable.Observables
|
||||
import no.iktdev.exfl.observable.observableMapOf
|
||||
import no.iktdev.streamit.content.common.CommonConfig
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.dto.ExplorerItem
|
||||
import no.iktdev.streamit.content.ui.dto.SimpleEventDataObject
|
||||
import no.iktdev.streamit.library.kafka.KafkaEnv
|
||||
import org.apache.kafka.clients.admin.AdminClient
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.ApplicationContext
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@SpringBootApplication
|
||||
class UIApplication {
|
||||
}
|
||||
|
||||
private var context: ApplicationContext? = null
|
||||
private val kafkaClearedLatch = CountDownLatch(1)
|
||||
|
||||
@Suppress("unused")
|
||||
fun getContext(): ApplicationContext? {
|
||||
return context
|
||||
}
|
||||
|
||||
val memSimpleConvertedEventsMap: ObservableMap<String, SimpleEventDataObject> = observableMapOf()
|
||||
val memActiveEventMap: ObservableMap<String, EventDataObject> = observableMapOf()
|
||||
val fileRegister: ObservableMap<String, ExplorerItem> = observableMapOf()
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Coroutines.addListener(object : Observables.ObservableValue.ValueListener<Throwable> {
|
||||
override fun onUpdated(value: Throwable) {
|
||||
logger.error { "Received error: ${value.message}" }
|
||||
value.cause?.printStackTrace()
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
val admincli = AdminClient.create(mapOf(
|
||||
AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to KafkaEnv.servers,
|
||||
AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG to "1000",
|
||||
AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG to "5000"
|
||||
))
|
||||
val go = admincli.listConsumerGroupOffsets("${KafkaEnv.consumerId}:UIDataComposer")
|
||||
go.partitionsToOffsetAndMetadata().whenComplete { result, throwable ->
|
||||
val partitions = result.entries.filter { it.key.topic() == CommonConfig.kafkaTopic }
|
||||
.map { it.key }
|
||||
val deleteResult = admincli.deleteConsumerGroupOffsets("${KafkaEnv.consumerId}:UIDataComposer", partitions.toSet())
|
||||
deleteResult.all().whenComplete { result, throwable ->
|
||||
kafkaClearedLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
kafkaClearedLatch.countDown()
|
||||
}
|
||||
|
||||
logger.info { "Waiting for kafka to clear offset!" }
|
||||
kafkaClearedLatch.await(5, TimeUnit.MINUTES)
|
||||
logger.info { "Offset cleared!" }
|
||||
Thread.sleep(10000)
|
||||
context = runApplication<UIApplication>(*args)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
10
UI/src/main/kotlin/no/iktdev/streamit/content/ui/UIEnv.kt
Normal file
10
UI/src/main/kotlin/no/iktdev/streamit/content/ui/UIEnv.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package no.iktdev.streamit.content.ui
|
||||
|
||||
import java.io.File
|
||||
|
||||
class UIEnv {
|
||||
companion object {
|
||||
var incomingContent: File = if (!System.getenv("DIRECTORY_CONTENT_INCOMING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_INCOMING")) else File("/src/input")
|
||||
val socketEncoder: String = if (System.getenv("WS_ENCODER").isNullOrBlank()) System.getenv("WS_ENCODER") else "ws://encoder:8080"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package no.iktdev.streamit.content.ui.dto
|
||||
|
||||
enum class SimpleEventDataState {
|
||||
NA,
|
||||
QUEUED,
|
||||
STARTED,
|
||||
ENDED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
fun toSimpleEventDataStateFromStatus(state: String?): SimpleEventDataState {
|
||||
return when(state) {
|
||||
"QUEUED" -> SimpleEventDataState.QUEUED
|
||||
"STARTED" -> SimpleEventDataState.STARTED
|
||||
"UPDATED" -> SimpleEventDataState.STARTED
|
||||
"FAILURE" -> SimpleEventDataState.FAILED
|
||||
"ENDED" -> SimpleEventDataState.ENDED
|
||||
else -> SimpleEventDataState.NA
|
||||
}
|
||||
}
|
||||
|
||||
data class SimpleEventDataObject(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val path: String?,
|
||||
val givenTitle: String? = null,
|
||||
val givenSanitizedName: String? = null,
|
||||
val givenCollection: String? = null,
|
||||
val determinedType: String? = null,
|
||||
val eventEncoded: SimpleEventDataState = SimpleEventDataState.NA,
|
||||
val eventExtracted: SimpleEventDataState = SimpleEventDataState.NA,
|
||||
val eventConverted: SimpleEventDataState = SimpleEventDataState.NA,
|
||||
val eventCollected: SimpleEventDataState = SimpleEventDataState.NA,
|
||||
var encodingProgress: Int? = null,
|
||||
val encodingTimeLeft: Long? = null
|
||||
)
|
||||
|
||||
|
||||
data class EventDataObject(
|
||||
val id: String,
|
||||
var details: Details? = null,
|
||||
var metadata: Metadata? = null,
|
||||
var encode: Encode? = null,
|
||||
var io: IO? = null,
|
||||
var events: List<String> = emptyList()
|
||||
) {
|
||||
fun toSimple() = SimpleEventDataObject(
|
||||
id = id,
|
||||
name = details?.name,
|
||||
path = details?.file,
|
||||
givenTitle = details?.title,
|
||||
givenSanitizedName = details?.sanitizedName,
|
||||
givenCollection = details?.collection,
|
||||
determinedType = details?.type,
|
||||
eventEncoded = toSimpleEventDataStateFromStatus(encode?.state),
|
||||
encodingProgress = encode?.progress,
|
||||
encodingTimeLeft = encode?.timeLeft
|
||||
)
|
||||
}
|
||||
|
||||
data class Details(
|
||||
val name: String,
|
||||
val file: String,
|
||||
var title: String? = null,
|
||||
val sanitizedName: String,
|
||||
var collection: String? = null,
|
||||
var type: String? = null
|
||||
)
|
||||
|
||||
data class Metadata(
|
||||
val source: String
|
||||
)
|
||||
|
||||
interface ProcessableItem {
|
||||
var state: String
|
||||
}
|
||||
|
||||
data class IO(
|
||||
val inputFile: String,
|
||||
val outputFile: String
|
||||
)
|
||||
|
||||
data class Encode(
|
||||
override var state: String,
|
||||
var progress: Int = 0,
|
||||
var timeLeft: Long? = null
|
||||
) : ProcessableItem
|
||||
@ -0,0 +1,9 @@
|
||||
package no.iktdev.streamit.content.ui.dto
|
||||
|
||||
interface ExplorerAttr {
|
||||
val created: Long
|
||||
}
|
||||
|
||||
data class ExplorerAttributes(
|
||||
override val created: Long
|
||||
): ExplorerAttr
|
||||
@ -0,0 +1,19 @@
|
||||
package no.iktdev.streamit.content.ui.dto
|
||||
|
||||
data class ExplorerCursor (
|
||||
val name: String,
|
||||
val path: String,
|
||||
val items: List<ExplorerItem>,
|
||||
)
|
||||
enum class ExplorerItemType {
|
||||
FILE,
|
||||
FOLDER
|
||||
}
|
||||
|
||||
data class ExplorerItem(
|
||||
val name: String,
|
||||
val path: String,
|
||||
val extension: String? = null,
|
||||
val created: Long,
|
||||
val type: ExplorerItemType
|
||||
)
|
||||
@ -0,0 +1,73 @@
|
||||
package no.iktdev.streamit.content.ui.explorer
|
||||
|
||||
import no.iktdev.streamit.content.ui.UIEnv
|
||||
import no.iktdev.streamit.content.ui.dto.*
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.BasicFileAttributeView
|
||||
|
||||
class ExplorerCore {
|
||||
|
||||
fun getCursor(path: String): ExplorerCursor? {
|
||||
val file = File(path)
|
||||
if (!file.exists() || file.isFile) {
|
||||
return null
|
||||
}
|
||||
return ExplorerCursor(
|
||||
name = file.name,
|
||||
path = file.absolutePath,
|
||||
items = getFiles(file) + getFolders(file),
|
||||
)
|
||||
}
|
||||
|
||||
fun fromFile(file: File): ExplorerItem? {
|
||||
if (!file.exists())
|
||||
return null
|
||||
val attr = getAttr(file)
|
||||
return ExplorerItem(
|
||||
path = file.absolutePath,
|
||||
name = file.nameWithoutExtension,
|
||||
extension = file.extension,
|
||||
created = attr.created,
|
||||
type = ExplorerItemType.FILE
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFiles(inDirectory: File): List<ExplorerItem> {
|
||||
return inDirectory.listFiles(FileFilter { it.isFile })?.map {
|
||||
val attr = getAttr(it)
|
||||
ExplorerItem(
|
||||
path = it.absolutePath,
|
||||
name = it.nameWithoutExtension,
|
||||
extension = it.extension,
|
||||
created = attr.created,
|
||||
type = ExplorerItemType.FILE
|
||||
)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getFolders(inDirectory: File): List<ExplorerItem> {
|
||||
return inDirectory.listFiles(FileFilter { it.isDirectory })?.map {
|
||||
val attr = getAttr(it)
|
||||
ExplorerItem(
|
||||
name = it.name,
|
||||
path = it.absolutePath,
|
||||
created = attr.created,
|
||||
type = ExplorerItemType.FOLDER
|
||||
)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getAttr(item: File): ExplorerAttributes {
|
||||
val attrView = Files.getFileAttributeView(item.toPath(), BasicFileAttributeView::class.java).readAttributes()
|
||||
return ExplorerAttributes(
|
||||
created = attrView.creationTime().toMillis()
|
||||
)
|
||||
}
|
||||
|
||||
fun getHomeCursor(): ExplorerCursor? {
|
||||
return getCursor(UIEnv.incomingContent.absolutePath)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package no.iktdev.streamit.content.ui.kafka
|
||||
|
||||
import com.google.gson.Gson
|
||||
import mu.KotlinLogging
|
||||
import no.iktdev.streamit.content.common.CommonConfig
|
||||
import no.iktdev.streamit.content.common.DefaultKafkaReader
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.kafka.converter.EventDataConverter
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import no.iktdev.streamit.library.kafka.listener.ManualAcknowledgeMessageListener
|
||||
import no.iktdev.streamit.library.kafka.listener.SimpleMessageListener
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.kafka.listener.ContainerProperties
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class EventConsumer: DefaultKafkaReader() {
|
||||
@Autowired private lateinit var converter: EventDataConverter
|
||||
companion object {
|
||||
val idAndEvents: MutableMap<String, MutableMap<String, Message>> = mutableMapOf()
|
||||
}
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
private final val listener = object : ManualAcknowledgeMessageListener(
|
||||
topic = CommonConfig.kafkaTopic,
|
||||
consumer = defaultConsumer,
|
||||
accepts = listOf()
|
||||
) {
|
||||
override fun onMessageReceived(data: ConsumerRecord<String, Message>) {
|
||||
applyUpdate(data.value().referenceId, data.key(), data.value())
|
||||
log.info { data.key() + Gson().toJson(data.value()) }
|
||||
converter.convertEventToObject(data.value().referenceId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyUpdate(referenceId: String, eventKey: String, value: Message) {
|
||||
val existingData = idAndEvents[referenceId] ?: mutableMapOf()
|
||||
existingData[eventKey] = value
|
||||
idAndEvents[referenceId] = existingData
|
||||
}
|
||||
|
||||
init {
|
||||
defaultConsumer.autoCommit = false
|
||||
defaultConsumer.ackModeOverride = ContainerProperties.AckMode.MANUAL
|
||||
listener.listen()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.memActiveEventMap
|
||||
import no.iktdev.streamit.content.ui.kafka.EventConsumer
|
||||
import no.iktdev.streamit.content.ui.memSimpleConvertedEventsMap
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EventDataConverter {
|
||||
|
||||
@Autowired private lateinit var detailsConverter: EventDataDetailsSubConverter
|
||||
@Autowired private lateinit var encodeConverter: EventDataEncodeSubConverter
|
||||
@Autowired private lateinit var metadataConverter: EventDataMetadataSubConverter
|
||||
@Autowired private lateinit var fileNameConverter: EventDataFilenameAndTypeDeterminerSubConverter
|
||||
|
||||
fun convertEventToObject(eventReferenceId: String) {
|
||||
val data = memActiveEventMap[eventReferenceId] ?: EventDataObject(id = eventReferenceId)
|
||||
val collection = EventConsumer.idAndEvents[eventReferenceId] ?: emptyMap<String, Message>()
|
||||
|
||||
detailsConverter.convertAndUpdate(data, collection.toMap())
|
||||
encodeConverter.convertAndUpdate(data, collection.toMap())
|
||||
metadataConverter.convertAndUpdate(data, collection.toMap())
|
||||
fileNameConverter.convertAndUpdate(data, collection.toMap())
|
||||
|
||||
|
||||
memActiveEventMap[eventReferenceId] = data
|
||||
memSimpleConvertedEventsMap[eventReferenceId] = data.toSimple()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.common.deserializers.DeserializerRegistry
|
||||
import no.iktdev.streamit.content.common.dto.reader.FileResult
|
||||
import no.iktdev.streamit.content.ui.dto.Details
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.library.kafka.KafkaEvents
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EventDataDetailsSubConverter : EventDataSubConverterBase() {
|
||||
override fun convertEvents(eventData: EventDataObject, events: Map<String, Message>) {
|
||||
val event = events.entries
|
||||
.asSequence()
|
||||
.filter { it.key == KafkaEvents.EVENT_READER_RECEIVED_FILE.event }
|
||||
.filter { it.value.isSuccessful() }
|
||||
.map { DeserializerRegistry.getDeserializerForEvent(it.key)?.deserialize(it.value) }
|
||||
.filterIsInstance<FileResult>()
|
||||
.lastOrNull() ?: return
|
||||
|
||||
val deserialized = Details(
|
||||
name = event.title,
|
||||
file = event.file,
|
||||
sanitizedName = event.sanitizedName
|
||||
)
|
||||
eventData.details = deserialized
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.common.deserializers.DeserializerRegistry
|
||||
import no.iktdev.streamit.content.common.dto.State
|
||||
import no.iktdev.streamit.content.common.dto.reader.work.EncodeWork
|
||||
import no.iktdev.streamit.content.ui.dto.Encode
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.dto.IO
|
||||
import no.iktdev.streamit.library.kafka.KafkaEvents
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EventDataEncodeSubConverter : EventDataSubConverterBase() {
|
||||
override fun convertEvents(eventData: EventDataObject, events: Map<String, Message>) {
|
||||
val filteredEvents = events.entries
|
||||
.asSequence()
|
||||
.filter {
|
||||
listOf(
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_QUEUED.event,
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_STARTED.event,
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_ENDED.event
|
||||
).contains(it.key)
|
||||
}
|
||||
|
||||
val event = filteredEvents
|
||||
.map { DeserializerRegistry.getDeserializerForEvent(it.key)?.deserialize(it.value) }
|
||||
.filterIsInstance<EncodeWork>()
|
||||
.lastOrNull() ?: return
|
||||
|
||||
event.let {
|
||||
eventData.details?.apply { this.collection = it.collection }
|
||||
}
|
||||
|
||||
eventData.io = IO(event.inFile, event.outFile)
|
||||
eventData.encode =
|
||||
if (eventData.encode != null)
|
||||
eventData.encode?.apply { state = getState(filteredEvents).name }
|
||||
else
|
||||
Encode(state = getState(filteredEvents).name)
|
||||
|
||||
}
|
||||
|
||||
private fun getState(events: Sequence<Map.Entry<String, Message>>): State {
|
||||
val last = events.lastOrNull()
|
||||
?: return State.QUEUED
|
||||
if (!last.value.isSuccessful()) return State.FAILURE
|
||||
return when (last.key) {
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_STARTED.event -> State.STARTED
|
||||
KafkaEvents.EVENT_ENCODER_VIDEO_FILE_ENDED.event -> State.ENDED
|
||||
else -> State.QUEUED
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.common.deserializers.DeserializerRegistry
|
||||
import no.iktdev.streamit.content.common.dto.ContentOutName
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.library.kafka.KafkaEvents
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class EventDataFilenameAndTypeDeterminerSubConverter : EventDataSubConverterBase() {
|
||||
override fun convertEvents(eventData: EventDataObject, events: Map<String, Message>) {
|
||||
|
||||
val convertedFileNameEvent = events.entries
|
||||
.asSequence()
|
||||
.filter { it.key == KafkaEvents.EVENT_READER_DETERMINED_FILENAME.event }
|
||||
.filter { it.value.isSuccessful() }
|
||||
.map { DeserializerRegistry.getDeserializerForEvent(it.key)?.deserialize(it.value) }
|
||||
.filterIsInstance<ContentOutName>()
|
||||
.lastOrNull() ?: return
|
||||
|
||||
val convertedType = events.entries
|
||||
.asSequence()
|
||||
.filter { it -> listOf(KafkaEvents.EVENT_READER_DETERMINED_SERIE.event, KafkaEvents.EVENT_READER_DETERMINED_MOVIE.event).contains(it.key) }
|
||||
.lastOrNull()
|
||||
?.toPair()
|
||||
val type = when(convertedType?.first) {
|
||||
KafkaEvents.EVENT_READER_DETERMINED_SERIE.event -> "serie"
|
||||
KafkaEvents.EVENT_READER_DETERMINED_MOVIE.event -> "movie"
|
||||
else -> null
|
||||
}
|
||||
|
||||
eventData.details = eventData.details?.apply {
|
||||
this.title = convertedFileNameEvent.baseName
|
||||
this.type = type
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
|
||||
@Component
|
||||
class EventDataMetadataSubConverter: EventDataSubConverterBase() {
|
||||
override fun convertEvents(eventData: EventDataObject, events: Map<String, Message>) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package no.iktdev.streamit.content.ui.kafka.converter
|
||||
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.dto.SimpleEventDataObject
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
|
||||
abstract class EventDataSubConverterBase {
|
||||
|
||||
protected abstract fun convertEvents(eventData: EventDataObject, events: Map<String, Message>)
|
||||
|
||||
fun convertAndUpdate(eventData: EventDataObject, events: Map<String, Message>) {
|
||||
try {
|
||||
convertEvents(eventData, events)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package no.iktdev.streamit.content.ui.service
|
||||
|
||||
import dev.vishna.watchservice.KWatchEvent
|
||||
import dev.vishna.watchservice.asWatchChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import no.iktdev.exfl.coroutines.Coroutines
|
||||
import no.iktdev.streamit.content.common.CommonConfig
|
||||
import no.iktdev.streamit.content.ui.explorer.ExplorerCore
|
||||
import no.iktdev.streamit.content.ui.fileRegister
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
@Service
|
||||
class FileRegisterService {
|
||||
val watcherChannel = CommonConfig.incomingContent.asWatchChannel()
|
||||
val core = ExplorerCore()
|
||||
|
||||
fun fid(name: String): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
return BigInteger(1, md.digest(name.toByteArray())).toString(16).padStart(32, '0')
|
||||
}
|
||||
|
||||
private fun addFileToIndex(it: KWatchEvent) {
|
||||
core.fromFile(it.file)?.let { info ->
|
||||
val fid = fid(it.file.name)
|
||||
fileRegister.put(fid, info)
|
||||
}
|
||||
}
|
||||
private fun indexItemsInFolder(it: File) {
|
||||
|
||||
}
|
||||
|
||||
private fun indexItems() {
|
||||
|
||||
}
|
||||
|
||||
init {
|
||||
Coroutines.io().launch {
|
||||
|
||||
}
|
||||
Coroutines.io().launch {
|
||||
watcherChannel.consumeEach {
|
||||
when (it.kind) {
|
||||
KWatchEvent.Kind.Created, KWatchEvent.Kind.Modified, KWatchEvent.Kind.Initialized -> {
|
||||
if (it.file.isDirectory) {
|
||||
indexItemsInFolder(it.file)
|
||||
} else {
|
||||
addFileToIndex(it)
|
||||
}
|
||||
}
|
||||
KWatchEvent.Kind.Deleted -> {
|
||||
val fid = fid(it.file.name)
|
||||
fileRegister.remove(fid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package no.iktdev.streamit.content.ui.socket
|
||||
|
||||
import no.iktdev.streamit.content.ui.explorer.ExplorerCore
|
||||
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
|
||||
|
||||
@Controller
|
||||
class ExplorerTopic(
|
||||
@Autowired private val template: SimpMessagingTemplate?,
|
||||
val explorer: ExplorerCore = ExplorerCore()
|
||||
): TopicSupport() {
|
||||
|
||||
@MessageMapping("/explorer/home")
|
||||
fun goHome() {
|
||||
explorer.getHomeCursor()?.let {
|
||||
template?.convertAndSend("/topic/explorer/go", it)
|
||||
}
|
||||
}
|
||||
|
||||
@MessageMapping("/explorer/navigate")
|
||||
fun navigateTo(@Payload path: String) {
|
||||
val cursor = explorer.getCursor(path)
|
||||
cursor?.let {
|
||||
template?.convertAndSend("/topic/explorer/go", it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package no.iktdev.streamit.content.ui.socket
|
||||
|
||||
import no.iktdev.streamit.content.common.CommonConfig
|
||||
import no.iktdev.streamit.library.kafka.KafkaEvents
|
||||
import no.iktdev.streamit.library.kafka.dto.Message
|
||||
import no.iktdev.streamit.library.kafka.dto.Status
|
||||
import no.iktdev.streamit.library.kafka.dto.StatusType
|
||||
import no.iktdev.streamit.library.kafka.producer.DefaultProducer
|
||||
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 java.io.File
|
||||
|
||||
@Controller
|
||||
class RequestTopic(
|
||||
@Autowired private val template: SimpMessagingTemplate?
|
||||
) {
|
||||
val messageProducer = DefaultProducer(CommonConfig.kafkaTopic)
|
||||
|
||||
@MessageMapping("/request/start")
|
||||
fun requestStartOn(@Payload fullName: String) {
|
||||
val file = File(fullName)
|
||||
if (file.exists()) {
|
||||
try {
|
||||
val message = Message(
|
||||
status = Status(StatusType.SUCCESS),
|
||||
data = fullName
|
||||
)
|
||||
messageProducer.sendMessage(KafkaEvents.REQUEST_FILE_READ.event, message)
|
||||
template?.convertAndSend("/response/request", RequestResponse(true, fullName))
|
||||
|
||||
} catch (e: Exception) {
|
||||
template?.convertAndSend("/response/request", RequestResponse(false, fullName))
|
||||
|
||||
}
|
||||
} else {
|
||||
template?.convertAndSend("/response/request", RequestResponse(false, fullName))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
data class RequestResponse(
|
||||
val success: Boolean,
|
||||
val file: String
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
package no.iktdev.streamit.content.ui.socket
|
||||
|
||||
import com.google.gson.Gson
|
||||
|
||||
abstract class TopicSupport {
|
||||
|
||||
fun toJson(item: Any?): String? {
|
||||
return if (item != null) Gson().toJson(item) else null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package no.iktdev.streamit.content.ui.socket
|
||||
|
||||
import mu.KotlinLogging
|
||||
import no.iktdev.exfl.observable.ObservableMap
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.dto.SimpleEventDataObject
|
||||
import no.iktdev.streamit.content.ui.memActiveEventMap
|
||||
import no.iktdev.streamit.content.ui.memSimpleConvertedEventsMap
|
||||
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 UISocketService(
|
||||
@Autowired private val template: SimpMessagingTemplate?
|
||||
) {
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
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!" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@MessageMapping("/items")
|
||||
fun sendItems() {
|
||||
template?.convertAndSend("/topic/event/items", memActiveEventMap.values.reversed())
|
||||
template?.convertAndSend("/topic/event/flat", memSimpleConvertedEventsMap.values.reversed())
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package no.iktdev.streamit.content.ui.socket.internal
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import mu.KotlinLogging
|
||||
import no.iktdev.streamit.content.common.dto.WorkOrderItem
|
||||
import no.iktdev.streamit.content.ui.UIEnv
|
||||
import no.iktdev.streamit.content.ui.dto.EventDataObject
|
||||
import no.iktdev.streamit.content.ui.dto.SimpleEventDataObject
|
||||
import no.iktdev.streamit.content.ui.memActiveEventMap
|
||||
import no.iktdev.streamit.content.ui.memSimpleConvertedEventsMap
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
13
UI/src/main/resources/application.properties
Normal file
13
UI/src/main/resources/application.properties
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
#logging.level.org.springframework=INFO
|
||||
#logging.level.root=INFO
|
||||
|
||||
spring.output.ansi.enabled=always
|
||||
logging.level.org.apache.kafka=INFO
|
||||
logging.level.org.springframework.web.socket.config.WebSocketMessageBrokerStats = INFO
|
||||
spring.cloud.stream.kafka.binder.replication-factor=1
|
||||
logging.level.org.springframework.messaging.simp=INFO
|
||||
#spring.kafka.bootstrap-servers=192.168.2.250:19092
|
||||
|
||||
management.endpoints.web.exposure.include=health
|
||||
|
||||
23
UI/web/.gitignore
vendored
Normal file
23
UI/web/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
46
UI/web/README.md
Normal file
46
UI/web/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
31559
UI/web/package-lock.json
generated
Normal file
31559
UI/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
UI/web/package.json
Normal file
64
UI/web/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.9",
|
||||
"@mui/material": "^5.14.10",
|
||||
"@mui/x-data-grid": "^6.15.0",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.38",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/styled-components": "^5.1.27",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-stomp": "^5.1.0",
|
||||
"react-stomp-hooks": "^2.1.0",
|
||||
"react-use-websocket": "^4.4.0",
|
||||
"redux": "^4.2.1",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"stompjs": "^2.3.3",
|
||||
"styled-components": "^6.0.8",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/stompjs": "^2.3.5"
|
||||
}
|
||||
}
|
||||
BIN
UI/web/public/favicon.ico
Normal file
BIN
UI/web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
UI/web/public/index.html
Normal file
43
UI/web/public/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
UI/web/public/logo192.png
Normal file
BIN
UI/web/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
UI/web/public/logo512.png
Normal file
BIN
UI/web/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
UI/web/public/manifest.json
Normal file
25
UI/web/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
UI/web/public/robots.txt
Normal file
3
UI/web/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
0
UI/web/src/App.css
Normal file
0
UI/web/src/App.css
Normal file
9
UI/web/src/App.test.tsx
Normal file
9
UI/web/src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
73
UI/web/src/App.tsx
Normal file
73
UI/web/src/App.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import { Box, CssBaseline } from '@mui/material';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import Footer from './app/features/footer';
|
||||
import LaunchPage from './app/page/LaunchPage';
|
||||
import { useWsSubscription } from './app/ws/subscriptions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useStompClient, useSubscription } from 'react-stomp-hooks';
|
||||
import { updateItems } from './app/store/composed-slice';
|
||||
import ExplorePage from './app/page/ExplorePage';
|
||||
import { ThemeProvider } from '@mui/material';
|
||||
import theme from './theme';
|
||||
import { simpleEventsUpdate } from './app/store/kafka-items-flat-slice';
|
||||
import { EventDataObject, SimpleEventDataObject } from './types';
|
||||
|
||||
function App() {
|
||||
const client = useStompClient();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useWsSubscription<Array<EventDataObject>>("/topic/event/items", (response) => {
|
||||
dispatch(updateItems(response))
|
||||
});
|
||||
|
||||
useWsSubscription<Array<SimpleEventDataObject>>("/topic/event/flat", (response) => {
|
||||
dispatch(simpleEventsUpdate(response))
|
||||
});
|
||||
|
||||
|
||||
const testButton = () => {
|
||||
client?.publish({
|
||||
"destination": "/app/items",
|
||||
"body": "Potato"
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Kjør din funksjon her når komponenten lastes inn for første gang
|
||||
// Sjekk om cursor er null
|
||||
|
||||
// Kjør din funksjon her når cursor er null og client ikke er null
|
||||
client?.publish({
|
||||
destination: "/app/items",
|
||||
body: undefined
|
||||
})
|
||||
|
||||
// Alternativt, du kan dispatche en Redux handling her
|
||||
// dispatch(fetchDataAction()); // Eksempel på å dispatche en handling
|
||||
|
||||
}, [client, dispatch]);
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<BrowserRouter>
|
||||
<Box sx={{ marginTop: "70px", minHeight: "50vh", width: "100%", display: "block", overflow: "hidden" }}>
|
||||
<button onClick={testButton}>Click me</button>
|
||||
<Routes>
|
||||
<Route path='/files' element={<ExplorePage />} />
|
||||
<Route path='/' element={<LaunchPage />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
69
UI/web/src/app/features/CategorySidebar.tsx
Normal file
69
UI/web/src/app/features/CategorySidebar.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import InboxIcon from '@mui/icons-material/Inbox';
|
||||
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const SidebarWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const DrawerWrapper = styled(Drawer)({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
const DrawerPaperWrapper = styled('div')({
|
||||
width: drawerWidth,
|
||||
});
|
||||
|
||||
const ContentWrapper = styled('div')({
|
||||
flexGrow: 1,
|
||||
padding: '16px',
|
||||
});
|
||||
|
||||
export default function Sidebar() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarWrapper>
|
||||
<DrawerWrapper
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
PaperProps={{
|
||||
sx: { width: drawerWidth },
|
||||
}}
|
||||
>
|
||||
<DrawerPaperWrapper />
|
||||
<List>
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Incoming" />
|
||||
</ListItem>
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<LibraryBooksIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Library" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</DrawerWrapper>
|
||||
<ContentWrapper>
|
||||
{/* Main content */}
|
||||
</ContentWrapper>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
||||
9
UI/web/src/app/features/footer.tsx
Normal file
9
UI/web/src/app/features/footer.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<>
|
||||
<div></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
6
UI/web/src/app/hooks.ts
Normal file
6
UI/web/src/app/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
22
UI/web/src/app/store.ts
Normal file
22
UI/web/src/app/store.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
|
||||
import composedSlice from './store/composed-slice';
|
||||
import explorerSlice from './store/explorer-slice';
|
||||
import kafkaItemsFlatSlice from './store/kafka-items-flat-slice';
|
||||
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
composed: composedSlice,
|
||||
explorer: explorerSlice,
|
||||
kafkaComposedFlat: kafkaItemsFlatSlice
|
||||
},
|
||||
});
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>;
|
||||
57
UI/web/src/app/ws/client.ts
Normal file
57
UI/web/src/app/ws/client.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as Stomp from 'stompjs';
|
||||
import SockJS from 'sockjs-client';
|
||||
|
||||
export class WebSocketClient {
|
||||
private stompClient: Stomp.Client | undefined;
|
||||
private subscriptions: { [id: string]: Stomp.Subscription } = {};
|
||||
private isConnecting = false;
|
||||
private connectCallbacks: (() => void)[] = [];
|
||||
private wsUrl: string | undefined;
|
||||
|
||||
public connect(wsUrl: string) {
|
||||
this.wsUrl = wsUrl;
|
||||
if (this.isConnecting) {
|
||||
return;
|
||||
}
|
||||
this.isConnecting = true;
|
||||
const socket = new SockJS(this.wsUrl);
|
||||
this.stompClient = Stomp.over(socket);
|
||||
this.stompClient.connect({}, (frame) => {
|
||||
this.isConnecting = false;
|
||||
this.connectCallbacks.forEach((cb) => cb());
|
||||
});
|
||||
}
|
||||
|
||||
public subscribe<T>(path: string, processData: (data: T) => void) {
|
||||
const subscription = this.stompClient?.subscribe(path, (data) => {
|
||||
const converted = JSON.parse(data.body) as T;
|
||||
processData(converted);
|
||||
});
|
||||
if (subscription !== undefined) {
|
||||
this.subscriptions[path] = subscription;
|
||||
}
|
||||
}
|
||||
|
||||
public unsubscribe(path: string) {
|
||||
const subscription = this.subscriptions[path];
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
delete this.subscriptions[path];
|
||||
}
|
||||
}
|
||||
|
||||
public onConnect(callback: () => void) {
|
||||
if (this.isConnecting) {
|
||||
this.connectCallbacks.push(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.stompClient?.disconnect(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const webSocketClient = new WebSocketClient();
|
||||
18
UI/web/src/app/ws/subscriptions.ts
Normal file
18
UI/web/src/app/ws/subscriptions.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { IMessage } from "@stomp/stompjs/esm6";
|
||||
import { useEffect } from 'react';
|
||||
import { webSocketClient } from './client';
|
||||
import { useSubscription } from "react-stomp-hooks";
|
||||
|
||||
|
||||
|
||||
export function useWsSubscription<T>(path: string, processData: (data: T) => void,
|
||||
) {
|
||||
return useSubscription(path, (payload: IMessage) => {
|
||||
const converted = toType<T>(payload);
|
||||
processData(converted)
|
||||
})
|
||||
}
|
||||
|
||||
function toType<T>(data: IMessage): T {
|
||||
return JSON.parse(data.body) as T;
|
||||
}
|
||||
15
UI/web/src/index.css
Normal file
15
UI/web/src/index.css
Normal file
@ -0,0 +1,15 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
|
||||
}
|
||||
42
UI/web/src/index.tsx
Normal file
42
UI/web/src/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import { store } from './app/store';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { Provider } from 'react-redux';
|
||||
import { StompSessionProvider } from 'react-stomp-hooks';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<StompSessionProvider url={"http://localhost:8080/ws"} connectHeaders={{}} logRawCommunication={true}
|
||||
debug={(str) => {
|
||||
if (str === "Opening Web Socket...") {
|
||||
}
|
||||
console.log(str);
|
||||
}}
|
||||
onUnhandledMessage={(val) => {
|
||||
console.log(val)
|
||||
}}
|
||||
onStompError={(val) => {
|
||||
console.log(val)
|
||||
}}
|
||||
onChangeState={(val) => {
|
||||
console.log(val)
|
||||
}}
|
||||
|
||||
>
|
||||
<App />
|
||||
</StompSessionProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
1
UI/web/src/logo.svg
Normal file
1
UI/web/src/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
UI/web/src/react-app-env.d.ts
vendored
Normal file
1
UI/web/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
UI/web/src/reportWebVitals.ts
Normal file
15
UI/web/src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
UI/web/src/setupTests.ts
Normal file
5
UI/web/src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
26
UI/web/tsconfig.json
Normal file
26
UI/web/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user