feature clients
new frontend
This commit is contained in:
parent
b8c008ddba
commit
0cc25cb108
44
clients/app/build.gradle.kts
Normal file
44
clients/app/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
11
clients/app/src/jsMain/kotlin/main.kt
Normal file
11
clients/app/src/jsMain/kotlin/main.kt
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
12
clients/app/src/jvmMain/kotlin/main.kt
Normal file
12
clients/app/src/jvmMain/kotlin/main.kt
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
10
clients/app/src/wasmJsMain/kotlin/main.kt
Normal file
10
clients/app/src/wasmJsMain/kotlin/main.kt
Normal file
|
|
@ -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 {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
group = "at.mocode"
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
|
|
@ -11,25 +13,35 @@ kotlin {
|
|||
js {
|
||||
browser()
|
||||
}
|
||||
// Keep WASM for dev since sources already present
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
|
||||
jvmToolchain(21)
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
// Contract from backend
|
||||
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.contentNegotiation)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
|
||||
// Coroutines and serialization
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// ViewModel lifecycle
|
||||
implementation(libs.androidx.lifecycle.viewmodelCompose)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
34
clients/shared/common-ui/build.gradle.kts
Normal file
34
clients/shared/common-ui/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
32
clients/shared/navigation/build.gradle.kts
Normal file
32
clients/shared/navigation/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package at.mocode.clients.shared.navigation
|
||||
|
||||
sealed class AppScreen {
|
||||
data object Home : AppScreen()
|
||||
data object Ping : AppScreen()
|
||||
}
|
||||
|
|
@ -80,11 +80,14 @@ class RedisDistributedCacheEdgeCasesTest {
|
|||
try {
|
||||
cache.set("circular-reference", circularObject as Any)
|
||||
logger.info { "Circular reference object was handled (possibly with Jackson's circular reference handling)" }
|
||||
} catch (e: Exception) {
|
||||
logger.info { "Circular reference object caused expected serialization issue: ${e::class.simpleName}" }
|
||||
assertTrue(e is com.fasterxml.jackson.databind.JsonMappingException ||
|
||||
e is StackOverflowError ||
|
||||
e is RuntimeException, "Expected serialization-related exception")
|
||||
} catch (t: Throwable) {
|
||||
logger.info { "Circular reference object caused expected serialization issue: ${t::class.simpleName}" }
|
||||
assertTrue(
|
||||
t is com.fasterxml.jackson.databind.JsonMappingException ||
|
||||
t is StackOverflowError ||
|
||||
t is RuntimeException,
|
||||
"Expected serialization-related exception"
|
||||
)
|
||||
}
|
||||
|
||||
// Test 2: Very deep nesting that might cause issues
|
||||
|
|
@ -93,8 +96,8 @@ class RedisDistributedCacheEdgeCasesTest {
|
|||
cache.set("deep-nested", deepObject as Any)
|
||||
cache.get("deep-nested", DeeplyNestedObject::class.java)
|
||||
logger.info { "Deep nested object serialized successfully" }
|
||||
} catch (e: Exception) {
|
||||
logger.info { "Deep nested object caused expected issues: ${e::class.simpleName}" }
|
||||
} catch (t: Throwable) {
|
||||
logger.info { "Deep nested object caused expected issues: ${t::class.simpleName}" }
|
||||
}
|
||||
|
||||
// Verify that the cache remains stable after problematic serialization attempts
|
||||
|
|
|
|||
|
|
@ -59,7 +59,10 @@ include(":services:ping:ping-api")
|
|||
include(":services:ping:ping-service")
|
||||
|
||||
// Client modules
|
||||
include(":clients:ping-client")
|
||||
include(":clients:app")
|
||||
include(":clients:ping-feature")
|
||||
include(":clients:shared:common-ui")
|
||||
include(":clients:shared:navigation")
|
||||
|
||||
// Documentation module
|
||||
include(":docs")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user