Skip to content

feat(stepfunctions): Show state machine executions in the Explorer view. #7847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions packages/core/src/shared/clients/stepFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
DescribeStateMachineCommand,
DescribeStateMachineCommandInput,
DescribeStateMachineCommandOutput,
ExecutionListItem,
ListExecutionsCommand,
ListExecutionsCommandInput,
ListExecutionsCommandOutput,
ListStateMachinesCommand,
ListStateMachinesCommandInput,
ListStateMachinesCommandOutput,
Expand Down Expand Up @@ -44,6 +48,21 @@ export class StepFunctionsClient extends ClientWrapper<SFNClient> {
} while (request.nextToken)
}

public async *listExecutions(
request: ListExecutionsCommandInput = {},
resultsToReturn: number
): AsyncIterableIterator<ExecutionListItem> {
do {
request.maxResults = resultsToReturn
const response: ListExecutionsCommandOutput = await this.makeRequest(ListExecutionsCommand, request)
if (response.executions) {
resultsToReturn -= response.executions.length
yield* response.executions
}
request.nextToken = response.nextToken
} while (request.nextToken && resultsToReturn > 0)
}

public async getStateMachineDetails(
request: DescribeStateMachineCommandInput
): Promise<DescribeStateMachineCommandOutput> {
Expand Down
126 changes: 111 additions & 15 deletions packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode'
import { makeChildrenNodes } from '../../shared/treeview/utils'
import { toArrayAsync, toMap, updateInPlace } from '../../shared/utilities/collectionUtils'
import { listStateMachines } from '../../stepFunctions/utils'
import { getIcon } from '../../shared/icons'

export const contextValueStateMachine = 'awsStateMachineNode'
import { listStateMachines, listExecutions } from '../../stepFunctions/utils'
import { getIcon, IconPath } from '../../shared/icons'

const sfnNodeMap = new Map<string, StepFunctionsNode>()

Expand Down Expand Up @@ -74,20 +72,70 @@ export class StepFunctionsNode extends AWSTreeNodeBase {
this.stateMachineNodes,
functions.keys(),
(key) => this.stateMachineNodes.get(key)!.update(functions.get(key)!),
(key) => makeStateMachineNode(this, this.regionCode, functions.get(key)!)
(key) => new StateMachineNode(this, this.regionCode, functions.get(key)!, this.client)
)
}
}

/**
* Represents a Step Functions state machine in the Explorer view. This node
* appears immediately underneath the "Step Functions" node. A StateMachineNode
* will contain children of type StateMachineExecutionNode, representing
* the most recent executions of that state machine.
*/
export class StateMachineNode extends AWSTreeNodeBase implements AWSResourceNode {
public static readonly contextValue = 'awsStateMachineNode'
public static readonly maxExecutionsToShow = 10

private readonly stateMachineExecutionNodes: Map<string, StateMachineExecutionNode>

public constructor(
public readonly parent: AWSTreeNodeBase,
public override readonly regionCode: string,
public details: StepFunctions.StateMachineListItem
public details: StepFunctions.StateMachineListItem,
private readonly client: StepFunctionsClient
) {
super('')
super('', vscode.TreeItemCollapsibleState.Collapsed)
this.stateMachineExecutionNodes = new Map<string, StateMachineExecutionNode>()
this.update(details)
this.iconPath = getIcon('aws-stepfunctions-preview')
this.contextValue = StateMachineNode.contextValue
}

public override async getChildren(): Promise<AWSTreeNodeBase[]> {
return await makeChildrenNodes({
getChildNodes: async () => {
await this.updateChildren()
return [...this.stateMachineExecutionNodes.values()]
},
getNoChildrenPlaceholderNode: async () =>
new PlaceholderNode(
this,
localize('AWS.explorerNode.stepfunctions.noStateMachineExecution', '[No Executions found]')
),
/*
* Note: although Step Functions returns the executions in the correct order, this sorting
* is still needed to ensure newly added nodes (via updateChildren()) appear in the correct place.
*/
sort: (nodeA, nodeB) => {
const dateA = nodeA.details.startDate as Date // startDate will never be undefined.
const dateB = nodeB.details.startDate as Date
return dateB.getTime() - dateA.getTime()
},
})
}

public async updateChildren(): Promise<void> {
const executions: Map<string, StepFunctions.ExecutionListItem> = toMap(
await toArrayAsync(listExecutions(this.client, this.arn, StateMachineNode.maxExecutionsToShow)),
(details) => details.name
)
updateInPlace(
this.stateMachineExecutionNodes,
executions.keys(),
(key) => this.stateMachineExecutionNodes.get(key)!.update(executions.get(key)!),
(key) => new StateMachineExecutionNode(this, this.regionCode, executions.get(key)!)
)
}

public update(details: StepFunctions.StateMachineListItem): void {
Expand All @@ -113,13 +161,61 @@ export class StateMachineNode extends AWSTreeNodeBase implements AWSResourceNode
}
}

