mirror of
https://github.com/openflocon/Flocon.git
synced 2026-04-28 10:09:32 +00:00
feat: [NETWORK] Share markdown (#462)
Some checks failed
docs / deploy (push) Has been cancelled
Some checks failed
docs / deploy (push) Has been cancelled
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
0a20ed44f6
commit
d4163217d5
13 changed files with 156 additions and 2 deletions
|
|
@ -5,6 +5,8 @@ import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableD
|
|||
import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableScoped
|
||||
import io.github.openflocon.flocondesktop.common.coroutines.dispatcherprovider.DispatcherProviderImpl
|
||||
import io.github.openflocon.flocondesktop.common.db.roomModule
|
||||
import io.github.openflocon.flocondesktop.common.ui.JsonFormatterImpl
|
||||
import io.github.openflocon.domain.common.JsonFormatter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.koin.core.module.dsl.bind
|
||||
|
|
@ -18,6 +20,9 @@ val commonModule =
|
|||
singleOf(::DispatcherProviderImpl) {
|
||||
bind<DispatcherProvider>()
|
||||
}
|
||||
singleOf(::JsonFormatterImpl) {
|
||||
bind<JsonFormatter>()
|
||||
}
|
||||
single {
|
||||
val dispatcherProvider = get<DispatcherProvider>()
|
||||
CoroutineScope(dispatcherProvider.data + SupervisorJob()) // the application scope
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.github.openflocon.flocondesktop.common.ui
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import io.github.openflocon.domain.common.JsonFormatter
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
|
|
@ -37,3 +38,11 @@ object JsonPrettyPrinter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class JsonFormatterImpl : JsonFormatter {
|
||||
override fun toPrettyJson(text: String) : String {
|
||||
return JsonPrettyPrinter.prettyPrint(text)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,6 @@ sealed interface NetworkDetailAction {
|
|||
data class Request(val item: NetworkDetailViewState) : OpenBodyExternally
|
||||
data class Response(val item: NetworkDetailViewState.Response.Success) : OpenBodyExternally
|
||||
}
|
||||
|
||||
data object ShareAsMarkdown : NetworkDetailAction
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import io.github.openflocon.domain.common.DispatcherProvider
|
|||
import io.github.openflocon.domain.feedback.FeedbackDisplayer
|
||||
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
|
||||
import io.github.openflocon.domain.network.usecase.DecodeJwtTokenUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkCallAsMarkdownUseCase
|
||||
import io.github.openflocon.domain.network.usecase.ObserveNetworkRequestsByIdUseCase
|
||||
import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableDelegate
|
||||
import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableScoped
|
||||
|
|
@ -42,6 +43,7 @@ class NetworkDetailDelegate(
|
|||
KoinComponent {
|
||||
|
||||
private val openBodyDelegate: OpenBodyDelegate by inject()
|
||||
private val getNetworkCallAsMarkdownUseCase: GetNetworkCallAsMarkdownUseCase by inject()
|
||||
|
||||
private val requestId = MutableStateFlow("")
|
||||
|
||||
|
|
@ -88,6 +90,7 @@ class NetworkDetailDelegate(
|
|||
is NetworkDetailAction.JsonDetail -> onJsonDetail(action)
|
||||
is NetworkDetailAction.OpenBodyExternally.Request -> openBodyDelegate.openBodyExternally(action.item)
|
||||
is NetworkDetailAction.OpenBodyExternally.Response -> openBodyDelegate.openBodyExternally(action.item)
|
||||
is NetworkDetailAction.ShareAsMarkdown -> copyAsMarkdown(requestId.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,4 +110,13 @@ class NetworkDetailDelegate(
|
|||
onJsonDetail(NetworkDetailAction.JsonDetail(json = it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyAsMarkdown(requestId: String) {
|
||||
coroutineScope.launch {
|
||||
getNetworkCallAsMarkdownUseCase(requestId)?.let {
|
||||
copyToClipboard(it)
|
||||
feedbackDisplayer.displayMessage("Markdown copied to clipboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
package io.github.openflocon.flocondesktop.features.network.detail
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.github.openflocon.domain.feedback.FeedbackDisplayer
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class NetworkDetailViewModel(
|
||||
requestId: String,
|
||||
private val delegate: NetworkDetailDelegate
|
||||
) : ViewModel(),
|
||||
KoinComponent {
|
||||
) : ViewModel(delegate) {
|
||||
|
||||
val uiState = delegate.uiState
|
||||
|
||||
|
|
@ -18,4 +19,5 @@ class NetworkDetailViewModel(
|
|||
fun onAction(action: NetworkDetailAction) {
|
||||
delegate.onAction(action)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package io.github.openflocon.flocondesktop.features.network.detail.view
|
|||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -17,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.CopyAll
|
||||
import androidx.compose.material.icons.outlined.OpenInFull
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -95,6 +98,17 @@ fun NetworkDetailContent(
|
|||
.verticalScroll(scrollState)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
FloconIconButton(
|
||||
tooltip = "Share as Markdown",
|
||||
imageVector = Icons.Outlined.Share,
|
||||
onClick = { onAction(NetworkDetailAction.ShareAsMarkdown) }
|
||||
)
|
||||
}
|
||||
Request(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.github.openflocon.domain.network.models.NetworkFilterDomainModel.Filte
|
|||
import io.github.openflocon.domain.network.models.NetworkTextFilterColumns
|
||||
import io.github.openflocon.domain.network.usecase.ExportNetworkCallsToCsvUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GenerateCurlCommandUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkCallAsMarkdownUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkRequestsUseCase
|
||||
import io.github.openflocon.domain.network.usecase.ImportNetworkCallsFromCsvUseCase
|
||||
import io.github.openflocon.domain.network.usecase.ObserveNetworkRequestsByIdUseCase
|
||||
|
|
@ -98,6 +99,7 @@ class NetworkViewModel(
|
|||
private val feedbackDisplayer: FeedbackDisplayer by inject()
|
||||
private val exportNetworkCallsToCsv: ExportNetworkCallsToCsvUseCase by inject()
|
||||
private val replayNetworkCallUseCase: ReplayNetworkCallUseCase by inject()
|
||||
private val getNetworkCallAsMarkdownUseCase: GetNetworkCallAsMarkdownUseCase by inject()
|
||||
|
||||
private val contentState = MutableStateFlow(
|
||||
ContentUiState(
|
||||
|
|
@ -276,6 +278,16 @@ class NetworkViewModel(
|
|||
NetworkAction.DeleteSelection -> onDeleteSelection()
|
||||
is NetworkAction.DoubleClicked -> onDoubleClicked(action)
|
||||
NetworkAction.OpenDeepSearch -> navigationState.navigate(NetworkRoutes.DeepSearch)
|
||||
is NetworkAction.ShareAsMarkdown -> onShareAsMarkdown(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShareAsMarkdown(action: NetworkAction.ShareAsMarkdown) {
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
getNetworkCallAsMarkdownUseCase(action.item.uuid)?.let {
|
||||
copyToClipboard(it)
|
||||
feedbackDisplayer.displayMessage("Markdown copied to clipboard")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ sealed interface NetworkAction {
|
|||
val itemIdToSelect: String,
|
||||
) : NetworkAction
|
||||
|
||||
data class ShareAsMarkdown(val item: NetworkItemViewState) : NetworkAction
|
||||
|
||||
data class Down(
|
||||
val itemIdToSelect: String,
|
||||
) : NetworkAction
|
||||
|
|
|
|||
|
|
@ -237,6 +237,10 @@ private fun contextualActions(
|
|||
label = "Replay",
|
||||
onClick = { onActionCallback(NetworkAction.Replay(state)) }
|
||||
)
|
||||
item(
|
||||
label = "Share as Markdown",
|
||||
onClick = { onActionCallback(NetworkAction.ShareAsMarkdown(state)) }
|
||||
)
|
||||
}
|
||||
item(label = "Select Item", onClick = { onActionCallback(NetworkAction.SelectLine(state.uuid, selected = true)) })
|
||||
separator()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.ScreenSearchDesktop
|
|||
import androidx.compose.material.icons.outlined.CleaningServices
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.ImportExport
|
||||
import androidx.compose.material.icons.outlined.PlayCircle
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
|
|
@ -218,6 +219,15 @@ fun NetworkScreen(
|
|||
imageVector = Icons.Outlined.SignalWifiStatusbarConnectedNoInternet4
|
||||
)
|
||||
}
|
||||
FloconIconToggleButton(
|
||||
value = uiState.filterState.displayOldSessions,
|
||||
tooltip = "Display old sessions",
|
||||
onValueChange = { onAction(NetworkAction.UpdateDisplayOldSessions(it)) }
|
||||
) {
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.History
|
||||
)
|
||||
}
|
||||
FloconIconButton(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
onClick = { onAction(NetworkAction.Reset) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package io.github.openflocon.domain.common
|
||||
|
||||
interface JsonFormatter {
|
||||
fun toPrettyJson(text: String) : String
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package io.github.openflocon.domain.network
|
|||
import io.github.openflocon.domain.network.usecase.DecodeJwtTokenUseCase
|
||||
import io.github.openflocon.domain.network.usecase.ExportNetworkCallsToCsvUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GenerateCurlCommandUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkCallAsMarkdownUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkFilterUseCase
|
||||
import io.github.openflocon.domain.network.usecase.GetNetworkRequestsUseCase
|
||||
import io.github.openflocon.domain.network.usecase.ObserveNetworkFilterUseCase
|
||||
|
|
@ -63,6 +64,8 @@ internal val networkModule = module {
|
|||
factoryOf(::ObserveNetworkWebsocketIdsUseCase)
|
||||
factoryOf(::SendNetworkWebsocketMockUseCase)
|
||||
// bad quality
|
||||
factoryOf(::GetNetworkCallAsMarkdownUseCase)
|
||||
|
||||
factoryOf(::ObserveNetworkBadQualityUseCase)
|
||||
factoryOf(::SaveNetworkBadQualityUseCase)
|
||||
factoryOf(::DeleteBadQualityUseCase)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package io.github.openflocon.domain.network.usecase
|
||||
|
||||
import io.github.openflocon.domain.common.JsonFormatter
|
||||
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
|
||||
import io.github.openflocon.domain.network.models.responseBody
|
||||
import io.github.openflocon.domain.network.models.responseByteSizeFormatted
|
||||
import io.github.openflocon.domain.network.models.responseHeaders
|
||||
import io.github.openflocon.domain.network.repository.NetworkRepository
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
class GetNetworkCallAsMarkdownUseCase(
|
||||
private val networkRepository: NetworkRepository,
|
||||
private val jsonFormatter: JsonFormatter,
|
||||
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(callId: String): String? {
|
||||
val deviceIdAndPackageName = getCurrentDeviceIdAndPackageNameUseCase() ?: return null
|
||||
val call = networkRepository.observeRequest(
|
||||
deviceIdAndPackageName = deviceIdAndPackageName,
|
||||
requestId = callId
|
||||
).firstOrNull() ?: return null
|
||||
|
||||
|
||||
return buildString {
|
||||
appendLine("### ${call.request.method} ${call.request.url}")
|
||||
appendLine()
|
||||
appendLine("**Status**: ${call.response?.statusFormatted ?: "Pending"}")
|
||||
appendLine("**Time**: ${call.request.startTimeFormatted}")
|
||||
appendLine("**Duration**: ${call.response?.durationFormatted ?: "-"}")
|
||||
appendLine("**Size**: ${call.responseByteSizeFormatted() ?: "-"}")
|
||||
appendLine()
|
||||
|
||||
appendLine("#### Request Headers")
|
||||
appendLine("```")
|
||||
call.request.headers.forEach { (key, value) ->
|
||||
appendLine("$key: $value")
|
||||
}
|
||||
appendLine("```")
|
||||
appendLine()
|
||||
|
||||
if (!call.request.body.isNullOrBlank()) {
|
||||
appendLine("#### Request Body")
|
||||
appendLine("```json")
|
||||
appendLine(jsonFormatter.toPrettyJson(call.request.body))
|
||||
appendLine("```")
|
||||
appendLine()
|
||||
}
|
||||
|
||||
if (call.response != null) {
|
||||
appendLine("#### Response Headers")
|
||||
appendLine("```")
|
||||
call.responseHeaders()?.forEach { (key, value) ->
|
||||
appendLine("$key: $value")
|
||||
}
|
||||
appendLine("```")
|
||||
appendLine()
|
||||
|
||||
if (!call.responseBody().isNullOrBlank()) {
|
||||
appendLine("#### Response Body")
|
||||
appendLine("```json")
|
||||
appendLine(call.responseBody()?.let {
|
||||
jsonFormatter.toPrettyJson(it)
|
||||
} )
|
||||
appendLine("```")
|
||||
} else if (call.response is io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel.Response.Failure) {
|
||||
appendLine("#### Error")
|
||||
appendLine("```")
|
||||
appendLine((call.response as io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel.Response.Failure).issue)
|
||||
appendLine("```")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue