diff --git a/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json b/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json new file mode 100644 index 00000000000..71c1583e77b --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-ab31cbb6-3fe4-4ee3-a0a3-290430277856.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Q CodeTransformation: add more job metadata to history table" +} diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index d171eae31bf..5d21c6b77f7 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -403,9 +403,11 @@ export class GumbyController { await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal') break case ButtonActions.STOP_TRANSFORMATION_JOB: - await stopTransformByQ(transformByQState.getJobId()) - await postTransformationJob() - await cleanupTransformationJob() + if (transformByQState.isRunning() || transformByQState.isRefreshInProgress()) { + await stopTransformByQ(transformByQState.getJobId()) + await postTransformationJob() + await cleanupTransformationJob() + } break case ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW: this.resetTransformationChatFlow() diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 409ee89ab04..fb8da4c7096 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -400,11 +400,22 @@ export class Messenger { } public sendJobRefreshInProgressMessage(tabID: string, jobId: string) { - this.dispatcher.sendAsyncEventProgress( - new AsyncEventProgressMessage(tabID, { - inProgress: true, - message: CodeWhispererConstants.refreshingJobChatMessage(jobId), - }) + const buttons: ChatItemButton[] = [] + buttons.push({ + keepCardAfterClick: true, + text: CodeWhispererConstants.stopTransformationButtonText, + id: ButtonActions.STOP_TRANSFORMATION_JOB, + disabled: false, + }) + this.dispatcher.sendChatMessage( + new ChatMessage( + { + message: CodeWhispererConstants.refreshingJobChatMessage(jobId), + messageType: 'ai-prompt', + buttons: buttons, + }, + tabID + ) ) } diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index aa8bea11da2..410465a55c1 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -769,7 +769,12 @@ export async function postTransformationJob() { latest.status, latest.duration, transformByQState.getJobId(), - transformByQState.getJobHistoryPath() + transformByQState.getJobHistoryPath(), + latest.transformationType, + latest.sourceJDKVersion, + latest.targetJDKVersion, + latest.customDependencyVersionsFilePath, + latest.customBuildCommand ) } } diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index bcfa50c6a71..f074fe74bd6 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -688,7 +688,17 @@ export const jobPlanProgress: { } export let sessionJobHistory: { - [jobId: string]: { startTime: string; projectName: string; status: string; duration: string } + [jobId: string]: { + startTime: string + projectName: string + status: string + duration: string + transformationType: string + sourceJDKVersion: string + targetJDKVersion: string + customDependencyVersionsFilePath: string + customBuildCommand: string + } } = {} export class TransformByQState { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index 45d95ec9a75..c2934dc24ba 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -68,12 +68,29 @@ export function throwIfCancelled() { } export function updateJobHistory() { - if (transformByQState.getJobId() !== '') { + if (transformByQState.getJobId() !== '' && transformByQState.getSourceJDKVersion() !== undefined) { sessionJobHistory[transformByQState.getJobId()] = { startTime: transformByQState.getStartTime(), projectName: transformByQState.getProjectName(), status: transformByQState.getPolledJobStatus(), duration: convertToTimeString(calculateTotalLatency(CodeTransformTelemetryState.instance.getStartTime())), + transformationType: transformByQState.getTransformationType() ?? 'N/A', + sourceJDKVersion: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? (transformByQState.getSourceJDKVersion() ?? 'N/A') + : 'N/A', + targetJDKVersion: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? (transformByQState.getTargetJDKVersion() ?? 'N/A') + : 'N/A', + customDependencyVersionsFilePath: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? transformByQState.getCustomDependencyVersionFilePath() || 'N/A' + : 'N/A', + customBuildCommand: + transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE + ? transformByQState.getCustomBuildCommand() || 'N/A' + : 'N/A', } } return sessionJobHistory diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts index 1876ac059e4..6aba818b4fc 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHistoryHandler.ts @@ -26,6 +26,11 @@ export interface HistoryObject { diffPath: string summaryPath: string jobId: string + transformationType: string + sourceJDKVersion: string + targetJDKVersion: string + customDependencyVersionFilePath: string + customBuildCommand: string } export interface JobMetadata { @@ -71,6 +76,11 @@ export async function readHistoryFile(): Promise { diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6], + transformationType: jobInfo[7], + sourceJDKVersion: jobInfo[8], + targetJDKVersion: jobInfo[9], + customDependencyVersionFilePath: jobInfo[10], + customBuildCommand: jobInfo[11], }) } } @@ -125,14 +135,22 @@ export async function writeToHistoryFile( status: string, duration: string, jobId: string, - jobHistoryPath: string + jobHistoryPath: string, + transformationType: string, + sourceJDKVersion: string, + targetJDKVersion: string, + customDependencyVersionFilePath: string, + customBuildCommand: string ) { const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') // create transform folder if necessary if (!(await fs.existsFile(historyLogFilePath))) { await fs.mkdir(path.dirname(historyLogFilePath)) // create headers of new transformation history file - await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + await fs.writeFile( + historyLogFilePath, + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) } const artifactsExist = status === 'COMPLETED' || status === 'PARTIALLY_COMPLETED' const fields = [ @@ -143,6 +161,11 @@ export async function writeToHistoryFile( artifactsExist ? path.join(jobHistoryPath, 'diff.patch') : '', artifactsExist ? path.join(jobHistoryPath, 'summary', 'summary.md') : '', jobId, + transformationType, + sourceJDKVersion, + targetJDKVersion, + customDependencyVersionFilePath, + customBuildCommand, ] const jobDetails = fields.join('\t') + '\n' @@ -318,7 +341,8 @@ async function updateHistoryFile(status: string, duration: string, jobHistoryPat for (const job of jobs) { if (job) { const jobInfo = job.split('\t') - // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6] + // 0: startTime, 1: projectName, 2: status, 3: duration, 4: diffPath, 5: summaryPath, 6: jobId + // 7: transformationType, 8: sourceJDKVersion, 9: targetJDKVersion, 10: customDependencyVersionFilePath, 11: customBuildCommand if (jobInfo[6] === jobId) { // update any values if applicable jobInfo[2] = status @@ -341,7 +365,10 @@ async function updateHistoryFile(status: string, duration: string, jobHistoryPat } // rewrite file - await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + await fs.writeFile( + historyLogFilePath, + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) const tsvContent = history.map((row) => row.join('\t')).join('\n') + '\n' await fs.appendFile(historyLogFilePath, tsvContent) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 35e8319ab46..97e69570c76 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -120,6 +120,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider diffPath: '', summaryPath: '', jobId: transformByQState.getJobId(), + transformationType: current.transformationType, + sourceJDKVersion: current.sourceJDKVersion, + targetJDKVersion: current.targetJDKVersion, + customDependencyVersionFilePath: current.customDependencyVersionsFilePath, + customBuildCommand: current.customBuildCommand, }) } return ` @@ -208,6 +213,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider Summary File Job Id Refresh Job + Transformation Type + Source JDK Version + Target JDK Version + Custom Dependency Version File Path + Custom Build Command @@ -242,6 +252,11 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider ↻ + ${job.transformationType ?? ''} + ${job.sourceJDKVersion ?? ''} + ${job.targetJDKVersion ?? ''} + ${job.customDependencyVersionFilePath ?? ''} + ${job.customBuildCommand ? `mvn ${job.customBuildCommand}` : ''} ` ) diff --git a/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts index b286ad6537f..f5a92299255 100644 --- a/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts +++ b/packages/core/src/test/amazonqGumby/transformationJobHistory.test.ts @@ -98,23 +98,51 @@ describe('Transformation History Handler', function () { it('Creates history file with headers when it does not exist', async function () { sinon.stub(fs, 'existsFile').resolves(false) - await writeToHistoryFile('01/01/25, 10:00 AM', 'test-project', 'COMPLETED', '5 min', 'job-123', '/job/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'test-project', + 'COMPLETED', + '5 min', + 'job-123', + '/job/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) assert(fileContent) - assert(fileContent.includes('date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n')) assert( fileContent.includes( - `01/01/25, 10:00 AM\ttest-project\tCOMPLETED\t5 min\t${path.join('/job/path', 'diff.patch')}\t${path.join('/job/path', 'summary', 'summary.md')}\tjob-123\n` + 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\ttransformation_type\tsource_jdk_version\ttarget_jdk_version\tcustom_dependency_version_file_path\tcustom_build_command\n' + ) + ) + assert( + fileContent.includes( + `01/01/25, 10:00 AM\ttest-project\tCOMPLETED\t5 min\t${path.join('/job/path', 'diff.patch')}\t${path.join('/job/path', 'summary', 'summary.md')}\tjob-123\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/path/here\tclean test-compile\n` ) ) }) it('Excludes artifact paths for failed jobs', async function () { sinon.stub(fs, 'existsFile').resolves(false) - await writeToHistoryFile('01/01/25, 10:00 AM', 'test-project', 'FAILED', '5 min', 'job-123', '/job/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'test-project', + 'FAILED', + '5 min', + 'job-123', + '/job/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) @@ -130,7 +158,7 @@ describe('Transformation History Handler', function () { it('Appends new job to existing history file', async function () { const existingContent = 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n' + - '12/31/24, 09:00 AM\told-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\n' + '12/31/24, 09:00 AM\told-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\t/old/path\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/old/path2\tclean test-compile\n' writtenFiles.set( path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv'), @@ -139,14 +167,28 @@ describe('Transformation History Handler', function () { sinon.stub(fs, 'existsFile').resolves(true) - await writeToHistoryFile('01/01/25, 10:00 AM', 'new-project', 'FAILED', '2 min', 'new-job-789', '/new/path') + await writeToHistoryFile( + '01/01/25, 10:00 AM', + 'new-project', + 'FAILED', + '2 min', + 'new-job-789', + '/new/path', + 'LANGUAGE_UPGRADE', + 'JDK8', + 'JDK17', + '/path/here', + 'clean test-compile' + ) const expectedPath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv') const fileContent = writtenFiles.get(expectedPath) // Verify old data is preserved assert( - fileContent?.includes('old-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456') + fileContent?.includes( + 'old-project\tCOMPLETED\t3 min\t/old/diff.patch\t/old/summary.md\told-job-456\t/old/path\tLANGUAGE_UPGRADE\tJDK8\tJDK17\t/old/path2\tclean test-compile\n' + ) ) // Verify new data is added diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index 218fa384c1e..f3ae2fbceff 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -6,7 +6,7 @@ import assert, { fail } from 'assert' import * as vscode from 'vscode' import * as sinon from 'sinon' -import { DB, transformByQState, TransformByQStoppedError } from '../../../codewhisperer/models/model' +import { DB, JDKVersion, transformByQState, TransformByQStoppedError } from '../../../codewhisperer/models/model' import { stopTransformByQ, finalizeTransformationJob } from '../../../codewhisperer/commands/startTransformByQ' import { HttpResponse } from 'aws-sdk' import * as codeWhisperer from '../../../codewhisperer/client/codewhisperer' @@ -283,6 +283,8 @@ dependencyManagement: it(`WHEN update job history called THEN returns details of last run job`, async function () { transformByQState.setJobId('abc-123') + transformByQState.setSourceJDKVersion(JDKVersion.JDK8) + transformByQState.setTargetJDKVersion(JDKVersion.JDK17) transformByQState.setProjectName('test-project') transformByQState.setPolledJobStatus('COMPLETED') transformByQState.setStartTime('05/03/24, 11:35 AM')