Skip to content

feat(amazonq): show new transformation plan #5731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ data class PlanTable(
@JsonProperty("columnNames")
val columns: List<String>,
@JsonProperty("rows")
val rows: List<PlanTableRow>,
val rows: MutableList<PlanTableRow>,
@JsonProperty("name")
val name: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class ZipManifest(
val version: String = UPLOAD_ZIP_MANIFEST_VERSION,
val hilCapabilities: List<String> = 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<String> = listOf(EXPLAINABILITY_V1),
val customBuildCommand: String = MAVEN_BUILD_RUN_UNIT_TESTS,
val requestedConversions: RequestedConversions? = null, // only used for SQL conversions for now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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<PlanTable>(it) }?.let { transformationPlanAppendix(it) },
tableMapping[APPENDIX_TABLE_KEY]?.get(0)?.let { MAPPER.readValue<PlanTable>(it) }?.let { transformationPlanAppendix(it) },
CodeModernizerUIConstants.transformationPlanPlaneConstraint,
)
}
Expand Down Expand Up @@ -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<PlanTable>(it)
val parsedTables = tables?.map { table ->
MAPPER.readValue<PlanTable>(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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -285,20 +286,36 @@ fun findDownloadArtifactProgressUpdate(transformationSteps: List<TransformationS
update.downloadArtifacts()?.firstOrNull()?.downloadArtifactId() != null
}

// once dependency changes table (key of "1") available, plan is complete
fun isPlanComplete(plan: TransformationPlan?) = plan?.transformationSteps()?.get(0)?.progressUpdates()?.any { update -> 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<TransformationProgressUpdate>): Map<String, String> {
if (stepZeroProgressUpdates.isNotEmpty()) {
return stepZeroProgressUpdates.associate {
it.name() to it.description()
fun getTableMapping(stepZeroProgressUpdates: List<TransformationProgressUpdate>): Map<String, List<String>> =
stepZeroProgressUpdates.groupBy(
{ it.name() },
{ it.description() }
)

// ID of '0' reserved for job statistics table; only 1 table there
fun parseTableMapping(tableMapping: Map<String, List<String>>): PlanTable {
val statsTable = tableMapping[JOB_STATISTICS_TABLE_KEY]?.get(0) ?: error("No transformation statistics table found in GetPlan response")
return MAPPER.readValue<PlanTable>(statsTable)
}

// columns and name are shared between all PlanTables, so just combine the rows here
fun combineTableRows(tables: List<PlanTable>?): 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<String, String>): PlanTable? =
tableMapping[JOB_STATISTICS_TABLE_KEY]?.let { MAPPER.readValue<PlanTable>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<PlanTable>(table1Json),
MAPPER.readValue<PlanTable>(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()
Expand Down
Loading