diff --git a/intellij-plugin/build.gradle.kts b/intellij-plugin/build.gradle.kts index 06feaa68dd1..2a7b9919ff3 100644 --- a/intellij-plugin/build.gradle.kts +++ b/intellij-plugin/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id("org.jetbrains.kotlin.jvm") id("org.jetbrains.intellij") id("maven-publish") + alias(libs.plugins.apollo.published) } commonSetup() @@ -219,6 +220,25 @@ dependencies { implementation(project(":apollo-tooling")) implementation(project(":apollo-normalized-cache-sqlite")) implementation(libs.sqlite.jdbc) + implementation(libs.apollo.runtime.published) } fun isSnapshotBuild() = System.getenv("COM_APOLLOGRAPHQL_IJ_PLUGIN_SNAPSHOT").toBoolean() + +apollo { + service("apolloDebug") { + packageName.set("com.apollographql.apollo3.debug") + schemaFile.set(file("../libraries/apollo-debug-server/src/androidMain/resources/schema.graphqls")) + introspection { + endpointUrl.set("http://localhost:12200/") + schemaFile.set(file("../libraries/apollo-debug-server/src/androidMain/resources/schema.graphqls")) + } + } +} + +// We're using project(":apollo-gradle-plugin-external") and the published "apollo-runtime" which do not have the same version +tasks.configureEach { + if (name == "checkApolloVersions") { + enabled = false + } +} diff --git a/intellij-plugin/src/main/graphql/operations.graphql b/intellij-plugin/src/main/graphql/operations.graphql new file mode 100644 index 00000000000..d776e454597 --- /dev/null +++ b/intellij-plugin/src/main/graphql/operations.graphql @@ -0,0 +1,24 @@ +query GetApolloClients { + apolloClients { + id + displayName + normalizedCaches { + id + displayName + recordCount + } + } +} + +query GetNormalizedCache($apolloClientId: ID!, $normalizedCacheId: ID!) { + apolloClient(id: $apolloClientId) { + normalizedCache(id: $normalizedCacheId) { + displayName + records { + key + size + fields + } + } + } +} diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/apollodebugserver/ApolloDebugClient.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/apollodebugserver/ApolloDebugClient.kt new file mode 100644 index 00000000000..378dc7f098f --- /dev/null +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/apollodebugserver/ApolloDebugClient.kt @@ -0,0 +1,109 @@ +package com.apollographql.ijplugin.apollodebugserver + +import com.android.ddmlib.IDevice +import com.android.tools.idea.adb.AdbShellCommandsUtil +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.debug.GetApolloClientsQuery +import com.apollographql.apollo3.debug.GetNormalizedCacheQuery +import com.apollographql.ijplugin.util.logw +import java.io.Closeable + +private const val SOCKET_NAME_PREFIX = "apollo_debug_" +private const val BASE_PORT = 12200 + +class ApolloDebugClient( + private val device: IDevice, + val packageName: String, +) : Closeable { + companion object { + private var uniquePort = 0 + + private fun getUniquePort(): Int { + return BASE_PORT + uniquePort++ + } + + private fun IDevice.getApolloDebugPackageList(): Result> { + val commandResult = runCatching { + AdbShellCommandsUtil.create(this).executeCommandBlocking("cat /proc/net/unix | grep $SOCKET_NAME_PREFIX | cat") + } + if (commandResult.isFailure) { + val e = commandResult.exceptionOrNull()!! + logw(e, "Could not list Apollo Debug packages") + return Result.failure(e) + } + val result = commandResult.getOrThrow() + if (result.isError) { + val message = "Could not list Apollo Debug packages: ${result.output.joinToString()}" + logw(message) + return Result.failure(Exception(message)) + } + // Results are in the form: + // 0000000000000000: 00000002 00000000 00010000 0001 01 116651 @apollo_debug_com.example.myapplication + return Result.success( + result.output + .filter { it.contains(SOCKET_NAME_PREFIX) } + .map { it.substringAfterLast(SOCKET_NAME_PREFIX) } + .sorted() + ) + } + + fun IDevice.getApolloDebugClients(): Result> { + return getApolloDebugPackageList().map { packageNames -> + packageNames.map { packageName -> + ApolloDebugClient(this, packageName) + } + } + } + } + + private val port = getUniquePort() + private var hasPortForward: Boolean = false + + private val apolloClient = ApolloClient.Builder() + .serverUrl("http://localhost:$port") + .build() + + private fun createPortForward() { + device.createForward(port, "$SOCKET_NAME_PREFIX$packageName", IDevice.DeviceUnixSocketNamespace.ABSTRACT) + hasPortForward = true + } + + private fun removePortForward() { + device.removeForward(port) + hasPortForward = false + } + + private fun ensurePortForward() { + if (!hasPortForward) { + createPortForward() + } + } + + suspend fun getApolloClients(): Result> = runCatching { + ensurePortForward() + apolloClient.query(GetApolloClientsQuery()).execute().dataOrThrow().apolloClients + } + + suspend fun getNormalizedCache( + apolloClientId: String, + normalizedCacheId: String, + ): Result = runCatching { + ensurePortForward() + apolloClient.query(GetNormalizedCacheQuery(apolloClientId, normalizedCacheId)).execute().dataOrThrow().apolloClient?.normalizedCache + ?: error("No normalized cache returned by server") + } + + override fun close() { + if (hasPortForward) { + removePortForward() + } + apolloClient.close() + } +} + +val String.normalizedCacheSimpleName: String + get() = when (this) { + "com.apollographql.apollo3.cache.normalized.api.MemoryCache" -> "MemoryCache" + "com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCache" -> "SqlNormalizedCache" + else -> this + } diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCache.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCache.kt index cc4139d13d1..170db4f3671 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCache.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCache.kt @@ -6,6 +6,7 @@ data class NormalizedCache( data class Record( val key: String, val fields: List, + val size: Int, ) data class Field( diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheProvider.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheProvider.kt deleted file mode 100644 index 3b584cd0348..00000000000 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.apollographql.ijplugin.normalizedcache - -interface NormalizedCacheProvider

{ - fun provide(parameters: P): Result -} diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheToolWindowFactory.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheToolWindowFactory.kt index f0269653224..55935dd0dbd 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheToolWindowFactory.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/NormalizedCacheToolWindowFactory.kt @@ -1,6 +1,8 @@ package com.apollographql.ijplugin.normalizedcache import com.apollographql.ijplugin.ApolloBundle +import com.apollographql.ijplugin.apollodebugserver.ApolloDebugClient +import com.apollographql.ijplugin.apollodebugserver.normalizedCacheSimpleName import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.BooleanValue import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.CompositeValue import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.ListValue @@ -8,6 +10,8 @@ import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.Nul import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.NumberValue import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.Reference import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.StringValue +import com.apollographql.ijplugin.normalizedcache.provider.ApolloDebugNormalizedCacheProvider +import com.apollographql.ijplugin.normalizedcache.provider.DatabaseNormalizedCacheProvider import com.apollographql.ijplugin.telemetry.TelemetryEvent import com.apollographql.ijplugin.telemetry.telemetryService import com.apollographql.ijplugin.util.logw @@ -64,6 +68,7 @@ import com.intellij.util.ui.ColumnInfo import com.intellij.util.ui.JBUI import com.intellij.util.ui.ListUiUtil import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.runBlocking import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.sqlite.SQLiteException import java.awt.Color @@ -136,7 +141,7 @@ class NormalizedCacheToolWindowFactory : ToolWindowFactory, DumbAware, Disposabl class NormalizedCacheWindowPanel( private val project: Project, private val setTabName: (tabName: String) -> Unit, -) : SimpleToolWindowPanel(false, true) { +) : SimpleToolWindowPanel(false, true), Disposable { private lateinit var normalizedCache: NormalizedCache private lateinit var recordList: JBList @@ -148,6 +153,11 @@ class NormalizedCacheWindowPanel( private val history = History() private var updateHistory = true + private var apolloDebugClient: ApolloDebugClient? = null + private var apolloDebugApolloClientId: String? = null + private var apolloDebugNormalizedCacheId: String? = null + private var isRefreshing = false + init { setContent(createEmptyContent()) } @@ -162,11 +172,12 @@ class NormalizedCacheWindowPanel( emptyText.appendLine(ApolloBundle.message("normalizedCacheViewer.empty.pullFromDevice"), SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES) { PullFromDeviceDialog( project, - onFilePullError = { throwable -> + onPullError = { throwable -> showNotification(project, title = ApolloBundle.message("normalizedCacheViewer.pullFromDevice.pull.error"), content = throwable.message ?: "", type = NotificationType.ERROR) }, onFilePullSuccess = ::openFile, + onApolloDebugCacheSelected = ::openApolloDebugNormalizedCache, ).show() } } @@ -260,6 +271,25 @@ class NormalizedCacheWindowPanel( add(CommonActionsManager.getInstance().createCollapseAllAction(fieldTreeExpander, this@NormalizedCacheWindowPanel).apply { getTemplatePresentation().setDescription(ApolloBundle.message("normalizedCacheViewer.toolbar.collapseAll")) }) + addSeparator() + add(object : DumbAwareAction(ApolloBundle.messagePointer("normalizedCacheViewer.toolbar.refresh"), AllIcons.Actions.Refresh) { + init { + ActionUtil.copyFrom(this, IdeActions.ACTION_REFRESH) + registerCustomShortcutSet(this.shortcutSet, this@NormalizedCacheWindowPanel) + } + + override fun actionPerformed(e: AnActionEvent) { + refreshApolloDebugNormalizedCache() + } + + override fun update(e: AnActionEvent) { + e.presentation.isVisible = apolloDebugNormalizedCacheId != null && apolloDebugClient != null + e.presentation.isEnabled = !isRefreshing + } + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + }) + } val actionToolBar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, false) @@ -550,9 +580,76 @@ class NormalizedCacheWindowPanel( showNotification(project, title = ApolloBundle.message("normalizedCacheViewer.openFileError.title"), content = details, type = NotificationType.ERROR) } + private fun openApolloDebugNormalizedCache(apolloDebugClient: ApolloDebugClient, apolloClientId: String, normalizedCacheId: String) { + project.telemetryService.logEvent(TelemetryEvent.ApolloIjNormalizedCacheOpenApolloDebugCache()) + setContent(createLoadingContent()) + object : Task.Backgroundable( + project, + ApolloBundle.message("normalizedCacheViewer.loading.message"), + false, + ) { + override fun run(indicator: ProgressIndicator) { + var tabName = "" + val normalizedCacheResult = runBlocking { + apolloDebugClient.getNormalizedCache(apolloClientId = apolloClientId, normalizedCacheId = normalizedCacheId) + }.mapCatching { apolloDebugNormalizedCache -> + val tabNamePrefix = apolloClientId.takeIf { it != "client" }?.let { "$it - " } ?: "" + tabName = tabNamePrefix + apolloDebugNormalizedCache.displayName.normalizedCacheSimpleName + ApolloDebugNormalizedCacheProvider().provide(apolloDebugNormalizedCache).getOrThrow() + } + invokeLater { + if (normalizedCacheResult.isFailure) { + showOpenFileError(normalizedCacheResult.exceptionOrNull()!!) + setContent(createEmptyContent()) + return@invokeLater + } + this@NormalizedCacheWindowPanel.apolloDebugClient = apolloDebugClient + this@NormalizedCacheWindowPanel.apolloDebugApolloClientId = apolloClientId + this@NormalizedCacheWindowPanel.apolloDebugNormalizedCacheId = normalizedCacheId + normalizedCache = normalizedCacheResult.getOrThrow().sorted() + setContent(createNormalizedCacheContent()) + toolbar = createToolbar() + setTabName(tabName) + } + } + }.queue() + } + + private fun refreshApolloDebugNormalizedCache() { + object : Task.Backgroundable( + project, + ApolloBundle.message("normalizedCacheViewer.loading.message"), + false, + ) { + override fun run(indicator: ProgressIndicator) { + isRefreshing = true + val normalizedCacheResult = runBlocking { + apolloDebugClient!!.getNormalizedCache(apolloClientId = apolloDebugApolloClientId!!, normalizedCacheId = apolloDebugNormalizedCacheId!!) + }.mapCatching { apolloDebugNormalizedCache -> + ApolloDebugNormalizedCacheProvider().provide(apolloDebugNormalizedCache).getOrThrow() + } + isRefreshing = false + invokeLater { + if (normalizedCacheResult.isFailure) { + showOpenFileError(normalizedCacheResult.exceptionOrNull()!!) + return@invokeLater + } + normalizedCache = normalizedCacheResult.getOrThrow().sorted() + setContent(createNormalizedCacheContent()) + toolbar = null + toolbar = createToolbar() + } + } + }.queue() + } + private class NormalizedCacheFieldTreeNode(val field: NormalizedCache.Field) : DefaultMutableTreeNode() { init { userObject = field.name } } + + override fun dispose() { + apolloDebugClient?.close() + } } diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDevice.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDevice.kt index fca660b7695..1053da16f44 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDevice.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDevice.kt @@ -68,7 +68,6 @@ fun IDevice.getDatabaseList(packageName: String, databasesDir: String): Result { val remoteFilePath = "$remoteDirName/$remoteFileName" - val localFile = File.createTempFile(remoteFileName.substringBeforeLast(".")+"-tmp", ".db") + val localFile = File.createTempFile(remoteFileName.substringBeforeLast(".") + "-tmp", ".db") logd("Pulling $remoteFilePath to ${localFile.absolutePath}") val intermediateRemoteFilePath = "/data/local/tmp/${localFile.name}" val shellCommandsUtil = AdbShellCommandsUtil.create(device) diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDeviceDialog.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDeviceDialog.kt index fd39b1a0f01..93bef4bd28f 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDeviceDialog.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/PullFromDeviceDialog.kt @@ -1,8 +1,13 @@ package com.apollographql.ijplugin.normalizedcache import android.annotation.SuppressLint +import com.android.ddmlib.Client import com.android.ddmlib.IDevice +import com.apollographql.apollo3.debug.GetApolloClientsQuery import com.apollographql.ijplugin.ApolloBundle +import com.apollographql.ijplugin.apollodebugserver.ApolloDebugClient +import com.apollographql.ijplugin.apollodebugserver.ApolloDebugClient.Companion.getApolloDebugClients +import com.apollographql.ijplugin.apollodebugserver.normalizedCacheSimpleName import com.apollographql.ijplugin.icons.ApolloIcons import com.apollographql.ijplugin.ui.tree.DynamicNode import com.apollographql.ijplugin.ui.tree.RootDynamicNode @@ -27,6 +32,7 @@ import com.intellij.ui.treeStructure.SimpleTree import com.intellij.ui.treeStructure.SimpleTreeStructure import com.intellij.util.ui.tree.TreeUtil import icons.StudioIcons +import kotlinx.coroutines.runBlocking import java.awt.event.InputEvent import java.io.File import javax.swing.event.TreeExpansionEvent @@ -38,11 +44,14 @@ import javax.swing.tree.TreeSelectionModel class PullFromDeviceDialog( private val project: Project, private val onFilePullSuccess: (File) -> Unit, - private val onFilePullError: (Throwable) -> Unit, + private val onApolloDebugCacheSelected: (apolloDebugClient: ApolloDebugClient, apolloClientId: String, normalizedCacheId: String) -> Unit, + private val onPullError: (Throwable) -> Unit, ) : DialogWrapper(project, true), Disposable { private lateinit var tree: SimpleTree private lateinit var model: StructureTreeModel + private val apolloDebugClientsToClose = mutableListOf() + init { title = ApolloBundle.message("normalizedCacheViewer.pullFromDevice.title") init() @@ -87,7 +96,8 @@ class PullFromDeviceDialog( }) addTreeSelectionListener { - okAction.isEnabled = it.path.lastPathComponent.cast()?.userObject is DatabaseNode + val selectedNode = it.path.lastPathComponent.cast()?.userObject + okAction.isEnabled = selectedNode is DatabaseNode || selectedNode is ApolloDebugNormalizedCacheNode } } return tree @@ -106,21 +116,31 @@ class PullFromDeviceDialog( } override fun doOKAction() { - val dbNode = tree.selectionPath?.lastPathComponent.cast()?.userObject as? DatabaseNode ?: return - pullFileAsync( - project = project, - device = dbNode.device, - packageName = dbNode.packageName, - remoteDirName = dbNode.databasesDir, - remoteFileName = dbNode.databaseFileName, - onFilePullSuccess = onFilePullSuccess, - onFilePullError = onFilePullError, - ) + when (val selectedNode = tree.selectionPath?.lastPathComponent.cast()?.userObject) { + is DatabaseNode -> { + pullFileAsync( + project = project, + device = selectedNode.device, + packageName = selectedNode.packageName, + remoteDirName = selectedNode.databasesDir, + remoteFileName = selectedNode.databaseFileName, + onFilePullSuccess = onFilePullSuccess, + onFilePullError = onPullError, + ) + } + + is ApolloDebugNormalizedCacheNode -> { + // Don't close the apolloClient, it will be closed later by the caller + apolloDebugClientsToClose.remove(selectedNode.apolloDebugClient) + onApolloDebugCacheSelected(selectedNode.apolloDebugClient, selectedNode.apolloClient.id, selectedNode.normalizedCache.id) + } + } super.doOKAction() } override fun dispose() { super.dispose() + apolloDebugClientsToClose.forEach { runCatching { it.close() } } } private inner class PullFromDeviceTreeStructure(project: Project, invalidate: () -> Unit) : SimpleTreeStructure() { @@ -158,26 +178,57 @@ class PullFromDeviceDialog( } override fun computeChildren() { + val apolloDebugClients: List = device.getApolloDebugClients().getOrDefault(emptyList()) + apolloDebugClientsToClose.addAll(apolloDebugClients) + val clients: List = device.clients + .filter { client -> + client.isValid && + client.clientData.packageName != null && + // If a package has the Apollo Debug running, don't show it as a database package + client.clientData.packageName !in apolloDebugClients.map { it.packageName } + } + .sortedBy { it.clientData.packageName } + + val allClients = (apolloDebugClients + clients).sortedBy { + when (it) { + is ApolloDebugClient -> it.packageName + is Client -> it.clientData.packageName + else -> throw IllegalStateException() + } + } + updateChildren( buildList { + val autoExpand = allClients.size <= 4 + // Add running apps - val clients = device.clients - .filter { it.isValid && it.clientData.packageName != null } - val autoExpand = clients.size <= 4 - addAll(clients - .sortedBy { it.clientData.packageName } - .map { client -> - val packageName = client.clientData.packageName - val databasesDir = client.clientData.dataDir + "/databases" - PackageNode( - project = project, - parent = this@DeviceNode, - device = device, - packageName = packageName, - databasesDir = databasesDir, - computeChildrenOn = ComputeChildrenOn.INIT, - autoExpand = autoExpand, - ) + addAll( + allClients.map { client -> + when (client) { + is ApolloDebugClient -> ApolloDebugPackageNode( + project = project, + parent = this@DeviceNode, + apolloDebugClient = client, + computeChildrenOn = ComputeChildrenOn.INIT, + autoExpand = autoExpand, + ) + + is Client -> { + val packageName = client.clientData.packageName + val databasesDir = client.clientData.dataDir + "/databases" + DatabasePackageNode( + project = project, + parent = this@DeviceNode, + device = device, + packageName = packageName, + databasesDir = databasesDir, + computeChildrenOn = ComputeChildrenOn.INIT, + autoExpand = autoExpand, + ) + } + + else -> throw IllegalStateException() + } } ) @@ -211,7 +262,7 @@ class PullFromDeviceDialog( } else { updateChildren( it.map { packageName -> - PackageNode( + DatabasePackageNode( project = project, parent = this, device = device, @@ -227,7 +278,7 @@ class PullFromDeviceDialog( } } - private inner class PackageNode( + private inner class DatabasePackageNode( project: Project, parent: DynamicNode, private val device: IDevice, @@ -268,6 +319,48 @@ class PullFromDeviceDialog( } } + private inner class ApolloDebugPackageNode( + project: Project, + parent: DynamicNode, + private val apolloDebugClient: ApolloDebugClient, + computeChildrenOn: ComputeChildrenOn, + private val autoExpand: Boolean, + ) : DynamicNode(project, parent, computeChildrenOn) { + init { + myName = apolloDebugClient.packageName + icon = ApolloIcons.Node.Package + } + + override fun computeChildren() { + runBlocking { apolloDebugClient.getApolloClients() }.onFailure { + updateChild(ErrorNode(ApolloBundle.message("normalizedCacheViewer.pullFromDevice.listApolloClients.error"))) + }.onSuccess { apolloClients -> + if (apolloClients.isEmpty()) { + updateChild(EmptyNode(ApolloBundle.message("normalizedCacheViewer.pullFromDevice.listApolloClients.empty"))) + } else { + val showClientName = apolloClients.size > 1 + updateChildren( + apolloClients + .flatMap { apolloClient -> apolloClient.normalizedCaches.map { normalizedCache -> apolloClient to normalizedCache } } + .filter { (_, normalizedCacheInfo) -> normalizedCacheInfo.recordCount != 0 } + .map { (apolloClient, normalizedCache) -> + ApolloDebugNormalizedCacheNode( + apolloDebugClient = apolloDebugClient, + apolloClient = apolloClient, + normalizedCache = normalizedCache, + showClientName = showClientName, + ) + } + ) + } + } + } + + override fun isAutoExpandNode(): Boolean { + return autoExpand + } + } + private inner class DatabaseNode( val device: IDevice, val packageName: String, @@ -284,6 +377,27 @@ class PullFromDeviceDialog( } } + private inner class ApolloDebugNormalizedCacheNode( + val apolloDebugClient: ApolloDebugClient, + val apolloClient: GetApolloClientsQuery.ApolloClient, + val normalizedCache: GetApolloClientsQuery.NormalizedCach, + showClientName: Boolean, + ) : NullNode() { + init { + myName = if (showClientName) { + "${apolloClient.displayName} - ${normalizedCache.displayName.normalizedCacheSimpleName}" + } else { + normalizedCache.displayName.normalizedCacheSimpleName + } + presentation.locationString = ApolloBundle.message("normalizedCacheViewer.pullFromDevice.apolloDebugNormalizedCache.records", normalizedCache.recordCount) + icon = StudioIcons.DatabaseInspector.DATABASE + } + + override fun handleDoubleClickOrEnter(tree: SimpleTree, inputEvent: InputEvent) { + doOKAction() + } + } + private class ErrorNode(message: String) : NullNode() { init { presentation.addText(message, SimpleTextAttributes.GRAYED_ATTRIBUTES) diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/ApolloDebugNormalizedCacheProvider.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/ApolloDebugNormalizedCacheProvider.kt new file mode 100644 index 00000000000..e24be207a7b --- /dev/null +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/ApolloDebugNormalizedCacheProvider.kt @@ -0,0 +1,58 @@ +package com.apollographql.ijplugin.normalizedcache.provider + +import com.apollographql.apollo3.cache.normalized.api.CacheKey +import com.apollographql.apollo3.debug.GetNormalizedCacheQuery +import com.apollographql.ijplugin.normalizedcache.NormalizedCache +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.Field +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.BooleanValue +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.CompositeValue +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.ListValue +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.Null +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.NumberValue +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.Reference +import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.StringValue + +class ApolloDebugNormalizedCacheProvider : NormalizedCacheProvider { + override fun provide(parameters: GetNormalizedCacheQuery.NormalizedCache): Result { + return runCatching { + NormalizedCache( + parameters.records.map { record -> + NormalizedCache.Record( + key = record.key, + fields = record.fields.toFields(), + size = record.size + ) + } + ) + } + } +} + +@Suppress("UNCHECKED_CAST") +private fun Any.toFields(): List { + this as Map + return map { (name, value) -> + Field( + name, + value.toFieldValue() + ) + } +} + +private fun Any?.toFieldValue(): FieldValue { + return when (this) { + null -> Null + is String -> if (CacheKey.canDeserialize(this)) { + Reference(CacheKey.deserialize(this).key) + } else { + StringValue(this) + } + + is Number -> NumberValue(this) + is Boolean -> BooleanValue(this) + is List<*> -> ListValue(map { it.toFieldValue() }) + is Map<*, *> -> CompositeValue(map { Field(it.key as String, it.value.toFieldValue()) }) + else -> error("Unsupported type ${this::class}") + } +} diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/DatabaseNormalizedCacheProvider.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/DatabaseNormalizedCacheProvider.kt similarity index 90% rename from intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/DatabaseNormalizedCacheProvider.kt rename to intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/DatabaseNormalizedCacheProvider.kt index 120fe2bb767..85067c6be33 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/DatabaseNormalizedCacheProvider.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/DatabaseNormalizedCacheProvider.kt @@ -1,7 +1,8 @@ -package com.apollographql.ijplugin.normalizedcache +package com.apollographql.ijplugin.normalizedcache.provider import com.apollographql.apollo3.cache.normalized.api.CacheKey import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory +import com.apollographql.ijplugin.normalizedcache.NormalizedCache import com.apollographql.ijplugin.normalizedcache.NormalizedCache.Field import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue import com.apollographql.ijplugin.normalizedcache.NormalizedCache.FieldValue.BooleanValue @@ -37,10 +38,11 @@ class DatabaseNormalizedCacheProvider : NormalizedCacheProvider { NormalizedCache( apolloRecords.map { (key, apolloRecord) -> NormalizedCache.Record( - key, - apolloRecord.map { (fieldName, fieldValue) -> + key = key, + fields = apolloRecord.map { (fieldName, fieldValue) -> Field(fieldName, fieldValue.toFieldValue()) - } + }, + size = apolloRecord.size ) } ) diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/NormalizedCacheProvider.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/NormalizedCacheProvider.kt new file mode 100644 index 00000000000..d0a0f3c5c8f --- /dev/null +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/normalizedcache/provider/NormalizedCacheProvider.kt @@ -0,0 +1,7 @@ +package com.apollographql.ijplugin.normalizedcache.provider + +import com.apollographql.ijplugin.normalizedcache.NormalizedCache + +interface NormalizedCacheProvider

{ + fun provide(parameters: P): Result +} diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt index c59a13021ca..0cb15d6292a 100644 --- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt +++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/telemetry/TelemetrySession.kt @@ -294,6 +294,11 @@ sealed class TelemetryEvent( */ class ApolloIjNormalizedCacheOpenFile : TelemetryEvent("akij_normalized_cache_open_file", null) + /** + * User opened a normalized cache pulled from an app using Apollo Debug in the Normalized cache viewer. + */ + class ApolloIjNormalizedCacheOpenApolloDebugCache : TelemetryEvent("akij_normalized_cache_open_apollo_debug_cache", null) + /** * User used the 'Migrate to operationBased codegen' Apollo Kotlin IntelliJ plugin action. */ diff --git a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties index 991942159b3..391dacb695c 100644 --- a/intellij-plugin/src/main/resources/messages/ApolloBundle.properties +++ b/intellij-plugin/src/main/resources/messages/ApolloBundle.properties @@ -165,6 +165,7 @@ normalizedCacheViewer.toolbar.expandAll=Expand all keys normalizedCacheViewer.toolbar.collapseAll=Collapse all keys normalizedCacheViewer.toolbar.back=Back normalizedCacheViewer.toolbar.forward=Forward +normalizedCacheViewer.toolbar.refresh=Refresh normalizedCacheViewer.empty.message=Open or drag and drop a normalized cache .db file. normalizedCacheViewer.empty.openFile=Open file... normalizedCacheViewer.empty.pullFromDevice=Pull from device @@ -183,7 +184,10 @@ normalizedCacheViewer.pullFromDevice.listDatabases.empty=No databases normalizedCacheViewer.pullFromDevice.listDebuggablePackages.title=All packages normalizedCacheViewer.pullFromDevice.listDebuggablePackages.error=Could not list debuggable apps normalizedCacheViewer.pullFromDevice.listDebuggablePackages.empty=No debuggable apps -normalizedCacheViewer.pullFromDevice.pull.ongoing=Pulling file from device +normalizedCacheViewer.pullFromDevice.listApolloClients.error=Error listing Apollo clients +normalizedCacheViewer.pullFromDevice.listApolloClients.empty=No Apollo clients +normalizedCacheViewer.pullFromDevice.apolloDebugNormalizedCache.records={0,choice, 0#no records|1#one record|2#{0,number} records} +normalizedCacheViewer.pullFromDevice.pull.ongoing=Pulling normalized cache from device normalizedCacheViewer.pullFromDevice.pull.error=Could not pull normalized cache from device tree.dynamicNode.loading=Loading...