function makeStateMachineNode(
parent: AWSTreeNodeBase,
regionCode: string,
details: StepFunctions.StateMachineListItem
): StateMachineNode {
const node = new StateMachineNode(parent, regionCode, details)
node.contextValue = contextValueStateMachine
/**
* Represents a single execution of a Step Functions state machine in the Explorer
* view. This node appears immediately underneath the corresponding StateMachineNode.
*/
export class StateMachineExecutionNode extends AWSTreeNodeBase implements AWSResourceNode {
public static contextValue = 'awsStateMachineExecutionNode'

public constructor(
public readonly parent: AWSTreeNodeBase,
public override readonly regionCode: string,
public details: StepFunctions.ExecutionListItem
) {
super('')
this.update(details)
this.contextValue = StateMachineExecutionNode.contextValue
}

public update(details: StepFunctions.ExecutionListItem): void {
this.details = details
this.label = this.details.name || ''
this.tooltip = this.getToolTip(this.details)
this.iconPath = this.getIconPathForStatus(this.details.status)
}

public get arn(): string {
return this.details.executionArn || ''
}

return node
public get name(): string {
return this.details.name || ''
}

private getIconPathForStatus(status?: string): IconPath {
switch (status) {
case 'RUNNING':
return getIcon('vscode-sync')
case 'SUCCEEDED':
return getIcon('vscode-check')
default:
return getIcon('vscode-error')
}
}

private getToolTip(details: StepFunctions.ExecutionListItem) {
const startTimeText = localize('AWS.explorerNode.stepfunctions.startTime', 'Start Time')
const endTimeText = localize('AWS.explorerNode.stepfunctions.endTime', 'End Time')
const durationText = localize('AWS.explorerNode.stepfunctions.duration', 'Duration')
const secondsText = localize('AWS.explorerNode.stepfunctions.seconds', 'seconds')

let text: string = `${details.status}${os.EOL}${startTimeText}: ${details.startDate?.toLocaleString()}${os.EOL}`
if (details.status !== 'RUNNING') {
text += `${endTimeText}: ${details.stopDate?.toLocaleString()}${os.EOL}`
const endDate = details.stopDate ? details.stopDate : new Date()
text += `${durationText}: ${Math.trunc((endDate.getTime() - details.startDate!.getTime()) / 1000)} ${secondsText}${os.EOL}`
}
return text
}
}
23 changes: 23 additions & 0 deletions packages/core/src/stepFunctions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ export async function* listStateMachines(
}
}

export async function* listExecutions(
client: StepFunctionsClient,
stateMachineArn: string,
resultsToReturn: number
): AsyncIterableIterator<StepFunctions.ExecutionListItem> {
const status = vscode.window.setStatusBarMessage(
localize('AWS.message.statusBar.loading.statemachineexecutions', 'Loading State Machine Executions...')
)

try {
yield* client.listExecutions(
{
stateMachineArn: stateMachineArn,
},
resultsToReturn
)
} finally {
if (status) {
status.dispose()
}
}
}

/**
* Checks if the given IAM Role is assumable by AWS Step Functions.
* @param role The IAM role to check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface StateMachine {
arn: string
name: string
region: string
explorerNode?: StateMachineNode
}

export class ExecuteStateMachineWebview extends VueWebview {
Expand Down Expand Up @@ -64,6 +65,7 @@ export class ExecuteStateMachineWebview extends VueWebview {
this.logger.info('started execution for Step Functions State Machine')
this.channel.appendLine(localize('AWS.stepFunctions.executeStateMachine.info.started', 'Execution started'))
this.channel.appendLine(startExecResponse.executionArn || '')
await this.refreshExplorerNode(this.stateMachine.explorerNode)
} catch (e) {
executeResult = 'Failed'
const error = e as Error
Expand All @@ -79,6 +81,16 @@ export class ExecuteStateMachineWebview extends VueWebview {
telemetry.stepfunctions_executeStateMachine.emit({ result: executeResult })
}
}

/**
* Assuming the state machine execution was started via a StateMachineNode in the
* AWS Explorer, refresh the list of executions, so this new execution will appear.
*/
private async refreshExplorerNode(node?: StateMachineNode): Promise<void> {
if (node) {
return vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node)
}
}
}

const Panel = VueWebview.compilePanel(ExecuteStateMachineWebview)
Expand All @@ -88,6 +100,7 @@ export async function executeStateMachine(context: ExtContext, node: StateMachin
arn: node.details.stateMachineArn || '',
name: node.details.name || '',
region: node.regionCode,
explorerNode: node,
})

await wv.show({
Expand Down
Loading