Skip to content

Commit cb63aa1

Browse files
feat(stepfunctions): Show state machine executions in the Explorer view.
1 parent 14611c3 commit cb63aa1

File tree

7 files changed

+309
-19
lines changed

7 files changed

+309
-19
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
DescribeStateMachineCommand,
1111
DescribeStateMachineCommandInput,
1212
DescribeStateMachineCommandOutput,
13+
ExecutionListItem,
14+
ListExecutionsCommand,
15+
ListExecutionsCommandInput,
16+
ListExecutionsCommandOutput,
1317
ListStateMachinesCommand,
1418
ListStateMachinesCommandInput,
1519
ListStateMachinesCommandOutput,
@@ -44,6 +48,21 @@ export class StepFunctionsClient extends ClientWrapper<SFNClient> {
4448
} while (request.nextToken)
4549
}
4650

51+
public async *listExecutions(
52+
request: ListExecutionsCommandInput = {},
53+
resultsToReturn: number
54+
): AsyncIterableIterator<ExecutionListItem> {
55+
do {
56+
request.maxResults = resultsToReturn
57+
const response: ListExecutionsCommandOutput = await this.makeRequest(ListExecutionsCommand, request)
58+
if (response.executions) {
59+
resultsToReturn -= response.executions.length
60+
yield* response.executions
61+
}
62+
request.nextToken = response.nextToken
63+
} while (request.nextToken && resultsToReturn > 0)
64+
}
65+
4766
public async getStateMachineDetails(
4867
request: DescribeStateMachineCommandInput
4968
): Promise<DescribeStateMachineCommandOutput> {

packages/core/src/stepFunctions/explorer/stepFunctionsNodes.ts

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
1616
import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode'
1717
import { makeChildrenNodes } from '../../shared/treeview/utils'
1818
import { toArrayAsync, toMap, updateInPlace } from '../../shared/utilities/collectionUtils'
19-
import { listStateMachines } from '../../stepFunctions/utils'
20-
import { getIcon } from '../../shared/icons'
21-
22-
export const contextValueStateMachine = 'awsStateMachineNode'
19+
import { listStateMachines, listExecutions } from '../../stepFunctions/utils'
20+
import { getIcon, IconPath } from '../../shared/icons'
2321

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

@@ -74,20 +72,70 @@ export class StepFunctionsNode extends AWSTreeNodeBase {
7472
this.stateMachineNodes,
7573
functions.keys(),
7674
(key) => this.stateMachineNodes.get(key)!.update(functions.get(key)!),
77-
(key) => makeStateMachineNode(this, this.regionCode, functions.get(key)!)
75+
(key) => new StateMachineNode(this, this.regionCode, functions.get(key)!, this.client)
7876
)
7977
}
8078
}
8179

