Skip to content

Commit 358666e

Browse files
authored
feat(stepfunctions): Support calling TestState API from Workflow Studio #6421
## Problem The Workflow Studio webview currently does not allow for calling the [TestState API](https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html). This API is used for testing individual states in isolation, and helps with debugging when constructing a state machine. It is available in the console version of Workflow Studio. ## Solution Adding support for calling APIs from the webview using message passing. This is the added flow: 1. The webview sends a message to the extension to call sfn:TestState or iam:ListRoles 2. The extension performs the API call using its credentials and default credential region 3. The extension sends the response as a message to the webview Note: this PR is dependent on [this PR](#6375) being merged first since it requires an [aws-sdk version update](f4e0893).
1 parent 3925aa7 commit 358666e

File tree

10 files changed

+261
-27
lines changed

10 files changed

+261
-27
lines changed

packages/core/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export async function activate(context: vscode.ExtensionContext) {
196196

197197
await activateStepFunctions(context, globals.awsContext, globals.outputChannel)
198198

199-
await activateStepFunctionsWorkflowStudio(context)
199+
await activateStepFunctionsWorkflowStudio()
200200

201201
await activateRedshift(extContext)
202202

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export class DefaultStepFunctionsClient {
6767
return client.updateStateMachine(params).promise()
6868
}
6969

70+
public async testState(params: StepFunctions.TestStateInput): Promise<StepFunctions.TestStateOutput> {
71+
const client = await this.createSdkClient()
72+
73+
return await client.testState(params).promise()
74+
}
75+
7076
private async createSdkClient(): Promise<StepFunctions> {
7177
return await globals.sdkClientBuilder.createAwsService(StepFunctions, undefined, this.regionCode)
7278
}

packages/core/src/shared/logger/logger.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
import * as vscode from 'vscode'
77

8-
export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'childProcess' | 'unknown'
8+
export type LogTopic =
9+
| 'crashMonitoring'
10+
| 'dev/beta'
11+
| 'notifications'
12+
| 'test'
13+
| 'childProcess'
14+
| 'unknown'
15+
| 'stepfunctions'
916

1017
class ErrorLog {
1118
constructor(

packages/core/src/stepFunctions/workflowStudio/activation.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66
import * as vscode from 'vscode'
77
import { WorkflowStudioEditorProvider } from './workflowStudioEditorProvider'
88
import { Commands } from '../../shared/vscode/commands2'
9+
import { globals } from '../../shared'
910

1011
/**
1112
* Activates the extension and registers all necessary components.
12-
* @param extensionContext The extension context object.
1313
*/
14-
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
15-
extensionContext.subscriptions.push(WorkflowStudioEditorProvider.register(extensionContext))
14+
export async function activate(): Promise<void> {
15+
globals.context.subscriptions.push(WorkflowStudioEditorProvider.register())
1616

1717
// Open the file with Workflow Studio editor in a new tab, or focus on the tab with WFS if it is already open
18-
extensionContext.subscriptions.push(
18+
globals.context.subscriptions.push(
1919
Commands.register('aws.stepfunctions.openWithWorkflowStudio', async (uri: vscode.Uri) => {
2020
await vscode.commands.executeCommand('vscode.openWith', uri, WorkflowStudioEditorProvider.viewType)
2121
})
2222
)
2323

2424
// Close the active editor and open the file with Workflow Studio (or close and switch to the existing relevant tab).
2525
// This command is expected to always be called from the active tab in the default editor mode
26-
extensionContext.subscriptions.push(
26+
globals.context.subscriptions.push(
2727
Commands.register('aws.stepfunctions.switchToWorkflowStudio', async (uri: vscode.Uri) => {
2828
await vscode.commands.executeCommand('workbench.action.closeActiveEditor')
2929
await vscode.commands.executeCommand('vscode.openWith', uri, WorkflowStudioEditorProvider.viewType)

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ import {
1313
FileChangedMessage,
1414
FileChangeEventTrigger,
1515
SyncFileRequestMessage,
16+
ApiCallRequestMessage,
1617
} from './types'
1718
import { submitFeedback } from '../../feedback/vue/submitFeedback'
1819
import { placeholder } from '../../shared/vscode/commands2'
1920
import * as nls from 'vscode-nls'
2021
import vscode from 'vscode'
2122
import { telemetry } from '../../shared/telemetry/telemetry'
2223
import { ToolkitError } from '../../shared/errors'
24+
import { WorkflowStudioApiHandler } from './workflowStudioApiHandler'
25+
import { getLogger, globals } from '../../shared'
2326
const localize = nls.loadMessageBundle()
2427

2528
/**
@@ -48,6 +51,9 @@ export async function handleMessage(message: Message, context: WebviewContext) {
4851
case Command.OPEN_FEEDBACK:
4952
void submitFeedback(placeholder, 'Workflow Studio')
5053
break
54+
case Command.API_CALL:
55+
apiCallMessageHandler(message as ApiCallRequestMessage, context)
56+
break
5157
}
5258
} else if (messageType === MessageType.BROADCAST) {
5359
switch (command) {
@@ -150,7 +156,9 @@ async function saveFileMessageHandler(request: SaveFileRequestMessage, context:
150156
)
151157
)
152158
} catch (err) {
153-
throw ToolkitError.chain(err, 'Could not save asl file.', { code: 'SaveFailed' })
159+
throw ToolkitError.chain(err, 'Could not save asl file.', {
160+
code: 'SaveFailed',
161+
})
154162
}
155163
})
156164
}
@@ -179,7 +187,20 @@ async function autoSyncFileMessageHandler(request: SyncFileRequestMessage, conte
179187
)
180188
await vscode.workspace.applyEdit(edit)
181189
} catch (err) {
182-
throw ToolkitError.chain(err, 'Could not autosave asl file.', { code: 'AutoSaveFailed' })
190+
throw ToolkitError.chain(err, 'Could not autosave asl file.', {
191+
code: 'AutoSaveFailed',
192+
})
183193
}
184194
})
185195
}
196+
197+
/**
198+
* Handler for making API calls from the webview and returning the response.
199+
* @param request The request message containing the API to call and the parameters
200+
* @param context The webview context used for returning the API response to the webview
201+
*/
202+
function apiCallMessageHandler(request: ApiCallRequestMessage, context: WebviewContext) {
203+
const logger = getLogger('stepfunctions')
204+
const apiHandler = new WorkflowStudioApiHandler(globals.awsContext.getCredentialDefaultRegion(), context)
205+
apiHandler.performApiCall(request).catch((error) => logger.error('%s API call failed: %O', request.apiName, error))
206+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5+
import { IAM, StepFunctions } from 'aws-sdk'
56
import * as vscode from 'vscode'
67

78
export type WebviewContext = {
@@ -39,6 +40,7 @@ export enum Command {
3940
LOAD_STAGE = 'LOAD_STAGE',
4041
OPEN_FEEDBACK = 'OPEN_FEEDBACK',
4142
CLOSE_WFS = 'CLOSE_WFS',
43+
API_CALL = 'API_CALL',
4244
}
4345

4446
export type FileWatchInfo = {
@@ -71,3 +73,26 @@ export interface SaveFileRequestMessage extends Message {
7173
export interface SyncFileRequestMessage extends SaveFileRequestMessage {
7274
fileContents: string
7375
}
76+
77+
export enum ApiAction {
78+
IAMListRoles = 'iam:ListRoles',
79+
SFNTestState = 'sfn:TestState',
80+
}
81+
82+
type ApiCallRequestMapping = {
83+
[ApiAction.IAMListRoles]: IAM.ListRolesRequest
84+
[ApiAction.SFNTestState]: StepFunctions.TestStateInput
85+
}
86+
87+
interface ApiCallRequestMessageBase<ApiName extends ApiAction> extends Message {
88+
requestId: string
89+
apiName: ApiName
90+
params: ApiCallRequestMapping[ApiName]
91+
}
92+
93+
/**
94+
* The message from the webview describing what API and parameters to call.
95+
*/
96+
export type ApiCallRequestMessage =
97+
| ApiCallRequestMessageBase<ApiAction.IAMListRoles>
98+
| ApiCallRequestMessageBase<ApiAction.SFNTestState>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { IAM, StepFunctions } from 'aws-sdk'
7+
import { DefaultIamClient } from '../../shared/clients/iamClient'
8+
import { DefaultStepFunctionsClient } from '../../shared/clients/stepFunctionsClient'
9+
import { ApiAction, ApiCallRequestMessage, Command, MessageType, WebviewContext } from './types'
10+
import { telemetry } from '../../shared/telemetry/telemetry'
11+
12+
export class WorkflowStudioApiHandler {
13+
public constructor(
14+
region: string,
15+
private readonly context: WebviewContext,
16+
private readonly clients = {
17+
sfn: new DefaultStepFunctionsClient(region),
18+
iam: new DefaultIamClient(region),
19+
}
20+
) {}
21+
22+
/**
23+
* Performs the API call on behalf of the webview, and sends the sucesss or error response to the webview.
24+
*/
25+
public async performApiCall({ apiName, params, requestId }: ApiCallRequestMessage): Promise<void> {
26+
try {
27+
let response
28+
switch (apiName) {
29+
case ApiAction.IAMListRoles:
30+
response = await this.listRoles(params)
31+
break
32+
case ApiAction.SFNTestState:
33+
response = await this.testState(params)
34+
break
35+
default:
36+
throw new Error(`Unknown API: ${apiName}`)
37+
}
38+
39+
await this.context.panel.webview.postMessage({
40+
messageType: MessageType.RESPONSE,
41+
command: Command.API_CALL,
42+
apiName,
43+
response,
44+
requestId,
45+
isSuccess: true,
46+
})
47+
} catch (err) {
48+
await this.context.panel.webview.postMessage({
49+
messageType: MessageType.RESPONSE,
50+
command: Command.API_CALL,
51+
apiName,
52+
error:
53+
err instanceof Error
54+
? {
55+
message: err.message,
56+
name: err.name,
57+
stack: err.stack,
58+
}
59+
: {
60+
message: String(err),
61+
},
62+
requestId,
63+
isSuccess: false,
64+
})
65+
}
66+
}
67+
68+
public async testState(params: StepFunctions.TestStateInput): Promise<StepFunctions.TestStateOutput> {
69+
telemetry.ui_click.emit({
70+
elementId: 'stepfunctions_testState',
71+
})
72+
return this.clients.sfn.testState(params)
73+
}
74+
75+
public async listRoles(params: IAM.ListRolesRequest): Promise<IAM.Role[]> {
76+
return this.clients.iam.listRoles(params)
77+
}
78+
}

packages/core/src/stepFunctions/workflowStudio/workflowStudioEditor.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CancellationError } from '../../shared/utilities/timeoutUtils'
1313
import { handleMessage } from './handleMessage'
1414
import { isInvalidJsonFile } from '../utils'
1515
import { setContext } from '../../shared/vscode/setContext'
16+
import { globals } from '../../shared'
1617

1718
/**
1819
* The main class for Workflow Studio Editor. This class handles the creation and management
@@ -37,7 +38,6 @@ export class WorkflowStudioEditor {
3738
public constructor(
3839
textDocument: vscode.TextDocument,
3940
webviewPanel: vscode.WebviewPanel,
40-
context: vscode.ExtensionContext,
4141
fileId: string,
4242
getWebviewContent: () => Promise<string>
4343
) {
@@ -54,7 +54,7 @@ export class WorkflowStudioEditor {
5454
id: this.fileId,
5555
})
5656

57-
this.setupWebviewPanel(textDocument, context)
57+
this.setupWebviewPanel(textDocument)
5858
}
5959

6060
public get onVisualizationDisposeEvent(): vscode.Event<void> {
@@ -70,11 +70,11 @@ export class WorkflowStudioEditor {
7070
this.getPanel()?.reveal()
7171
}
7272

73-
public async refreshPanel(context: vscode.ExtensionContext) {
73+
public async refreshPanel() {
7474
if (!this.isPanelDisposed) {
7575
this.webviewPanel.dispose()
7676
const document = await vscode.workspace.openTextDocument(this.documentUri)
77-
this.setupWebviewPanel(document, context)
77+
this.setupWebviewPanel(document)
7878
}
7979
}
8080

@@ -87,10 +87,9 @@ export class WorkflowStudioEditor {
8787
* panel, setting up the webview content, and handling the communication between the webview
8888
* and the extension context.
8989
* @param textDocument The text document to be displayed in the webview panel.
90-
* @param context The extension context.
9190
* @private
9291
*/
93-
private setupWebviewPanel(textDocument: vscode.TextDocument, context: vscode.ExtensionContext) {
92+
private setupWebviewPanel(textDocument: vscode.TextDocument) {
9493
const documentUri = textDocument.uri
9594

9695
const contextObject: WebviewContext = {
@@ -131,7 +130,7 @@ export class WorkflowStudioEditor {
131130
// Initialise webview panel for Workflow Studio and set up initial content
132131
this.webviewPanel.webview.options = {
133132
enableScripts: true,
134-
localResourceRoots: [context.extensionUri],
133+
localResourceRoots: [globals.context.extensionUri],
135134
}
136135

137136
// Set the initial html for the webpage
@@ -183,9 +182,9 @@ export class WorkflowStudioEditor {
183182
this.isPanelDisposed = true
184183
resolve()
185184
this.onVisualizationDisposeEmitter.fire()
186-
this.disposables.forEach((disposable) => {
185+
for (const disposable of this.disposables) {
187186
disposable.dispose()
188-
})
187+
}
189188
this.onVisualizationDisposeEmitter.dispose()
190189
})
191190
)

packages/core/src/stepFunctions/workflowStudio/workflowStudioEditorProvider.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
3535
* @remarks This should only be called once per extension.
3636
* @param context The extension context
3737
*/
38-
public static register(context: vscode.ExtensionContext): vscode.Disposable {
39-
const provider = new WorkflowStudioEditorProvider(context)
38+
public static register(): vscode.Disposable {
39+
const provider = new WorkflowStudioEditorProvider()
4040
return vscode.window.registerCustomEditorProvider(WorkflowStudioEditorProvider.viewType, provider, {
4141
webviewOptions: {
4242
enableFindWidget: true,
@@ -45,13 +45,11 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
4545
})
4646
}
4747

48-
protected extensionContext: vscode.ExtensionContext
4948
protected webviewHtml: string
5049
protected readonly managedVisualizations = new Map<string, WorkflowStudioEditor>()
5150
protected readonly logger = getLogger()
5251

53-
constructor(context: vscode.ExtensionContext) {
54-
this.extensionContext = context
52+
constructor() {
5553
this.webviewHtml = ''
5654
}
5755

@@ -65,7 +63,7 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
6563
this.webviewHtml = await response.text()
6664

6765
for (const visualization of this.managedVisualizations.values()) {
68-
await visualization.refreshPanel(this.extensionContext)
66+
await visualization.refreshPanel()
6967
}
7068
}
7169

@@ -98,7 +96,7 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
9896
htmlFileSplit = html.split('<body>')
9997

10098
const script = await fs.readFileText(
101-
vscode.Uri.joinPath(this.extensionContext.extensionUri, 'resources', 'js', 'vsCodeExtensionInterface.js')
99+
vscode.Uri.joinPath(globals.context.extensionUri, 'resources', 'js', 'vsCodeExtensionInterface.js')
102100
)
103101

104102
return `${htmlFileSplit[0]} <body> <script nonce='${nonce}'>${script}</script> ${htmlFileSplit[1]}`
@@ -151,7 +149,6 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
151149
const newVisualization = new WorkflowStudioEditor(
152150
document,
153151
webviewPanel,
154-
this.extensionContext,
155152
fileId,
156153
this.getWebviewContent
157154
)
@@ -171,6 +168,6 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
171168
const visualizationDisposable = visualization.onVisualizationDisposeEvent(() => {
172169
this.managedVisualizations.delete(key)
173170
})
174-
this.extensionContext.subscriptions.push(visualizationDisposable)
171+
globals.context.subscriptions.push(visualizationDisposable)
175172
}
176173
}

0 commit comments

Comments
 (0)