Skip to content

Commit 68253c3

Browse files
l0minousDiler Zaza
andauthored
feat(stepfunctions): Supporting Express Executions in Execution Details (#7664)
## Problem `View Execution Details by Execution ARN` command currently only supports Standard Executions ## Solution - Extending Lambda Client to support `getFunctionConfiguration` (used when viewing executions that have a Lambda state) - Extending `activation.ts` to differentiate between standard and express executions, express executions prompt for a second input box to pass in `startTime` - Calling `CloudWatchLogsClient` to be used alongside lambda client in ApiHandler - Passing `startTime` through meta tag ## Verification ![test7](https://github.com/user-attachments/assets/4c8e4812-c7fe-4837-9d0b-9b7901087799) --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. Co-authored-by: Diler Zaza <[email protected]>
1 parent 3d63696 commit 68253c3

File tree

11 files changed

+219
-20
lines changed

11 files changed

+219
-20
lines changed

packages/core/package.nls.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
"AWS.stepFunctions.executeStateMachine.info.executing": "Starting execution of '{0}' in {1}...",
3333
"AWS.stepFunctions.executeStateMachine.info.started": "Execution started",
3434
"AWS.stepFunctions.executeStateMachine.error.failedToStart": "There was an error starting execution for '{0}', check AWS Toolkit logs for more information.",
35+
"AWS.stepFunctions.viewExecutionDetails.executionArn.validation.empty": "Execution ARN cannot be empty",
36+
"AWS.stepFunctions.viewExecutionDetails.executionArn.validation.invalid": "Invalid ARN format. Please provide a valid Step Functions execution ARN",
37+
"AWS.stepFunctions.viewExecutionDetails.executionArn.title": "Enter Execution ARN",
38+
"AWS.stepFunctions.viewExecutionDetails.startTime.validation.empty": "Start time cannot be empty for express executions",
39+
"AWS.stepFunctions.viewExecutionDetails.startTime.validation.invalid": "Invalid time format. Use Unix timestamp or ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)",
40+
"AWS.stepFunctions.viewExecutionDetails.startTime.title": "Enter Start Time",
41+
"AWS.stepFunctions.viewExecutionDetails.startTime.placeholder": "Start time of the express execution (e.g., 2023-12-01T10:00:00.000Z)",
42+
"AWS.stepFunctions.viewExecutionDetails.error.general": "Failed to view execution details",
3543
"AWS.stepFunctions.asl.format.enable.desc": "Enables the default formatter used with Amazon States Language files",
3644
"AWS.stepFunctions.asl.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).",
3745
"AWS.stepFunctions.workflowStudio.actions.progressMessage": "Opening asl file in Workflow Studio",

packages/core/src/shared/clients/lambdaClient.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ export class DefaultLambdaClient {
120120
}
121121
}
122122

