From fd656bbc03b889c7cc179f541b2d6b72d0416b8b Mon Sep 17 00:00:00 2001 From: Diler Zaza Date: Mon, 14 Jul 2025 12:07:32 -0700 Subject: [PATCH] Supporting express executions --- packages/core/package.nls.json | 8 ++ .../core/src/shared/clients/lambdaClient.ts | 12 ++ packages/core/src/stepFunctions/activation.ts | 17 +-- .../commands/viewExecutionDetails.ts | 39 +++++++ .../executionDetailProvider.ts | 15 ++- .../executionDetails/handleMessage.ts | 1 + .../messageHandlers/stepFunctionApiHandler.ts | 10 ++ .../stepFunctions/messageHandlers/types.ts | 9 ++ packages/core/src/stepFunctions/utils.ts | 15 +++ .../wizards/viewExecutionDetailsWizard.ts | 109 ++++++++++++++++++ .../apiHandler/stepFunctionApiHandler.test.ts | 4 + 11 files changed, 219 insertions(+), 20 deletions(-) create mode 100644 packages/core/src/stepFunctions/commands/viewExecutionDetails.ts create mode 100644 packages/core/src/stepFunctions/wizards/viewExecutionDetailsWizard.ts diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 4bef50bcb2d..75bb7fe5672 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -32,6 +32,14 @@ "AWS.stepFunctions.executeStateMachine.info.executing": "Starting execution of '{0}' in {1}...", "AWS.stepFunctions.executeStateMachine.info.started": "Execution started", "AWS.stepFunctions.executeStateMachine.error.failedToStart": "There was an error starting execution for '{0}', check AWS Toolkit logs for more information.", + "AWS.stepFunctions.viewExecutionDetails.executionArn.validation.empty": "Execution ARN cannot be empty", + "AWS.stepFunctions.viewExecutionDetails.executionArn.validation.invalid": "Invalid ARN format. Please provide a valid Step Functions execution ARN", + "AWS.stepFunctions.viewExecutionDetails.executionArn.title": "Enter Execution ARN", + "AWS.stepFunctions.viewExecutionDetails.startTime.validation.empty": "Start time cannot be empty for express executions", + "AWS.stepFunctions.viewExecutionDetails.startTime.validation.invalid": "Invalid time format. Use Unix timestamp or ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)", + "AWS.stepFunctions.viewExecutionDetails.startTime.title": "Enter Start Time", + "AWS.stepFunctions.viewExecutionDetails.startTime.placeholder": "Start time of the express execution (e.g., 2023-12-01T10:00:00.000Z)", + "AWS.stepFunctions.viewExecutionDetails.error.general": "Failed to view execution details", "AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files", "AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", "AWS.stepFunctions.workflowStudio.actions.progressMessage": "Opening asl file in Workflow Studio", diff --git a/packages/core/src/shared/clients/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index 331564521ee..9dac95eee50 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -120,6 +120,18 @@ export class DefaultLambdaClient { } } + public async getFunctionConfiguration(name: string): Promise { + const client = await this.createSdkClient() + try { + const request = client.getFunctionConfiguration({ FunctionName: name }) + const response = await request.promise() + return response + } catch (e) { + getLogger().error('Failed to get function configuration: %s', e) + throw e + } + } + private async createSdkClient(): Promise { return await globals.sdkClientBuilder.createAwsService( Lambda, diff --git a/packages/core/src/stepFunctions/activation.ts b/packages/core/src/stepFunctions/activation.ts index a70af265852..2f4c72c56cb 100644 --- a/packages/core/src/stepFunctions/activation.ts +++ b/packages/core/src/stepFunctions/activation.ts @@ -12,6 +12,7 @@ import * as vscode from 'vscode' import { AwsContext } from '../shared/awsContext' import { createStateMachineFromTemplate } from './commands/createStateMachineFromTemplate' import { publishStateMachine } from './commands/publishStateMachine' +import { viewExecutionDetails } from './commands/viewExecutionDetails' import { Commands } from '../shared/vscode/commands2' import { ASL_FORMATS, YAML_ASL, JSON_ASL } from './constants/aslFormats' @@ -23,8 +24,6 @@ import { ASLLanguageClient } from './asl/client' import { WorkflowStudioEditorProvider } from './workflowStudio/workflowStudioEditorProvider' import { StateMachineNode } from './explorer/stepFunctionsNodes' import { downloadStateMachineDefinition } from './commands/downloadStateMachineDefinition' -import { ExecutionDetailProvider } from './executionDetails/executionDetailProvider' -import { validate } from '@aws-sdk/util-arn-parser' /** * Activate Step Functions related functionality for the extension. @@ -101,19 +100,7 @@ async function registerStepFunctionCommands( await publishStateMachine({ awsContext: awsContext, outputChannel: outputChannel, region: region }) }), Commands.register('aws.stepfunctions.viewExecutionDetailsByExecutionARN', async () => { - const arn = await vscode.window.showInputBox({ - prompt: 'Enter Execution ARN', - placeHolder: - 'arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:12345678-1234-1234-1234-123456789012', - }) - - if (validate(arn)) { - await ExecutionDetailProvider.openExecutionDetails(arn!) - } else { - void vscode.window.showErrorMessage( - 'Invalid ARN format. Please provide a valid Step Functions execution ARN (e.g., arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:12345678-1234-1234-1234-123456789012)' - ) - } + await viewExecutionDetails({ awsContext: awsContext, outputChannel: outputChannel }) }) ) } diff --git a/packages/core/src/stepFunctions/commands/viewExecutionDetails.ts b/packages/core/src/stepFunctions/commands/viewExecutionDetails.ts new file mode 100644 index 00000000000..2de9641ad45 --- /dev/null +++ b/packages/core/src/stepFunctions/commands/viewExecutionDetails.ts @@ -0,0 +1,39 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import * as nls from 'vscode-nls' +import { AwsContext } from '../../shared/awsContext' +import { ExecutionDetailProvider } from '../executionDetails/executionDetailProvider' +import { ViewExecutionDetailsWizard } from '../wizards/viewExecutionDetailsWizard' + +const localize = nls.loadMessageBundle() + +interface ViewExecutionDetailsParams { + awsContext: AwsContext + outputChannel: vscode.OutputChannel +} + +export async function viewExecutionDetails(params: ViewExecutionDetailsParams): Promise { + try { + const wizard = new ViewExecutionDetailsWizard() + const wizardResponse = await wizard.run() + + if (wizardResponse) { + const { executionArn, startTime } = wizardResponse + + await ExecutionDetailProvider.openExecutionDetails(executionArn, startTime) + } + } catch (error) { + const errorMessage = localize( + 'AWS.stepFunctions.viewExecutionDetails.error.general', + 'Failed to view execution details' + ) + + params.outputChannel.appendLine('') + params.outputChannel.appendLine(errorMessage) + params.outputChannel.show() + } +} diff --git a/packages/core/src/stepFunctions/executionDetails/executionDetailProvider.ts b/packages/core/src/stepFunctions/executionDetails/executionDetailProvider.ts index 7a275f177fb..29710c5357f 100644 --- a/packages/core/src/stepFunctions/executionDetails/executionDetailProvider.ts +++ b/packages/core/src/stepFunctions/executionDetails/executionDetailProvider.ts @@ -28,6 +28,7 @@ export class ExecutionDetailProvider { */ public static async openExecutionDetails( executionArn: string, + startTime?: string, params?: vscode.WebviewPanelOptions & vscode.WebviewOptions ): Promise { // Create and show the webview panel @@ -43,7 +44,7 @@ export class ExecutionDetailProvider { ) // Create the provider and initialize the panel const provider = new ExecutionDetailProvider() - await provider.initializePanel(panel, executionArn) + await provider.initializePanel(panel, executionArn, startTime) } protected webviewHtml: string @@ -67,7 +68,7 @@ export class ExecutionDetailProvider { * Gets the webview content for Execution Details. * @private */ - private getWebviewContent = async (executionArn: string): Promise => { + private getWebviewContent = async (executionArn: string, startTime?: string): Promise => { const htmlFileSplit = this.webviewHtml.split('') // Set asset source to CDN @@ -85,7 +86,10 @@ export class ExecutionDetailProvider { const componentTypeTag = `` const executionArnTag = `` - return `${htmlFileSplit[0]} ${baseTag} ${localeTag} ${darkModeTag} ${componentTypeTag} ${executionArnTag} ${htmlFileSplit[1]}` + // Only include start time tag for express executions (when startTime is provided) + const startTimeTag = startTime ? `` : '' + + return `${htmlFileSplit[0]} ${baseTag} ${localeTag} ${darkModeTag} ${componentTypeTag} ${executionArnTag} ${startTimeTag} ${htmlFileSplit[1]}` } /** @@ -93,18 +97,19 @@ export class ExecutionDetailProvider { * @param panel The WebView panel to initialize * @param executionArn The ARN of the execution to display */ - public async initializePanel(panel: vscode.WebviewPanel, executionArn: string): Promise { + public async initializePanel(panel: vscode.WebviewPanel, executionArn: string, startTime?: string): Promise { try { if (!this.webviewHtml) { await this.fetchWebviewHtml() } // Set up the content - panel.webview.html = await this.getWebviewContent(executionArn) + panel.webview.html = await this.getWebviewContent(executionArn, startTime) const context: ExecutionDetailsContext = { panel, loaderNotification: undefined, executionArn, + startTime, } // Handle messages from the webview diff --git a/packages/core/src/stepFunctions/executionDetails/handleMessage.ts b/packages/core/src/stepFunctions/executionDetails/handleMessage.ts index 3dce63b22b4..4970889f032 100644 --- a/packages/core/src/stepFunctions/executionDetails/handleMessage.ts +++ b/packages/core/src/stepFunctions/executionDetails/handleMessage.ts @@ -63,6 +63,7 @@ async function initMessageHandler(context: ExecutionDetailsContext) { messageType: MessageType.BROADCAST, command: Command.INIT, executionArn: context.executionArn, + startTime: context.startTime, }) } catch (e) { await context.panel.webview.postMessage({ diff --git a/packages/core/src/stepFunctions/messageHandlers/stepFunctionApiHandler.ts b/packages/core/src/stepFunctions/messageHandlers/stepFunctionApiHandler.ts index 4a466d9f4ab..f6f4576d0cb 100644 --- a/packages/core/src/stepFunctions/messageHandlers/stepFunctionApiHandler.ts +++ b/packages/core/src/stepFunctions/messageHandlers/stepFunctionApiHandler.ts @@ -6,6 +6,8 @@ import * as StepFunctions from '@aws-sdk/client-sfn' import { IamClient, IamRole } from '../../shared/clients/iam' import { StepFunctionsClient } from '../../shared/clients/stepFunctions' +import { CloudWatchLogsClient } from '../../shared/clients/cloudWatchLogs' +import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { ApiAction, ApiCallRequestMessage, Command, MessageType, BaseContext } from './types' import { telemetry } from '../../shared/telemetry/telemetry' import { ListRolesRequest } from '@aws-sdk/client-iam' @@ -17,6 +19,8 @@ export class StepFunctionApiHandler { private readonly clients = { sfn: new StepFunctionsClient(region), iam: new IamClient(region), + cwl: new CloudWatchLogsClient(region), + lambda: new DefaultLambdaClient(region), } ) {} @@ -57,6 +61,12 @@ export class StepFunctionApiHandler { case ApiAction.SFNStopExecution: response = await this.clients.sfn.stopExecution(params) break + case ApiAction.CWlFilterLogEvents: + response = await this.clients.cwl.filterLogEvents(params) + break + case ApiAction.LambdaGetFunctionConfiguration: + response = await this.clients.lambda.getFunctionConfiguration(params.FunctionName!) + break default: throw new Error(`Unknown API: ${apiName}`) } diff --git a/packages/core/src/stepFunctions/messageHandlers/types.ts b/packages/core/src/stepFunctions/messageHandlers/types.ts index 43dd0b97468..52e8964b5a7 100644 --- a/packages/core/src/stepFunctions/messageHandlers/types.ts +++ b/packages/core/src/stepFunctions/messageHandlers/types.ts @@ -4,6 +4,8 @@ */ import { IAM } from 'aws-sdk' import * as StepFunctions from '@aws-sdk/client-sfn' +import * as CloudWatchLogs from '@aws-sdk/client-cloudwatch-logs' +import * as Lambda from '@aws-sdk/client-lambda' import * as vscode from 'vscode' export enum ComponentType { @@ -35,6 +37,7 @@ export interface WebviewContext extends BaseContext { export interface ExecutionDetailsContext extends BaseContext { executionArn: string + startTime?: string } export type LoaderNotification = { @@ -111,6 +114,8 @@ export enum ApiAction { SFNRedriveExecution = 'sfn:redriveExecution', SFNStartExecution = 'sfn:startExecution', SFNStopExecution = 'sfn:stopExecution', + CWlFilterLogEvents = 'cwl:filterLogEvents', + LambdaGetFunctionConfiguration = 'lambda:getFunctionConfiguration', } type ApiCallRequestMapping = { @@ -124,6 +129,8 @@ type ApiCallRequestMapping = { [ApiAction.SFNRedriveExecution]: StepFunctions.RedriveExecutionInput [ApiAction.SFNStartExecution]: StepFunctions.StartExecutionInput [ApiAction.SFNStopExecution]: StepFunctions.StopExecutionInput + [ApiAction.CWlFilterLogEvents]: CloudWatchLogs.FilterLogEventsCommandInput + [ApiAction.LambdaGetFunctionConfiguration]: Lambda.GetFunctionConfigurationCommandInput } interface ApiCallRequestMessageBase extends Message { @@ -146,3 +153,5 @@ export type ApiCallRequestMessage = | ApiCallRequestMessageBase | ApiCallRequestMessageBase | ApiCallRequestMessageBase + | ApiCallRequestMessageBase + | ApiCallRequestMessageBase diff --git a/packages/core/src/stepFunctions/utils.ts b/packages/core/src/stepFunctions/utils.ts index f578d6cda86..93597000f32 100644 --- a/packages/core/src/stepFunctions/utils.ts +++ b/packages/core/src/stepFunctions/utils.ts @@ -21,6 +21,9 @@ import { IamRole } from '../shared/clients/iam' const documentSettings: DocumentLanguageSettings = { comments: 'error', trailingCommas: 'error' } const languageService = getLanguageService({}) +const arnResourceTypeSegmentIndex = 5 +const expressExecutionArnSegmentCount = 9 + export async function* listStateMachines( client: StepFunctionsClient ): AsyncIterableIterator { @@ -92,6 +95,18 @@ export const isInvalidYamlFile = (textDocument: vscode.TextDocument): boolean => } } +/** + * Determines if execution ARN is for an express execution + * @param arn Execution ARN to check + * @returns true if it's an express execution, false if its a standard execution + */ +export const isExpressExecution = (arn: string): boolean => { + const arnSegments = arn.split(':') + return ( + arnSegments.length === expressExecutionArnSegmentCount && arnSegments[arnResourceTypeSegmentIndex] === 'express' + ) +} + const isInvalidJson = (content: string): boolean => { try { JSON.parse(content) diff --git a/packages/core/src/stepFunctions/wizards/viewExecutionDetailsWizard.ts b/packages/core/src/stepFunctions/wizards/viewExecutionDetailsWizard.ts new file mode 100644 index 00000000000..38a49b16fe5 --- /dev/null +++ b/packages/core/src/stepFunctions/wizards/viewExecutionDetailsWizard.ts @@ -0,0 +1,109 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as nls from 'vscode-nls' +const localize = nls.loadMessageBundle() + +import { createCommonButtons } from '../../shared/ui/buttons' +import { createInputBox, InputBoxPrompter } from '../../shared/ui/inputPrompter' +import { Wizard } from '../../shared/wizards/wizard' +import { validate } from '@aws-sdk/util-arn-parser' +import { isExpressExecution } from '../utils' + +function createExecutionArnPrompter(): InputBoxPrompter { + function validateArn(value: string): string | undefined { + if (!value) { + return localize( + 'AWS.stepFunctions.viewExecutionDetails.executionArn.validation.empty', + 'Execution ARN cannot be empty' + ) + } + + if (!validate(value)) { + return localize( + 'AWS.stepFunctions.viewExecutionDetails.executionArn.validation.invalid', + 'Invalid ARN format. Please provide a valid Step Functions execution ARN' + ) + } + + return undefined + } + + const prompter = createInputBox({ + title: localize('AWS.stepFunctions.viewExecutionDetails.executionArn.title', 'Enter Execution ARN'), + placeholder: + 'arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:12345678-1234-1234-1234-123456789012', + validateInput: validateArn, + buttons: createCommonButtons(), + }) + + return prompter +} + +function createStartTimePrompter(): InputBoxPrompter { + function validateStartTime(value: string): string | undefined { + if (!value) { + return localize( + 'AWS.stepFunctions.viewExecutionDetails.startTime.validation.empty', + 'Start time cannot be empty for express executions' + ) + } + + // Checking if the value is a numeric string (Unix timestamp) + if (/^\d+$/.test(value)) { + const timestamp = Number(value) + const date = new Date(timestamp) + if (!isNaN(date.getTime())) { + return undefined + } + } + + // parsing ISO date format + const date = new Date(value) + if (isNaN(date.getTime())) { + return localize( + 'AWS.stepFunctions.viewExecutionDetails.startTime.validation.invalid', + 'Invalid time format. Use Unix timestamp or ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)' + ) + } + + return undefined + } + + const prompter = createInputBox({ + title: localize('AWS.stepFunctions.viewExecutionDetails.startTime.title', 'Enter Start Time'), + placeholder: localize( + 'AWS.stepFunctions.viewExecutionDetails.startTime.placeholder', + 'Start time of the express execution (e.g., 2023-12-01T10:00:00.000Z)' + ), + validateInput: validateStartTime, + buttons: createCommonButtons(), + }) + + return prompter +} + +export interface ViewExecutionDetailsWizardState { + readonly executionArn: string + readonly startTime?: string +} + +export class ViewExecutionDetailsWizard extends Wizard { + public constructor() { + super() + const form = this.form + + form.executionArn.bindPrompter(() => createExecutionArnPrompter()) + + form.startTime.bindPrompter(() => createStartTimePrompter(), { + showWhen: (state) => { + if (!state.executionArn) { + return false + } + return isExpressExecution(state.executionArn) + }, + }) + } +} diff --git a/packages/core/src/test/stepFunctions/apiHandler/stepFunctionApiHandler.test.ts b/packages/core/src/test/stepFunctions/apiHandler/stepFunctionApiHandler.test.ts index d2f44096874..30818238b90 100644 --- a/packages/core/src/test/stepFunctions/apiHandler/stepFunctionApiHandler.test.ts +++ b/packages/core/src/test/stepFunctions/apiHandler/stepFunctionApiHandler.test.ts @@ -17,6 +17,8 @@ import { import * as vscode from 'vscode' import { assertTelemetry } from '../../testUtil' import { StepFunctionsClient } from '../../../shared/clients/stepFunctions' +import { CloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogs' +import { DefaultLambdaClient } from '../../../shared/clients/lambdaClient' import { IamClient } from '../../../shared/clients/iam' describe('stepFunctionApiHandler', function () { @@ -68,6 +70,8 @@ describe('stepFunctionApiHandler', function () { apiHandler = new StepFunctionApiHandler('us-east-1', context, { sfn: sfnClient, iam: new IamClient('us-east-1'), + cwl: new CloudWatchLogsClient('us-east-1'), + lambda: new DefaultLambdaClient('us-east-1'), }) testState = sinon.stub(sfnClient, 'testState')