80+
/**
81+
* Represents a Step Functions state machine in the Explorer view. This node
82+
* appears immediately underneath the "Step Functions" node. A StateMachineNode
83+
* will contain children of type StateMachineExecutionNode, representing
84+
* the most recent executions of that state machine.
85+
*/
8286
export class StateMachineNode extends AWSTreeNodeBase implements AWSResourceNode {
87+
public static readonly contextValue = 'awsStateMachineNode'
88+
public static readonly maxExecutionsToShow = 10
89+
90+
private readonly stateMachineExecutionNodes: Map<string, StateMachineExecutionNode>
91+
8392
public constructor(
8493
public readonly parent: AWSTreeNodeBase,
8594
public override readonly regionCode: string,
86-
public details: StepFunctions.StateMachineListItem
95+
public details: StepFunctions.StateMachineListItem,
96+
private readonly client: StepFunctionsClient
8797
) {
88-
super('')
98+
super('', vscode.TreeItemCollapsibleState.Collapsed)
99+
this.stateMachineExecutionNodes = new Map<string, StateMachineExecutionNode>()
89100
this.update(details)
90101
this.iconPath = getIcon('aws-stepfunctions-preview')
102+
this.contextValue = StateMachineNode.contextValue
103+
}
104+
105+
public override async getChildren(): Promise<AWSTreeNodeBase[]> {
106+
return await makeChildrenNodes({
107+
getChildNodes: async () => {
108+
await this.updateChildren()
109+
return [...this.stateMachineExecutionNodes.values()]
110+
},
111+
getNoChildrenPlaceholderNode: async () =>
112+
new PlaceholderNode(
113+
this,
114+
localize('AWS.explorerNode.stepfunctions.noStateMachineExecution', '[No Executions found]')
115+
),
116+
/*
117+
* Note: although Step Functions returns the executions in the correct order, this sorting
118+
* is still needed to ensure newly added nodes (via updateChildren()) appear in the correct place.
119+
*/
120+
sort: (nodeA, nodeB) => {
121+
const dateA = nodeA.details.startDate as Date // startDate will never be undefined.
122+
const dateB = nodeB.details.startDate as Date
123+
return dateB.getTime() - dateA.getTime()
124+
},
125+
})
126+
}
127+
128+
public async updateChildren(): Promise<void> {
129+
const executions: Map<string, StepFunctions.ExecutionListItem> = toMap(
130+
await toArrayAsync(listExecutions(this.client, this.arn, StateMachineNode.maxExecutionsToShow)),
131+
(details) => details.name
132+
)
133+
updateInPlace(
134+
this.stateMachineExecutionNodes,
135+
executions.keys(),
136+
(key) => this.stateMachineExecutionNodes.get(key)!.update(executions.get(key)!),
137+
(key) => new StateMachineExecutionNode(this, this.regionCode, executions.get(key)!)
138+
)
91139
}
92140

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

116-
function makeStateMachineNode(
117-
parent: AWSTreeNodeBase,
118-
regionCode: string,
119-
details: StepFunctions.StateMachineListItem
120-
): StateMachineNode {
121-
const node = new StateMachineNode(parent, regionCode, details)
122-
node.contextValue = contextValueStateMachine
164+
/**
165+
* Represents a single execution of a Step Functions state machine in the Explorer
166+
* view. This node appears immediately underneath the corresponding StateMachineNode.
167+
*/
168+
export class StateMachineExecutionNode extends AWSTreeNodeBase implements AWSResourceNode {
169+
public static contextValue = 'awsStateMachineExecutionNode'
170+
171+
public constructor(
172+
public readonly parent: AWSTreeNodeBase,
173+
public override readonly regionCode: string,
174+
public details: StepFunctions.ExecutionListItem
175+
) {
176+
super('')
177+
this.update(details)
178+
this.contextValue = StateMachineExecutionNode.contextValue
179+
}
180+
181+
public update(details: StepFunctions.ExecutionListItem): void {
182+
this.details = details
183+
this.label = this.details.name || ''
184+
this.tooltip = this.getToolTip(this.details)
185+
this.iconPath = this.getIconPathForStatus(this.details.status)
186+
}
187+
188+
public get arn(): string {
189+
return this.details.executionArn || ''
190+
}
123191

124-
return node
192+
public get name(): string {
193+
return this.details.name || ''
194+
}
195+
196+
private getIconPathForStatus(status?: string): IconPath {
197+
switch (status) {
198+
case 'RUNNING':
199+
return getIcon('vscode-sync')
200+
case 'SUCCEEDED':
201+
return getIcon('vscode-check')
202+
default:
203+
return getIcon('vscode-error')
204+
}
205+
}
206+
207+
private getToolTip(details: StepFunctions.ExecutionListItem) {
208+
const startTimeText = localize('AWS.explorerNode.stepfunctions.startTime', 'Start Time')
209+
const endTimeText = localize('AWS.explorerNode.stepfunctions.endTime', 'End Time')
210+
const durationText = localize('AWS.explorerNode.stepfunctions.duration', 'Duration')
211+
const secondsText = localize('AWS.explorerNode.stepfunctions.seconds', 'seconds')
212+
213+
let text: string = `${details.status}${os.EOL}${startTimeText}: ${details.startDate?.toLocaleString()}${os.EOL}`
214+
if (details.status !== 'RUNNING') {
215+
text += `${endTimeText}: ${details.stopDate?.toLocaleString()}${os.EOL}`
216+
const endDate = details.stopDate ? details.stopDate : new Date()
217+
text += `${durationText}: ${Math.trunc((endDate.getTime() - details.startDate!.getTime()) / 1000)} ${secondsText}${os.EOL}`
218+
}
219+
return text
220+
}
125221
}

packages/core/src/stepFunctions/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,29 @@ export async function* listStateMachines(
3737
}
3838
}
3939

40+
export async function* listExecutions(
41+
client: StepFunctionsClient,
42+
stateMachineArn: string,
43+
resultsToReturn: number
44+
): AsyncIterableIterator<StepFunctions.ExecutionListItem> {
45+
const status = vscode.window.setStatusBarMessage(
46+
localize('AWS.message.statusBar.loading.statemachineexecutions', 'Loading State Machine Executions...')
47+
)
48+
49+
try {
50+
yield* client.listExecutions(
51+
{
52+
stateMachineArn: stateMachineArn,
53+
},
54+
resultsToReturn
55+
)
56+
} finally {
57+
if (status) {
58+
status.dispose()
59+
}
60+
}
61+
}
62+
4063
/**
4164
* Checks if the given IAM Role is assumable by AWS Step Functions.
4265
* @param role The IAM role to check

packages/core/src/stepFunctions/vue/executeStateMachine/executeStateMachine.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface StateMachine {
2020
arn: string
2121
name: string
2222
region: string
23+
explorerNode?: StateMachineNode
2324
}
2425

2526
export class ExecuteStateMachineWebview extends VueWebview {
@@ -64,6 +65,7 @@ export class ExecuteStateMachineWebview extends VueWebview {
6465
this.logger.info('started execution for Step Functions State Machine')
6566
this.channel.appendLine(localize('AWS.stepFunctions.executeStateMachine.info.started', 'Execution started'))
6667
this.channel.appendLine(startExecResponse.executionArn || '')
68+
await this.refreshExplorerNode(this.stateMachine.explorerNode)
6769
} catch (e) {
6870
executeResult = 'Failed'
6971
const error = e as Error
@@ -79,6 +81,16 @@ export class ExecuteStateMachineWebview extends VueWebview {
7981
telemetry.stepfunctions_executeStateMachine.emit({ result: executeResult })
8082
}
8183
}
84+
85+
/**
86+
* Assuming the state machine execution was started via a StateMachineNode in the
87+
* AWS Explorer, refresh the list of executions, so this new execution will appear.
88+
*/
89+
private async refreshExplorerNode(node?: StateMachineNode): Promise<void> {
90+
if (node) {
91+
return vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node)
92+
}
93+
}
8294
}
8395

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

93106
await wv.show({

0 commit comments

Comments
 (0)