123+
public async getFunctionConfiguration(name: string): Promise<Lambda.FunctionConfiguration> {
124+
const client = await this.createSdkClient()
125+
try {
126+
const request = client.getFunctionConfiguration({ FunctionName: name })
127+
const response = await request.promise()
128+
return response
129+
} catch (e) {
130+
getLogger().error('Failed to get function configuration: %s', e)
131+
throw e
132+
}
133+
}
134+
123135
private async createSdkClient(): Promise<Lambda> {
124136
return await globals.sdkClientBuilder.createAwsService(
125137
Lambda,

packages/core/src/stepFunctions/activation.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as vscode from 'vscode'
1212
import { AwsContext } from '../shared/awsContext'
1313
import { createStateMachineFromTemplate } from './commands/createStateMachineFromTemplate'
1414
import { publishStateMachine } from './commands/publishStateMachine'
15+
import { viewExecutionDetails } from './commands/viewExecutionDetails'
1516
import { Commands } from '../shared/vscode/commands2'
1617

1718
import { ASL_FORMATS, YAML_ASL, JSON_ASL } from './constants/aslFormats'
@@ -23,8 +24,6 @@ import { ASLLanguageClient } from './asl/client'
2324
import { WorkflowStudioEditorProvider } from './workflowStudio/workflowStudioEditorProvider'
2425
import { StateMachineNode } from './explorer/stepFunctionsNodes'
2526
import { downloadStateMachineDefinition } from './commands/downloadStateMachineDefinition'
26-
import { ExecutionDetailProvider } from './executionDetails/executionDetailProvider'
27-
import { validate } from '@aws-sdk/util-arn-parser'
2827

2928
/**
3029
* Activate Step Functions related functionality for the extension.
@@ -101,19 +100,7 @@ async function registerStepFunctionCommands(
101100
await publishStateMachine({ awsContext: awsContext, outputChannel: outputChannel, region: region })
102101
}),
103102
Commands.register('aws.stepfunctions.viewExecutionDetailsByExecutionARN', async () => {
104-
const arn = await vscode.window.showInputBox({
105-
prompt: 'Enter Execution ARN',
106-
placeHolder:
107-
'arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:12345678-1234-1234-1234-123456789012',
108-
})
109-
110-
if (validate(arn)) {
111-
await ExecutionDetailProvider.openExecutionDetails(arn!)
112-
} else {
113-
void vscode.window.showErrorMessage(
114-
'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)'
115-
)
116-
}
103+
await viewExecutionDetails({ awsContext: awsContext, outputChannel: outputChannel })
117104
})
118105
)
119106
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import * as nls from 'vscode-nls'
8+
import { AwsContext } from '../../shared/awsContext'
9+
import { ExecutionDetailProvider } from '../executionDetails/executionDetailProvider'
10+
import { ViewExecutionDetailsWizard } from '../wizards/viewExecutionDetailsWizard'
11+
12+
const localize = nls.loadMessageBundle()
13+
14+
interface ViewExecutionDetailsParams {
15+
awsContext: AwsContext
16+
outputChannel: vscode.OutputChannel
17+
}
18+
19+
export async function viewExecutionDetails(params: ViewExecutionDetailsParams): Promise<void> {
20+
try {
21+
const wizard = new ViewExecutionDetailsWizard()
22+
const wizardResponse = await wizard.run()
23+
24+
if (wizardResponse) {
25+
const { executionArn, startTime } = wizardResponse
26+
27+
await ExecutionDetailProvider.openExecutionDetails(executionArn, startTime)
28+
}
29+
} catch (error) {
30+
const errorMessage = localize(
31+
'AWS.stepFunctions.viewExecutionDetails.error.general',
32+
'Failed to view execution details'
33+
)
34+
35+
params.outputChannel.appendLine('')
36+
params.outputChannel.appendLine(errorMessage)
37+
params.outputChannel.show()
38+
}
39+
}

packages/core/src/stepFunctions/executionDetails/executionDetailProvider.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class ExecutionDetailProvider {
2828
*/
2929
public static async openExecutionDetails(
3030
executionArn: string,
31+
startTime?: string,
3132
params?: vscode.WebviewPanelOptions & vscode.WebviewOptions
3233
): Promise<void> {
3334
// Create and show the webview panel
@@ -43,7 +44,7 @@ export class ExecutionDetailProvider {
4344
)
4445
// Create the provider and initialize the panel
4546
const provider = new ExecutionDetailProvider()
46-
await provider.initializePanel(panel, executionArn)
47+
await provider.initializePanel(panel, executionArn, startTime)
4748
}
4849

4950
protected webviewHtml: string
@@ -67,7 +68,7 @@ export class ExecutionDetailProvider {
6768
* Gets the webview content for Execution Details.
6869
* @private
6970
*/
70-
private getWebviewContent = async (executionArn: string): Promise<string> => {
71+
private getWebviewContent = async (executionArn: string, startTime?: string): Promise<string> => {
7172
const htmlFileSplit = this.webviewHtml.split('<head>')
7273

7374
// Set asset source to CDN
@@ -85,26 +86,30 @@ export class ExecutionDetailProvider {
8586
const componentTypeTag = `<meta name="component-type" content="${ComponentType.ExecutionDetails}" />`
8687
const executionArnTag = `<meta name="execution-arn" content="${executionArn}" />`
8788

88-
return `${htmlFileSplit[0]} <head> ${baseTag} ${localeTag} ${darkModeTag} ${componentTypeTag} ${executionArnTag} ${htmlFileSplit[1]}`
89+
// Only include start time tag for express executions (when startTime is provided)
90+
const startTimeTag = startTime ? `<meta name="start-time" content="${startTime}" />` : ''
91+
92+
return `${htmlFileSplit[0]} <head> ${baseTag} ${localeTag} ${darkModeTag} ${componentTypeTag} ${executionArnTag} ${startTimeTag} ${htmlFileSplit[1]}`
8993
}
9094

9195
/**
9296
* Initializes a WebView panel with execution details.
9397
* @param panel The WebView panel to initialize
9498
* @param executionArn The ARN of the execution to display
9599
*/
96-
public async initializePanel(panel: vscode.WebviewPanel, executionArn: string): Promise<void> {
100+
public async initializePanel(panel: vscode.WebviewPanel, executionArn: string, startTime?: string): Promise<void> {
97101
try {
98102
if (!this.webviewHtml) {
99103
await this.fetchWebviewHtml()
100104
}
101105

102106
// Set up the content
103-
panel.webview.html = await this.getWebviewContent(executionArn)
107+
panel.webview.html = await this.getWebviewContent(executionArn, startTime)
104108
const context: ExecutionDetailsContext = {
105109
panel,
106110
loaderNotification: undefined,
107111
executionArn,
112+
startTime,
108113
}
109114

110115
// Handle messages from the webview

packages/core/src/stepFunctions/executionDetails/handleMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ async function initMessageHandler(context: ExecutionDetailsContext) {
6363
messageType: MessageType.BROADCAST,
6464
command: Command.INIT,
6565
executionArn: context.executionArn,
66+
startTime: context.startTime,
6667
})
6768
} catch (e) {
6869
await context.panel.webview.postMessage({

packages/core/src/stepFunctions/messageHandlers/stepFunctionApiHandler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import * as StepFunctions from '@aws-sdk/client-sfn'
77
import { IamClient, IamRole } from '../../shared/clients/iam'
88
import { StepFunctionsClient } from '../../shared/clients/stepFunctions'
9+
import { CloudWatchLogsClient } from '../../shared/clients/cloudWatchLogs'
10+
import { DefaultLambdaClient } from '../../shared/clients/lambdaClient'
911
import { ApiAction, ApiCallRequestMessage, Command, MessageType, BaseContext } from './types'
1012
import { telemetry } from '../../shared/telemetry/telemetry'
1113
import { ListRolesRequest } from '@aws-sdk/client-iam'
@@ -17,6 +19,8 @@ export class StepFunctionApiHandler {
1719
private readonly clients = {
1820
sfn: new StepFunctionsClient(region),
1921
iam: new IamClient(region),
22+
cwl: new CloudWatchLogsClient(region),
23+
lambda: new DefaultLambdaClient(region),
2024
}
2125
) {}
2226

@@ -57,6 +61,12 @@ export class StepFunctionApiHandler {
5761
case ApiAction.SFNStopExecution:
5862
response = await this.clients.sfn.stopExecution(params)
5963
break
64+
case ApiAction.CWlFilterLogEvents:
65+
response = await this.clients.cwl.filterLogEvents(params)
66+
break
67+
case ApiAction.LambdaGetFunctionConfiguration:
68+
response = await this.clients.lambda.getFunctionConfiguration(params.FunctionName!)
69+
break
6070
default:
6171
throw new Error(`Unknown API: ${apiName}`)
6272
}

packages/core/src/stepFunctions/messageHandlers/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
import { IAM } from 'aws-sdk'
66
import * as StepFunctions from '@aws-sdk/client-sfn'
7+
import * as CloudWatchLogs from '@aws-sdk/client-cloudwatch-logs'
8+
import * as Lambda from '@aws-sdk/client-lambda'
79
import * as vscode from 'vscode'
810

911
export enum ComponentType {
@@ -35,6 +37,7 @@ export interface WebviewContext extends BaseContext {
3537

3638
export interface ExecutionDetailsContext extends BaseContext {
3739
executionArn: string
40+
startTime?: string
3841
}
3942

4043
export type LoaderNotification = {
@@ -111,6 +114,8 @@ export enum ApiAction {
111114
SFNRedriveExecution = 'sfn:redriveExecution',
112115
SFNStartExecution = 'sfn:startExecution',
113116
SFNStopExecution = 'sfn:stopExecution',
117+
CWlFilterLogEvents = 'cwl:filterLogEvents',
118+
LambdaGetFunctionConfiguration = 'lambda:getFunctionConfiguration',
114119
}
115120

116121
type ApiCallRequestMapping = {
@@ -124,6 +129,8 @@ type ApiCallRequestMapping = {
124129
[ApiAction.SFNRedriveExecution]: StepFunctions.RedriveExecutionInput
125130
[ApiAction.SFNStartExecution]: StepFunctions.StartExecutionInput
126131
[ApiAction.SFNStopExecution]: StepFunctions.StopExecutionInput
132+
[ApiAction.CWlFilterLogEvents]: CloudWatchLogs.FilterLogEventsCommandInput
133+
[ApiAction.LambdaGetFunctionConfiguration]: Lambda.GetFunctionConfigurationCommandInput
127134
}
128135

129136
interface ApiCallRequestMessageBase<ApiName extends ApiAction> extends Message {
@@ -146,3 +153,5 @@ export type ApiCallRequestMessage =
146153
| ApiCallRequestMessageBase<ApiAction.SFNRedriveExecution>
147154
| ApiCallRequestMessageBase<ApiAction.SFNStartExecution>
148155
| ApiCallRequestMessageBase<ApiAction.SFNStopExecution>
156+
| ApiCallRequestMessageBase<ApiAction.CWlFilterLogEvents>
157+
| ApiCallRequestMessageBase<ApiAction.LambdaGetFunctionConfiguration>

packages/core/src/stepFunctions/utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { IamRole } from '../shared/clients/iam'
2121
const documentSettings: DocumentLanguageSettings = { comments: 'error', trailingCommas: 'error' }
2222
const languageService = getLanguageService({})
2323

24+
const arnResourceTypeSegmentIndex = 5
25+
const expressExecutionArnSegmentCount = 9
26+
2427
export async function* listStateMachines(
2528
client: StepFunctionsClient
2629
): AsyncIterableIterator<StepFunctions.StateMachineListItem> {
@@ -92,6 +95,18 @@ export const isInvalidYamlFile = (textDocument: vscode.TextDocument): boolean =>
9295
}
9396
}
9497

98+
/**
99+
* Determines if execution ARN is for an express execution
100+
* @param arn Execution ARN to check
101+
* @returns true if it's an express execution, false if its a standard execution
102+
*/
103+
export const isExpressExecution = (arn: string): boolean => {
104+
const arnSegments = arn.split(':')
105+
return (
106+
arnSegments.length === expressExecutionArnSegmentCount && arnSegments[arnResourceTypeSegmentIndex] === 'express'
107+
)
108+
}
109+
95110
const isInvalidJson = (content: string): boolean => {
96111
try {
97112
JSON.parse(content)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as nls from 'vscode-nls'
7+
const localize = nls.loadMessageBundle()
8+
9+
import { createCommonButtons } from '../../shared/ui/buttons'
10+
import { createInputBox, InputBoxPrompter } from '../../shared/ui/inputPrompter'
11+
import { Wizard } from '../../shared/wizards/wizard'
12+
import { validate } from '@aws-sdk/util-arn-parser'
13+
import { isExpressExecution } from '../utils'
14+
15+
function createExecutionArnPrompter(): InputBoxPrompter {
16+
function validateArn(value: string): string | undefined {
17+
if (!value) {
18+
return localize(
19+
'AWS.stepFunctions.viewExecutionDetails.executionArn.validation.empty',
20+
'Execution ARN cannot be empty'
21+
)
22+
}
23+
24+
if (!validate(value)) {
25+
return localize(
26+
'AWS.stepFunctions.viewExecutionDetails.executionArn.validation.invalid',
27+
'Invalid ARN format. Please provide a valid Step Functions execution ARN'
28+
)
29+
}
30+
31+
return undefined
32+
}
33+
34+
const prompter = createInputBox({
35+
title: localize('AWS.stepFunctions.viewExecutionDetails.executionArn.title', 'Enter Execution ARN'),
36+
placeholder:
37+
'arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:12345678-1234-1234-1234-123456789012',
38+
validateInput: validateArn,
39+
buttons: createCommonButtons(),
40+
})
41+
42+
return prompter
43+
}
44+
45+
function createStartTimePrompter(): InputBoxPrompter {
46+
function validateStartTime(value: string): string | undefined {
47+
if (!value) {
48+
return localize(
49+
'AWS.stepFunctions.viewExecutionDetails.startTime.validation.empty',
50+
'Start time cannot be empty for express executions'
51+
)
52+
}
53+
54+
// Checking if the value is a numeric string (Unix timestamp)
55+
if (/^\d+$/.test(value)) {
56+
const timestamp = Number(value)
57+
const date = new Date(timestamp)
58+
if (!isNaN(date.getTime())) {
59+
return undefined
60+
}
61+
}
62+
63+
// parsing ISO date format
64+
const date = new Date(value)
65+
if (isNaN(date.getTime())) {
66+
return localize(
67+
'AWS.stepFunctions.viewExecutionDetails.startTime.validation.invalid',
68+
'Invalid time format. Use Unix timestamp or ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)'
69+
)
70+
}
71+
72+
return undefined
73+
}
74+
75+
const prompter = createInputBox({
76+
title: localize('AWS.stepFunctions.viewExecutionDetails.startTime.title', 'Enter Start Time'),
77+
placeholder: localize(
78+
'AWS.stepFunctions.viewExecutionDetails.startTime.placeholder',
79+
'Start time of the express execution (e.g., 2023-12-01T10:00:00.000Z)'
80+
),
81+
validateInput: validateStartTime,
82+
buttons: createCommonButtons(),
83+
})
84+
85+
return prompter
86+
}
87+
88+
export interface ViewExecutionDetailsWizardState {
89+
readonly executionArn: string
90+
readonly startTime?: string
91+
}
92+
93+
export class ViewExecutionDetailsWizard extends Wizard<ViewExecutionDetailsWizardState> {
94+
public constructor() {
95+
super()
96+
const form = this.form
97+
98+
form.executionArn.bindPrompter(() => createExecutionArnPrompter())
99+
100+
form.startTime.bindPrompter(() => createStartTimePrompter(), {
101+
showWhen: (state) => {
102+
if (!state.executionArn) {
103+
return false
104+
}
105+
return isExpressExecution(state.executionArn)
106+
},
107+
})
108+
}
109+
}

0 commit comments

Comments
 (0)