feature clients
new frontend
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
js {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmToolchain(21)
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
// Feature modules
|
||||||
|
implementation(project(":clients:ping-feature"))
|
||||||
|
|
||||||
|
// Shared modules
|
||||||
|
implementation(project(":clients:shared:common-ui"))
|
||||||
|
implementation(project(":clients:shared:navigation"))
|
||||||
|
|
||||||
|
// Compose dependencies
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
|
||||||
|
// ViewModel lifecycle
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package at.mocode.clients.app
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import at.mocode.clients.shared.commonui.components.AppHeader
|
||||||
|
import at.mocode.clients.shared.commonui.components.AppScaffold
|
||||||
|
import at.mocode.clients.shared.commonui.theme.AppTheme
|
||||||
|
import at.mocode.clients.shared.navigation.AppScreen
|
||||||
|
import at.mocode.clients.pingfeature.PingScreen
|
||||||
|
import at.mocode.clients.pingfeature.PingViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun App() {
|
||||||
|
var currentScreen: AppScreen by remember { mutableStateOf(AppScreen.Home) }
|
||||||
|
|
||||||
|
AppTheme {
|
||||||
|
AppScaffold(
|
||||||
|
header = {
|
||||||
|
AppHeader(
|
||||||
|
title = "Meldestelle",
|
||||||
|
onNavigateToPing = { currentScreen = AppScreen.Ping }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(modifier = Modifier.padding(paddingValues)) {
|
||||||
|
when (currentScreen) {
|
||||||
|
is AppScreen.Home -> {
|
||||||
|
LandingScreen()
|
||||||
|
}
|
||||||
|
is AppScreen.Ping -> {
|
||||||
|
val pingViewModel: PingViewModel = viewModel()
|
||||||
|
PingScreen(viewModel = pingViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package at.mocode.clients.app
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LandingScreen() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Willkommen bei Meldestelle",
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Eine moderne, skalierbare Frontend-Architektur",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Diese Anwendung demonstriert eine \"Shell + Feature-Module\"-Architektur " +
|
||||||
|
"basierend auf Kotlin Multiplatform. Sie spiegelt die DDD-Struktur des Backends " +
|
||||||
|
"wider und ist als native Desktop-Anwendung (JVM) und Web-Anwendung (JS/Wasm) lauffähig.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "🚀 Technologien:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
TechItem("Kotlin Multiplatform")
|
||||||
|
TechItem("Jetpack Compose Multiplatform")
|
||||||
|
TechItem("Material Design 3")
|
||||||
|
TechItem("Ktor Client")
|
||||||
|
TechItem("Domain-Driven Design")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Verwenden Sie das Ping Service Menü oben, um die API-Funktionalität zu testen.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TechItem(text: String) {
|
||||||
|
Text(
|
||||||
|
text = "• $text",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.window.ComposeViewport
|
||||||
|
import at.mocode.clients.app.App
|
||||||
|
import kotlinx.browser.document
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun main() {
|
||||||
|
ComposeViewport(document.body!!) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import at.mocode.clients.app.App
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "Meldestelle - Desktop Application"
|
||||||
|
) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.window.CanvasBasedWindow
|
||||||
|
import at.mocode.clients.app.App
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun main() {
|
||||||
|
CanvasBasedWindow("Meldestelle - WASM Application") {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
const val SERVER_PORT = 8081
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
class Greeting {
|
|
||||||
private val platform = getPlatform()
|
|
||||||
|
|
||||||
fun greet(): String {
|
|
||||||
return "Hello, ${platform.name}!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
interface Platform {
|
|
||||||
val name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
expect fun getPlatform(): Platform
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package at.mocode.model
|
|
||||||
|
|
||||||
// Deprecated local DTOs are replaced by typealiases to the shared API contract.
|
|
||||||
// This preserves binary/source compatibility for existing imports while enforcing SSoT.
|
|
||||||
|
|
||||||
typealias PingResponse = at.mocode.ping.api.PingResponse
|
|
||||||
|
|
||||||
typealias EnhancedPingResponse = at.mocode.ping.api.EnhancedPingResponse
|
|
||||||
|
|
||||||
typealias HealthResponse = at.mocode.ping.api.HealthResponse
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package at.mocode.ping.client
|
|
||||||
|
|
||||||
import at.mocode.ping.api.EnhancedPingResponse
|
|
||||||
import at.mocode.ping.api.HealthResponse
|
|
||||||
import at.mocode.ping.api.PingApi
|
|
||||||
import at.mocode.ping.api.PingResponse
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.call.body
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.request.parameter
|
|
||||||
import at.mocode.service.getBaseUrl
|
|
||||||
|
|
||||||
class PingApiClient(
|
|
||||||
private val client: HttpClient,
|
|
||||||
baseUrl: String = getBaseUrl()
|
|
||||||
) : PingApi {
|
|
||||||
private val base = "$baseUrl/api/ping"
|
|
||||||
|
|
||||||
override suspend fun simplePing(): PingResponse = client.get("$base/simple").body()
|
|
||||||
|
|
||||||
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse =
|
|
||||||
client.get("$base/enhanced") { parameter("simulate", simulate) }.body()
|
|
||||||
|
|
||||||
override suspend fun healthCheck(): HealthResponse = client.get("$base/health").body()
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package at.mocode.service
|
|
||||||
|
|
||||||
import at.mocode.model.EnhancedPingResponse
|
|
||||||
import at.mocode.model.HealthResponse
|
|
||||||
import at.mocode.model.PingResponse
|
|
||||||
import at.mocode.ping.client.PingApiClient
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.plugins.*
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
@Deprecated("Use PingApiClient directly for new code")
|
|
||||||
class PingService(
|
|
||||||
private val client: HttpClient = HttpClient {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
isLenient = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
install(HttpTimeout) {
|
|
||||||
requestTimeoutMillis = 10000
|
|
||||||
connectTimeoutMillis = 5000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
private val api = PingApiClient(client)
|
|
||||||
|
|
||||||
suspend fun ping(): Result<PingResponse> = runCatching { api.simplePing() }
|
|
||||||
|
|
||||||
suspend fun enhancedPing(simulate: Boolean = false): Result<EnhancedPingResponse> =
|
|
||||||
runCatching { api.enhancedPing(simulate) }
|
|
||||||
|
|
||||||
suspend fun health(): Result<HealthResponse> = runCatching { api.healthCheck() }
|
|
||||||
|
|
||||||
suspend fun testFailure(): Result<EnhancedPingResponse> = runCatching {
|
|
||||||
throw RuntimeException("Simulated failure for testing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform-specific base URL required by PingApiClient via getBaseUrl()
|
|
||||||
expect fun getBaseUrl(): String
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class SharedCommonTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun example() {
|
|
||||||
assertEquals(3, 1 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
class JsPlatform: Platform {
|
|
||||||
override val name: String = "Web with Kotlin/JS"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = JsPlatform()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package at.mocode.service
|
|
||||||
|
|
||||||
// Use direct ping-service for JS Development - based on central.toml
|
|
||||||
actual fun getBaseUrl(): String = "http://localhost:8082"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
class JVMPlatform: Platform {
|
|
||||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = JVMPlatform()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package at.mocode.service
|
|
||||||
|
|
||||||
// Use direct ping-service for JVM (Desktop) - based on central.toml
|
|
||||||
actual fun getBaseUrl(): String = "http://localhost:8082"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package at.mocode
|
|
||||||
|
|
||||||
class WasmPlatform: Platform {
|
|
||||||
override val name: String = "Web with Kotlin/Wasm"
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = WasmPlatform()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package at.mocode.service
|
|
||||||
|
|
||||||
// Use direct ping-service for WASM Development - based on central.toml
|
|
||||||
actual fun getBaseUrl(): String = "http://localhost:8082"
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "at.mocode"
|
group = "at.mocode.clients"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -11,25 +13,35 @@ kotlin {
|
|||||||
js {
|
js {
|
||||||
browser()
|
browser()
|
||||||
}
|
}
|
||||||
// Keep WASM for dev since sources already present
|
|
||||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
|
||||||
wasmJs {
|
|
||||||
browser()
|
|
||||||
}
|
|
||||||
|
|
||||||
jvmToolchain(21)
|
jvmToolchain(21)
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Contract from backend
|
||||||
implementation(projects.services.ping.pingApi)
|
implementation(projects.services.ping.pingApi)
|
||||||
|
|
||||||
|
// UI Kit
|
||||||
|
implementation(project(":clients:shared:common-ui"))
|
||||||
|
|
||||||
|
// Compose dependencies
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
|
||||||
|
// Ktor client for HTTP calls
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.contentNegotiation)
|
implementation(libs.ktor.client.contentNegotiation)
|
||||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||||
|
|
||||||
|
// Coroutines and serialization
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
// ViewModel lifecycle
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package at.mocode.clients.pingfeature
|
||||||
|
|
||||||
|
import at.mocode.ping.api.PingApi
|
||||||
|
import at.mocode.ping.api.PingResponse
|
||||||
|
import at.mocode.ping.api.EnhancedPingResponse
|
||||||
|
import at.mocode.ping.api.HealthResponse
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class PingApiClient(
|
||||||
|
private val baseUrl: String = "http://localhost:8080"
|
||||||
|
) : PingApi {
|
||||||
|
|
||||||
|
private val client = HttpClient {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun simplePing(): PingResponse {
|
||||||
|
return client.get("$baseUrl/api/ping/simple").body()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun enhancedPing(simulate: Boolean): EnhancedPingResponse {
|
||||||
|
return client.get("$baseUrl/api/ping/enhanced") {
|
||||||
|
parameter("simulate", simulate)
|
||||||
|
}.body()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun healthCheck(): HealthResponse {
|
||||||
|
return client.get("$baseUrl/api/ping/health").body()
|
||||||
|
}
|
||||||
|
}
|
||||||
+187
@@ -0,0 +1,187 @@
|
|||||||
|
package at.mocode.clients.pingfeature
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PingScreen(viewModel: PingViewModel) {
|
||||||
|
val uiState = viewModel.uiState
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Ping Service",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.performSimplePing() },
|
||||||
|
enabled = !uiState.isLoading,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Simple Ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.performEnhancedPing() },
|
||||||
|
enabled = !uiState.isLoading,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Enhanced Ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.performHealthCheck() },
|
||||||
|
enabled = !uiState.isLoading,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Health Check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error message
|
||||||
|
uiState.errorMessage?.let { error ->
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Error",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.clearError() }
|
||||||
|
) {
|
||||||
|
Text("Dismiss")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Ping Response
|
||||||
|
uiState.simplePingResponse?.let { response ->
|
||||||
|
ResponseCard(
|
||||||
|
title = "Simple Ping Response",
|
||||||
|
status = response.status,
|
||||||
|
timestamp = response.timestamp,
|
||||||
|
service = response.service
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced Ping Response
|
||||||
|
uiState.enhancedPingResponse?.let { response ->
|
||||||
|
ResponseCard(
|
||||||
|
title = "Enhanced Ping Response",
|
||||||
|
status = response.status,
|
||||||
|
timestamp = response.timestamp,
|
||||||
|
service = response.service,
|
||||||
|
additionalInfo = mapOf(
|
||||||
|
"Circuit Breaker State" to response.circuitBreakerState,
|
||||||
|
"Response Time" to "${response.responseTime}ms"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health Response
|
||||||
|
uiState.healthResponse?.let { response ->
|
||||||
|
ResponseCard(
|
||||||
|
title = "Health Check Response",
|
||||||
|
status = response.status,
|
||||||
|
timestamp = response.timestamp,
|
||||||
|
service = response.service,
|
||||||
|
additionalInfo = mapOf(
|
||||||
|
"Healthy" to response.healthy.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ResponseCard(
|
||||||
|
title: String,
|
||||||
|
status: String,
|
||||||
|
timestamp: String,
|
||||||
|
service: String,
|
||||||
|
additionalInfo: Map<String, String> = emptyMap()
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
InfoRow("Status", status)
|
||||||
|
InfoRow("Timestamp", timestamp)
|
||||||
|
InfoRow("Service", service)
|
||||||
|
|
||||||
|
additionalInfo.forEach { (key, value) ->
|
||||||
|
InfoRow(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(label: String, value: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$label:",
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(text = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
package at.mocode.clients.pingfeature
|
||||||
|
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.mocode.ping.api.PingResponse
|
||||||
|
import at.mocode.ping.api.EnhancedPingResponse
|
||||||
|
import at.mocode.ping.api.HealthResponse
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
data class PingUiState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val simplePingResponse: PingResponse? = null,
|
||||||
|
val enhancedPingResponse: EnhancedPingResponse? = null,
|
||||||
|
val healthResponse: HealthResponse? = null,
|
||||||
|
val errorMessage: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class PingViewModel : ViewModel() {
|
||||||
|
private val apiClient = PingApiClient()
|
||||||
|
|
||||||
|
var uiState by mutableStateOf(PingUiState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun performSimplePing() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||||
|
try {
|
||||||
|
val response = apiClient.simplePing()
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
simplePingResponse = response
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = "Simple ping failed: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performEnhancedPing(simulate: Boolean = false) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||||
|
try {
|
||||||
|
val response = apiClient.enhancedPing(simulate)
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
enhancedPingResponse = response
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = "Enhanced ping failed: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performHealthCheck() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
uiState = uiState.copy(isLoading = true, errorMessage = null)
|
||||||
|
try {
|
||||||
|
val response = apiClient.healthCheck()
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
healthResponse = response
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
isLoading = false,
|
||||||
|
errorMessage = "Health check failed: ${e.message}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
uiState = uiState.copy(errorMessage = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients.shared"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
js {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmToolchain(21)
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(compose.components.resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package at.mocode.clients.shared.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppFooter() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "© 2024 Meldestelle - Built with Kotlin Multiplatform",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package at.mocode.clients.shared.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppHeader(
|
||||||
|
title: String,
|
||||||
|
onNavigateToPing: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
onNavigateToPing?.let { navigateAction ->
|
||||||
|
TextButton(
|
||||||
|
onClick = navigateAction
|
||||||
|
) {
|
||||||
|
Text("Ping Service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package at.mocode.clients.shared.commonui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppScaffold(
|
||||||
|
header: @Composable () -> Unit = {
|
||||||
|
AppHeader(title = "Meldestelle")
|
||||||
|
},
|
||||||
|
footer: @Composable () -> Unit = {
|
||||||
|
AppFooter()
|
||||||
|
},
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = header,
|
||||||
|
bottomBar = footer,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package at.mocode.clients.shared.commonui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
// Define custom colors for the app
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Color(0xFF1976D2),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = Color(0xFFBBDEFB),
|
||||||
|
onPrimaryContainer = Color(0xFF0D47A1),
|
||||||
|
secondary = Color(0xFF03DAC6),
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
tertiary = Color(0xFF03A9F4),
|
||||||
|
background = Color(0xFFFAFAFA),
|
||||||
|
surface = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Color(0xFF90CAF9),
|
||||||
|
onPrimary = Color(0xFF0D47A1),
|
||||||
|
primaryContainer = Color(0xFF1565C0),
|
||||||
|
onPrimaryContainer = Color(0xFFBBDEFB),
|
||||||
|
secondary = Color(0xFF03DAC6),
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
tertiary = Color(0xFF03A9F4),
|
||||||
|
background = Color(0xFF121212),
|
||||||
|
surface = Color(0xFF1E1E1E),
|
||||||
|
onBackground = Color(0xFFE0E0E0),
|
||||||
|
onSurface = Color(0xFFE0E0E0)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppTheme(
|
||||||
|
darkTheme: Boolean = false, // For now, we'll default to light theme
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "at.mocode.clients.shared"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
js {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
|
jvmToolchain(21)
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
// No specific dependencies needed for navigation routes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package at.mocode.clients.shared.navigation
|
||||||
|
|
||||||
|
sealed class AppScreen {
|
||||||
|
data object Home : AppScreen()
|
||||||
|
data object Ping : AppScreen()
|
||||||
|
}
|
||||||
+10
-7
@@ -80,11 +80,14 @@ class RedisDistributedCacheEdgeCasesTest {
|
|||||||
try {
|
try {
|
||||||
cache.set("circular-reference", circularObject as Any)
|
cache.set("circular-reference", circularObject as Any)
|
||||||
logger.info { "Circular reference object was handled (possibly with Jackson's circular reference handling)" }
|
logger.info { "Circular reference object was handled (possibly with Jackson's circular reference handling)" }
|
||||||
} catch (e: Exception) {
|
} catch (t: Throwable) {
|
||||||
logger.info { "Circular reference object caused expected serialization issue: ${e::class.simpleName}" }
|
logger.info { "Circular reference object caused expected serialization issue: ${t::class.simpleName}" }
|
||||||
assertTrue(e is com.fasterxml.jackson.databind.JsonMappingException ||
|
assertTrue(
|
||||||
e is StackOverflowError ||
|
t is com.fasterxml.jackson.databind.JsonMappingException ||
|
||||||
e is RuntimeException, "Expected serialization-related exception")
|
t is StackOverflowError ||
|
||||||
|
t is RuntimeException,
|
||||||
|
"Expected serialization-related exception"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 2: Very deep nesting that might cause issues
|
// Test 2: Very deep nesting that might cause issues
|
||||||
@@ -93,8 +96,8 @@ class RedisDistributedCacheEdgeCasesTest {
|
|||||||
cache.set("deep-nested", deepObject as Any)
|
cache.set("deep-nested", deepObject as Any)
|
||||||
cache.get("deep-nested", DeeplyNestedObject::class.java)
|
cache.get("deep-nested", DeeplyNestedObject::class.java)
|
||||||
logger.info { "Deep nested object serialized successfully" }
|
logger.info { "Deep nested object serialized successfully" }
|
||||||
} catch (e: Exception) {
|
} catch (t: Throwable) {
|
||||||
logger.info { "Deep nested object caused expected issues: ${e::class.simpleName}" }
|
logger.info { "Deep nested object caused expected issues: ${t::class.simpleName}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the cache remains stable after problematic serialization attempts
|
// Verify that the cache remains stable after problematic serialization attempts
|
||||||
|
|||||||
+4
-1
@@ -59,7 +59,10 @@ include(":services:ping:ping-api")
|
|||||||
include(":services:ping:ping-service")
|
include(":services:ping:ping-service")
|
||||||
|
|
||||||
// Client modules
|
// Client modules
|
||||||
include(":clients:ping-client")
|
include(":clients:app")
|
||||||
|
include(":clients:ping-feature")
|
||||||
|
include(":clients:shared:common-ui")
|
||||||
|
include(":clients:shared:navigation")
|
||||||
|
|
||||||
// Documentation module
|
// Documentation module
|
||||||
include(":docs")
|
include(":docs")
|
||||||
|
|||||||
Reference in New Issue
Block a user