From bf86ba4e6aca209aa0e1afbdb1311b30387d1e60 Mon Sep 17 00:00:00 2001 From: Florent CHAMPIGNY Date: Sun, 5 Oct 2025 14:00:08 +0200 Subject: [PATCH] feat: [NETWORK] websockets (#305) Co-authored-by: Florent Champigny --- .../myapplication/DummyWebsocketCaller.kt | 67 ++++++++ .../flocon/myapplication/MainActivity.kt | 10 ++ .../plugins/network/FloconNetworkPlugin.kt | 8 + .../network/model/FloconWebSocketEvent.kt | 19 ++ .../io/github/openflocon/flocon/Protocol.kt | 1 + .../network/FloconNetworkPluginImpl.kt | 18 ++ .../mapper/FloconNetworkRequestToJson.kt | 35 ++++ .../okhttp/websocket/FloconWebSocket.kt | 120 +++++++++++++ .../flocondesktop/common/db/AppDatabase.kt | 2 +- .../detail/mapper/NetworkDetailUiMapper.kt | 162 ++++++++++++------ .../detail/model/NetworkDetailViewState.kt | 6 +- .../network/detail/view/NetworkDetailView.kt | 82 +++++---- .../features/network/list/NetworkViewModel.kt | 2 +- .../network/list/mapper/MethodUiMapper.kt | 4 +- .../network/list/mapper/StatusUiMapper.kt | 12 +- .../network/list/mapper/TypeUiMapper.kt | 18 +- .../list/model/NetworkItemViewState.kt | 12 ++ .../network/list/model/NetworkMethodUi.kt | 6 + .../network/list/view/NetworkItemView.kt | 87 ++++++++-- .../list/view/components/MethodView.kt | 4 + .../datasource/NetworkRemoteDataSource.kt | 2 + .../repository/NetworkRepositoryImpl.kt | 10 ++ .../local/network/mapper/MapperToDomain.kt | 3 + .../local/network/mapper/MapperToEntity.kt | 9 + .../network/models/FloconNetworkCallEntity.kt | 8 + .../datasource/NetworkRemoteDataSourceImpl.kt | 6 + .../data/remote/network/mapper/Mapper.kt | 48 ++++++ .../network/mapper/NetworkDomainExtractor.kt | 1 + .../network/mapper/NetworkMethodExtractor.kt | 1 + .../network/mapper/NetworkQueryExtractor.kt | 1 + .../models/FloconNetworkWebSocketEvent.kt | 14 ++ .../io/github/openflocon/domain/Protocol.kt | 1 + .../models/FloconNetworkCallDomainModel.kt | 3 + .../usecase/ExportNetworkCallsToCsvUseCase.kt | 6 +- 34 files changed, 673 insertions(+), 115 deletions(-) create mode 100644 FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/DummyWebsocketCaller.kt create mode 100644 FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt create mode 100644 FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt create mode 100644 FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/FloconNetworkWebSocketEvent.kt diff --git a/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/DummyWebsocketCaller.kt b/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/DummyWebsocketCaller.kt new file mode 100644 index 00000000..4599c2fb --- /dev/null +++ b/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/DummyWebsocketCaller.kt @@ -0,0 +1,67 @@ +package io.github.openflocon.flocon.myapplication + +import io.github.openflocon.flocon.okhttp.websocket.sendWithFlocon +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import io.github.openflocon.flocon.okhttp.websocket.listenWithFlocon + +class DummyWebsocketCaller(val client: OkHttpClient) { + + private var ws: WebSocket? = null + + fun connectToWebsocket() { + val request = Request.Builder() + .url("wss://ws.postman-echo.com/raw") + .build() + val listener = object : WebSocketListener() { + override fun onClosed( + webSocket: WebSocket, + code: Int, + reason: String + ) { + super.onClosed(webSocket, code, reason) + } + + override fun onClosing( + webSocket: WebSocket, + code: Int, + reason: String + ) { + super.onClosing(webSocket, code, reason) + } + + override fun onFailure( + webSocket: WebSocket, + t: Throwable, + response: Response? + ) { + super.onFailure(webSocket, t, response) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + super.onMessage(webSocket, bytes) + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + } + } + this.ws = client.newWebSocket( + request, + listener.listenWithFlocon(), + ) + } + + fun send(text: String) { + ws?.sendWithFlocon("\"$text\"") + } + +} diff --git a/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt b/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt index 69955a52..5077ba29 100644 --- a/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt +++ b/FloconAndroid/app/src/main/java/io/github/openflocon/flocon/myapplication/MainActivity.kt @@ -37,6 +37,7 @@ import io.github.openflocon.flocon.plugins.tables.table import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import okhttp3.OkHttpClient +import java.util.UUID import kotlin.random.Random class MainActivity : ComponentActivity() { @@ -63,6 +64,8 @@ class MainActivity : ComponentActivity() { initializeDeeplinks() val dummyHttpCaller = DummyHttpCaller(client = okHttpClient) + val dummyWebsocketCaller = DummyWebsocketCaller(client = okHttpClient) + GlobalScope.launch { dummyWebsocketCaller.connectToWebsocket() } val graphQlTester = GraphQlTester(client = okHttpClient) initializeImages(context = this, okHttpClient = okHttpClient) initializeDashboard(this) @@ -111,6 +114,13 @@ class MainActivity : ComponentActivity() { ) { Text("ktor test") } + Button( + onClick = { + dummyWebsocketCaller.send(UUID.randomUUID().toString()) + } + ) { + Text("websocket test") + } Button( onClick = { val value = Random.nextInt(from = 0, until = 1000).toString() diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt index 74ef572a..c2e0eca5 100644 --- a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPlugin.kt @@ -4,12 +4,20 @@ import io.github.openflocon.flocon.core.FloconPlugin import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse + + interface FloconNetworkPlugin : FloconPlugin { val mocks: Collection val badQualityConfig: BadQualityConfig? fun logRequest(request: FloconNetworkCallRequest) fun logResponse(response: FloconNetworkCallResponse) + + fun logWebSocket( + event: FloconWebSocketEvent, + ) + } diff --git a/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt new file mode 100644 index 00000000..5c0cc973 --- /dev/null +++ b/FloconAndroid/flocon-base/src/main/java/io/github/openflocon/flocon/plugins/network/model/FloconWebSocketEvent.kt @@ -0,0 +1,19 @@ +package io.github.openflocon.flocon.plugins.network.model + +class FloconWebSocketEvent( + val websocketUrl: String, + val timeStamp: Long, + val event: Event, + val size: Long, + val message: String? = null, + val error: Throwable? = null, +) { + enum class Event { + Closed, + Closing, + Error, + ReceiveMessage, + SendMessage, + Open, + } +} \ No newline at end of file diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt index 48ed14a8..527ea166 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/Protocol.kt @@ -67,6 +67,7 @@ object Protocol { object Method { const val LogNetworkCallRequest = "logNetworkCallRequest" const val LogNetworkCallResponse = "logNetworkCallResponse" + const val LogWebSocketEvent = "logWebSocketEvent" } } diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt index d6da15e7..29dfff9f 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/FloconNetworkPluginImpl.kt @@ -7,6 +7,7 @@ import io.github.openflocon.flocon.core.FloconMessageSender import io.github.openflocon.flocon.model.FloconMessageFromServer import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkCallRequestToJson import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkCallResponseToJson +import io.github.openflocon.flocon.plugins.network.mapper.floconNetworkWebSocketEventToJson import io.github.openflocon.flocon.plugins.network.mapper.parseBadQualityConfig import io.github.openflocon.flocon.plugins.network.mapper.parseMockResponses import io.github.openflocon.flocon.plugins.network.mapper.toJsonObject @@ -14,6 +15,7 @@ import io.github.openflocon.flocon.plugins.network.mapper.writeMockResponsesToJs import io.github.openflocon.flocon.plugins.network.model.BadQualityConfig import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent import io.github.openflocon.flocon.plugins.network.model.MockNetworkResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -67,6 +69,22 @@ internal class FloconNetworkPluginImpl( } } + override fun logWebSocket( + event: FloconWebSocketEvent, + ) { + coroutineScope.launch(Dispatchers.IO) { + try { + sender.send( + plugin = Protocol.FromDevice.Network.Plugin, + method = Protocol.FromDevice.Network.Method.LogWebSocketEvent, + body = floconNetworkWebSocketEventToJson(event).toString(), + ) + } catch (t: Throwable) { + FloconLogger.logError("Network json mapping error", t) + } + } + } + override fun onMessageReceived( messageFromServer: FloconMessageFromServer, sender: FloconMessageSender diff --git a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt index 4d88d6c5..6b78f89b 100644 --- a/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt +++ b/FloconAndroid/flocon/src/main/java/io/github/openflocon/flocon/plugins/network/mapper/FloconNetworkRequestToJson.kt @@ -2,7 +2,9 @@ package io.github.openflocon.flocon.plugins.network.mapper import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallRequest import io.github.openflocon.flocon.plugins.network.model.FloconNetworkCallResponse +import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent import org.json.JSONObject +import java.util.UUID internal fun floconNetworkCallRequestToJson(network: FloconNetworkCallRequest): JSONObject { val json = JSONObject() @@ -47,5 +49,38 @@ fun floconNetworkCallResponseToJson(network: FloconNetworkCallResponse): JSONObj response.error?.let { json.put("responseError", it) } } + return json +} + + +internal fun floconNetworkWebSocketEventToJson( + webSocketEvent: FloconWebSocketEvent, +): JSONObject { + val json = JSONObject() + + with(webSocketEvent) { + json.put("id", UUID.randomUUID().toString()) + json.put("event", when(event) { + FloconWebSocketEvent.Event.Closed -> "closed" + FloconWebSocketEvent.Event.Closing -> "closing" + FloconWebSocketEvent.Event.Error -> "error" + FloconWebSocketEvent.Event.ReceiveMessage -> "received" + FloconWebSocketEvent.Event.SendMessage -> "sent" + FloconWebSocketEvent.Event.Open -> "open" + }) + // json.put("isMocked", isMocked) + + json.put("url", websocketUrl) + json.put("timestamp", webSocketEvent.timeStamp) + json.put("size", webSocketEvent.size) + + webSocketEvent.message?.let { + json.put("message", it) + } + webSocketEvent.error?.message?.let { + json.put("error", it) + } + } + return json } \ No newline at end of file diff --git a/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt new file mode 100644 index 00000000..ec2bf606 --- /dev/null +++ b/FloconAndroid/okhttp-interceptor/src/main/java/io/github/openflocon/flocon/okhttp/websocket/FloconWebSocket.kt @@ -0,0 +1,120 @@ +package io.github.openflocon.flocon.okhttp.websocket + +import io.github.openflocon.flocon.FloconApp +import io.github.openflocon.flocon.plugins.network.model.FloconWebSocketEvent +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString + +object FloconWebSocket { + + private fun log( + webSocket: WebSocket, + event: FloconWebSocketEvent.Event, + message: String? = null, + error: Throwable? = null, + ) { + val size = message?.toByteArray()?.size?.toLong() + FloconApp.instance?.client?.networkPlugin?.logWebSocket( + FloconWebSocketEvent( + websocketUrl = webSocket.request().url.toString(), + timeStamp = System.currentTimeMillis(), + event = event, + message = message, + error = error, + size = size ?: 0L, + ) + ) + } + + fun send(webSocket: WebSocket, text: String) : Boolean { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.SendMessage, + message = text, + ) + return webSocket.send(text = text) + } + + fun send(webSocket: WebSocket, bytes: ByteString) : Boolean { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.SendMessage, + message = bytes.toString(), // not sure + ) + return webSocket.send(bytes = bytes) + } + + data class Listener(val listener: WebSocketListener) : WebSocketListener() { + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.Closed, + ) + listener.onClosed(webSocket, code, reason) + super.onClosed(webSocket, code, reason) + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.Closing, + ) + listener.onClosing(webSocket, code, reason) + super.onClosing(webSocket, code, reason) + } + + override fun onFailure( + webSocket: WebSocket, + t: Throwable, + response: Response? + ) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.Error, + error = t, + ) + listener.onFailure(webSocket, t, response) + super.onFailure(webSocket, t, response) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.ReceiveMessage, + message = text, + ) + listener.onMessage(webSocket, text) + super.onMessage(webSocket, text) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.ReceiveMessage, + message = bytes.toString(), // TODO not sure + ) + listener.onMessage(webSocket, bytes) + super.onMessage(webSocket, bytes) + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + log( + webSocket = webSocket, + event = FloconWebSocketEvent.Event.Open, + ) + listener.onOpen(webSocket, response) + super.onOpen(webSocket, response) + } + + } +} + +fun WebSocket.sendWithFlocon(text: String) = FloconWebSocket.send(this, text) +fun WebSocket.sendWithFlocon(bytes: ByteString) = FloconWebSocket.send(this, bytes) + +fun WebSocketListener.listenWithFlocon(): WebSocketListener { + return FloconWebSocket.Listener(this) +} \ No newline at end of file diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt index f24dec53..897e93e8 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/db/AppDatabase.kt @@ -40,7 +40,7 @@ import io.github.openflocon.flocondesktop.common.db.converters.MapStringsConvert import kotlinx.coroutines.Dispatchers @Database( - version = 63, + version = 64, entities = [ FloconNetworkCallEntity::class, FileEntity::class, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/mapper/NetworkDetailUiMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/mapper/NetworkDetailUiMapper.kt index f1cbed30..f608b62b 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/mapper/NetworkDetailUiMapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/mapper/NetworkDetailUiMapper.kt @@ -1,62 +1,104 @@ package io.github.openflocon.flocondesktop.features.network.detail.mapper -import io.github.openflocon.domain.common.time.formatDuration -import io.github.openflocon.domain.common.time.formatTimestamp import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.flocondesktop.common.ui.JsonPrettyPrinter import io.github.openflocon.flocondesktop.features.network.detail.model.NetworkDetailHeaderUi import io.github.openflocon.flocondesktop.features.network.detail.model.NetworkDetailViewState +import io.github.openflocon.flocondesktop.features.network.detail.model.NetworkDetailViewState.Method.Http +import io.github.openflocon.flocondesktop.features.network.detail.model.NetworkDetailViewState.Method.MethodName import io.github.openflocon.flocondesktop.features.network.list.mapper.getMethodUi import io.github.openflocon.flocondesktop.features.network.list.mapper.loadingStatus import io.github.openflocon.flocondesktop.features.network.list.mapper.toGraphQlNetworkStatusUi import io.github.openflocon.flocondesktop.features.network.list.mapper.toGrpcNetworkStatusUi import io.github.openflocon.flocondesktop.features.network.list.mapper.toHttpMethodUi import io.github.openflocon.flocondesktop.features.network.list.mapper.toNetworkStatusUi +import io.github.openflocon.flocondesktop.features.network.list.model.NetworkMethodUi import io.github.openflocon.flocondesktop.features.network.list.model.NetworkStatusUi -fun toDetailUi(request: FloconNetworkCallDomainModel): NetworkDetailViewState = NetworkDetailViewState( - callId = request.callId, - fullUrl = request.request.url, - method = toDetailMethodUi(request), - status = toDetailHttpStatusUi(request), - requestTimeFormatted = request.request.startTimeFormatted, - durationFormatted = request.response?.durationFormatted, - // request - requestBody = httpBodyToUi(request.request.body), - requestHeaders = toNetworkHeadersUi(request.request.headers), - requestSize = request.request.byteSizeFormatted, - // response - response = request.response?.let { - when(it) { - is FloconNetworkCallDomainModel.Response.Failure -> NetworkDetailViewState.Response.Error( - issue = it.issue, - ) - is FloconNetworkCallDomainModel.Response.Success -> NetworkDetailViewState.Response.Success( - body = httpBodyToUi(it.body), - size = it.byteSizeFormatted, - headers = toNetworkHeadersUi(it.headers), - ) +fun toDetailUi(request: FloconNetworkCallDomainModel): NetworkDetailViewState = + NetworkDetailViewState( + callId = request.callId, + fullUrl = request.request.url, + method = toDetailMethodUi(request), + statusLabel = toDetailStatusLabel(request), + status = toDetailHttpStatusUi(request), + requestTimeFormatted = request.request.startTimeFormatted, + durationFormatted = request.response?.durationFormatted, + // request + requestBodyTitle = requestBodyTitle(request), + requestBody = httpBodyToUi(request.request.body), + requestHeaders = toNetworkHeadersUi(request.request.headers), + requestSize = request.request.byteSizeFormatted, + // response + response = request.response?.let { + when (it) { + is FloconNetworkCallDomainModel.Response.Failure -> NetworkDetailViewState.Response.Error( + issue = it.issue, + ) - } + is FloconNetworkCallDomainModel.Response.Success -> NetworkDetailViewState.Response.Success( + body = httpBodyToUi(it.body), + size = it.byteSizeFormatted, + headers = toNetworkHeadersUi(it.headers), + ) - }, - graphQlSection = graphQlSection(request), -) + } -private fun toDetailHttpStatusUi(networkCall: FloconNetworkCallDomainModel): NetworkStatusUi = networkCall.response?.let { response -> - when (response) { - is FloconNetworkCallDomainModel.Response.Failure -> NetworkStatusUi( - text = response.issue, - status = NetworkStatusUi.Status.ERROR, - ) - is FloconNetworkCallDomainModel.Response.Success -> when(val s = response.specificInfos) { - is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Grpc -> toGrpcNetworkStatusUi(networkCall) - // here for grphql we want the http code, the graphql status will be displayed on the specific graphql section - is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.GraphQl -> toNetworkStatusUi(code = s.httpCode) - is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> toNetworkStatusUi(code = s.httpCode) - } + }, + graphQlSection = graphQlSection(request), + ) + +private fun toDetailStatusLabel(request: FloconNetworkCallDomainModel): String = + when (request.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> "Event" + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, + FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc, + FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> "Status" + } + +private fun requestBodyTitle(request: FloconNetworkCallDomainModel): String = + when (request.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> "Content" + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, + FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc, + FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> "Request - Body" + } + +private fun toDetailHttpStatusUi(networkCall: FloconNetworkCallDomainModel): NetworkStatusUi = + networkCall.response?.let { response -> + when (response) { + is FloconNetworkCallDomainModel.Response.Failure -> NetworkStatusUi( + text = response.issue, + status = NetworkStatusUi.Status.ERROR, + ) + + is FloconNetworkCallDomainModel.Response.Success -> when (val s = + response.specificInfos) { + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Grpc -> toGrpcNetworkStatusUi( + networkCall + ) + // here for grphql we want the http code, the graphql status will be displayed on the specific graphql section + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.GraphQl -> toNetworkStatusUi( + code = s.httpCode + ) + + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Http -> toNetworkStatusUi( + code = s.httpCode + ) + } + } + } ?: when (val s = networkCall.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> { + NetworkStatusUi( + text = s.event, + status = if (s.event == "error") NetworkStatusUi.Status.ERROR else NetworkStatusUi.Status.SUCCESS, + ) + } + + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, + is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc, + is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> loadingStatus() } -} ?: loadingStatus() fun graphQlSection(networkCall: FloconNetworkCallDomainModel): NetworkDetailViewState.GraphQlSection? { return (networkCall.request.specificInfos as? FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl)?.let { @@ -68,23 +110,28 @@ fun graphQlSection(networkCall: FloconNetworkCallDomainModel): NetworkDetailView } } -private fun graphQlStatus(networkCall: FloconNetworkCallDomainModel) : NetworkStatusUi? { - return when(val r = networkCall.response) { +private fun graphQlStatus(networkCall: FloconNetworkCallDomainModel): NetworkStatusUi? { + return when (val r = networkCall.response) { is FloconNetworkCallDomainModel.Response.Failure -> NetworkStatusUi( text = r.issue, status = NetworkStatusUi.Status.ERROR, ) - is FloconNetworkCallDomainModel.Response.Success -> when(val s = r.specificInfos) { - is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.GraphQl -> toGraphQlNetworkStatusUi(isSuccess = s.isSuccess) + + is FloconNetworkCallDomainModel.Response.Success -> when (val s = r.specificInfos) { + is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.GraphQl -> toGraphQlNetworkStatusUi( + isSuccess = s.isSuccess + ) + else -> null } + null -> loadingStatus() } } fun httpBodyToUi(body: String?): String = body?.let { JsonPrettyPrinter.prettyPrint(body) } ?: "" -fun toNetworkHeadersUi(headers: Map?): List = headers?.let { +fun toNetworkHeadersUi(headers: Map?): List? = headers?.let { it .map { (key, value) -> NetworkDetailHeaderUi( @@ -92,14 +139,19 @@ fun toNetworkHeadersUi(headers: Map?): List NetworkDetailViewState.Method.MethodName( - name = request.request.method, - ) +fun toDetailMethodUi(request: FloconNetworkCallDomainModel): NetworkDetailViewState.Method = + when (request.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> MethodName( + name = request.request.method, + ) - is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, - is FloconNetworkCallDomainModel.Request.SpecificInfos.Http, - -> NetworkDetailViewState.Method.Http(toHttpMethodUi(request.request.method)) -} + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, + is FloconNetworkCallDomainModel.Request.SpecificInfos.Http, + -> Http(toHttpMethodUi(request.request.method)) + + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> Http( + NetworkMethodUi.WebSocket + ) + } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/model/NetworkDetailViewState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/model/NetworkDetailViewState.kt index 32e56074..8ced2731 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/model/NetworkDetailViewState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/model/NetworkDetailViewState.kt @@ -12,14 +12,16 @@ data class NetworkDetailViewState( val durationFormatted: String?, val method: Method, + val statusLabel: String, val status: NetworkStatusUi, val graphQlSection: GraphQlSection?, // request + val requestBodyTitle: String, val requestBody: String, val requestSize: String, - val requestHeaders: List, + val requestHeaders: List?, // response val response: Response?, ) { @@ -29,7 +31,7 @@ data class NetworkDetailViewState( data class Success( val body: String, val size: String, - val headers: List, + val headers: List?, ) : Response @Immutable data class Error( diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt index 5081fcdc..8435010d 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/detail/view/NetworkDetailView.kt @@ -154,7 +154,7 @@ private fun Request( } FloconLineDescription( modifier = Modifier.fillMaxWidth(), - label = "Status", + label = state.statusLabel, contentColor = FloconTheme.colorPalette.onPrimary, labelWidth = linesLabelWidth, ) { @@ -214,28 +214,30 @@ private fun Request( } } - FloconSection( - title = "Request - Headers", - initialValue = true, - modifier = Modifier.fillMaxWidth() - ) { - DetailHeadersView( - headers = state.requestHeaders, - labelWidth = headersLabelWidth, - onAuthorizationClicked = { token -> - onAction( - NetworkAction.DisplayBearerJwt( - token + state.requestHeaders?.let { + FloconSection( + title = "Request - Headers", + initialValue = true, + modifier = Modifier.fillMaxWidth() + ) { + DetailHeadersView( + headers = state.requestHeaders, + labelWidth = headersLabelWidth, + onAuthorizationClicked = { token -> + onAction( + NetworkAction.DisplayBearerJwt( + token + ) ) - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } } FloconSection( - title = "Request - Body", + title = state.requestBodyTitle, initialValue = true, actions = { FloconIconButton( @@ -304,25 +306,27 @@ private fun Response( } is NetworkDetailViewState.Response.Success -> { - FloconSection( - title = "Response - Headers", - initialValue = true, - modifier = Modifier.fillMaxWidth() - ) { - DetailHeadersView( - headers = response.headers, - labelWidth = headersLabelWidth, - onAuthorizationClicked = { token -> - onAction( - NetworkAction.DisplayBearerJwt( - token + response.headers?.let { + FloconSection( + title = "Response - Headers", + initialValue = true, + modifier = Modifier.fillMaxWidth() + ) { + DetailHeadersView( + headers = response.headers, + labelWidth = headersLabelWidth, + onAuthorizationClicked = { token -> + onAction( + NetworkAction.DisplayBearerJwt( + token + ) ) - ) - }, - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } } FloconSection( title = "Response - Body", @@ -369,6 +373,7 @@ private fun NetworkDetailViewPreview() { callId = "", fullUrl = "http://www.google.com", method = NetworkDetailViewState.Method.Http(NetworkMethodUi.Http.GET), + statusLabel = "Status", status = NetworkStatusUi( text = "200", @@ -380,6 +385,7 @@ private fun NetworkDetailViewPreview() { previewNetworkDetailHeaderUi(), previewNetworkDetailHeaderUi(), ), + requestBodyTitle = "Request - Body", requestBody = """ { diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt index 80e25808..b90bdfaf 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/NetworkViewModel.kt @@ -415,5 +415,5 @@ private fun TextFilterStateUiModel.FilterItem.toDomain(): NetworkFilterDomainMod } private fun methodsToDomain(items: List): List? { - return items.map { it.text }.takeIf { it.isNotEmpty() } + return items.map { it.text }.takeIf { it.isNotEmpty() }?.takeIf { it.size != NetworkMethodUi.all().size } // returns null if we accept all } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/MethodUiMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/MethodUiMapper.kt index dd9f3a48..0bf67818 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/MethodUiMapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/MethodUiMapper.kt @@ -2,15 +2,17 @@ package io.github.openflocon.flocondesktop.features.network.list.mapper import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.flocondesktop.features.network.list.model.NetworkMethodUi +import io.github.openflocon.flocondesktop.features.network.list.model.NetworkMethodUi.* fun getMethodUi(networkCall: FloconNetworkCallDomainModel): NetworkMethodUi = when (val s = networkCall.request.specificInfos) { is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> when (val t = s.operationType.lowercase()) { "query" -> NetworkMethodUi.GraphQl.QUERY "mutation" -> NetworkMethodUi.GraphQl.MUTATION - else -> NetworkMethodUi.OTHER(s.operationType, icon = null) + else -> OTHER(s.operationType, icon = null) } is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> toHttpMethodUi(networkCall.request.method) is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> NetworkMethodUi.Grpc + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> NetworkMethodUi.WebSocket } fun toHttpMethodUi(httpMethod: String): NetworkMethodUi = when (httpMethod.lowercase()) { diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/StatusUiMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/StatusUiMapper.kt index 1f88f7a0..af0dbdce 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/StatusUiMapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/StatusUiMapper.kt @@ -22,7 +22,17 @@ fun getStatusUi(networkCall: FloconNetworkCallDomainModel): NetworkStatusUi = ne is FloconNetworkCallDomainModel.Response.Success.SpecificInfos.Grpc -> toGrpcNetworkStatusUi(networkCall) } } -} ?: loadingStatus() +} ?: when(val s = networkCall.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl, + is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc, + is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> loadingStatus() + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> { + NetworkStatusUi( + text = s.event, + status = if(s.event == "error") NetworkStatusUi.Status.ERROR else NetworkStatusUi.Status.SUCCESS, + ) + } +} fun toNetworkStatusUi(code: Int): NetworkStatusUi = NetworkStatusUi( text = code.toString(), diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/TypeUiMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/TypeUiMapper.kt index 213def7e..1e68de46 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/TypeUiMapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/mapper/TypeUiMapper.kt @@ -2,21 +2,33 @@ package io.github.openflocon.flocondesktop.features.network.list.mapper import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.flocondesktop.features.network.list.model.NetworkItemViewState +import io.github.openflocon.flocondesktop.features.network.list.model.NetworkItemViewState.NetworkTypeUi fun toTypeUi(call: FloconNetworkCallDomainModel): NetworkItemViewState.NetworkTypeUi = when (val s = call.request.specificInfos) { - is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> NetworkItemViewState.NetworkTypeUi.GraphQl( + is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> NetworkTypeUi.GraphQl( queryName = call.request.queryFormatted, ) is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> { - NetworkItemViewState.NetworkTypeUi.Url( + NetworkTypeUi.Url( query = call.request.queryFormatted, ) } is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> { - NetworkItemViewState.NetworkTypeUi.Grpc( + NetworkTypeUi.Grpc( method = call.request.queryFormatted, ) } + + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> NetworkTypeUi.WebSocket( + text = call.request.queryFormatted, + icon = getWebsocketEmoji(s.event), + ) +} + +private fun getWebsocketEmoji(event: String): NetworkTypeUi.WebSocket.IconType? = when (event) { + "sent" -> NetworkTypeUi.WebSocket.IconType.Up + "received" -> NetworkTypeUi.WebSocket.IconType.Down + else -> null } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkItemViewState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkItemViewState.kt index bbeebd45..1bb3040d 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkItemViewState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkItemViewState.kt @@ -53,6 +53,18 @@ data class NetworkItemViewState( override fun contains(text: String): Boolean = method.contains(text, ignoreCase = true) override val text = method } + + @Immutable + data class WebSocket( + override val text: String, + val icon: IconType?, + ) : NetworkTypeUi { + enum class IconType { + Up, + Down, + } + override fun contains(text: String): Boolean = text.contains(text, ignoreCase = true) + } } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkMethodUi.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkMethodUi.kt index 9f96ce1a..07680b77 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkMethodUi.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/model/NetworkMethodUi.kt @@ -50,6 +50,11 @@ sealed interface NetworkMethodUi { override val icon = null } + data object WebSocket : NetworkMethodUi { + override val text = "WebSocket" + override val icon = null + } + data class OTHER( override val text: String, override val icon: DrawableResource?, @@ -68,6 +73,7 @@ sealed interface NetworkMethodUi { GraphQl.QUERY, GraphQl.MUTATION, Grpc, + WebSocket, OTHER(text = "Other", icon = null), ) } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkItemView.kt index fd65e346..3dc0302c 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/NetworkItemView.kt @@ -1,5 +1,6 @@ package io.github.openflocon.flocondesktop.features.network.list.view +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -8,8 +9,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,6 +25,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -117,12 +125,7 @@ fun NetworkItemView( .weight(columnWidths.domainWeight) .padding(horizontal = 4.dp), ) - Text( - text = state.type.query, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = bodySmall, - color = FloconTheme.colorPalette.onSecondary, + Row( modifier = Modifier .weight(2f) .background( @@ -130,7 +133,32 @@ fun NetworkItemView( shape = RoundedCornerShape(4.dp), ) .padding(horizontal = 8.dp, vertical = 6.dp), - ) + verticalAlignment = Alignment.CenterVertically, + ) { + state.type.image()?.let { + Box( + modifier = Modifier + .padding(end = 8.dp) + .clip(RoundedCornerShape(4.dp)) + .background(state.type.imageColor()) + .padding(3.dp) + ) { + Image( + imageVector = it, + contentDescription = null, + modifier = Modifier.size(12.dp), + colorFilter = ColorFilter.tint(Color.White) + ) + } + } + Text( + text = state.type.query, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = bodySmall, + color = FloconTheme.colorPalette.onSecondary, + ) + } } // NetworkStatusUi - Fixed width for the tag from data class @@ -163,9 +191,13 @@ private fun contextualActions( return remember(state) { buildMenu { item(label = "Copy URL", onClick = { onActionCallback(NetworkAction.CopyUrl(state)) }) - if (state.type !is NetworkItemViewState.NetworkTypeUi.Grpc) { - item(label = "Copy cUrl", onClick = { onActionCallback(NetworkAction.CopyCUrl(state)) }) - item(label = "Create Mock", onClick = { onActionCallback(NetworkAction.CreateMock(state)) }) + if (state.type !is NetworkItemViewState.NetworkTypeUi.Grpc && state.type !is NetworkItemViewState.NetworkTypeUi.WebSocket) { + item( + label = "Copy cUrl", + onClick = { onActionCallback(NetworkAction.CopyCUrl(state)) }) + item( + label = "Create Mock", + onClick = { onActionCallback(NetworkAction.CreateMock(state)) }) } separator() subMenu(label = "Filter") { @@ -216,8 +248,12 @@ private fun contextualActions( } separator() item(label = "Remove", onClick = { onActionCallback(NetworkAction.Remove(state)) }) - item(label = "Remove lines above", onClick = { onActionCallback(NetworkAction.RemoveLinesAbove(state)) }) - item(label = "Clear old sessions", onClick = { onActionCallback(NetworkAction.ClearOldSession) }) + item( + label = "Remove lines above", + onClick = { onActionCallback(NetworkAction.RemoveLinesAbove(state)) }) + item( + label = "Clear old sessions", + onClick = { onActionCallback(NetworkAction.ClearOldSession) }) } } } @@ -227,8 +263,35 @@ private val NetworkItemViewState.NetworkTypeUi.query: String is NetworkItemViewState.NetworkTypeUi.GraphQl -> queryName is NetworkItemViewState.NetworkTypeUi.Grpc -> method is NetworkItemViewState.NetworkTypeUi.Url -> query + is NetworkItemViewState.NetworkTypeUi.WebSocket -> text } +fun NetworkItemViewState.NetworkTypeUi.image(): ImageVector? = when (this) { + is NetworkItemViewState.NetworkTypeUi.GraphQl, + is NetworkItemViewState.NetworkTypeUi.Grpc, + is NetworkItemViewState.NetworkTypeUi.Url -> null + + is NetworkItemViewState.NetworkTypeUi.WebSocket -> + when (this.icon) { + NetworkItemViewState.NetworkTypeUi.WebSocket.IconType.Up -> Icons.Default.Upload + NetworkItemViewState.NetworkTypeUi.WebSocket.IconType.Down -> Icons.Default.Download + null -> null + } +} + +fun NetworkItemViewState.NetworkTypeUi.imageColor(): Color = when (this) { + is NetworkItemViewState.NetworkTypeUi.GraphQl, + is NetworkItemViewState.NetworkTypeUi.Grpc, + is NetworkItemViewState.NetworkTypeUi.Url -> null + + is NetworkItemViewState.NetworkTypeUi.WebSocket -> + when (this.icon) { + NetworkItemViewState.NetworkTypeUi.WebSocket.IconType.Up -> Color(0xFF007BFF) + NetworkItemViewState.NetworkTypeUi.WebSocket.IconType.Down -> Color(0xFF28A745) + null -> null + } +} ?: Color.Green + @Composable @Preview private fun ItemViewPreview() { diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/components/MethodView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/components/MethodView.kt index 923464b7..280cb0b2 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/components/MethodView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/list/view/components/MethodView.kt @@ -42,6 +42,9 @@ val deleteMethodText = Color(0xFFDC3545) val otherMethodBackground = Color(0xFF6C757D).copy(alpha = 0.3f) // Muted gray for OTHER val otherMethodText = Color(0xFF6C757D) +val websocketMethodBackground = Color(0xFF17A2B8).copy(alpha = 0.3f) // Muted cyan for WEBSOCKET +val websocketMethodText = Color(0xFF17A2B8) + private val graphQlQueryMethodBackground = Color(0XAAE235A9).copy(alpha = 0.8f) // Muted gray for OTHER private val graphQlQueryMethodText = Color(0XAAFFFFFF) @@ -66,6 +69,7 @@ fun MethodView( is NetworkMethodUi.GraphQl.QUERY -> graphQlQueryMethodBackground to graphQlQueryMethodText is NetworkMethodUi.GraphQl.MUTATION -> graphQlQueryMethodBackground to graphQlQueryMethodText is NetworkMethodUi.Grpc -> grpcMethodBackground to grpcMethodText + is NetworkMethodUi.WebSocket -> websocketMethodBackground to websocketMethodText } NetworkTag( diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt index 1c4683bd..a5a091a4 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/datasource/NetworkRemoteDataSource.kt @@ -25,4 +25,6 @@ interface NetworkRemoteDataSource { fun getCallId(message: FloconIncomingMessageDomainModel): FloconNetworkCallIdDomainModel? fun getResponseData(message: FloconIncomingMessageDomainModel): FloconNetworkResponseOnlyDomainModel? + + fun getWebSocketData(message: FloconIncomingMessageDomainModel): FloconNetworkCallDomainModel? } diff --git a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt index f609ded2..41d1f7e4 100644 --- a/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt +++ b/FloconDesktop/data/core/src/commonMain/kotlin/io/github/openflocon/data/core/network/repository/NetworkRepositoryImpl.kt @@ -108,6 +108,16 @@ class NetworkRepositoryImpl( ) { withContext(dispatcherProvider.data) { when (message.method) { + Protocol.FromDevice.Network.Method.LogWebSocketEvent -> { + networkRemoteDataSource.getWebSocketData(message) + ?.let { wenSocketEvent -> + networkLocalDataSource.save( + deviceIdAndPackageName = deviceIdAndPackageName, + call = wenSocketEvent, + ) + } + } + Protocol.FromDevice.Network.Method.LogNetworkCallRequest -> { networkRemoteDataSource.getRequestData(message) ?.let { call -> diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToDomain.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToDomain.kt index 684e143b..1925ba52 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToDomain.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToDomain.kt @@ -41,6 +41,9 @@ private fun FloconNetworkCallEntity.toRequestDomainModel(): FloconNetworkCallDom } FloconNetworkCallType.GRPC -> FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc + FloconNetworkCallType.WEBSOCKET -> FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket( + event = request.websocket?.event ?: "unknown", + ) }, ) diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToEntity.kt index 2e1c5b7f..622b28f1 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToEntity.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/mapper/MapperToEntity.kt @@ -4,6 +4,7 @@ import io.github.openflocon.data.local.network.models.FloconNetworkCallEntity import io.github.openflocon.data.local.network.models.FloconNetworkCallType import io.github.openflocon.data.local.network.models.FloconNetworkRequestEmbedded import io.github.openflocon.data.local.network.models.FloconNetworkResponseEmbedded +import io.github.openflocon.data.local.network.models.NetworkCallWebSocketRequestEmbedded import io.github.openflocon.data.local.network.models.graphql.NetworkCallGraphQlRequestEmbedded import io.github.openflocon.data.local.network.models.graphql.NetworkCallGraphQlResponseEmbedded import io.github.openflocon.data.local.network.models.grpc.NetworkCallGrpcResponseEmbedded @@ -23,6 +24,7 @@ fun FloconNetworkCallDomainModel.toEntity( is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> FloconNetworkCallType.HTTP is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> FloconNetworkCallType.GRAPHQL is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> FloconNetworkCallType.GRPC + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> FloconNetworkCallType.WEBSOCKET }, request = FloconNetworkRequestEmbedded( url = request.url, @@ -42,6 +44,13 @@ fun FloconNetworkCallDomainModel.toEntity( else -> null }, + websocket = when (val s = this.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> NetworkCallWebSocketRequestEmbedded( + event = s.event, + ) + + else -> null + }, domainFormatted = request.domainFormatted, queryFormatted = request.queryFormatted, methodFormatted = request.methodFormatted, diff --git a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/FloconNetworkCallEntity.kt b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/FloconNetworkCallEntity.kt index 091a98dc..130d659b 100644 --- a/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/FloconNetworkCallEntity.kt +++ b/FloconDesktop/data/local/src/commonMain/kotlin/io/github/openflocon/data/local/network/models/FloconNetworkCallEntity.kt @@ -45,6 +45,7 @@ enum class FloconNetworkCallType { HTTP, GRAPHQL, GRPC, + WEBSOCKET, } data class FloconNetworkRequestEmbedded( @@ -64,6 +65,13 @@ data class FloconNetworkRequestEmbedded( @Embedded(prefix = "graphql_") val graphql: NetworkCallGraphQlRequestEmbedded?, + + @Embedded(prefix = "websocket_") + val websocket: NetworkCallWebSocketRequestEmbedded?, +) + +data class NetworkCallWebSocketRequestEmbedded( + val event: String, ) data class FloconNetworkResponseEmbedded( diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt index 0beddc82..81392242 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/datasource/NetworkRemoteDataSourceImpl.kt @@ -4,10 +4,12 @@ import com.flocon.data.remote.common.safeDecodeFromString import com.flocon.data.remote.models.FloconOutgoingMessageDataModel import com.flocon.data.remote.models.toRemote import com.flocon.data.remote.network.mapper.listToRemote +import com.flocon.data.remote.network.mapper.toDomain import com.flocon.data.remote.network.mapper.toRemote import com.flocon.data.remote.network.models.FloconNetworkCallIdDataModel import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel import com.flocon.data.remote.network.models.FloconNetworkResponseDataModel +import com.flocon.data.remote.network.models.FloconNetworkWebSocketEvent import com.flocon.data.remote.network.models.toDomain import com.flocon.data.remote.server.Server import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource @@ -68,4 +70,8 @@ class NetworkRemoteDataSourceImpl( override fun getResponseData(message: FloconIncomingMessageDomainModel): FloconNetworkResponseOnlyDomainModel? = json.safeDecodeFromString(message.body) ?.let(FloconNetworkResponseDataModel::toDomain) + + override fun getWebSocketData(message: FloconIncomingMessageDomainModel): FloconNetworkCallDomainModel? = json.safeDecodeFromString(message.body) + ?.let { it.toDomain(appInstance = message.appInstance) } + } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt index 1be09f82..4e04118b 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/Mapper.kt @@ -1,7 +1,9 @@ package com.flocon.data.remote.network.mapper +import com.flocon.data.remote.network.mapper.extractDomain import com.flocon.data.remote.network.models.BadQualityConfigDataModel import com.flocon.data.remote.network.models.FloconNetworkRequestDataModel +import com.flocon.data.remote.network.models.FloconNetworkWebSocketEvent import com.flocon.data.remote.network.models.MockNetworkResponseDataModel import io.github.openflocon.data.core.network.graphql.model.GraphQlExtracted import io.github.openflocon.data.core.network.graphql.model.GraphQlRequestBody @@ -12,6 +14,7 @@ import io.github.openflocon.domain.device.models.AppInstance import io.github.openflocon.domain.network.models.BadQualityConfigDomainModel import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel import io.github.openflocon.domain.network.models.MockNetworkDomainModel +import io.ktor.server.util.url import kotlinx.serialization.json.Json import kotlin.uuid.ExperimentalUuidApi @@ -184,3 +187,48 @@ fun toRemote(domain: BadQualityConfigDomainModel): BadQualityConfigDataModel = } }, ) + + +@OptIn(ExperimentalUuidApi::class) +fun FloconNetworkWebSocketEvent.toDomain( + appInstance: AppInstance, +): FloconNetworkCallDomainModel? { + return try { + val callId = id!! + val startTime = timestamp!! + + val specificInfos = FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket( + event = event!!, + ) + + val method = "websocket" + val body = message ?: error ?: event + val size = size ?: 0L + + val request = FloconNetworkCallDomainModel.Request( + url = url!!, + startTime = startTime, + startTimeFormatted = formatTimestamp(startTime), + method = method, + headers = emptyMap(), + body = body, + byteSize = size, + byteSizeFormatted = ByteFormatter.formatBytes(size), + isMocked = false, // TODO ? + specificInfos = specificInfos, + domainFormatted = extractDomain(url, specificInfos), + methodFormatted = method, + queryFormatted = body, + ) + + FloconNetworkCallDomainModel( + callId = callId, + appInstance = appInstance, + request = request, + response = null, // no response for websocket + ) + } catch (t: Throwable) { + t.printStackTrace() + null + } +} diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkDomainExtractor.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkDomainExtractor.kt index ad756128..3d823651 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkDomainExtractor.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkDomainExtractor.kt @@ -10,6 +10,7 @@ internal fun extractDomain( is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> extractDomainAndPath(requestUrl) is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> extractDomain(requestUrl) is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> requestUrl + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> extractDomainAndPath(requestUrl) } private fun extractDomain(url: String): String { diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkMethodExtractor.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkMethodExtractor.kt index cdda0a61..dd13fc3e 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkMethodExtractor.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkMethodExtractor.kt @@ -9,6 +9,7 @@ internal fun extractMethod( is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> specificInfos.operationType.lowercase() is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> toHttpMethodUi(requestMethod) is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> "grpc" + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> "websocket_extractMethod" // not called } private fun toHttpMethodUi(httpMethod: String) = httpMethod.lowercase() diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkQueryExtractor.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkQueryExtractor.kt index 567d1798..27395331 100644 --- a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkQueryExtractor.kt +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/mapper/NetworkQueryExtractor.kt @@ -10,4 +10,5 @@ internal fun extractQueryFormatted( is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> s.query is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> extractPath(requestUrl) is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> requestMethod + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> "websocket_extractQueryFormatted"// not called } diff --git a/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/FloconNetworkWebSocketEvent.kt b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/FloconNetworkWebSocketEvent.kt new file mode 100644 index 00000000..193dd0d3 --- /dev/null +++ b/FloconDesktop/data/remote/src/commonMain/kotlin/com/flocon/data/remote/network/models/FloconNetworkWebSocketEvent.kt @@ -0,0 +1,14 @@ +package com.flocon.data.remote.network.models + +import kotlinx.serialization.Serializable + +@Serializable +data class FloconNetworkWebSocketEvent( + val id: String? = null, + val event: String? = null, + val url: String? = null, + val size: Long? = null, + val timestamp: Long? = null, + val message: String? = null, + val error: String? = null +) diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt index bec479de..ee5b8742 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/Protocol.kt @@ -68,6 +68,7 @@ object Protocol { object Method { const val LogNetworkCallRequest = "logNetworkCallRequest" const val LogNetworkCallResponse = "logNetworkCallResponse" + const val LogWebSocketEvent = "logWebSocketEvent" } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/FloconNetworkCallDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/FloconNetworkCallDomainModel.kt index 924c1e43..e931cb42 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/FloconNetworkCallDomainModel.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/models/FloconNetworkCallDomainModel.kt @@ -26,6 +26,9 @@ data class FloconNetworkCallDomainModel( ) { sealed interface SpecificInfos { data object Http: SpecificInfos + data class WebSocket( + val event: String, + ): SpecificInfos data class GraphQl( val query: String, val operationType: String, diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkCallsToCsvUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkCallsToCsvUseCase.kt index 54bd744e..be1d0236 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkCallsToCsvUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/network/usecase/ExportNetworkCallsToCsvUseCase.kt @@ -55,7 +55,10 @@ private fun List.exportToCsv(file: File) { val status = when (call.response) { is FloconNetworkCallDomainModel.Response.Success -> "Success" is FloconNetworkCallDomainModel.Response.Failure -> "Failure" - null -> "Pending" + null -> when(val s = call.request.specificInfos) { + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> s.event + else -> "Pending" + } } val httpCode = when (call.response) { is FloconNetworkCallDomainModel.Response.Success -> call.response.specificInfos.httpCode().toString() @@ -104,6 +107,7 @@ private fun List.exportToCsv(file: File) { is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> "HTTP" is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> "GraphQL" is FloconNetworkCallDomainModel.Request.SpecificInfos.Grpc -> "gRPC" + is FloconNetworkCallDomainModel.Request.SpecificInfos.WebSocket -> "websocket" } val dataList = listOf(