mirror of
https://github.com/openflocon/Flocon.git
synced 2026-05-06 05:50:30 +00:00
feat: graphql (#14)
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
7b502a21d0
commit
cf1f96bc3c
81 changed files with 2539 additions and 155 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 17,
|
||||
"identityHash": "f2ab24c856bd7abdb58d0a171905f648",
|
||||
"identityHash": "450793ab50e712166d71e2599a386ca2",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "FloconHttpRequestEntity",
|
||||
|
|
@ -860,11 +860,124 @@
|
|||
"createSql": "CREATE INDEX IF NOT EXISTS `index_AnalyticsItemEntity_deviceId_analyticsTableId` ON `${TABLE_NAME}` (`deviceId`, `analyticsTableId`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "GraphQlRequestEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `url` TEXT NOT NULL, `durationMs` REAL NOT NULL, `request_startTime` INTEGER NOT NULL, `request_method` TEXT NOT NULL, `request_body` TEXT, `request_byteSize` INTEGER NOT NULL, `request_headers` TEXT NOT NULL, `response_httpCode` INTEGER NOT NULL, `response_contentType` TEXT, `response_body` TEXT, `response_byteSize` INTEGER NOT NULL, `response_headers` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "uuid",
|
||||
"columnName": "uuid",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "deviceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "durationMs",
|
||||
"columnName": "durationMs",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "request.startTime",
|
||||
"columnName": "request_startTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "request.method",
|
||||
"columnName": "request_method",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "request.body",
|
||||
"columnName": "request_body",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "request.byteSize",
|
||||
"columnName": "request_byteSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "request.headers",
|
||||
"columnName": "request_headers",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "response.httpCode",
|
||||
"columnName": "response_httpCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "response.contentType",
|
||||
"columnName": "response_contentType",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "response.body",
|
||||
"columnName": "response_body",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "response.byteSize",
|
||||
"columnName": "response_byteSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "response.headers",
|
||||
"columnName": "response_headers",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"uuid"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_GraphQlRequestEntity_deviceId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"deviceId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_GraphQlRequestEntity_deviceId` ON `${TABLE_NAME}` (`deviceId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_GraphQlRequestEntity_uuid",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"uuid"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GraphQlRequestEntity_uuid` ON `${TABLE_NAME}` (`uuid`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2ab24c856bd7abdb58d0a171905f648')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '450793ab50e712166d71e2599a386ca2')"
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -18,6 +18,8 @@ import com.florent37.flocondesktop.features.deeplinks.data.datasource.room.Floco
|
|||
import com.florent37.flocondesktop.features.deeplinks.data.datasource.room.model.DeeplinkEntity
|
||||
import com.florent37.flocondesktop.features.files.data.datasources.FloconFileDao
|
||||
import com.florent37.flocondesktop.features.files.data.datasources.model.FileEntity
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.GraphQlDao
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.model.GraphQlRequestEntity
|
||||
import com.florent37.flocondesktop.features.grpc.data.datasource.room.GrpcDao
|
||||
import com.florent37.flocondesktop.features.grpc.data.datasource.room.model.GrpcCallEntity
|
||||
import com.florent37.flocondesktop.features.grpc.data.datasource.room.model.GrpcResponseEntity
|
||||
|
|
@ -31,7 +33,7 @@ import com.florent37.flocondesktop.features.table.data.datasource.local.model.Ta
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Database(
|
||||
version = 17,
|
||||
version = 18,
|
||||
entities = [
|
||||
FloconHttpRequestEntity::class,
|
||||
FileEntity::class,
|
||||
|
|
@ -46,6 +48,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
GrpcCallEntity::class,
|
||||
GrpcResponseEntity::class,
|
||||
AnalyticsItemEntity::class,
|
||||
GraphQlRequestEntity::class,
|
||||
],
|
||||
)
|
||||
@TypeConverters(
|
||||
|
|
@ -62,6 +65,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
abstract val deeplinkDao: FloconDeeplinkDao
|
||||
abstract val grpcDao: GrpcDao
|
||||
abstract val analyticsDao: FloconAnalyticsDao
|
||||
abstract val graphQlDao: GraphQlDao
|
||||
}
|
||||
|
||||
fun getRoomDatabase(): AppDatabase = getDatabaseBuilder()
|
||||
|
|
|
|||
|
|
@ -34,4 +34,7 @@ val roomModule =
|
|||
single {
|
||||
get<AppDatabase>().analyticsDao
|
||||
}
|
||||
single {
|
||||
get<AppDatabase>().graphQlDao
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.florent37.flocondesktop.features.dashboard.di.dashboardModule
|
|||
import com.florent37.flocondesktop.features.database.di.databaseModule
|
||||
import com.florent37.flocondesktop.features.deeplinks.di.deeplinkModule
|
||||
import com.florent37.flocondesktop.features.files.di.filesModule
|
||||
import com.florent37.flocondesktop.features.graphql.di.graphqlModule
|
||||
import com.florent37.flocondesktop.features.grpc.di.grpcModule
|
||||
import com.florent37.flocondesktop.features.images.di.imagesModule
|
||||
import com.florent37.flocondesktop.features.network.di.networkModule
|
||||
|
|
@ -27,5 +28,6 @@ val featuresModule =
|
|||
dashboardModule,
|
||||
tableModule,
|
||||
deeplinkModule,
|
||||
graphqlModule,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data
|
||||
|
||||
import com.florent37.flocondesktop.DeviceId
|
||||
import com.florent37.flocondesktop.FloconIncomingMessageDataModel
|
||||
import com.florent37.flocondesktop.Protocol
|
||||
import com.florent37.flocondesktop.common.coroutines.dispatcherprovider.DispatcherProvider
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.LocalGraphQlDataSource
|
||||
import com.florent37.flocondesktop.features.graphql.data.model.FloconGraphQlRequestDataModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.FloconGraphQlRequestInfos
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
import com.florent37.flocondesktop.messages.domain.repository.sub.MessagesReceiverRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
class GraphQlRepositoryImpl(
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val localGraphQlDataSource: LocalGraphQlDataSource,
|
||||
) : GraphQlRepository,
|
||||
MessagesReceiverRepository {
|
||||
|
||||
private val graphQlParser =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
override val pluginName = listOf(Protocol.FromDevice.Graphql.Plugin)
|
||||
|
||||
override suspend fun onMessageReceived(
|
||||
deviceId: String,
|
||||
message: FloconIncomingMessageDataModel,
|
||||
) {
|
||||
withContext(dispatcherProvider.data) {
|
||||
when (message.method) {
|
||||
Protocol.FromDevice.Graphql.Method.LogNetworkCall -> decode(message)?.let {
|
||||
toDomain(it)
|
||||
}?.let {
|
||||
localGraphQlDataSource.insert(
|
||||
deviceId = deviceId,
|
||||
request = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decode(message: FloconIncomingMessageDataModel): FloconGraphQlRequestDataModel? = try {
|
||||
graphQlParser.decodeFromString<FloconGraphQlRequestDataModel>(message.body)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
override fun observeRequests(deviceId: DeviceId): Flow<List<GraphQlRequestDomainModel>> = localGraphQlDataSource.observeRequests(
|
||||
deviceId = deviceId,
|
||||
).flowOn(dispatcherProvider.data)
|
||||
|
||||
override fun observeRequest(
|
||||
currentDeviceId: DeviceId,
|
||||
requestId: GraphQlRequestId,
|
||||
): Flow<GraphQlRequestDomainModel?> = localGraphQlDataSource.observeRequest(
|
||||
deviceId = currentDeviceId,
|
||||
requestId = requestId,
|
||||
).flowOn(dispatcherProvider.data)
|
||||
|
||||
override suspend fun deleteRequest(deviceId: DeviceId, requestId: GraphQlRequestId) = withContext(dispatcherProvider.data) {
|
||||
localGraphQlDataSource.deleteRequest(deviceId = deviceId, requestId = requestId)
|
||||
}
|
||||
|
||||
override suspend fun deleteRequestsBefore(deviceId: DeviceId, requestId: GraphQlRequestId) = withContext(dispatcherProvider.data) {
|
||||
localGraphQlDataSource.deleteRequestsBefore(deviceId = deviceId, requestId = requestId)
|
||||
}
|
||||
|
||||
override suspend fun deleteRequestsForDevice(deviceId: DeviceId) = withContext(dispatcherProvider.data) {
|
||||
localGraphQlDataSource.clearDeviceCalls(deviceId = deviceId)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
fun toDomain(decoded: FloconGraphQlRequestDataModel): GraphQlRequestDomainModel? = try {
|
||||
GraphQlRequestDomainModel(
|
||||
uuid = Uuid.random().toString(),
|
||||
infos = FloconGraphQlRequestInfos(
|
||||
url = decoded.url!!,
|
||||
durationMs = decoded.durationMs!!,
|
||||
request = FloconGraphQlRequestInfos.Request(
|
||||
method = decoded.method!!,
|
||||
startTime = decoded.startTime!!,
|
||||
headers = decoded.requestHeaders!!,
|
||||
body = decoded.requestBody,
|
||||
byteSize = decoded.requestSize ?: 0L,
|
||||
),
|
||||
response = FloconGraphQlRequestInfos.Response(
|
||||
httpCode = decoded.responseHttpCode!!,
|
||||
contentType = decoded.responseContentType,
|
||||
body = decoded.responseBody,
|
||||
headers = decoded.responseHeaders!!,
|
||||
byteSize = decoded.responseSize ?: 0L,
|
||||
),
|
||||
),
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.datasource
|
||||
|
||||
import com.florent37.flocondesktop.DeviceId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalGraphQlDataSource {
|
||||
suspend fun insert(
|
||||
deviceId: DeviceId,
|
||||
request: GraphQlRequestDomainModel,
|
||||
)
|
||||
|
||||
fun observeRequests(deviceId: DeviceId): Flow<List<GraphQlRequestDomainModel>>
|
||||
fun observeRequest(deviceId: DeviceId, requestId: GraphQlRequestId): Flow<GraphQlRequestDomainModel?>
|
||||
suspend fun clearDeviceCalls(deviceId: DeviceId)
|
||||
|
||||
suspend fun deleteRequest(deviceId: DeviceId, requestId: GraphQlRequestId)
|
||||
suspend fun deleteRequestsBefore(deviceId: DeviceId, requestId: GraphQlRequestId)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.datasource.room
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.model.GraphQlRequestEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface GraphQlDao {
|
||||
@Insert
|
||||
suspend fun insertGraphQlCall(call: GraphQlRequestEntity)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM GraphQlRequestEntity
|
||||
WHERE deviceId = :deviceId
|
||||
ORDER BY request_startTime ASC
|
||||
""",
|
||||
)
|
||||
fun observeRequests(deviceId: String): Flow<List<GraphQlRequestEntity>>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM GraphQlRequestEntity
|
||||
WHERE deviceId = :deviceId
|
||||
AND uuid = :requestId
|
||||
LIMIT 1
|
||||
""",
|
||||
)
|
||||
fun observeRequest(deviceId: String, requestId: String): Flow<GraphQlRequestEntity?>
|
||||
|
||||
@Query("DELETE FROM GraphQlRequestEntity WHERE deviceId = :deviceId")
|
||||
suspend fun clearDeviceRequests(deviceId: String)
|
||||
|
||||
@Query("DELETE FROM GraphQlRequestEntity WHERE uuid = :requestId")
|
||||
suspend fun deleteRequestById(requestId: String)
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteRequestsBeforeTimestamp(deviceId: String, timestamp: Long) {
|
||||
// First, get IDs of requests to be deleted
|
||||
val idsToDelete = getRequestsIdsBeforeTimestamp(deviceId, timestamp)
|
||||
|
||||
// Delete associated headers and responses
|
||||
for (callId in idsToDelete) {
|
||||
deleteRequestById(callId)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT uuid FROM GraphQlRequestEntity WHERE deviceId = :deviceId AND request_startTime < :timestamp")
|
||||
suspend fun getRequestsIdsBeforeTimestamp(deviceId: String, timestamp: Long): List<String>
|
||||
|
||||
@Query("DELETE FROM GraphQlRequestEntity")
|
||||
suspend fun clearAllData()
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT request_startTime
|
||||
FROM GraphQlRequestEntity
|
||||
WHERE deviceId = :deviceId
|
||||
AND uuid = :requestId
|
||||
LIMIT 1
|
||||
""",
|
||||
)
|
||||
suspend fun getRequestTimestamp(deviceId: String, requestId: String): Long?
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.datasource.room
|
||||
|
||||
import com.florent37.flocondesktop.DeviceId
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.LocalGraphQlDataSource
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.mapper.toDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.mapper.toEntity
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class LocalGraphQlDataSourceImpl(
|
||||
private val graphQlDao: GraphQlDao,
|
||||
) : LocalGraphQlDataSource {
|
||||
|
||||
override suspend fun insert(
|
||||
deviceId: String,
|
||||
request: GraphQlRequestDomainModel,
|
||||
) {
|
||||
graphQlDao.insertGraphQlCall(request.toEntity(deviceId = deviceId))
|
||||
}
|
||||
|
||||
override fun observeRequests(deviceId: String): Flow<List<GraphQlRequestDomainModel>> = graphQlDao.observeRequests(deviceId = deviceId).map { entities ->
|
||||
entities.map { it.toDomainModel() }
|
||||
}
|
||||
|
||||
override fun observeRequest(deviceId: DeviceId, requestId: GraphQlRequestId) = graphQlDao.observeRequest(deviceId = deviceId, requestId = requestId).map {
|
||||
it?.toDomainModel()
|
||||
}
|
||||
|
||||
override suspend fun clearDeviceCalls(deviceId: String) {
|
||||
graphQlDao.clearDeviceRequests(deviceId)
|
||||
}
|
||||
|
||||
override suspend fun deleteRequest(deviceId: String, requestId: GraphQlRequestId) {
|
||||
graphQlDao.deleteRequestById(requestId)
|
||||
}
|
||||
|
||||
override suspend fun deleteRequestsBefore(deviceId: String, requestId: GraphQlRequestId) {
|
||||
val timestamp = graphQlDao.getRequestTimestamp(deviceId = deviceId, requestId = requestId) ?: return
|
||||
graphQlDao.deleteRequestsBeforeTimestamp(
|
||||
deviceId = deviceId,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
graphQlDao.clearAllData()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.datasource.room.mapper
|
||||
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.model.GraphQlRequestEntity
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.FloconGraphQlRequestInfos
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
|
||||
fun GraphQlRequestDomainModel.toEntity(deviceId: String): GraphQlRequestEntity = GraphQlRequestEntity(
|
||||
uuid = this.uuid,
|
||||
request = with(this.infos.request) {
|
||||
GraphQlRequestEntity.Request(
|
||||
method = this.method,
|
||||
startTime = this.startTime,
|
||||
headers = this.headers,
|
||||
body = this.body,
|
||||
byteSize = this.byteSize,
|
||||
)
|
||||
},
|
||||
response = with(this.infos.response) {
|
||||
GraphQlRequestEntity.Response(
|
||||
httpCode = this.httpCode,
|
||||
contentType = this.contentType,
|
||||
body = this.body,
|
||||
headers = this.headers,
|
||||
byteSize = this.byteSize,
|
||||
)
|
||||
},
|
||||
deviceId = deviceId,
|
||||
url = this.infos.url,
|
||||
durationMs = this.infos.durationMs,
|
||||
)
|
||||
|
||||
fun GraphQlRequestEntity.toDomainModel(): GraphQlRequestDomainModel = GraphQlRequestDomainModel(
|
||||
uuid = this.uuid,
|
||||
infos = FloconGraphQlRequestInfos(
|
||||
url = this.url,
|
||||
durationMs = this.durationMs,
|
||||
request = with(this.request) {
|
||||
FloconGraphQlRequestInfos.Request(
|
||||
method = this.method,
|
||||
startTime = this.startTime,
|
||||
headers = headers,
|
||||
body = body,
|
||||
byteSize = byteSize,
|
||||
)
|
||||
},
|
||||
response = with(this.response) {
|
||||
FloconGraphQlRequestInfos.Response(
|
||||
httpCode = httpCode,
|
||||
contentType = contentType,
|
||||
body = body,
|
||||
headers = headers,
|
||||
byteSize = byteSize,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.datasource.room.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
indices = [
|
||||
Index(value = ["deviceId"]),
|
||||
Index(value = ["uuid"], unique = true),
|
||||
],
|
||||
)
|
||||
data class GraphQlRequestEntity(
|
||||
@PrimaryKey val uuid: String,
|
||||
val deviceId: String,
|
||||
val url: String,
|
||||
val durationMs: Double,
|
||||
@Embedded(prefix = "request_") val request: Request,
|
||||
@Embedded(prefix = "response_") val response: Response,
|
||||
) {
|
||||
data class Request(
|
||||
val startTime: Long,
|
||||
val method: String,
|
||||
val body: String?,
|
||||
val byteSize: Long,
|
||||
val headers: Map<String, String>,
|
||||
)
|
||||
|
||||
data class Response(
|
||||
val httpCode: Int,
|
||||
val contentType: String?,
|
||||
val body: String?,
|
||||
val byteSize: Long,
|
||||
val headers: Map<String, String>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.di
|
||||
|
||||
import com.florent37.flocondesktop.features.graphql.data.GraphQlRepositoryImpl
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.LocalGraphQlDataSource
|
||||
import com.florent37.flocondesktop.features.graphql.data.datasource.room.LocalGraphQlDataSourceImpl
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
import com.florent37.flocondesktop.messages.domain.repository.sub.MessagesReceiverRepository
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val graphQlDataModule =
|
||||
module {
|
||||
factoryOf(::GraphQlRepositoryImpl) {
|
||||
bind<GraphQlRepository>()
|
||||
bind<MessagesReceiverRepository>()
|
||||
}
|
||||
singleOf(::LocalGraphQlDataSourceImpl) {
|
||||
bind<LocalGraphQlDataSource>()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.mapper
|
||||
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.GrpcRequestDomainModelWrapper
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.GrpcResponseDomainModelWrapper
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.fromdevice.GrpcHeaderDataModel
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.fromdevice.GrpcRequestDataModel
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.fromdevice.GrpcResponseDataModel
|
||||
import com.florent37.flocondesktop.features.grpc.domain.model.GrpcHeaderDomainModel
|
||||
import com.florent37.flocondesktop.features.grpc.domain.model.GrpcRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.grpc.domain.model.GrpcResponseDomainModel
|
||||
|
||||
fun toDomain(dataModel: GrpcRequestDataModel): GrpcRequestDomainModelWrapper? = GrpcRequestDomainModelWrapper(
|
||||
callId = dataModel.id,
|
||||
request = GrpcRequestDomainModel(
|
||||
timestamp = dataModel.timestamp,
|
||||
authority = dataModel.authority,
|
||||
method = dataModel.method,
|
||||
headers = dataModel.headers.map {
|
||||
toDomain(it)
|
||||
},
|
||||
data = dataModel.data,
|
||||
),
|
||||
)
|
||||
|
||||
fun toDomain(dataModel: GrpcResponseDataModel): GrpcResponseDomainModelWrapper? {
|
||||
return GrpcResponseDomainModelWrapper(
|
||||
callId = dataModel.id,
|
||||
response = GrpcResponseDomainModel(
|
||||
timestamp = dataModel.timestamp,
|
||||
status = dataModel.status,
|
||||
headers = dataModel.headers.map {
|
||||
toDomain(it)
|
||||
},
|
||||
result = when {
|
||||
dataModel.data != null -> GrpcResponseDomainModel.CallResult.Success(
|
||||
data = dataModel.data,
|
||||
)
|
||||
dataModel.cause != null -> GrpcResponseDomainModel.CallResult.Error(
|
||||
cause = dataModel.cause,
|
||||
)
|
||||
else -> return null
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toDomain(model: GrpcHeaderDataModel): GrpcHeaderDomainModel = GrpcHeaderDomainModel(
|
||||
key = model.key,
|
||||
value = model.value,
|
||||
)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.florent37.flocondesktop.features.graphql.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FloconGraphQlRequestDataModel(
|
||||
val url: String? = null,
|
||||
val method: String? = null,
|
||||
val startTime: Long? = null,
|
||||
val durationMs: Double? = null,
|
||||
// request
|
||||
val requestHeaders: Map<String, String>? = null,
|
||||
val requestBody: String? = null,
|
||||
val requestSize: Long? = null,
|
||||
// response
|
||||
val responseHttpCode: Int? = null, // ex: 200
|
||||
val responseContentType: String? = null,
|
||||
val responseBody: String? = null,
|
||||
val responseHeaders: Map<String, String>? = null,
|
||||
val responseSize: Long? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.florent37.flocondesktop.features.graphql.di
|
||||
|
||||
import com.florent37.flocondesktop.features.graphql.data.di.graphQlDataModule
|
||||
import com.florent37.flocondesktop.features.graphql.domain.di.graphQlDomainModule
|
||||
import com.florent37.flocondesktop.features.graphql.ui.di.graphQlUiModule
|
||||
import org.koin.dsl.module
|
||||
|
||||
val graphqlModule =
|
||||
module {
|
||||
includes(
|
||||
graphQlDataModule,
|
||||
graphQlDomainModule,
|
||||
graphQlUiModule,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain
|
||||
|
||||
import com.florent37.flocondesktop.core.domain.device.GetCurrentDeviceIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
|
||||
class DeleteGraphQlRequestUseCase(
|
||||
private val graphQlRepository: GraphQlRepository,
|
||||
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(requestId: GraphQlRequestId) {
|
||||
val deviceId = getCurrentDeviceIdUseCase() ?: return
|
||||
graphQlRepository.deleteRequest(deviceId, requestId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain
|
||||
|
||||
import com.florent37.flocondesktop.core.domain.device.GetCurrentDeviceIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
|
||||
class DeleteGraphQlRequestsBeforeUseCase(
|
||||
private val graphQlRepository: GraphQlRepository,
|
||||
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(requestId: GraphQlRequestId) {
|
||||
val deviceId = getCurrentDeviceIdUseCase() ?: return
|
||||
graphQlRepository.deleteRequestsBefore(deviceId = deviceId, requestId = requestId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain
|
||||
|
||||
import com.florent37.flocondesktop.core.domain.device.ObserveCurrentDeviceIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class ObserveGraphQlRequestsByIdUseCase(
|
||||
private val graphQlRepository: GraphQlRepository,
|
||||
private val observeCurrentDeviceIdUseCase: ObserveCurrentDeviceIdUseCase,
|
||||
) {
|
||||
operator fun invoke(callId: GraphQlRequestId): Flow<GraphQlRequestDomainModel?> = observeCurrentDeviceIdUseCase().flatMapLatest { currentDeviceId ->
|
||||
if (currentDeviceId == null) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
graphQlRepository.observeRequest(currentDeviceId, requestId = callId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain
|
||||
|
||||
import com.florent37.flocondesktop.core.domain.device.ObserveCurrentDeviceIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class ObserveGraphQlRequestsUseCase(
|
||||
private val graphQlRepository: GraphQlRepository,
|
||||
private val observeCurrentDeviceIdUseCase: ObserveCurrentDeviceIdUseCase,
|
||||
) {
|
||||
operator fun invoke(): Flow<List<GraphQlRequestDomainModel>> = observeCurrentDeviceIdUseCase().flatMapLatest { currentDeviceId ->
|
||||
if (currentDeviceId == null) {
|
||||
flowOf(emptyList())
|
||||
} else {
|
||||
graphQlRepository.observeRequests(currentDeviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain
|
||||
|
||||
import com.florent37.flocondesktop.core.domain.device.GetCurrentDeviceIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.repository.GraphQlRepository
|
||||
|
||||
class ResetCurrentDeviceGraphQlRequestsUseCase(
|
||||
private val graphQlRepository: GraphQlRepository,
|
||||
private val getCurrentDeviceIdUseCase: GetCurrentDeviceIdUseCase,
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
val deviceId = getCurrentDeviceIdUseCase() ?: return
|
||||
graphQlRepository.deleteRequestsForDevice(deviceId = deviceId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.di
|
||||
|
||||
import com.florent37.flocondesktop.features.graphql.domain.DeleteGraphQlRequestUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.DeleteGraphQlRequestsBeforeUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ObserveGraphQlRequestsByIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ObserveGraphQlRequestsUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ResetCurrentDeviceGraphQlRequestsUseCase
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val graphQlDomainModule =
|
||||
module {
|
||||
factoryOf(::ObserveGraphQlRequestsUseCase)
|
||||
factoryOf(::ObserveGraphQlRequestsByIdUseCase)
|
||||
factoryOf(::DeleteGraphQlRequestUseCase)
|
||||
factoryOf(::DeleteGraphQlRequestsBeforeUseCase)
|
||||
factoryOf(::ResetCurrentDeviceGraphQlRequestsUseCase)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.model
|
||||
|
||||
data class FloconGraphQlRequestInfos(
|
||||
val url: String,
|
||||
val durationMs: Double,
|
||||
val request: Request,
|
||||
val response: Response,
|
||||
) {
|
||||
data class Request(
|
||||
val method: String,
|
||||
val startTime: Long,
|
||||
val headers: Map<String, String>,
|
||||
val body: String?,
|
||||
val byteSize: Long,
|
||||
)
|
||||
|
||||
data class Response(
|
||||
val httpCode: Int, // ex: 200
|
||||
val contentType: String? = null,
|
||||
val body: String? = null,
|
||||
val headers: Map<String, String>,
|
||||
val byteSize: Long,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.model
|
||||
|
||||
data class GraphQlHeaderDomainModel(
|
||||
val key: String,
|
||||
val value: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.model
|
||||
|
||||
data class GraphQlRequestDomainModel(
|
||||
val uuid: String,
|
||||
val infos: FloconGraphQlRequestInfos,
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.model
|
||||
|
||||
typealias GraphQlRequestId = String
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.florent37.flocondesktop.features.graphql.domain.repository
|
||||
|
||||
import com.florent37.flocondesktop.DeviceId
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface GraphQlRepository {
|
||||
fun observeRequests(deviceId: DeviceId): Flow<List<GraphQlRequestDomainModel>>
|
||||
fun observeRequest(currentDeviceId: DeviceId, requestId: GraphQlRequestId): Flow<GraphQlRequestDomainModel?>
|
||||
|
||||
suspend fun deleteRequest(deviceId: DeviceId, requestId: GraphQlRequestId)
|
||||
suspend fun deleteRequestsBefore(deviceId: DeviceId, requestId: GraphQlRequestId)
|
||||
suspend fun deleteRequestsForDevice(deviceId: DeviceId)
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.florent37.flocondesktop.common.coroutines.dispatcherprovider.DispatcherProvider
|
||||
import com.florent37.flocondesktop.common.ui.feedback.FeedbackDisplayer
|
||||
import com.florent37.flocondesktop.copyToClipboard
|
||||
import com.florent37.flocondesktop.features.graphql.domain.DeleteGraphQlRequestUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.DeleteGraphQlRequestsBeforeUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ObserveGraphQlRequestsByIdUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ObserveGraphQlRequestsUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.domain.ResetCurrentDeviceGraphQlRequestsUseCase
|
||||
import com.florent37.flocondesktop.features.graphql.ui.mapper.toDetailUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.mapper.toUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.OnGraphQlItemUserAction
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GraphQlViewModel(
|
||||
observeGraphQlRequestsUseCase: ObserveGraphQlRequestsUseCase,
|
||||
private val observeGraphQlRequestsByIdUseCase: ObserveGraphQlRequestsByIdUseCase,
|
||||
private val resetCurrentDeviceGraphQlRequestsUseCase: ResetCurrentDeviceGraphQlRequestsUseCase,
|
||||
private val removeGraphQlRequestsBeforeUseCase: DeleteGraphQlRequestsBeforeUseCase,
|
||||
private val removeGraphQlRequestUseCase: DeleteGraphQlRequestUseCase,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val feedbackDisplayer: FeedbackDisplayer,
|
||||
) : ViewModel() {
|
||||
|
||||
val state: StateFlow<List<GraphQlItemViewState>> =
|
||||
observeGraphQlRequestsUseCase()
|
||||
.map { list -> list.map { toUi(it) } }
|
||||
.flowOn(dispatcherProvider.viewModel)
|
||||
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
private val clickedRequestId = MutableStateFlow<String?>(null)
|
||||
|
||||
val detailState: StateFlow<GraphQlDetailViewState?> =
|
||||
clickedRequestId
|
||||
.flatMapLatest { id ->
|
||||
if (id == null) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
observeGraphQlRequestsByIdUseCase(id)
|
||||
.distinctUntilChanged()
|
||||
.map {
|
||||
it?.let {
|
||||
toDetailUi(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(dispatcherProvider.viewModel)
|
||||
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
fun onGraphQlItemUserAction(action: OnGraphQlItemUserAction) {
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
when (action) {
|
||||
is OnGraphQlItemUserAction.CopyUrl -> {
|
||||
val domainModel =
|
||||
observeGraphQlRequestsByIdUseCase(action.item.uuid).firstOrNull()
|
||||
?: return@launch
|
||||
copyToClipboard(domainModel.infos.url)
|
||||
}
|
||||
|
||||
is OnGraphQlItemUserAction.OnClicked -> {
|
||||
clickedRequestId.update {
|
||||
if (it == action.item.uuid) {
|
||||
null
|
||||
} else {
|
||||
action.item.uuid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is OnGraphQlItemUserAction.Remove -> {
|
||||
removeGraphQlRequestUseCase(requestId = action.item.uuid)
|
||||
}
|
||||
|
||||
is OnGraphQlItemUserAction.RemoveLinesAbove -> {
|
||||
removeGraphQlRequestsBeforeUseCase(requestId = action.item.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCopyText(text: String) {
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
copyToClipboard(text)
|
||||
feedbackDisplayer.displayMessage("copied")
|
||||
}
|
||||
}
|
||||
|
||||
fun closeDetailPanel() {
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
clickedRequestId.update { null }
|
||||
}
|
||||
}
|
||||
|
||||
fun onReset() {
|
||||
viewModelScope.launch(dispatcherProvider.viewModel) {
|
||||
resetCurrentDeviceGraphQlRequestsUseCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.di
|
||||
|
||||
import com.florent37.flocondesktop.features.graphql.ui.GraphQlViewModel
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val graphQlUiModule =
|
||||
module {
|
||||
viewModelOf(::GraphQlViewModel)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.mapper
|
||||
|
||||
import com.florent37.flocondesktop.common.ui.ByteFormatter
|
||||
import com.florent37.flocondesktop.common.ui.JsonPrettyPrinter
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailHeaderUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailViewState
|
||||
import com.florent37.flocondesktop.features.network.ui.mapper.formatDuration
|
||||
import com.florent37.flocondesktop.features.network.ui.mapper.formatTimestamp
|
||||
|
||||
fun toDetailUi(request: GraphQlRequestDomainModel): GraphQlDetailViewState = GraphQlDetailViewState(
|
||||
fullUrl = request.infos.url,
|
||||
method = toMethodUi(request.infos.request.method),
|
||||
status = toGraphQlStatusUi(request.infos.response.httpCode),
|
||||
requestTimeFormatted = request.infos.request.startTime.let { formatTimestamp(it) },
|
||||
durationFormatted = formatDuration(request.infos.durationMs),
|
||||
// request
|
||||
requestBody = httpBodyToUi(request.infos.request.body),
|
||||
requestHeaders = toGraphQlHeadersUi(request.infos.request.headers),
|
||||
requestSize = ByteFormatter.formatBytes(request.infos.request.byteSize),
|
||||
// response
|
||||
responseBody = httpBodyToUi(request.infos.response.body),
|
||||
responseHeaders = toGraphQlHeadersUi(request.infos.response.headers),
|
||||
responseSize = ByteFormatter.formatBytes(request.infos.response.byteSize),
|
||||
)
|
||||
|
||||
fun httpBodyToUi(body: String?): String = body?.let { JsonPrettyPrinter.prettyPrint(body) } ?: ""
|
||||
|
||||
fun toGraphQlHeadersUi(headers: Map<String, String>?): List<GraphQlDetailHeaderUi> = headers?.let {
|
||||
it
|
||||
.map { (key, value) ->
|
||||
GraphQlDetailHeaderUi(
|
||||
name = key,
|
||||
value = value,
|
||||
)
|
||||
}.sortedBy { it.name }
|
||||
} ?: emptyList()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.mapper
|
||||
|
||||
import com.florent37.flocondesktop.common.ui.ByteFormatter
|
||||
import com.florent37.flocondesktop.features.graphql.domain.model.GraphQlRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlMethodUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlStatusUi
|
||||
import com.florent37.flocondesktop.features.network.ui.mapper.formatDuration
|
||||
import com.florent37.flocondesktop.features.network.ui.mapper.formatTimestamp
|
||||
|
||||
fun listToUi(httpRequests: List<GraphQlRequestDomainModel>): List<GraphQlItemViewState> = httpRequests.map { toUi(it) }
|
||||
|
||||
fun toUi(httpRequest: GraphQlRequestDomainModel): GraphQlItemViewState = GraphQlItemViewState(
|
||||
uuid = httpRequest.uuid,
|
||||
dateFormatted = formatTimestamp(httpRequest.infos.request.startTime),
|
||||
method = toMethodUi(httpRequest.infos.request.method),
|
||||
graphQlStatusUi = toGraphQlStatusUi(code = 200),
|
||||
route = httpRequest.infos.url,
|
||||
timeFormatted = formatDuration(httpRequest.infos.durationMs),
|
||||
requestSize = ByteFormatter.formatBytes(httpRequest.infos.request.byteSize),
|
||||
responseSize = ByteFormatter.formatBytes(httpRequest.infos.response.byteSize),
|
||||
)
|
||||
|
||||
fun toGraphQlStatusUi(code: Int): GraphQlStatusUi = GraphQlStatusUi(
|
||||
code = code,
|
||||
isSuccess = code >= 200 && code < 300,
|
||||
)
|
||||
|
||||
fun toMethodUi(httpMethod: String): GraphQlMethodUi = when (httpMethod.lowercase()) {
|
||||
"get" -> GraphQlMethodUi.GET
|
||||
"post" -> GraphQlMethodUi.POST
|
||||
else -> GraphQlMethodUi.Other(httpMethod)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class GraphQlDetailHeaderUi(
|
||||
val name: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
fun previewGraphQlDetailHeaderUi() = GraphQlDetailHeaderUi(
|
||||
name = "name",
|
||||
value = "value",
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class GraphQlDetailViewState(
|
||||
val fullUrl: String,
|
||||
val requestTimeFormatted: String,
|
||||
val durationFormatted: String,
|
||||
val method: GraphQlMethodUi,
|
||||
val status: GraphQlStatusUi,
|
||||
// request
|
||||
val requestBody: String,
|
||||
val requestSize: String,
|
||||
val requestHeaders: List<GraphQlDetailHeaderUi>,
|
||||
// response
|
||||
val responseBody: String,
|
||||
val responseSize: String,
|
||||
val responseHeaders: List<GraphQlDetailHeaderUi>,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class GraphQlItemViewState(
|
||||
val uuid: String,
|
||||
val dateFormatted: String,
|
||||
val route: String,
|
||||
val method: GraphQlMethodUi,
|
||||
val graphQlStatusUi: GraphQlStatusUi,
|
||||
val requestSize: String,
|
||||
val responseSize: String,
|
||||
val timeFormatted: String,
|
||||
)
|
||||
|
||||
fun previewGraphQlItemViewState(): GraphQlItemViewState = GraphQlItemViewState(
|
||||
uuid = "0",
|
||||
dateFormatted = "00:00:00.0000",
|
||||
route = "www.google.com.test",
|
||||
method = GraphQlMethodUi.GET,
|
||||
graphQlStatusUi = GraphQlStatusUi(200, true),
|
||||
requestSize = "10.kb",
|
||||
responseSize = "0.B",
|
||||
timeFormatted = "333ms",
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class GraphQlMethodUi(
|
||||
open val text: String,
|
||||
) {
|
||||
@Immutable
|
||||
data object GET : GraphQlMethodUi(text = "GET")
|
||||
|
||||
@Immutable
|
||||
data object POST : GraphQlMethodUi(text = "POST")
|
||||
|
||||
@Immutable
|
||||
data class Other(override val text: String) : GraphQlMethodUi(text = "POST")
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class GraphQlStatusUi(
|
||||
val code: Int,
|
||||
val isSuccess: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.model
|
||||
|
||||
sealed interface OnGraphQlItemUserAction {
|
||||
data class OnClicked(
|
||||
val item: GraphQlItemViewState,
|
||||
) : OnGraphQlItemUserAction
|
||||
|
||||
data class CopyUrl(
|
||||
val item: GraphQlItemViewState,
|
||||
) : OnGraphQlItemUserAction
|
||||
|
||||
data class Remove(
|
||||
val item: GraphQlItemViewState,
|
||||
) : OnGraphQlItemUserAction
|
||||
|
||||
data class RemoveLinesAbove(
|
||||
val item: GraphQlItemViewState,
|
||||
) : OnGraphQlItemUserAction
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.florent37.flocondesktop.common.ui.FloconColors
|
||||
import com.florent37.flocondesktop.common.ui.FloconTheme
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlMethodUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlStatusUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.previewGraphQlDetailHeaderUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.components.DetailHeadersView
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.components.MethodView
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.components.StatusView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.detail.CodeBlockView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.detail.DetailLineTextView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.detail.DetailLineView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.detail.DetailSectionTitleView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.detail.ExpandedSectionView
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
fun GraphQlDetailView(
|
||||
state: GraphQlDetailViewState,
|
||||
onCopy: (String) -> Unit, // Le lambda onCopy remplace ClipboardManager
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
var isRequestExpanded by remember { mutableStateOf(true) }
|
||||
var isRequestBodyExpanded by remember { mutableStateOf(true) }
|
||||
var isRequestHeadersExpanded by remember { mutableStateOf(true) }
|
||||
|
||||
var isResponseExpanded by remember { mutableStateOf(true) }
|
||||
var isResponseHeadersExpanded by remember { mutableStateOf(true) }
|
||||
var isResponseBodyExpanded by remember { mutableStateOf(true) }
|
||||
|
||||
val linesLabelWidth: Dp = 130.dp
|
||||
val headersLabelWidth: Dp = 150.dp
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.background(FloconColors.background)
|
||||
.verticalScroll(scrollState) // Rendre le contenu défilable
|
||||
.padding(all = 12.dp),
|
||||
) {
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isRequestExpanded,
|
||||
title = "Request",
|
||||
onCopy = null,
|
||||
onToggle = {
|
||||
isRequestExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isRequestExpanded,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
).padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
DetailLineTextView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "Full url",
|
||||
value = state.fullUrl,
|
||||
labelWidth = linesLabelWidth,
|
||||
)
|
||||
DetailLineView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "Method",
|
||||
labelWidth = linesLabelWidth,
|
||||
) {
|
||||
MethodView(method = state.method)
|
||||
}
|
||||
DetailLineView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "Status",
|
||||
labelWidth = linesLabelWidth,
|
||||
) {
|
||||
StatusView(status = state.status)
|
||||
}
|
||||
DetailLineTextView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "Request Time",
|
||||
value = state.requestTimeFormatted,
|
||||
labelWidth = linesLabelWidth,
|
||||
)
|
||||
DetailLineTextView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = "Time",
|
||||
value = state.durationFormatted,
|
||||
labelWidth = linesLabelWidth,
|
||||
)
|
||||
}
|
||||
|
||||
// headers
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isRequestHeadersExpanded,
|
||||
title = "Request Headers",
|
||||
onCopy = null,
|
||||
onToggle = {
|
||||
isRequestHeadersExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isRequestHeadersExpanded,
|
||||
) {
|
||||
DetailHeadersView(
|
||||
headers = state.requestHeaders,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
labelWidth = headersLabelWidth,
|
||||
)
|
||||
}
|
||||
|
||||
// body
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isRequestBodyExpanded,
|
||||
title = "Request Body",
|
||||
onCopy = {
|
||||
onCopy(state.requestBody)
|
||||
},
|
||||
onToggle = {
|
||||
isRequestBodyExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isRequestBodyExpanded,
|
||||
) {
|
||||
CodeBlockView(
|
||||
code = state.requestBody,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp)
|
||||
.padding(vertical = 12.dp),
|
||||
)
|
||||
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isResponseExpanded,
|
||||
title = "Response",
|
||||
onCopy = null,
|
||||
onToggle = {
|
||||
isResponseExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isResponseExpanded,
|
||||
) {
|
||||
// headers
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isResponseHeadersExpanded,
|
||||
title = "Response Headers",
|
||||
onCopy = null,
|
||||
onToggle = {
|
||||
isResponseHeadersExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isResponseHeadersExpanded,
|
||||
) {
|
||||
DetailHeadersView(
|
||||
headers = state.responseHeaders,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
labelWidth = headersLabelWidth,
|
||||
)
|
||||
}
|
||||
|
||||
// body
|
||||
DetailSectionTitleView(
|
||||
isExpanded = isResponseBodyExpanded,
|
||||
title = "Response Body",
|
||||
onCopy = {
|
||||
onCopy(state.responseBody)
|
||||
},
|
||||
onToggle = {
|
||||
isResponseBodyExpanded = it
|
||||
},
|
||||
)
|
||||
ExpandedSectionView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isExpanded = isResponseBodyExpanded,
|
||||
) {
|
||||
CodeBlockView(
|
||||
code = state.responseBody,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun GraphQlDetailViewPreview() {
|
||||
FloconTheme {
|
||||
GraphQlDetailView(
|
||||
state =
|
||||
GraphQlDetailViewState(
|
||||
fullUrl = "http://www.google.com",
|
||||
method = GraphQlMethodUi.GET,
|
||||
status =
|
||||
GraphQlStatusUi(
|
||||
code = 200,
|
||||
isSuccess = true,
|
||||
),
|
||||
requestHeaders =
|
||||
listOf(
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
),
|
||||
requestBody =
|
||||
"""
|
||||
{
|
||||
"id": "123",
|
||||
"name": "Flocon App",
|
||||
"version": "1.0.0",
|
||||
"data": {
|
||||
"items": [
|
||||
{"key": "value1"},
|
||||
{"key": "value2"}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
responseHeaders =
|
||||
listOf(
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
previewGraphQlDetailHeaderUi(),
|
||||
),
|
||||
requestTimeFormatted = "00:00:00.000",
|
||||
durationFormatted = "300ms",
|
||||
responseBody =
|
||||
"""
|
||||
{
|
||||
"graphQlStatusUi": "success",
|
||||
"message": "Data received and processed.",
|
||||
"result": {
|
||||
"timestamp": "2025-07-05T23:59:00Z",
|
||||
"processed_count": 2
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
requestSize = "0kb",
|
||||
responseSize = "0kb",
|
||||
),
|
||||
modifier = Modifier.padding(16.dp), // Padding pour la preview
|
||||
onCopy = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.florent37.flocondesktop.common.ui.ContextualItem
|
||||
import com.florent37.flocondesktop.common.ui.ContextualView
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.OnGraphQlItemUserAction
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.previewGraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.components.MethodView
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.components.StatusView
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
/**
|
||||
* Data class to define the fixed widths for each column in GraphQlItemView.
|
||||
* This allows for easy configuration and consistency across all items in a LazyColumn.
|
||||
*/
|
||||
data class GraphQlItemColumnWidths(
|
||||
val dateWidth: Dp = 90.dp,
|
||||
val methodWidth: Dp = 65.dp,
|
||||
val statusCodeWidth: Dp = 50.dp,
|
||||
val requestSizeWidth: Dp = 65.dp,
|
||||
val responseSizeWidth: Dp = 65.dp,
|
||||
val timeWidth: Dp = 60.dp,
|
||||
// The 'route' column will use Modifier.weight(1f) to take remaining space
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GraphQlItemView(
|
||||
state: GraphQlItemViewState,
|
||||
columnWidths: GraphQlItemColumnWidths = GraphQlItemColumnWidths(), // Default widths provided
|
||||
onUserAction: (OnGraphQlItemUserAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Use MaterialTheme.typography for consistent text sizes
|
||||
val bodySmall = MaterialTheme.typography.bodySmall // Typically 12.sp or similar
|
||||
val labelSmall = MaterialTheme.typography.labelSmall // Even smaller, good for labels/tags
|
||||
|
||||
ContextualView(
|
||||
listOf(
|
||||
ContextualItem(
|
||||
id = "copy_url",
|
||||
text = "Copy url",
|
||||
),
|
||||
ContextualItem(
|
||||
id = "remove",
|
||||
text = "Remove",
|
||||
),
|
||||
ContextualItem(
|
||||
id = "remove_lines_above",
|
||||
text = "Remove lines above ",
|
||||
),
|
||||
),
|
||||
onSelect = {
|
||||
when (it.id) {
|
||||
"copy_url" -> onUserAction(OnGraphQlItemUserAction.CopyUrl(state))
|
||||
"remove" -> onUserAction(OnGraphQlItemUserAction.Remove(state))
|
||||
"remove_lines_above" -> onUserAction(OnGraphQlItemUserAction.RemoveLinesAbove(state))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Padding for the entire item
|
||||
.clip(shape = RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = {
|
||||
onUserAction(OnGraphQlItemUserAction.OnClicked(state))
|
||||
})
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surface, // Use surface color for the item background
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
// Inner padding for content
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
// Date - Fixed width from data class
|
||||
Box(
|
||||
modifier = Modifier.width(columnWidths.dateWidth),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
state.dateFormatted,
|
||||
style = bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
|
||||
// Method - Fixed width for the tag from data class
|
||||
MethodView(
|
||||
method = state.method,
|
||||
modifier =
|
||||
Modifier
|
||||
.width(columnWidths.methodWidth),
|
||||
)
|
||||
|
||||
// Route - Takes remaining space (weight)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = state.route,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
|
||||
// GraphQlStatusUi - Fixed width for the tag from data class
|
||||
StatusView(
|
||||
state.graphQlStatusUi,
|
||||
modifier = Modifier.width(columnWidths.statusCodeWidth), // Apply fixed width to the StatusView composable
|
||||
)
|
||||
|
||||
// Request Size - Fixed width from data class
|
||||
Box(
|
||||
modifier = Modifier.width(columnWidths.requestSizeWidth),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
state.requestSize,
|
||||
style = bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
|
||||
// Response Size - Fixed width from data class
|
||||
Box(
|
||||
modifier = Modifier.width(columnWidths.responseSizeWidth),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
state.responseSize,
|
||||
style = bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
|
||||
// Time - Fixed width from data class
|
||||
Box(
|
||||
modifier = Modifier.width(columnWidths.timeWidth),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
state.timeFormatted,
|
||||
style = bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun ItemViewPreview() {
|
||||
MaterialTheme {
|
||||
GraphQlItemView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = previewGraphQlItemViewState(),
|
||||
onUserAction = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.florent37.flocondesktop.common.ui.FloconColors
|
||||
import com.florent37.flocondesktop.common.ui.FloconTheme
|
||||
import com.florent37.flocondesktop.features.graphql.ui.GraphQlViewModel
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.OnGraphQlItemUserAction
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.previewGraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.header.GraphQlFilterBar
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.header.GraphQlItemHeaderView
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun GraphQlScreen(modifier: Modifier = Modifier) {
|
||||
val viewModel: GraphQlViewModel = koinViewModel()
|
||||
val items by viewModel.state.collectAsStateWithLifecycle()
|
||||
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
|
||||
GraphQlScreen(
|
||||
graphQlItems = items,
|
||||
modifier = modifier,
|
||||
detailState = detailState,
|
||||
onGraphQlItemUserAction = viewModel::onGraphQlItemUserAction,
|
||||
onCopyText = viewModel::onCopyText,
|
||||
onReset = viewModel::onReset,
|
||||
closeDetailPanel = viewModel::closeDetailPanel,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GraphQlScreen(
|
||||
graphQlItems: List<GraphQlItemViewState>,
|
||||
detailState: GraphQlDetailViewState?,
|
||||
onGraphQlItemUserAction: (OnGraphQlItemUserAction) -> Unit,
|
||||
onCopyText: (String) -> Unit,
|
||||
closeDetailPanel: () -> Unit,
|
||||
onReset: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val columnWidths: GraphQlItemColumnWidths =
|
||||
remember { GraphQlItemColumnWidths() } // Default widths provided
|
||||
|
||||
var filteredItems by remember { mutableStateOf<List<GraphQlItemViewState>>(emptyList()) }
|
||||
|
||||
Surface(modifier = modifier) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
text = "GraphQl",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(FloconColors.pannel)
|
||||
.padding(all = 12.dp),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
GraphQlFilterBar(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(FloconColors.pannel)
|
||||
.padding(horizontal = 12.dp),
|
||||
graphQlItems = graphQlItems,
|
||||
onResetClicked = onReset,
|
||||
onItemsChange = {
|
||||
filteredItems = it
|
||||
},
|
||||
)
|
||||
GraphQlItemHeaderView(
|
||||
columnWidths = columnWidths,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
enabled = detailState != null,
|
||||
) {
|
||||
closeDetailPanel()
|
||||
},
|
||||
) {
|
||||
items(filteredItems) {
|
||||
GraphQlItemView(
|
||||
state = it,
|
||||
columnWidths = columnWidths,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onUserAction = onGraphQlItemUserAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
detailState?.let {
|
||||
GraphQlDetailView(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.fillMaxHeight()
|
||||
.width(500.dp),
|
||||
state = it,
|
||||
onCopy = onCopyText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun GraphQlScreenPreview() {
|
||||
FloconTheme {
|
||||
val graphQlItems =
|
||||
remember {
|
||||
listOf(
|
||||
previewGraphQlItemViewState(),
|
||||
previewGraphQlItemViewState(),
|
||||
)
|
||||
}
|
||||
GraphQlScreen(
|
||||
graphQlItems = graphQlItems,
|
||||
detailState = null,
|
||||
closeDetailPanel = {},
|
||||
onGraphQlItemUserAction = {},
|
||||
onCopyText = {},
|
||||
onReset = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.florent37.flocondesktop.common.ui.FloconTheme
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlDetailHeaderUi
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.previewGraphQlDetailHeaderUi
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
fun DetailHeadersView(
|
||||
headers: List<GraphQlDetailHeaderUi>,
|
||||
labelWidth: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
headers.fastForEachIndexed { index, item ->
|
||||
DetailHeadersItemView(
|
||||
state = item,
|
||||
labelWidth = labelWidth,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (index != headers.lastIndex) {
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailHeadersItemView(
|
||||
state: GraphQlDetailHeaderUi,
|
||||
labelWidth: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SelectionContainer {
|
||||
Row(
|
||||
modifier = modifier.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = state.name,
|
||||
style = MaterialTheme.typography.bodySmall, // Slightly smaller title for details
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), // Muted label color
|
||||
modifier = Modifier.width(labelWidth).padding(end = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = state.value,
|
||||
style = MaterialTheme.typography.bodySmall, // Body text for the URL
|
||||
color = MaterialTheme.colorScheme.onBackground, // Primary text color
|
||||
modifier = Modifier.weight(1f), // Takes remaining space
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DetailHeadersItemViewPreview() {
|
||||
FloconTheme {
|
||||
DetailHeadersItemView(
|
||||
state = previewGraphQlDetailHeaderUi(),
|
||||
labelWidth = 100.dp,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.florent37.flocondesktop.common.ui.FloconTheme
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlMethodUi
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
private val getMethodBackground = Color(0xFF007BFF).copy(alpha = 0.3f) // Muted blue for GET
|
||||
private val getMethodText = Color(0xFF007BFF)
|
||||
|
||||
private val postMethodBackground = Color(0xFF28A745).copy(alpha = 0.3f) // Muted green for POST
|
||||
private val postMethodText = Color(0xFF28A745)
|
||||
|
||||
private val otherMethodBackground = Color(0xFF6C757D).copy(alpha = 0.3f) // Muted gray for OTHER
|
||||
private val otherMethodText = Color(0xFF6C757D)
|
||||
|
||||
@Composable
|
||||
fun MethodView(
|
||||
method: GraphQlMethodUi,
|
||||
textSize: TextUnit = 12.sp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (backgroundColor, textColor) =
|
||||
when (method) {
|
||||
GraphQlMethodUi.GET -> getMethodBackground to getMethodText
|
||||
is GraphQlMethodUi.Other -> otherMethodBackground to otherMethodText
|
||||
GraphQlMethodUi.POST -> postMethodBackground to postMethodText
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = RoundedCornerShape(20.dp), // Pill shape
|
||||
).padding(horizontal = 4.dp),
|
||||
// Padding inside the tag
|
||||
contentAlignment = Alignment.Center, // Center content if Box is larger than text
|
||||
) {
|
||||
Text(
|
||||
method.text,
|
||||
color = textColor,
|
||||
fontSize = textSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun MethodView_Preview() {
|
||||
FloconTheme {
|
||||
MethodView(method = GraphQlMethodUi.GET)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.florent37.flocondesktop.common.ui.FloconTheme
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlStatusUi
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
// Custom colors for graphQlStatusUi/method views to integrate better with the theme
|
||||
val successTagBackground = Color(0xFF28A745).copy(alpha = 0.3f) // Muted green for success
|
||||
val successTagText = Color(0xFF28A745) // Brighter green for text
|
||||
|
||||
val errorTagBackground = Color(0xFFDC3545).copy(alpha = 0.3f) // Muted red for error
|
||||
val errorTagText = Color(0xFFDC3545) // Brighter red for text
|
||||
|
||||
@Composable
|
||||
fun StatusView(
|
||||
status: GraphQlStatusUi,
|
||||
textSize: TextUnit = 12.sp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = status.code.toString(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = textSize,
|
||||
color =
|
||||
when (status.isSuccess) {
|
||||
true -> successTagText
|
||||
false -> errorTagText
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall, // Use typography for consistency
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun StatusView_Preview() {
|
||||
FloconTheme {
|
||||
StatusView(
|
||||
status =
|
||||
GraphQlStatusUi(
|
||||
code = 200,
|
||||
isSuccess = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view.header
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.florent37.flocondesktop.features.graphql.ui.model.GraphQlItemViewState
|
||||
import com.florent37.flocondesktop.features.network.ui.view.components.FilterBar
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.bin
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
fun GraphQlFilterBar(
|
||||
graphQlItems: List<GraphQlItemViewState>,
|
||||
onItemsChange: (List<GraphQlItemViewState>) -> Unit,
|
||||
onResetClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var filterText by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val onItemsChangeCallback by rememberUpdatedState(onItemsChange)
|
||||
val filteredGraphQlItems: List<GraphQlItemViewState> =
|
||||
remember(graphQlItems, filterText) {
|
||||
if (filterText.isBlank()) {
|
||||
graphQlItems
|
||||
} else {
|
||||
graphQlItems.filter {
|
||||
it.route.contains(filterText, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(filteredGraphQlItems) {
|
||||
onItemsChangeCallback(filteredGraphQlItems)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterBar(
|
||||
placeholderText = "Filter",
|
||||
modifier = Modifier.weight(1f),
|
||||
onTextChange = {
|
||||
filterText = it
|
||||
},
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable(onClick = onResetClicked)
|
||||
.padding(all = 8.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(Res.drawable.bin),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.florent37.flocondesktop.features.graphql.ui.view.header
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.florent37.flocondesktop.common.ui.FloconColors
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.GraphQlItemColumnWidths
|
||||
import com.florent37.flocondesktop.features.network.ui.view.components.HeaderLabelItem
|
||||
|
||||
@Composable
|
||||
fun GraphQlItemHeaderView(
|
||||
columnWidths: GraphQlItemColumnWidths = GraphQlItemColumnWidths(), // Default widths provided
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.background(FloconColors.pannel)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Padding for the entire item
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
// Date - Fixed width from data class
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.dateWidth),
|
||||
text = "Request Time",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.methodWidth),
|
||||
text = "Method",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.TopStart,
|
||||
text = "Route",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.statusCodeWidth),
|
||||
text = "Status",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.requestSizeWidth),
|
||||
text = "Request Size",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.responseSizeWidth),
|
||||
text = "Response Size",
|
||||
)
|
||||
HeaderLabelItem(
|
||||
modifier = Modifier.width(columnWidths.timeWidth),
|
||||
text = "Time",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.florent37.flocondesktop.DeviceId
|
|||
import com.florent37.flocondesktop.FloconIncomingMessageDataModel
|
||||
import com.florent37.flocondesktop.Protocol
|
||||
import com.florent37.flocondesktop.common.coroutines.dispatcherprovider.DispatcherProvider
|
||||
import com.florent37.flocondesktop.features.graphql.data.mapper.toDomain
|
||||
import com.florent37.flocondesktop.features.grpc.data.datasource.LocalGrpcDataSource
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.fromdevice.GrpcRequestDataModel
|
||||
import com.florent37.flocondesktop.features.grpc.data.model.fromdevice.GrpcResponseDataModel
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ class ImagesRepositoryImpl(
|
|||
imagesLocalDataSource.addImage(
|
||||
deviceId = deviceId,
|
||||
image = DeviceImageDomainModel(
|
||||
url = request.infos.url,
|
||||
time = (request.infos.startTime + request.infos.durationMs).toLong(),
|
||||
url = request.url,
|
||||
time = (request.request.startTime + request.durationMs).toLong(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.florent37.flocondesktop.features.network.data
|
||||
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestInfos
|
||||
import kotlin.time.Clock
|
||||
|
||||
object FloconHttpRequestGenerator {
|
||||
|
|
@ -66,13 +65,14 @@ object FloconHttpRequestGenerator {
|
|||
null
|
||||
}
|
||||
|
||||
FloconHttpRequestInfos(
|
||||
FloconHttpRequestDomainModel(
|
||||
uuid = index.toString(),
|
||||
url = "$urlScheme://$domain$path/${index + 1}",
|
||||
method = method,
|
||||
startTime = Clock.System.now()
|
||||
.toEpochMilliseconds() - (index * 500L), // Temps de démarrage décroissant
|
||||
durationMs = (100.0 + (index * 25.0)), // Durée croissante
|
||||
request = FloconHttpRequestInfos.Request(
|
||||
request = FloconHttpRequestDomainModel.Request(
|
||||
method = method,
|
||||
startTime = Clock.System.now()
|
||||
.toEpochMilliseconds() - (index * 500L), // Temps de démarrage décroissant
|
||||
headers =
|
||||
mapOf(
|
||||
"Content-Type" to contentType,
|
||||
|
|
@ -82,7 +82,7 @@ object FloconHttpRequestGenerator {
|
|||
body = requestBodyContent,
|
||||
byteSize = 300,
|
||||
),
|
||||
response = FloconHttpRequestInfos.Response(
|
||||
response = FloconHttpRequestDomainModel.Response(
|
||||
body = responseBodyContent,
|
||||
httpCode = 200,
|
||||
byteSize = 1500,
|
||||
|
|
@ -93,9 +93,7 @@ object FloconHttpRequestGenerator {
|
|||
"X-Response-ID" to "res-$index",
|
||||
),
|
||||
),
|
||||
).let {
|
||||
FloconHttpRequestDomainModel(uuid = index.toString(), infos = it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.florent37.flocondesktop.common.coroutines.dispatcherprovider.Dispatch
|
|||
import com.florent37.flocondesktop.features.network.data.datasource.local.NetworkLocalDataSource
|
||||
import com.florent37.flocondesktop.features.network.data.model.FloconHttpRequestDataModel
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestInfos
|
||||
import com.florent37.flocondesktop.features.network.domain.repository.NetworkImageRepository
|
||||
import com.florent37.flocondesktop.features.network.domain.repository.NetworkRepository
|
||||
import com.florent37.flocondesktop.messages.domain.repository.sub.MessagesReceiverRepository
|
||||
|
|
@ -50,8 +49,8 @@ class NetworkRepositoryImpl(
|
|||
) {
|
||||
withContext(dispatcherProvider.data) {
|
||||
decode(message)?.let { toDomain(it) }?.let { request ->
|
||||
val responseContentType = request.infos.response.contentType
|
||||
if (request.infos.response.contentType != null && responseContentType.startsWith("image/")) {
|
||||
val responseContentType = request.response.contentType
|
||||
if (request.response.contentType != null && responseContentType.startsWith("image/")) {
|
||||
networkImageRepository.onImageReceived(deviceId = deviceId, request = request)
|
||||
}
|
||||
networkLocalDataSource.save(deviceId = deviceId, request = request)
|
||||
|
|
@ -99,24 +98,21 @@ class NetworkRepositoryImpl(
|
|||
fun toDomain(decoded: FloconHttpRequestDataModel): FloconHttpRequestDomainModel? = try {
|
||||
FloconHttpRequestDomainModel(
|
||||
uuid = Uuid.random().toString(),
|
||||
infos =
|
||||
FloconHttpRequestInfos(
|
||||
url = decoded.url!!,
|
||||
url = decoded.url!!,
|
||||
durationMs = decoded.durationMs!!,
|
||||
request = FloconHttpRequestDomainModel.Request(
|
||||
method = decoded.method!!,
|
||||
startTime = decoded.startTime!!,
|
||||
durationMs = decoded.durationMs!!,
|
||||
request = FloconHttpRequestInfos.Request(
|
||||
headers = decoded.requestHeaders!!,
|
||||
body = decoded.requestBody,
|
||||
byteSize = decoded.requestSize ?: 0L,
|
||||
),
|
||||
response = FloconHttpRequestInfos.Response(
|
||||
httpCode = decoded.responseHttpCode!!,
|
||||
contentType = decoded.responseContentType,
|
||||
body = decoded.responseBody,
|
||||
headers = decoded.responseHeaders!!,
|
||||
byteSize = decoded.responseSize ?: 0L,
|
||||
),
|
||||
headers = decoded.requestHeaders!!,
|
||||
body = decoded.requestBody,
|
||||
byteSize = decoded.requestSize ?: 0L,
|
||||
),
|
||||
response = FloconHttpRequestDomainModel.Response(
|
||||
httpCode = decoded.responseHttpCode!!,
|
||||
contentType = decoded.responseContentType,
|
||||
body = decoded.responseBody,
|
||||
headers = decoded.responseHeaders!!,
|
||||
byteSize = decoded.responseSize ?: 0L,
|
||||
),
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@ package com.florent37.flocondesktop.features.network.data.datasource.local.mappe
|
|||
import com.florent37.flocondesktop.features.network.data.datasource.local.FloconHttpRequestEntity
|
||||
import com.florent37.flocondesktop.features.network.data.datasource.local.FloconHttpRequestInfosEntity
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestDomainModel
|
||||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestInfos
|
||||
|
||||
fun FloconHttpRequestDomainModel.toEntity(deviceId: String): FloconHttpRequestEntity = FloconHttpRequestEntity(
|
||||
uuid = this.uuid,
|
||||
infos = this.infos.toInfosEntity(),
|
||||
infos = this.toInfosEntity(),
|
||||
deviceId = deviceId,
|
||||
)
|
||||
|
||||
fun FloconHttpRequestInfos.toInfosEntity(): FloconHttpRequestInfosEntity = FloconHttpRequestInfosEntity(
|
||||
private fun FloconHttpRequestDomainModel.toInfosEntity(): FloconHttpRequestInfosEntity = FloconHttpRequestInfosEntity(
|
||||
url = this.url,
|
||||
method = this.method,
|
||||
startTime = this.startTime,
|
||||
method = this.request.method,
|
||||
startTime = this.request.startTime,
|
||||
durationMs = this.durationMs,
|
||||
requestHeaders = this.request.headers,
|
||||
requestBody = this.request.body,
|
||||
|
|
@ -28,24 +27,20 @@ fun FloconHttpRequestInfos.toInfosEntity(): FloconHttpRequestInfosEntity = Floco
|
|||
|
||||
fun FloconHttpRequestEntity.toDomainModel(): FloconHttpRequestDomainModel = FloconHttpRequestDomainModel(
|
||||
uuid = this.uuid,
|
||||
infos = this.infos.toInfosDomainModel(),
|
||||
)
|
||||
|
||||
fun FloconHttpRequestInfosEntity.toInfosDomainModel(): FloconHttpRequestInfos = FloconHttpRequestInfos(
|
||||
url = this.url,
|
||||
method = this.method,
|
||||
startTime = this.startTime,
|
||||
durationMs = this.durationMs,
|
||||
request = FloconHttpRequestInfos.Request(
|
||||
headers = this.requestHeaders,
|
||||
body = this.requestBody,
|
||||
byteSize = this.requestByteSize,
|
||||
url = this.infos.url,
|
||||
durationMs = this.infos.durationMs,
|
||||
request = FloconHttpRequestDomainModel.Request(
|
||||
method = this.infos.method,
|
||||
startTime = this.infos.startTime,
|
||||
headers = this.infos.requestHeaders,
|
||||
body = this.infos.requestBody,
|
||||
byteSize = this.infos.requestByteSize,
|
||||
),
|
||||
response = FloconHttpRequestInfos.Response(
|
||||
httpCode = this.responseHttpCode,
|
||||
contentType = this.responseContentType,
|
||||
body = this.responseBody,
|
||||
headers = this.responseHeaders,
|
||||
byteSize = this.responseByteSize,
|
||||
response = FloconHttpRequestDomainModel.Response(
|
||||
httpCode = this.infos.responseHttpCode,
|
||||
contentType = this.infos.responseContentType,
|
||||
body = this.infos.responseBody,
|
||||
headers = this.infos.responseHeaders,
|
||||
byteSize = this.infos.responseByteSize,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ package com.florent37.flocondesktop.features.network.domain
|
|||
import com.florent37.flocondesktop.features.network.domain.model.FloconHttpRequestDomainModel
|
||||
|
||||
class GenerateCurlCommandUseCase {
|
||||
operator fun invoke(request: FloconHttpRequestDomainModel): String {
|
||||
val infos = request.infos
|
||||
|
||||
operator fun invoke(infos: FloconHttpRequestDomainModel): String {
|
||||
val commandBuilder = StringBuilder("curl")
|
||||
|
||||
// 1. Add HTTP Method
|
||||
commandBuilder.append(" -X ${infos.method}")
|
||||
commandBuilder.append(" -X ${infos.request.method}")
|
||||
|
||||
// 2. Add Request Headers
|
||||
infos.request.headers.forEach { (key, value) ->
|
||||
|
|
|
|||
|
|
@ -2,19 +2,15 @@ package com.florent37.flocondesktop.features.network.domain.model
|
|||
|
||||
data class FloconHttpRequestDomainModel(
|
||||
val uuid: String,
|
||||
val infos: FloconHttpRequestInfos,
|
||||
)
|
||||
|
||||
data class FloconHttpRequestInfos(
|
||||
val url: String,
|
||||
val method: String,
|
||||
val startTime: Long,
|
||||
val durationMs: Double,
|
||||
val request: Request,
|
||||
val response: Response,
|
||||
|
||||
) {
|
||||
data class Request(
|
||||
val startTime: Long,
|
||||
val method: String,
|
||||
val headers: Map<String, String>,
|
||||
val body: String?,
|
||||
val byteSize: Long,
|
||||
|
|
@ -79,7 +79,7 @@ class NetworkViewModel(
|
|||
is OnNetworkItemUserAction.CopyUrl -> {
|
||||
val domainModel = observeHttpRequestsByIdUseCase(action.item.uuid).firstOrNull()
|
||||
?: return@launch
|
||||
copyToClipboard(domainModel.infos.url)
|
||||
copyToClipboard(domainModel.url)
|
||||
}
|
||||
|
||||
is OnNetworkItemUserAction.OnClicked -> {
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ import com.florent37.flocondesktop.features.network.ui.model.NetworkDetailHeader
|
|||
import com.florent37.flocondesktop.features.network.ui.model.NetworkDetailViewState
|
||||
|
||||
fun toDetailUi(request: FloconHttpRequestDomainModel): NetworkDetailViewState = NetworkDetailViewState(
|
||||
fullUrl = request.infos.url ?: "",
|
||||
method = toMethodUi(request.infos.method),
|
||||
status = toNetworkStatusUi(request.infos.response.httpCode ?: 0),
|
||||
requestTimeFormatted = request.infos.startTime?.let { formatTimestamp(it) } ?: "",
|
||||
durationFormatted = formatDuration(request.infos.durationMs),
|
||||
fullUrl = request.url,
|
||||
method = toMethodUi(request.request.method),
|
||||
status = toNetworkStatusUi(request.response.httpCode),
|
||||
requestTimeFormatted = request.request.startTime.let { formatTimestamp(it) },
|
||||
durationFormatted = formatDuration(request.durationMs),
|
||||
// request
|
||||
requestBody = httpBodyToUi(request.infos.request.body),
|
||||
requestHeaders = toNetworkHeadersUi(request.infos.request.headers),
|
||||
requestSize = ByteFormatter.formatBytes(request.infos.request.byteSize), // TODO
|
||||
requestBody = httpBodyToUi(request.request.body),
|
||||
requestHeaders = toNetworkHeadersUi(request.request.headers),
|
||||
requestSize = ByteFormatter.formatBytes(request.request.byteSize),
|
||||
// response
|
||||
responseBody = httpBodyToUi(request.infos.response.body),
|
||||
responseHeaders = toNetworkHeadersUi(request.infos.response.headers),
|
||||
responseSize = ByteFormatter.formatBytes(request.infos.response.byteSize), // TODO
|
||||
responseBody = httpBodyToUi(request.response.body),
|
||||
responseHeaders = toNetworkHeadersUi(request.response.headers),
|
||||
responseSize = ByteFormatter.formatBytes(request.response.byteSize),
|
||||
)
|
||||
|
||||
fun httpBodyToUi(body: String?): String = body?.let { JsonPrettyPrinter.prettyPrint(body) } ?: ""
|
||||
|
|
|
|||
|
|
@ -15,19 +15,18 @@ fun listToUi(httpRequests: List<FloconHttpRequestDomainModel>): List<NetworkItem
|
|||
|
||||
fun toUi(httpRequest: FloconHttpRequestDomainModel): NetworkItemViewState = NetworkItemViewState(
|
||||
uuid = httpRequest.uuid,
|
||||
dateFormatted = formatTimestamp(httpRequest.infos.startTime),
|
||||
method = toMethodUi(httpRequest.infos.method),
|
||||
networkStatusUi = toNetworkStatusUi(code = 200),
|
||||
route = httpRequest.infos.url,
|
||||
timeFormatted = formatDuration(httpRequest.infos.durationMs),
|
||||
requestSize = ByteFormatter.formatBytes(httpRequest.infos.request.byteSize),
|
||||
responseSize = ByteFormatter.formatBytes(httpRequest.infos.response.byteSize),
|
||||
dateFormatted = formatTimestamp(httpRequest.request.startTime),
|
||||
method = toMethodUi(httpRequest.request.method),
|
||||
networkStatusUi = toNetworkStatusUi(code = httpRequest.response.httpCode),
|
||||
route = httpRequest.url,
|
||||
timeFormatted = formatDuration(httpRequest.durationMs),
|
||||
requestSize = ByteFormatter.formatBytes(httpRequest.request.byteSize),
|
||||
responseSize = ByteFormatter.formatBytes(httpRequest.response.byteSize),
|
||||
)
|
||||
|
||||
// TODO
|
||||
fun toNetworkStatusUi(code: Int): NetworkStatusUi = NetworkStatusUi(
|
||||
code = code,
|
||||
isSuccess = true, // TODO
|
||||
isSuccess = code >= 200 && code < 300,
|
||||
)
|
||||
|
||||
fun toMethodUi(httpMethod: String): NetworkMethodUi = when (httpMethod.lowercase()) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.florent37.flocondesktop.common.ui.ContextualItem
|
||||
|
|
@ -25,7 +26,6 @@ import com.florent37.flocondesktop.features.network.ui.model.previewNetworkItemV
|
|||
import com.florent37.flocondesktop.features.network.ui.view.components.MethodView
|
||||
import com.florent37.flocondesktop.features.network.ui.view.components.StatusView
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
/**
|
||||
* Data class to define the fixed widths for each column in NetworkItemView.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.florent37.flocondesktop.main.ui
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
|
@ -17,6 +18,7 @@ import com.florent37.flocondesktop.features.dashboard.ui.view.DashboardScreen
|
|||
import com.florent37.flocondesktop.features.database.ui.view.DatabaseScreen
|
||||
import com.florent37.flocondesktop.features.deeplinks.ui.view.DeeplinkScreen
|
||||
import com.florent37.flocondesktop.features.files.ui.view.FilesScreen
|
||||
import com.florent37.flocondesktop.features.graphql.ui.view.GraphQlScreen
|
||||
import com.florent37.flocondesktop.features.grpc.ui.view.GRPCScreen
|
||||
import com.florent37.flocondesktop.features.images.ui.view.ImagesScreen
|
||||
import com.florent37.flocondesktop.features.network.ui.view.NetworkScreen
|
||||
|
|
@ -63,9 +65,10 @@ private fun MainScreen(
|
|||
) {
|
||||
Column(modifier) {
|
||||
// TODO navigation
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
LeftPanelView(
|
||||
modifier = Modifier.width(300.dp),
|
||||
modifier = Modifier.width(300.dp)
|
||||
.fillMaxHeight(),
|
||||
onClickItem = onClickLeftPanelItem,
|
||||
state = leftPanelState,
|
||||
devicesState = devicesState,
|
||||
|
|
@ -145,6 +148,12 @@ private fun MainScreen(
|
|||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
SubScreen.GraphQl ->
|
||||
GraphQlScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ class MainViewModel(
|
|||
}
|
||||
|
||||
val leftPanelState = subScreen.map { subScreen ->
|
||||
buildLeftPannelState(
|
||||
buildLeftPanelState(
|
||||
selectedId = subScreen.id,
|
||||
)
|
||||
}.flowOn(dispatcherProvider.ui)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = kotlinx.coroutines.flow.SharingStarted.Eagerly,
|
||||
initialValue = buildLeftPannelState(subScreen.value.id),
|
||||
initialValue = buildLeftPanelState(subScreen.value.id),
|
||||
)
|
||||
|
||||
val devicesState: StateFlow<DevicesStateUiModel> = devicesDelegate.devicesState
|
||||
|
|
@ -65,7 +65,7 @@ class MainViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun buildLeftPannelState(selectedId: String?) = LeftPanelState(
|
||||
fun buildLeftPanelState(selectedId: String?) = LeftPanelState(
|
||||
bottomItems = listOf(
|
||||
item(subScreen = SubScreen.Settings, selectedId = selectedId),
|
||||
),
|
||||
|
|
@ -75,6 +75,7 @@ fun buildLeftPannelState(selectedId: String?) = LeftPanelState(
|
|||
items = listOf(
|
||||
item(subScreen = SubScreen.Network, selectedId = selectedId),
|
||||
item(subScreen = SubScreen.Images, selectedId = selectedId),
|
||||
item(subScreen = SubScreen.GraphQl, selectedId = selectedId),
|
||||
item(subScreen = SubScreen.GRPC, selectedId = selectedId),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ enum class SubScreen {
|
|||
Network,
|
||||
Images, // network images
|
||||
|
||||
// GraphQl ?
|
||||
GraphQl,
|
||||
GRPC,
|
||||
|
||||
// storage
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import flocondesktop.composeapp.generated.resources.dashboard
|
|||
import flocondesktop.composeapp.generated.resources.database
|
||||
import flocondesktop.composeapp.generated.resources.deeplinks
|
||||
import flocondesktop.composeapp.generated.resources.files
|
||||
import flocondesktop.composeapp.generated.resources.graphql
|
||||
import flocondesktop.composeapp.generated.resources.grpc
|
||||
import flocondesktop.composeapp.generated.resources.images
|
||||
import flocondesktop.composeapp.generated.resources.network
|
||||
|
|
@ -28,6 +29,7 @@ fun SubScreen.displayName(): String = when (this) {
|
|||
SubScreen.Dashboard -> "Dashboard"
|
||||
SubScreen.Settings -> "Settings"
|
||||
SubScreen.Deeplinks -> "Deeplinks"
|
||||
SubScreen.GraphQl -> "GraphQl"
|
||||
}
|
||||
|
||||
// Extension function to get the icon for each SubScreen
|
||||
|
|
@ -43,4 +45,5 @@ fun SubScreen.icon(): DrawableResource = when (this) {
|
|||
SubScreen.Settings -> Res.drawable.settings
|
||||
SubScreen.Dashboard -> Res.drawable.dashboard
|
||||
SubScreen.Deeplinks -> Res.drawable.deeplinks
|
||||
SubScreen.GraphQl -> Res.drawable.graphql
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -73,12 +76,17 @@ fun LeftPanelView(
|
|||
),
|
||||
)
|
||||
}
|
||||
LeftPannelDivider(
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
.height(12.dp),
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
state.sections.fastForEachIndexed { index, section ->
|
||||
PannelLabel(
|
||||
text = section.title,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ object Protocol {
|
|||
}
|
||||
}
|
||||
|
||||
object Graphql {
|
||||
const val Plugin = "graphql"
|
||||
|
||||
object Method {
|
||||
const val LogNetworkCall = "logNetworkCall"
|
||||
}
|
||||
}
|
||||
|
||||
object GRPC {
|
||||
const val Plugin = "gRPC"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue