diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt index d468097d25..88b055a41b 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt @@ -51,6 +51,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.STATES_AFTE import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getModuleOrProjectNameForFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getPathToHilDependencyReportDir +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isPlanComplete import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isValidCodeTransformConnection import software.aws.toolkits.jetbrains.services.codemodernizer.utils.pollTransformationStatusAndPlan import software.aws.toolkits.jetbrains.services.codemodernizer.utils.toTransformationLanguage @@ -477,10 +478,12 @@ class CodeModernizerSession( } } - // Open the transformation plan detail panel once transformation plan is available (no plan for SQL conversions) - if (transformType != CodeTransformType.SQL_CONVERSION && state.transformationPlan != null && !isTransformationPlanEditorOpened) { - tryOpenTransformationPlanEditor() - isTransformationPlanEditorOpened = true + if (!isTransformationPlanEditorOpened && transformType == CodeTransformType.LANGUAGE_UPGRADE) { + val isPlanComplete = isPlanComplete(state.transformationPlan) + if (isPlanComplete) { + tryOpenTransformationPlanEditor() + isTransformationPlanEditorOpened = true + } } val instant = Instant.now() // Set the job start time diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/PlanTable.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/PlanTable.kt index ef1784b9e7..781ef05ca0 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/PlanTable.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/PlanTable.kt @@ -11,7 +11,7 @@ data class PlanTable( @JsonProperty("columnNames") val columns: List, @JsonProperty("rows") - val rows: List, + val rows: MutableList, @JsonProperty("name") val name: String, ) diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt index e65b08f7e1..9fb6b14a38 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/ZipManifest.kt @@ -11,6 +11,7 @@ data class ZipManifest( val version: String = UPLOAD_ZIP_MANIFEST_VERSION, val hilCapabilities: List = listOf(HIL_1P_UPGRADE_CAPABILITY), // TODO: add CLIENT_SIDE_BUILD to transformCapabilities when releasing CSB + // TODO: add AGENTIC_PLAN_V1 or something here AND in processCodeTransformSkipTests when backend allowlists everyone val transformCapabilities: List = listOf(EXPLAINABILITY_V1), val customBuildCommand: String = MAVEN_BUILD_RUN_UNIT_TESTS, val requestedConversions: RequestedConversions? = null, // only used for SQL conversions for now diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/managers/CodeModernizerBottomWindowPanelManager.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/managers/CodeModernizerBottomWindowPanelManager.kt index 0d51765749..eec38b7327 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/managers/CodeModernizerBottomWindowPanelManager.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/panels/managers/CodeModernizerBottomWindowPanelManager.kt @@ -28,6 +28,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.panels.CodeModern import software.aws.toolkits.jetbrains.services.codemodernizer.panels.LoadingPanel import software.aws.toolkits.jetbrains.services.codemodernizer.state.CodeModernizerSessionState import software.aws.toolkits.jetbrains.services.codemodernizer.toolwindow.CodeModernizerBottomToolWindowFactory +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isPlanComplete import software.aws.toolkits.resources.message import java.awt.BorderLayout import java.awt.Component @@ -251,7 +252,7 @@ class CodeModernizerBottomWindowPanelManager(private val project: Project) : JPa TransformationStatus.PAUSED, TransformationStatus.COMPLETED, TransformationStatus.PARTIALLY_COMPLETED - ) && transformType != CodeTransformType.SQL_CONVERSION // no plan for SQL conversions + ) && transformType == CodeTransformType.LANGUAGE_UPGRADE && isPlanComplete(plan) ) { addPlanToBanner() } diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditor.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditor.kt index 74202e35f1..11372b8528 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditor.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditor.kt @@ -24,6 +24,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.constants.LOC_THR import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact.Companion.MAPPER import software.aws.toolkits.jetbrains.services.codemodernizer.model.PlanTable import software.aws.toolkits.jetbrains.services.codemodernizer.plan.CodeModernizerPlanEditorProvider.Companion.MIGRATION_PLAN_KEY +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.combineTableRows import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getAuthType import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getBillingText import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getLinesOfCodeSubmitted @@ -76,7 +77,7 @@ class CodeModernizerPlanEditor(val project: Project, private val virtualFile: Vi // comes from "name" field of each progressUpdate in step zero of plan if (JOB_STATISTICS_TABLE_KEY in tableMapping) { val planTable = parseTableMapping(tableMapping) - val linesOfCode = planTable?.let { getLinesOfCodeSubmitted(it) } + val linesOfCode = getLinesOfCodeSubmitted(planTable) if (linesOfCode != null && linesOfCode > LOC_THRESHOLD && getAuthType(project) == CredentialSourceId.IamIdentityCenter) { val billingText = getBillingText(linesOfCode) val billingTextComponent = @@ -98,15 +99,15 @@ class CodeModernizerPlanEditor(val project: Project, private val virtualFile: Vi add(billingTextComponent, CodeModernizerUIConstants.transformationPlanPlaneConstraint) } add( - planTable?.let { transformationPlanInfo(it) }, + transformationPlanInfo(planTable), CodeModernizerUIConstants.transformationPlanPlaneConstraint, ) } add(transformationPlanPanel(plan), CodeModernizerUIConstants.transformationPlanPlaneConstraint) - // key "-1" reserved for appendix table + // key "-1" reserved for appendix table; only 1 table there if (APPENDIX_TABLE_KEY in tableMapping) { add( - tableMapping[APPENDIX_TABLE_KEY]?.let { MAPPER.readValue(it) }?.let { transformationPlanAppendix(it) }, + tableMapping[APPENDIX_TABLE_KEY]?.get(0)?.let { MAPPER.readValue(it) }?.let { transformationPlanAppendix(it) }, CodeModernizerUIConstants.transformationPlanPlaneConstraint, ) } @@ -393,10 +394,17 @@ class CodeModernizerPlanEditor(val project: Project, private val virtualFile: Vi border = CodeModernizerUIConstants.DESCRIPTION_BORDER } - val table = tableMapping[step.id()] + val tables = tableMapping[step.id()] - var parsedTable = table?.let { - MAPPER.readValue(it) + val parsedTables = tables?.map { table -> + MAPPER.readValue(table) + } + + var parsedTable: PlanTable? = if (parsedTables?.size == 1) { + parsedTables.first() + } else { + // for multiple tables under 1 step, the table headers are the same, so combine the rows and just show 1 combined table + combineTableRows(parsedTables) } if (parsedTable?.rows?.isEmpty() == true) { diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt index 6495508328..730ab8ea26 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/utils/CodeTransformApiUtils.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.utils import com.fasterxml.jackson.module.kotlin.readValue +import com.intellij.grazie.utils.orFalse import com.intellij.notification.NotificationAction import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runWriteAction @@ -285,20 +286,36 @@ fun findDownloadArtifactProgressUpdate(transformationSteps: List update.name() == "1" }.orFalse() + // "name" holds the ID of the corresponding plan step (where table will go) and "description" holds the plan data -fun getTableMapping(stepZeroProgressUpdates: List): Map { - if (stepZeroProgressUpdates.isNotEmpty()) { - return stepZeroProgressUpdates.associate { - it.name() to it.description() +fun getTableMapping(stepZeroProgressUpdates: List): Map> = + stepZeroProgressUpdates.groupBy( + { it.name() }, + { it.description() } + ) + +// ID of '0' reserved for job statistics table; only 1 table there +fun parseTableMapping(tableMapping: Map>): PlanTable { + val statsTable = tableMapping[JOB_STATISTICS_TABLE_KEY]?.get(0) ?: error("No transformation statistics table found in GetPlan response") + return MAPPER.readValue(statsTable) +} + +// columns and name are shared between all PlanTables, so just combine the rows here +fun combineTableRows(tables: List?): PlanTable? { + if (tables == null) { + return null + } + val combinedTable = PlanTable(tables.first().columns, mutableListOf(), tables.first().name) + tables.forEach { table -> + table.rows.forEach { row -> + combinedTable.rows.add(row) } - } else { - error("GetPlan response missing step 0 progress updates with table data") } + return combinedTable } -fun parseTableMapping(tableMapping: Map): PlanTable? = - tableMapping[JOB_STATISTICS_TABLE_KEY]?.let { MAPPER.readValue(it) } - fun getBillingText(linesOfCode: Int): String { val estimatedCost = String.format(Locale.US, "%.2f", linesOfCode.times(BILLING_RATE)) return message("codemodernizer.migration_plan.header.billing_text", linesOfCode, BILLING_RATE, estimatedCost) diff --git a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt index 3748266943..a257245a23 100644 --- a/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt +++ b/plugins/amazonq/codetransform/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codemodernizer/CodeWhispererCodeModernizerUtilsTest.kt @@ -3,6 +3,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer +import com.fasterxml.jackson.module.kotlin.readValue import com.intellij.testFramework.LightVirtualFile import io.mockk.every import io.mockk.just @@ -26,11 +27,15 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Transformation import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStep import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException +import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact.Companion.MAPPER import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformType +import software.aws.toolkits.jetbrains.services.codemodernizer.model.PlanTable +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.combineTableRows import software.aws.toolkits.jetbrains.services.codemodernizer.utils.createClientSideBuildUploadZip import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getBillingText import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getClientInstructionArtifactId import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getTableMapping +import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isPlanComplete import software.aws.toolkits.jetbrains.services.codemodernizer.utils.parseBuildFile import software.aws.toolkits.jetbrains.services.codemodernizer.utils.pollTransformationStatusAndPlan import software.aws.toolkits.jetbrains.services.codemodernizer.utils.refreshToken @@ -224,10 +229,73 @@ class CodeWhispererCodeModernizerUtilsTest : CodeWhispererCodeModernizerTestBase val step0Update2 = TransformationProgressUpdate.builder().name("2").status("COMPLETED").description(apiChanges).build() val step0Update3 = TransformationProgressUpdate.builder().name("-1").status("COMPLETED").description(fileChanges).build() val actual = getTableMapping(listOf(step0Update0, step0Update1, step0Update2, step0Update3)) - val expected = mapOf("0" to jobStats, "1" to depChanges, "2" to apiChanges, "-1" to fileChanges) + val expected = mapOf("0" to listOf(jobStats), "1" to listOf(depChanges), "2" to listOf(apiChanges), "-1" to listOf(fileChanges)) assertThat(expected).isEqualTo(actual) } + @Test + fun `combineTableRows combines multiple dependency tables correctly`() { + val table1Json = """ + {"name":"Dependency changes", "columnNames":["dependencyName","action","currentVersion","targetVersion"], + "rows":[{"dependencyName":"org.springframework.boot","action":"Update","currentVersion":"2.1","targetVersion":"2.4"}]} + """.trimIndent() + val table2Json = """ + {"name":"Dependency changes", "columnNames":["dependencyName","action","currentVersion","targetVersion"], + "rows":[{"dependencyName":"junit","action":"Add","currentVersion":"","targetVersion":"4.13"}]} + """.trimIndent() + val tables = listOf( + MAPPER.readValue(table1Json), + MAPPER.readValue(table2Json) + ) + val combinedTable = combineTableRows(tables) + assertThat(combinedTable?.rows).hasSize(2) + assertThat(combinedTable?.name).isEqualTo("Dependency changes") + assertThat(combinedTable?.columns).hasSize(4) + } + + @Test + fun `isPlanComplete returns true when plan has progress update with name '1'`() { + // Arrange + val plan = TransformationPlan.builder() + .transformationSteps( + listOf( + TransformationStep.builder() + .progressUpdates( + listOf( + TransformationProgressUpdate.builder() + .name("1") + .build() + ) + ) + .build() + ) + ) + .build() + val result = isPlanComplete(plan) + assertThat(result).isTrue() + } + + @Test + fun `isPlanComplete returns false when plan has no progress update with name '1'`() { + val plan = TransformationPlan.builder() + .transformationSteps( + listOf( + TransformationStep.builder() + .progressUpdates( + listOf( + TransformationProgressUpdate.builder() + .name("2") + .build() + ) + ) + .build() + ) + ) + .build() + val result = isPlanComplete(plan) + assertThat(result).isFalse() + } + @Test fun `getClientInstructionArtifactId extracts artifact ID from transformation plan`() { val step1 = TransformationStep.builder()