From ab1d404e0f0c5a13454d9793d6ed2305fbf99ca6 Mon Sep 17 00:00:00 2001 From: Trisha Godara Date: Wed, 18 Jun 2025 13:40:28 -0700 Subject: [PATCH 1/7] modify job status table to show transformation history; store diff patches locally; write history details to local log file --- packages/amazonq/package.json | 2 +- packages/core/package.nls.json | 2 +- .../chat/controller/controller.ts | 8 ++ .../chat/controller/messenger/messenger.ts | 21 +++++ .../controller/messenger/messengerUtils.ts | 2 + .../commands/startTransformByQ.ts | 29 +++++++ .../core/src/codewhisperer/models/model.ts | 9 ++ .../transformationHubViewProvider.ts | 86 ++++++++++++++++--- .../transformationResultsViewProvider.ts | 16 ++++ 9 files changed, 161 insertions(+), 14 deletions(-) diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 071f2000031..c155a4604be 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -746,7 +746,7 @@ }, { "command": "aws.amazonq.showHistoryInHub", - "title": "%AWS.command.q.transform.viewJobStatus%" + "title": "%AWS.command.q.transform.viewJobHistory%" }, { "command": "aws.amazonq.selectCustomization", diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 45db449c625..06343f17c75 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -281,7 +281,7 @@ "AWS.command.q.transform.rejectChanges": "Reject", "AWS.command.q.transform.stopJobInHub": "Stop job", "AWS.command.q.transform.viewJobProgress": "View job progress", - "AWS.command.q.transform.viewJobStatus": "View job status", + "AWS.command.q.transform.viewJobHistory": "View job history", "AWS.command.q.transform.showTransformationPlan": "View plan", "AWS.command.q.transform.showChangeSummary": "View summary", "AWS.command.threatComposer.createNew": "Create New Threat Composer File", diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index 7e3e799a046..d92810db34d 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -57,6 +57,7 @@ import { } from '../../../codewhisperer/service/transformByQ/transformFileHandler' import { getAuthType } from '../../../auth/utils' import fs from '../../../shared/fs/fs' +import { setContext } from '../../../shared/vscode/setContext' // These events can be interactions within the chat, // or elsewhere in the IDE @@ -188,6 +189,8 @@ export class GumbyController { } private async transformInitiated(message: any) { + this.messenger.sendViewHistoryMessage(message.tabID) + // silently check for projects eligible for SQL conversion let embeddedSQLProjects: TransformationCandidateProject[] = [] try { @@ -383,6 +386,11 @@ export class GumbyController { case ButtonActions.VIEW_TRANSFORMATION_HUB: await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB, CancelActionPositions.Chat) break + case ButtonActions.VIEW_JOB_HISTORY: + await setContext('gumby.wasQCodeTransformationUsed', true) + await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB) + await vscode.commands.executeCommand(GumbyCommands.FOCUS_JOB_HISTORY, CancelActionPositions.Chat) + break case ButtonActions.VIEW_SUMMARY: await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal') break diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 699e3b77938..fb250657cdf 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -377,6 +377,27 @@ export class Messenger { this.dispatcher.sendChatMessage(jobSubmittedMessage) } + public sendViewHistoryMessage(tabID: string) { + const buttons: ChatItemButton[] = [] + + buttons.push({ + keepCardAfterClick: true, + text: 'Open job history', + id: ButtonActions.VIEW_JOB_HISTORY, + disabled: false, + }) + + const message = new ChatMessage( + { + message: 'View previous transformations run from the IDE', + messageType: 'ai-prompt', + buttons, + }, + tabID + ) + this.dispatcher.sendChatMessage(message) + } + public sendMessage(prompt: string, tabID: string, type: 'prompt' | 'ai-prompt') { this.dispatcher.sendChatMessage( new ChatMessage( diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts index 4df65fe9d1d..2c64a050547 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts @@ -13,6 +13,7 @@ import DependencyVersions from '../../../models/dependencies' export enum ButtonActions { STOP_TRANSFORMATION_JOB = 'gumbyStopTransformationJob', VIEW_TRANSFORMATION_HUB = 'gumbyViewTransformationHub', + VIEW_JOB_HISTORY = 'gumbyViewJobHistory', VIEW_SUMMARY = 'gumbyViewSummary', CONFIRM_LANGUAGE_UPGRADE_TRANSFORMATION_FORM = 'gumbyLanguageUpgradeTransformFormConfirm', CONFIRM_SQL_CONVERSION_TRANSFORMATION_FORM = 'gumbySQLConversionTransformFormConfirm', @@ -33,6 +34,7 @@ export enum GumbyCommands { CLEAR_CHAT = 'aws.awsq.clearchat', START_TRANSFORMATION_FLOW = 'aws.awsq.transform', FOCUS_TRANSFORMATION_HUB = 'aws.amazonq.showTransformationHub', + FOCUS_JOB_HISTORY = 'aws.amazonq.showHistoryInHub', } export default class MessengerUtils { diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 56e54a97a8a..adc7f542918 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -20,6 +20,7 @@ import { TransformationType, TransformationCandidateProject, RegionProfile, + sessionJobHistory, } from '../models/model' import { createZipManifest, @@ -78,6 +79,7 @@ import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { AuthUtil } from '../util/authUtil' +import { homedir } from 'os' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() @@ -739,6 +741,33 @@ export async function postTransformationJob() { if (transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded()) { await vscode.commands.executeCommand('aws.amazonq.transformationHub.reviewChanges.startReview') } + + // store job details and diff path locally (history) + // TODO: ideally when job is cancelled, should be stored as CANCELLED instead of FAILED (remove this if statement after bug is fixed) + if (!transformByQState.isCancelled()) { + const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') + // create transform folder if necessary + if (!fs.existsSync(jobHistoryFilePath)) { + fs.mkdirSync(path.dirname(jobHistoryFilePath), { recursive: true }) + // create headers of new transformation history file + fs.writeFileSync(jobHistoryFilePath, 'date\tproject_name\tstatus\tduration\tdiff_path\tjob_id\n') + } + const latest = sessionJobHistory[transformByQState.getJobId()] + const jobDetails: string = + latest.startTime + + '\t' + + latest.projectName + + '\t' + + latest.status + + '\t' + + latest.duration + + '\t' + + transformByQState.getDiffPatchFilePath() + + '\t' + + transformByQState.getJobId() + + '\n' + fs.writeFileSync(jobHistoryFilePath, jobDetails, { flag: 'a' }) + } } export async function transformationJobErrorHandler(error: any) { diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 7681c34e613..304a7cd802b 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -730,6 +730,7 @@ export class TransformByQState { private planFilePath: string = '' private summaryFilePath: string = '' private preBuildLogFilePath: string = '' + private diffPatchFilePath: string = '' private resultArchiveFilePath: string = '' private projectCopyFilePath: string = '' @@ -881,6 +882,10 @@ export class TransformByQState { return this.summaryFilePath } + public getDiffPatchFilePath() { + return this.diffPatchFilePath + } + public getResultArchiveFilePath() { return this.resultArchiveFilePath } @@ -1055,6 +1060,10 @@ export class TransformByQState { this.summaryFilePath = filePath } + public setDiffPatchFilePath(filePath: string) { + return (this.diffPatchFilePath = filePath) + } + public setResultArchiveFilePath(filePath: string) { this.resultArchiveFilePath = filePath } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 052ef53b56c..35c53a4d250 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -24,6 +24,9 @@ import { startInterval } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' import { AuthUtil } from '../../util/authUtil' +import fs from 'fs' +import path from 'path' +import { homedir } from 'os' export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' @@ -88,6 +91,48 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider } private showJobHistory(): string { + const history: { + startTime: string + projectName: string + status: string + duration: string + diffPath: string + jobId: string + }[] = [] + const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') + if (fs.existsSync(jobHistoryFilePath)) { + const historyFile = fs.readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) + const jobs = historyFile.split('\n') + jobs.shift() // removes headers + if (jobs.length > 0) { + jobs.forEach((job) => { + if (job) { + const jobInfo = job.split('\t') + const jobObject = { + startTime: jobInfo[0], + projectName: jobInfo[1], + status: jobInfo[2], + duration: jobInfo[3], + diffPath: jobInfo[4], + jobId: jobInfo[5], + } + history.push(jobObject) + } + }) + } + } + if (transformByQState.isRunning()) { + const current = sessionJobHistory[transformByQState.getJobId()] + history.push({ + startTime: current.startTime, + projectName: current.projectName, + status: current.status, + duration: current.duration, + diffPath: transformByQState.getDiffPatchFilePath(), + jobId: transformByQState.getJobId(), + }) + } + history.reverse() // to show in descending order, chronologically return ` @@ -99,17 +144,26 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider -

Transformation Status

+

Transformation History

${ - Object.keys(sessionJobHistory).length === 0 + history.length === 0 ? `

${CodeWhispererConstants.nothingToShowMessage}

` - : this.getTableMarkup(sessionJobHistory[transformByQState.getJobId()]) + : this.getTableMarkup(history) } ` } - private getTableMarkup(job: { startTime: string; projectName: string; status: string; duration: string }) { + private getTableMarkup( + history: { + startTime: string + projectName: string + status: string + duration: string + diffPath: string + jobId: string + }[] + ) { return ` @@ -118,17 +172,25 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider - + + - - - - - - - + ${history + .map( + (job) => ` + + + + + + + + + ` + ) + .join('')}
Project Status DurationIdDiff PathJob Id
${job.startTime}${job.projectName}${job.status}${job.duration}${transformByQState.getJobId()}
${job.startTime}${job.projectName}${job.status}${job.duration}${job.diffPath}${job.jobId}
` diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index 0b678f8120d..ab7c4c18b5a 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -426,6 +426,21 @@ export class ProposedTransformationExplorer { let deserializeErrorMessage = undefined let pathContainingArchive = '' patchFiles = [] // reset patchFiles if there was a previous transformation + + // create transform, project, and job folders if needed (to store diff patch) + const jobDiffPath = path.join( + os.homedir(), + '.aws', + 'transform', + transformByQState.getProjectName(), + transformByQState.getJobId(), + 'diff.patch' + ) + if (!fs.existsSync(jobDiffPath)) { + fs.mkdirSync(path.dirname(jobDiffPath), { recursive: true }) + } + transformByQState.setDiffPatchFilePath(jobDiffPath) + try { // Download and deserialize the zip pathContainingArchive = path.dirname(pathToArchive) @@ -433,6 +448,7 @@ export class ProposedTransformationExplorer { zip.extractAllTo(pathContainingArchive) const files = fs.readdirSync(path.join(pathContainingArchive, ExportResultArchiveStructure.PathToPatch)) singlePatchFile = path.join(pathContainingArchive, ExportResultArchiveStructure.PathToPatch, files[0]) + fs.copyFileSync(singlePatchFile, transformByQState.getDiffPatchFilePath()) // store diff patch locally patchFiles.push(singlePatchFile) diffModel.parseDiff(patchFiles[0], transformByQState.getProjectPath()) From c86fa100799d18f944b080e532b10518b69360c3 Mon Sep 17 00:00:00 2001 From: Trisha Godara Date: Wed, 25 Jun 2025 10:47:04 -0700 Subject: [PATCH 2/7] Refresh button original functionalilty (updates status and downloads diff patch if available), also added isWithin30Days util func for future use --- .../transformationHubViewProvider.ts | 169 +++++++++++++++++- packages/core/src/shared/datetime.ts | 22 +++ 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 35c53a4d250..88535eab495 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -14,12 +14,13 @@ import { transformByQState, } from '../../models/model' import { getLogger } from '../../../shared/logger/logger' -import { getTransformationSteps } from './transformApiHandler' +import { getTransformationSteps, downloadAndExtractResultArchive } from './transformApiHandler' import { TransformationSteps, ProgressUpdates, TransformationStatus, } from '../../../codewhisperer/client/codewhispereruserclient' +import { codeWhispererClient } from '../../../codewhisperer/client/codewhisperer' import { startInterval } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' @@ -72,6 +73,14 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider ): void | Thenable { this._view = webviewView + this._view.webview.onDidReceiveMessage((message) => { + if (message.command === 'refreshJob') { + this.refreshJob(message.jobId, message.currentStatus, message.projectName).catch((error) => { + getLogger().error('refreshJob failed: %s', (error as Error).message) + }) + } + }) + this._view.webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri], @@ -150,6 +159,23 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider ? `

${CodeWhispererConstants.nothingToShowMessage}

` : this.getTableMarkup(history) } + ` } @@ -165,6 +191,18 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider }[] ) { return ` + @@ -187,6 +225,17 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider + ` ) @@ -196,6 +245,124 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider ` } + private async refreshJob(jobId: string, currentStatus: string, projectName: string) { + console.log('refreshing job id: %s', jobId) + + // fetch status from server + let status = '' + let duration = '' + try { + const response = await codeWhispererClient.codeModernizerGetCodeTransformation({ + transformationJobId: jobId, + profileArn: undefined, + }) + status = response.transformationJob.status! + if (response.transformationJob.endExecutionTime && response.transformationJob.creationTime) { + duration = convertToTimeString( + response.transformationJob.endExecutionTime.getTime() - + response.transformationJob.creationTime.getTime() + ) + } + // status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl, undefined) + + console.log('status returned: %s', status) + console.log('duration returned: %s', duration) + } catch (error) { + console.error('error fetching status: %s', (error as Error).message) + return + } + + // retrieve artifacts and updated duration if available + let jobDiffPath: string = '' + if ( + CodeWhispererConstants.validStatesForCheckingDownloadUrl.includes(status) && + !CodeWhispererConstants.failureStates.includes(status) + ) { + // status is COMPLETED or PARTIALLY_COMPLETED on sertver side + console.log('valid successful status') + + // artifacts should be available to download + jobDiffPath = await this.retrieveArtifacts(jobId, projectName) + } else { + console.log('no artifacts available') + } + + if (status === currentStatus && !jobDiffPath) { + // no changes, no need to update file/table + return + } + + // update local file and history table + this.updateHistoryFile(status, duration, jobDiffPath, jobId) + } + + private async retrieveArtifacts(jobId: string, projectName: string) { + const resultsPath = path.join(homedir(), '.aws', 'transform', projectName, 'results') + let jobDiffPath = path.join(homedir(), '.aws', 'transform', projectName, jobId, 'diff.patch') + + if (fs.existsSync(jobDiffPath)) { + console.log('diff patch already exists') + jobDiffPath = '' + } else { + try { + await downloadAndExtractResultArchive(jobId, resultsPath) + console.log('artifacts downloaded') + + if (!fs.existsSync(path.dirname(jobDiffPath))) { + fs.mkdirSync(path.dirname(jobDiffPath), { recursive: true }) + } + fs.copyFileSync(path.join(resultsPath, 'patch', 'diff.patch'), jobDiffPath) + } catch (error) { + console.error('error downloading artifacts: %s', (error as Error).message) + jobDiffPath = '' + } finally { + if (fs.existsSync(resultsPath)) { + fs.rmSync(resultsPath, { recursive: true, force: true }) + } + console.log('deleted temporary extraction directory') + } + } + return jobDiffPath + } + + private updateHistoryFile(status: string, duration: string, diffPath: string, jobId: string) { + const history: string[][] = [] + const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') + if (fs.existsSync(jobHistoryFilePath)) { + const historyFile = fs.readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) + const jobs = historyFile.split('\n') + jobs.shift() // removes headers + if (jobs.length > 0) { + jobs.forEach((job) => { + if (job) { + const jobInfo = job.split('\t') + // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], jobId: jobInfo[5] + if (jobInfo[5] === jobId) { + // update any values if applicable + jobInfo[2] = status + if (duration) { + jobInfo[3] = duration + } + if (diffPath) { + jobInfo[4] = diffPath + } + } + history.push(jobInfo) + } + }) + } + } + if (history.length > 0) { + // rewrite file + fs.writeFileSync(jobHistoryFilePath, 'date\tproject_name\tstatus\tduration\tdiff_path\tjob_id\n') + const tsvContent = history.map((row) => row.join('\t')).join('\n') + fs.writeFileSync(jobHistoryFilePath, tsvContent, { flag: 'a' }) + + // update table content + this.updateContent('job history') + } + } + private generateTransformationStepMarkup( name: string, startTime: Date | undefined, diff --git a/packages/core/src/shared/datetime.ts b/packages/core/src/shared/datetime.ts index 6123421666a..8043f94d343 100644 --- a/packages/core/src/shared/datetime.ts +++ b/packages/core/src/shared/datetime.ts @@ -154,3 +154,25 @@ export function formatDateTimestamp(forceUTC: boolean, d: Date = new Date()): st // trim 'Z' (last char of iso string) and add offset string return `${iso.substring(0, iso.length - 1)}${offsetString}` } + +/** + * Checks if a given timestamp is within 30 days of the current day + * @param timeStamp + * @returns true if timeStamp is within 30 days, false otherwise + */ +export function isWithin30Days(timeStamp: string): boolean { + if (!timeStamp) { + return false // No timestamp given + } + + const startDate = new Date(timeStamp) + const currentDate = new Date() + + // Calculate the difference in milliseconds + const timeDifference = currentDate.getTime() - startDate.getTime() + + // Convert milliseconds to days (1000ms * 60s * 60min * 24hr) + const daysDifference = timeDifference / (1000 * 60 * 60 * 24) + + return daysDifference <= 30 +} From 941d18e8c8fe597c06c52d9e669f4c2518523ed2 Mon Sep 17 00:00:00 2001 From: Trisha Godara Date: Wed, 9 Jul 2025 11:49:37 -0700 Subject: [PATCH 3/7] Modify refresh button functionality to handle CSB; store summary and build log; open diff patch and summary directly in VSCode --- .../chat/controller/controller.ts | 27 ++ .../commands/startTransformByQ.ts | 79 ++++-- .../core/src/codewhisperer/models/model.ts | 30 ++- .../transformationHubViewProvider.ts | 245 +++++++++++++++--- .../transformationResultsViewProvider.ts | 29 +-- 5 files changed, 330 insertions(+), 80 deletions(-) diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index d92810db34d..ef977c7cf6e 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -190,6 +190,15 @@ export class GumbyController { private async transformInitiated(message: any) { this.messenger.sendViewHistoryMessage(message.tabID) + if (transformByQState.isRefreshInProgress()) { + transformByQState.setBlockedByRefresh(true) + this.messenger.sendMessage( + 'A job refresh is currently in progress. Please wait for it to complete.', + message.tabID, + 'ai-prompt' + ) + return + } // silently check for projects eligible for SQL conversion let embeddedSQLProjects: TransformationCandidateProject[] = [] @@ -460,6 +469,15 @@ export class GumbyController { } private async handleUserLanguageUpgradeProjectChoice(message: any) { + if (transformByQState.isRefreshInProgress()) { + transformByQState.setBlockedByRefresh(true) + this.messenger.sendMessage( + 'A job refresh is currently in progress. Please wait for it to complete.', + message.tabID, + 'ai-prompt' + ) + return + } await telemetry.codeTransform_submitSelection.run(async () => { const pathToProject: string = message.formSelectedValues['GumbyTransformLanguageUpgradeProjectForm'] const toJDKVersion: JDKVersion = message.formSelectedValues['GumbyTransformJdkToForm'] @@ -492,6 +510,15 @@ export class GumbyController { } private async handleUserSQLConversionProjectSelection(message: any) { + if (transformByQState.isRefreshInProgress()) { + transformByQState.setBlockedByRefresh(true) + this.messenger.sendMessage( + 'A job refresh is currently in progress. Please wait for it to complete.', + message.tabID, + 'ai-prompt' + ) + return + } await telemetry.codeTransform_submitSelection.run(async () => { const pathToProject: string = message.formSelectedValues['GumbyTransformSQLConversionProjectForm'] const schema: string = message.formSelectedValues['GumbyTransformSQLSchemaForm'] diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index adc7f542918..2c268ffb6b3 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -79,7 +79,6 @@ import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { AuthUtil } from '../util/authUtil' -import { homedir } from 'os' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() @@ -476,6 +475,30 @@ export async function startTransformationJob( codeTransformRunTimeLatency: calculateTotalLatency(transformStartTime), }) }) + + // create local history folder(s) and store metadata + const jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', transformByQState.getProjectName(), jobId) + if (!fs.existsSync(jobHistoryPath)) { + fs.mkdirSync(jobHistoryPath, { recursive: true }) + } + transformByQState.setJobHistoryPath(jobHistoryPath) + // save a copy of the upload zip + fs.copyFileSync(transformByQState.getPayloadFilePath(), path.join(jobHistoryPath, 'zipped-code.zip')) + + const fields = [ + jobId, + transformByQState.getTransformationType(), + transformByQState.getSourceJDKVersion(), + transformByQState.getTargetJDKVersion(), + transformByQState.getCustomDependencyVersionFilePath(), + transformByQState.getCustomBuildCommand(), + transformByQState.getTargetJavaHome(), + transformByQState.getProjectPath(), + transformByQState.getStartTime(), + ] + + const jobDetails = fields.join('\t') + fs.writeFileSync(path.join(jobHistoryPath, 'metadata.txt'), jobDetails) } catch (error) { getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToStartJobNotification}`, error) const errorMessage = (error as Error).message.toLowerCase() @@ -726,9 +749,19 @@ export async function postTransformationJob() { }) } - if (transformByQState.getPayloadFilePath()) { - // delete original upload ZIP at very end of transformation - fs.rmSync(transformByQState.getPayloadFilePath(), { force: true }) + if ( + transformByQState.isSucceeded() || + transformByQState.isPartiallySucceeded() || + transformByQState.isCancelled() + ) { + if (transformByQState.getPayloadFilePath()) { + // delete original upload ZIP at very end of transformation + fs.rmSync(transformByQState.getPayloadFilePath(), { force: true }) + // delete the copy of the upload ZIP + fs.rmSync(path.join(transformByQState.getJobHistoryPath(), 'zipped-code.zip'), { force: true }) + } + // delete transformation job metadata file (no longer needed) + fs.rmSync(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'), { force: true }) } // delete temporary build logs file const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') @@ -745,28 +778,30 @@ export async function postTransformationJob() { // store job details and diff path locally (history) // TODO: ideally when job is cancelled, should be stored as CANCELLED instead of FAILED (remove this if statement after bug is fixed) if (!transformByQState.isCancelled()) { - const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') + const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation-history.tsv') // create transform folder if necessary - if (!fs.existsSync(jobHistoryFilePath)) { - fs.mkdirSync(path.dirname(jobHistoryFilePath), { recursive: true }) + if (!fs.existsSync(historyLogFilePath)) { + fs.mkdirSync(path.dirname(historyLogFilePath), { recursive: true }) // create headers of new transformation history file - fs.writeFileSync(jobHistoryFilePath, 'date\tproject_name\tstatus\tduration\tdiff_path\tjob_id\n') + fs.writeFileSync(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') } const latest = sessionJobHistory[transformByQState.getJobId()] - const jobDetails: string = - latest.startTime + - '\t' + - latest.projectName + - '\t' + - latest.status + - '\t' + - latest.duration + - '\t' + - transformByQState.getDiffPatchFilePath() + - '\t' + - transformByQState.getJobId() + - '\n' - fs.writeFileSync(jobHistoryFilePath, jobDetails, { flag: 'a' }) + const fields = [ + latest.startTime, + latest.projectName, + latest.status, + latest.duration, + transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded() + ? path.join(transformByQState.getJobHistoryPath(), 'diff.patch') + : '', + transformByQState.isSucceeded() || transformByQState.isPartiallySucceeded() + ? path.join(transformByQState.getJobHistoryPath(), 'summary', 'summary.md') + : '', + transformByQState.getJobId(), + ] + + const jobDetails = fields.join('\t') + '\n' + fs.writeFileSync(historyLogFilePath, jobDetails, { flag: 'a' }) } } diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 304a7cd802b..5cebc22ec3b 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -730,7 +730,7 @@ export class TransformByQState { private planFilePath: string = '' private summaryFilePath: string = '' private preBuildLogFilePath: string = '' - private diffPatchFilePath: string = '' + private jobHistoryPath: string = '' private resultArchiveFilePath: string = '' private projectCopyFilePath: string = '' @@ -762,6 +762,9 @@ export class TransformByQState { private intervalId: NodeJS.Timeout | undefined = undefined + private refreshInProgress: boolean = false + private blockedByRefresh: boolean = false + public isNotStarted() { return this.transformByQState === TransformByQStatus.NotStarted } @@ -786,6 +789,14 @@ export class TransformByQState { return this.transformByQState === TransformByQStatus.PartiallySucceeded } + public isRefreshInProgress() { + return this.refreshInProgress + } + + public wasBlockedByRefresh() { + return this.blockedByRefresh + } + public getHasSeenTransforming() { return this.hasSeenTransforming } @@ -882,8 +893,8 @@ export class TransformByQState { return this.summaryFilePath } - public getDiffPatchFilePath() { - return this.diffPatchFilePath + public getJobHistoryPath() { + return this.jobHistoryPath } public getResultArchiveFilePath() { @@ -980,6 +991,14 @@ export class TransformByQState { this.transformByQState = TransformByQStatus.PartiallySucceeded } + public setRefreshInProgress(inProgress: boolean) { + this.refreshInProgress = inProgress + } + + public setBlockedByRefresh(blocked: boolean): void { + this.blockedByRefresh = blocked + } + public setHasSeenTransforming(hasSeen: boolean) { this.hasSeenTransforming = hasSeen } @@ -1060,8 +1079,8 @@ export class TransformByQState { this.summaryFilePath = filePath } - public setDiffPatchFilePath(filePath: string) { - return (this.diffPatchFilePath = filePath) + public setJobHistoryPath(filePath: string) { + this.jobHistoryPath = filePath } public setResultArchiveFilePath(filePath: string) { @@ -1130,6 +1149,7 @@ export class TransformByQState { public setJobDefaults() { this.setToNotStarted() + this.refreshInProgress = false this.hasSeenTransforming = false this.jobFailureErrorNotification = undefined this.jobFailureErrorChatMessage = undefined diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index 88535eab495..d62d095b7fa 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode' import globals from '../../../shared/extensionGlobals' import * as CodeWhispererConstants from '../../models/constants' import { + JDKVersion, StepProgress, TransformationType, jobPlanProgress, @@ -21,13 +22,14 @@ import { TransformationStatus, } from '../../../codewhisperer/client/codewhispereruserclient' import { codeWhispererClient } from '../../../codewhisperer/client/codewhisperer' -import { startInterval } from '../../commands/startTransformByQ' +import { startInterval, pollTransformationStatusUntilComplete } from '../../commands/startTransformByQ' import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState' import { convertToTimeString } from '../../../shared/datetime' import { AuthUtil } from '../../util/authUtil' import fs from 'fs' import path from 'path' -import { homedir } from 'os' +import os from 'os' +import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' @@ -74,10 +76,16 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider this._view = webviewView this._view.webview.onDidReceiveMessage((message) => { - if (message.command === 'refreshJob') { - this.refreshJob(message.jobId, message.currentStatus, message.projectName).catch((error) => { - getLogger().error('refreshJob failed: %s', (error as Error).message) - }) + switch (message.command) { + case 'refreshJob': + this.refreshJob(message.jobId, message.currentStatus, message.projectName) + break + case 'openSummaryPreview': + vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(message.filePath)) + break + case 'openDiffFile': + vscode.commands.executeCommand('vscode.open', vscode.Uri.file(message.filePath)) + break } }) @@ -106,9 +114,10 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider status: string duration: string diffPath: string + summaryPath: string jobId: string }[] = [] - const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') + const jobHistoryFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation-history.tsv') if (fs.existsSync(jobHistoryFilePath)) { const historyFile = fs.readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) const jobs = historyFile.split('\n') @@ -123,7 +132,8 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], - jobId: jobInfo[5], + summaryPath: jobInfo[5], + jobId: jobInfo[6], } history.push(jobObject) } @@ -137,7 +147,8 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider projectName: current.projectName, status: current.status, duration: current.duration, - diffPath: transformByQState.getDiffPatchFilePath(), + diffPath: '', + summaryPath: '', jobId: transformByQState.getJobId(), }) } @@ -154,6 +165,10 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider

Transformation History

+

This table lists the jobs that you have run in the past 30 days. + To open the diff patch and summary files, choose the provided links. To get an updated job status, choose the refresh icon. + The diff path and summary will appear once they are available. +

${ history.length === 0 ? `

${CodeWhispererConstants.nothingToShowMessage}

` @@ -174,6 +189,24 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider currentStatus: status }); } + + if (event.target.classList.contains('summary-link')) { + event.preventDefault(); + const summaryPath = event.target.getAttribute('summary-path'); + vscode.postMessage({ + command: 'openSummaryPreview', + filePath: summaryPath + }); + } + + if (event.target.classList.contains('diff-link')) { + event.preventDefault(); + const diffPath = event.target.getAttribute('diff-path'); + vscode.postMessage({ + command: 'openDiffFile', + filePath: diffPath + }); + } }); @@ -187,6 +220,7 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider status: string duration: string diffPath: string + summaryPath: string jobId: string }[] ) { @@ -202,6 +236,9 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider opacity: 0.3; cursor: not-allowed; } + td:last-child { + text-align: center; + }
${job.duration} ${job.diffPath} ${job.jobId} + +
@@ -210,8 +247,10 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider - + + + @@ -223,7 +262,8 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider - + +
Project Status DurationDiff PathDiff PatchSummary File Job IdRefresh Job
${job.projectName} ${job.status} ${job.duration}${job.diffPath}${job.diffPath ? `diff.patch` : ''}${job.summaryPath ? `summary.md` : ''} ${job.jobId} @@ -263,7 +313,6 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider response.transformationJob.creationTime.getTime() ) } - // status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl, undefined) console.log('status returned: %s', status) console.log('duration returned: %s', duration) @@ -273,48 +322,167 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider } // retrieve artifacts and updated duration if available - let jobDiffPath: string = '' + let jobHistoryPath: string = '' if ( CodeWhispererConstants.validStatesForCheckingDownloadUrl.includes(status) && !CodeWhispererConstants.failureStates.includes(status) ) { - // status is COMPLETED or PARTIALLY_COMPLETED on sertver side + // status is COMPLETED or PARTIALLY_COMPLETED on server side console.log('valid successful status') // artifacts should be available to download - jobDiffPath = await this.retrieveArtifacts(jobId, projectName) + jobHistoryPath = await this.retrieveArtifacts(jobId, projectName) + + // delete metadata file, if it exists + fs.rmSync(path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'metadata.txt'), { force: true }) + } else if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) { + // still in progress on server side + console.log('job in progress on BE') + if (transformByQState.isRunning()) { + console.log('there is a job currently running; cannot resume polling BE job') + return + } + transformByQState.setRefreshInProgress(true) + const messenger = transformByQState.getChatMessenger() // to send message to chat when refresh completes + + // set state to prepare to resume job + transformByQState.setJobId(jobId) + transformByQState.setPolledJobStatus(status) + + try { + transformByQState.setJobHistoryPath(path.join(os.homedir(), '.aws', 'transform', projectName, jobId)) + const metadataFile = fs.readFileSync(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'), { + encoding: 'utf8', + flag: 'r', + }) + const metadata = metadataFile.split('\t') + transformByQState.setTransformationType(metadata[1] as TransformationType) + transformByQState.setSourceJDKVersion(metadata[2] as JDKVersion) + transformByQState.setTargetJDKVersion(metadata[3] as JDKVersion) + transformByQState.setCustomDependencyVersionFilePath(metadata[4]) + transformByQState.setPayloadFilePath( + path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'zipped-code.zip') + ) + transformByQState.setMavenName('mvn') + transformByQState.setCustomBuildCommand(metadata[5]) + transformByQState.setTargetJavaHome(metadata[6]) + transformByQState.setProjectPath(metadata[7]) + transformByQState.setStartTime(metadata[8]) + } catch (e: any) { + console.warn('no metadata file, not enough info to poll') + console.error('error: %s', (e as Error).message) + transformByQState.setJobDefaults() + if (messenger && transformByQState.wasBlockedByRefresh()) { + transformByQState.setBlockedByRefresh(false) + messenger.sendJobFinishedMessage( + ChatSessionManager.Instance.getSession().tabID!, + "Sorry, I couldn't refresh the job. Please try again or start a new transformation." + ) + void vscode.window.showErrorMessage(`There was an error refreshing this job. Job Id: ${jobId}`) + } else { + // just show notification + void vscode.window.showErrorMessage(`There was an error refreshing this job. Job Id: ${jobId}`) + } + return + } + + // resume polling job + try { + this.updateContent('job history') // refreshing the table disables all jobs' refresh buttons while this one is polling + status = await pollTransformationStatusUntilComplete( + jobId, + AuthUtil.instance.regionProfileManager.activeRegionProfile + ) + if ( + CodeWhispererConstants.validStatesForCheckingDownloadUrl.includes(status) && + !CodeWhispererConstants.failureStates.includes(status) + ) { + duration = convertToTimeString( + new Date().getTime() - new Date(transformByQState.getStartTime()).getTime() + ) + jobHistoryPath = await this.retrieveArtifacts(jobId, projectName) + // delete payload and metadata files + if (transformByQState.getPayloadFilePath()) { + fs.rmSync(transformByQState.getPayloadFilePath(), { force: true }) + } + fs.rmSync(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'), { force: true }) + // delete temporary build logs file + const logFilePath = path.join(os.tmpdir(), 'build-logs.txt') + if (fs.existsSync(logFilePath)) { + fs.rmSync(logFilePath, { force: true }) + } + } + } catch (e: any) { + console.error('error resuming job %s: %s', jobId, (e as Error).message) + transformByQState.setJobDefaults() + if (messenger && transformByQState.wasBlockedByRefresh()) { + transformByQState.setBlockedByRefresh(false) + messenger.sendJobFinishedMessage( + ChatSessionManager.Instance.getSession().tabID!, + "Sorry, I couldn't refresh the job. Please try again or start a new transformation." + ) + void vscode.window.showErrorMessage(`There was an error refreshing this job. Job Id: ${jobId}`) + } else { + // just show notification + void vscode.window.showErrorMessage(`There was an error refreshing this job. Job Id: ${jobId}`) + } + return + } + + // reset state + transformByQState.setJobDefaults() + if (messenger && transformByQState.wasBlockedByRefresh()) { + transformByQState.setBlockedByRefresh(false) + messenger.sendJobFinishedMessage( + ChatSessionManager.Instance.getSession().tabID!, + 'Job refresh completed. Please see the transformation history table for the updated status and artifacts.' + ) + void vscode.window.showInformationMessage(`Job refresh completed. (Job Id: ${jobId})`) + } else { + // just show notification + void vscode.window.showInformationMessage(`Job refresh completed. (Job Id: ${jobId})`) + } } else { + // FAILED or STOPPED job console.log('no artifacts available') } - if (status === currentStatus && !jobDiffPath) { + if (status === currentStatus && !jobHistoryPath) { // no changes, no need to update file/table return } // update local file and history table - this.updateHistoryFile(status, duration, jobDiffPath, jobId) + this.updateHistoryFile(status, duration, jobHistoryPath, jobId) } private async retrieveArtifacts(jobId: string, projectName: string) { - const resultsPath = path.join(homedir(), '.aws', 'transform', projectName, 'results') - let jobDiffPath = path.join(homedir(), '.aws', 'transform', projectName, jobId, 'diff.patch') + const resultsPath = path.join(os.homedir(), '.aws', 'transform', projectName, 'results') // temporary directory for extraction + let jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', projectName, jobId) - if (fs.existsSync(jobDiffPath)) { + if (fs.existsSync(path.join(jobHistoryPath, 'diff.patch'))) { console.log('diff patch already exists') - jobDiffPath = '' + jobHistoryPath = '' } else { try { await downloadAndExtractResultArchive(jobId, resultsPath) console.log('artifacts downloaded') - if (!fs.existsSync(path.dirname(jobDiffPath))) { - fs.mkdirSync(path.dirname(jobDiffPath), { recursive: true }) + if (!fs.existsSync(path.join(jobHistoryPath, 'summary'))) { + fs.mkdirSync(path.join(jobHistoryPath, 'summary'), { recursive: true }) } - fs.copyFileSync(path.join(resultsPath, 'patch', 'diff.patch'), jobDiffPath) + fs.copyFileSync(path.join(resultsPath, 'patch', 'diff.patch'), path.join(jobHistoryPath, 'diff.patch')) + fs.copyFileSync( + path.join(resultsPath, 'summary', 'summary.md'), + path.join(jobHistoryPath, 'summary', 'summary.md') + ) + fs.copyFileSync( + path.join(resultsPath, 'summary', 'buildCommandOutput.log'), + path.join(jobHistoryPath, 'summary', 'buildCommandOutput.log') + ) } catch (error) { console.error('error downloading artifacts: %s', (error as Error).message) - jobDiffPath = '' + jobHistoryPath = '' } finally { if (fs.existsSync(resultsPath)) { fs.rmSync(resultsPath, { recursive: true, force: true }) @@ -322,29 +490,30 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider console.log('deleted temporary extraction directory') } } - return jobDiffPath + return jobHistoryPath } - private updateHistoryFile(status: string, duration: string, diffPath: string, jobId: string) { + private updateHistoryFile(status: string, duration: string, jobHistoryPath: string, jobId: string) { const history: string[][] = [] - const jobHistoryFilePath = path.join(homedir(), '.aws', 'transform', 'transformation-history.tsv') - if (fs.existsSync(jobHistoryFilePath)) { - const historyFile = fs.readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) + const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation-history.tsv') + if (fs.existsSync(historyLogFilePath)) { + const historyFile = fs.readFileSync(historyLogFilePath, { encoding: 'utf8', flag: 'r' }) const jobs = historyFile.split('\n') jobs.shift() // removes headers if (jobs.length > 0) { jobs.forEach((job) => { if (job) { const jobInfo = job.split('\t') - // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], jobId: jobInfo[5] - if (jobInfo[5] === jobId) { + // startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6] + if (jobInfo[6] === jobId) { // update any values if applicable jobInfo[2] = status if (duration) { jobInfo[3] = duration } - if (diffPath) { - jobInfo[4] = diffPath + if (jobHistoryPath) { + jobInfo[4] = path.join(jobHistoryPath, 'diff.patch') + jobInfo[5] = path.join(jobHistoryPath, 'summary', 'summary.md') } } history.push(jobInfo) @@ -354,9 +523,9 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider } if (history.length > 0) { // rewrite file - fs.writeFileSync(jobHistoryFilePath, 'date\tproject_name\tstatus\tduration\tdiff_path\tjob_id\n') - const tsvContent = history.map((row) => row.join('\t')).join('\n') - fs.writeFileSync(jobHistoryFilePath, tsvContent, { flag: 'a' }) + fs.writeFileSync(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n') + const tsvContent = history.map((row) => row.join('\t')).join('\n') + '\n' + fs.writeFileSync(historyLogFilePath, tsvContent, { flag: 'a' }) // update table content this.updateContent('job history') diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index ab7c4c18b5a..311283ed13c 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -427,20 +427,6 @@ export class ProposedTransformationExplorer { let pathContainingArchive = '' patchFiles = [] // reset patchFiles if there was a previous transformation - // create transform, project, and job folders if needed (to store diff patch) - const jobDiffPath = path.join( - os.homedir(), - '.aws', - 'transform', - transformByQState.getProjectName(), - transformByQState.getJobId(), - 'diff.patch' - ) - if (!fs.existsSync(jobDiffPath)) { - fs.mkdirSync(path.dirname(jobDiffPath), { recursive: true }) - } - transformByQState.setDiffPatchFilePath(jobDiffPath) - try { // Download and deserialize the zip pathContainingArchive = path.dirname(pathToArchive) @@ -448,7 +434,7 @@ export class ProposedTransformationExplorer { zip.extractAllTo(pathContainingArchive) const files = fs.readdirSync(path.join(pathContainingArchive, ExportResultArchiveStructure.PathToPatch)) singlePatchFile = path.join(pathContainingArchive, ExportResultArchiveStructure.PathToPatch, files[0]) - fs.copyFileSync(singlePatchFile, transformByQState.getDiffPatchFilePath()) // store diff patch locally + fs.copyFileSync(singlePatchFile, path.join(transformByQState.getJobHistoryPath(), 'diff.patch')) // store diff patch locally patchFiles.push(singlePatchFile) diffModel.parseDiff(patchFiles[0], transformByQState.getProjectPath()) @@ -457,6 +443,19 @@ export class ProposedTransformationExplorer { transformByQState.setSummaryFilePath( path.join(pathContainingArchive, ExportResultArchiveStructure.PathToSummary) ) + // store summary and build log locally for history + if (!fs.existsSync(path.join(transformByQState.getJobHistoryPath(), 'summary'))) { + fs.mkdirSync(path.join(transformByQState.getJobHistoryPath(), 'summary')) + } + fs.copyFileSync( + transformByQState.getSummaryFilePath(), + path.join(transformByQState.getJobHistoryPath(), 'summary', 'summary.md') + ) + fs.copyFileSync( + path.join(path.dirname(transformByQState.getSummaryFilePath()), 'buildCommandOutput.log'), + path.join(transformByQState.getJobHistoryPath(), 'summary', 'buildCommandOutput.log') + ) + transformByQState.setResultArchiveFilePath(pathContainingArchive) await setContext('gumby.isSummaryAvailable', true) From 0bb9102eff5b893e038bfb6c333b0b75bd29eec0 Mon Sep 17 00:00:00 2001 From: Trisha Godara Date: Mon, 14 Jul 2025 12:54:11 -0700 Subject: [PATCH 4/7] Change job history table file reading logic - store array, read file less often, limit table to 10 rows and validate dates --- packages/core/src/amazonqGumby/activation.ts | 2 +- .../commands/startTransformByQ.ts | 2 + .../transformByQ/transformFileHandler.ts | 30 ++++ .../transformationHubViewProvider.ts | 132 ++++++++---------- 4 files changed, 89 insertions(+), 77 deletions(-) diff --git a/packages/core/src/amazonqGumby/activation.ts b/packages/core/src/amazonqGumby/activation.ts index 74823f6fbc6..8fe87c55220 100644 --- a/packages/core/src/amazonqGumby/activation.ts +++ b/packages/core/src/amazonqGumby/activation.ts @@ -21,7 +21,7 @@ import { setContext } from '../shared/vscode/setContext' export async function activate(context: ExtContext) { void setContext('gumby.wasQCodeTransformationUsed', false) - const transformationHubViewProvider = new TransformationHubViewProvider() + const transformationHubViewProvider = TransformationHubViewProvider.instance new ProposedTransformationExplorer(context.extensionContext) // Register an activation event listener to determine when the IDE opens, closes or users // select to open a new workspace diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 2c268ffb6b3..7c1df5e743d 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -79,6 +79,7 @@ import { convertDateToTimestamp } from '../../shared/datetime' import { findStringInDirectory } from '../../shared/utilities/workspaceUtils' import { makeTemporaryToolkitFolder } from '../../shared/filesystemUtilities' import { AuthUtil } from '../util/authUtil' +import { updateHistoryTable } from '../service/transformByQ/transformationHubViewProvider' export function getFeedbackCommentData() { const jobId = transformByQState.getJobId() @@ -802,6 +803,7 @@ export async function postTransformationJob() { const jobDetails = fields.join('\t') + '\n' fs.writeFileSync(historyLogFilePath, jobDetails, { flag: 'a' }) + await updateHistoryTable(true) } } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts index 2ec6fdb7c37..e6b3eb9666c 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformFileHandler.ts @@ -17,6 +17,8 @@ import { AbsolutePathDetectedError } from '../../../amazonqGumby/errors' import { getLogger } from '../../../shared/logger/logger' import AdmZip from 'adm-zip' import { IManifestFile } from './humanInTheLoopManager' +import { HistoryObject } from './transformationHubViewProvider' +import { isWithin30Days } from '../../../shared/datetime' export async function getDependenciesFolderInfo(): Promise { const dependencyFolderName = `${CodeWhispererConstants.dependencyFolderName}${globals.clock.Date.now()}` @@ -182,6 +184,34 @@ export async function openBuildLogFile() { await vscode.window.showTextDocument(doc) } +export function readHistoryFile(): HistoryObject[] { + const history: HistoryObject[] = [] + const jobHistoryFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation-history.tsv') + if (existsSync(jobHistoryFilePath)) { + const historyFile = readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) + const jobs = historyFile.split('\n') + jobs.shift() // removes headers + + // Process from end, stop at 10 valid entries + for (let i = jobs.length - 1; i >= 0 && history.length < 10; i--) { + const job = jobs[i] + if (job && isWithin30Days(job.split('\t')[0])) { + const jobInfo = job.split('\t') + history.push({ + startTime: jobInfo[0], + projectName: jobInfo[1], + status: jobInfo[2], + duration: jobInfo[3], + diffPath: jobInfo[4], + summaryPath: jobInfo[5], + jobId: jobInfo[6], + }) + } + } + } + return history +} + export async function createPomCopy( dirname: string, pomFileVirtualFileReference: vscode.Uri, diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts index d62d095b7fa..279ac97a415 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationHubViewProvider.ts @@ -30,20 +30,40 @@ import fs from 'fs' import path from 'path' import os from 'os' import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' +import { setMaven, readHistoryFile } from './transformFileHandler' + +export interface HistoryObject { + startTime: string + projectName: string + status: string + duration: string + diffPath: string + summaryPath: string + jobId: string +} + +export async function updateHistoryTable(historyFileUpdated?: boolean) { + await TransformationHubViewProvider.instance.updateContent('job history', undefined, historyFileUpdated) +} export class TransformationHubViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'aws.amazonq.transformationHub' private _view?: vscode.WebviewView private lastClickedButton: string = '' private _extensionUri: vscode.Uri = globals.context.extensionUri + private transformationHistory: HistoryObject[] = [] constructor() {} static #instance: TransformationHubViewProvider public async updateContent( button: 'job history' | 'plan progress', - startTime: number = CodeTransformTelemetryState.instance.getStartTime() + startTime: number = CodeTransformTelemetryState.instance.getStartTime(), + historyFileUpdated?: boolean ) { this.lastClickedButton = button + if (historyFileUpdated) { + this.transformationHistory = readHistoryFile() + } if (this._view) { if (this.lastClickedButton === 'job history') { clearInterval(transformByQState.getIntervalId()) @@ -94,6 +114,7 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider localResourceRoots: [this._extensionUri], } + this.transformationHistory = readHistoryFile() if (this.lastClickedButton === 'job history') { this._view!.webview.html = this.showJobHistory() } else { @@ -108,41 +129,10 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider } private showJobHistory(): string { - const history: { - startTime: string - projectName: string - status: string - duration: string - diffPath: string - summaryPath: string - jobId: string - }[] = [] - const jobHistoryFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation-history.tsv') - if (fs.existsSync(jobHistoryFilePath)) { - const historyFile = fs.readFileSync(jobHistoryFilePath, { encoding: 'utf8', flag: 'r' }) - const jobs = historyFile.split('\n') - jobs.shift() // removes headers - if (jobs.length > 0) { - jobs.forEach((job) => { - if (job) { - const jobInfo = job.split('\t') - const jobObject = { - startTime: jobInfo[0], - projectName: jobInfo[1], - status: jobInfo[2], - duration: jobInfo[3], - diffPath: jobInfo[4], - summaryPath: jobInfo[5], - jobId: jobInfo[6], - } - history.push(jobObject) - } - }) - } - } + const jobsToDisplay: HistoryObject[] = [...this.transformationHistory] if (transformByQState.isRunning()) { const current = sessionJobHistory[transformByQState.getJobId()] - history.push({ + jobsToDisplay.unshift({ startTime: current.startTime, projectName: current.projectName, status: current.status, @@ -152,7 +142,6 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider jobId: transformByQState.getJobId(), }) } - history.reverse() // to show in descending order, chronologically return ` @@ -165,14 +154,14 @@ export class TransformationHubViewProvider implements vscode.WebviewViewProvider

Transformation History

-

This table lists the jobs that you have run in the past 30 days. +

This table lists the most recent jobs that you have run in the past 30 days. To open the diff patch and summary files, choose the provided links. To get an updated job status, choose the refresh icon. The diff path and summary will appear once they are available.

${ - history.length === 0 + jobsToDisplay.length === 0 ? `

${CodeWhispererConstants.nothingToShowMessage}

` - : this.getTableMarkup(history) + : this.getTableMarkup(jobsToDisplay) }