Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/shared/clients/lambdaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ export class DefaultLambdaClient {
}
}

public async getFunctionConfiguration(name: string): Promise<Lambda.FunctionConfiguration> {
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<Lambda> {
return await globals.sdkClientBuilder.createAwsService(
Lambda,
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/stepFunctions/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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.
Expand Down Expand Up @@ -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 })
})
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class ExecutionDetailProvider {
*/
public static async openExecutionDetails(
executionArn: string,
startTime?: string,
params?: vscode.WebviewPanelOptions & vscode.WebviewOptions
): Promise<void> {
// Create and show the webview panel
Expand All @@ -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
Expand All @@ -67,7 +68,7 @@ export class ExecutionDetailProvider {
* Gets the webview content for Execution Details.
* @private
*/
private getWebviewContent = async (executionArn: string): Promise<string> => {
private getWebviewContent = async (executionArn: string, startTime?: string): Promise<string> => {
const htmlFileSplit = this.webviewHtml.split('<head>')

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

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

return `${htmlFileSplit[0]} <head> ${baseTag} ${localeTag} ${darkModeTag} ${componentTypeTag} ${executionArnTag} ${startTimeTag} ${htmlFileSplit[1]}`
}

/**
* Initializes a WebView panel with execution details.
* @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<void> {
public async initializePanel(panel: vscode.WebviewPanel, executionArn: string, startTime?: string): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
Do we have dispose events for this Webview?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There arent any currently but maybe later if something that needs to be disposed is added

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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),
}
) {}

Expand Down Expand Up @@ -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}`)
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/stepFunctions/messageHandlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -35,6 +37,7 @@ export interface WebviewContext extends BaseContext {

export interface ExecutionDetailsContext extends BaseContext {
executionArn: string
startTime?: string
}

export type LoaderNotification = {
Expand Down Expand Up @@ -111,6 +114,8 @@ export enum ApiAction {
SFNRedriveExecution = 'sfn:redriveExecution',
SFNStartExecution = 'sfn:startExecution',
SFNStopExecution = 'sfn:stopExecution',
CWlFilterLogEvents = 'cwl:filterLogEvents',
LambdaGetFunctionConfiguration = 'lambda:getFunctionConfiguration',
}

type ApiCallRequestMapping = {
Expand All @@ -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<ApiName extends ApiAction> extends Message {
Expand All @@ -146,3 +153,5 @@ export type ApiCallRequestMessage =
| ApiCallRequestMessageBase<ApiAction.SFNRedriveExecution>
| ApiCallRequestMessageBase<ApiAction.SFNStartExecution>
| ApiCallRequestMessageBase<ApiAction.SFNStopExecution>
| ApiCallRequestMessageBase<ApiAction.CWlFilterLogEvents>
| ApiCallRequestMessageBase<ApiAction.LambdaGetFunctionConfiguration>
15 changes: 15 additions & 0 deletions packages/core/src/stepFunctions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StepFunctions.StateMachineListItem> {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ViewExecutionDetailsWizardState> {
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)
},
})
}
}
Loading
Loading