Skip to content

Commit 3ebe98b

Browse files
authored
Adds visualisation of yaml state machine definitions (#1099)
Authored-by: Alan Bogusiewicz <[email protected]>
1 parent 385da2e commit 3ebe98b

File tree

8 files changed

+240
-29
lines changed

8 files changed

+240
-29
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Add basic visualisation capability for step function state machines defined in YAML."
4+
}

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,8 @@
944944
"vue": "^2.5.16",
945945
"winston": "^3.2.1",
946946
"winston-transport": "^4.3.0",
947-
"xml2js": "^0.4.19"
947+
"xml2js": "^0.4.19",
948+
"yaml": "^1.9.2"
948949
},
949950
"prettier": {
950951
"printWidth": 120,

src/integrationTest/stepFunctions/visualizeStateMachine.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,21 @@ const sampleStateMachine = `
4747
"End": true
4848
}
4949
}
50-
}`
50+
}`
51+
52+
const samleStateMachineYaml = `
53+
Comment: "A Hello World example of the Amazon States Language using Pass states"
54+
StartAt: Hello
55+
States:
56+
Hello:
57+
Type: Pass
58+
Result: Hello
59+
Next: World
60+
World:
61+
Type: Pass
62+
Result: \$\{Text\}
63+
End: true
64+
`
5165

5266
let tempFolder: string
5367

@@ -138,6 +152,29 @@ describe('visualizeStateMachine', async () => {
138152
}
139153
})
140154

155+
it('correctly displays content when given a sample state machine in yaml', async () => {
156+
const fileName = 'mysamplestatemachine.yaml'
157+
const textEditor = await openATextEditorWithText(samleStateMachineYaml, fileName)
158+
159+
const result = await vscode.commands.executeCommand<vscode.WebviewPanel>('aws.previewStateMachine')
160+
161+
assert.ok(result)
162+
163+
await waitUntilWebviewIsVisible(result)
164+
165+
let expectedViewColumn
166+
if (textEditor.viewColumn) {
167+
expectedViewColumn = textEditor.viewColumn.valueOf() + 1
168+
}
169+
170+
if (result) {
171+
assert.deepStrictEqual(result.title, 'Graph: ' + fileName)
172+
assert.deepStrictEqual(result.viewColumn, expectedViewColumn)
173+
assert.deepStrictEqual(result.viewType, 'stateMachineVisualization')
174+
assert.ok(result.webview.html)
175+
}
176+
})
177+
141178
it('update webview is triggered when user saves correct text editor', async () => {
142179
const stateMachineFileText = '{}'
143180
const fileName = 'mysamplestatemachine.json'

src/stepFunctions/commands/visualizeStateMachine/aslVisualization.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import * as vscode from 'vscode'
1111
import { ext } from '../../../shared/extensionGlobals'
1212
import { getLogger, Logger } from '../../../shared/logger'
1313
import { isDocumentValid } from '../../utils'
14+
import * as yaml from 'yaml'
15+
16+
const YAML_OPTIONS: yaml.Options = {
17+
merge: false,
18+
maxAliasCount: 0,
19+
schema: 'yaml-1.1',
20+
version: '1.1',
21+
prettyErrors: true,
22+
}
1423

1524
export interface MessageObject {
1625
command: string
@@ -51,6 +60,44 @@ export class AslVisualization {
5160
this.getPanel()?.reveal()
5261
}
5362

63+
public async sendUpdateMessage(updatedTextDocument: vscode.TextDocument) {
64+
const logger: Logger = getLogger()
65+
const isYaml = updatedTextDocument.languageId === 'yaml'
66+
const text = updatedTextDocument.getText()
67+
let stateMachineData = text
68+
let yamlErrors: string[] = []
69+
70+
if (isYaml) {
71+
const parsed = yaml.parseDocument(text, YAML_OPTIONS)
72+
yamlErrors = parsed.errors.map(error => error.message)
73+
let json: any
74+
75+
try {
76+
json = parsed.toJSON()
77+
} catch (e) {
78+
yamlErrors.push(e.message)
79+
}
80+
81+
stateMachineData = JSON.stringify(json)
82+
}
83+
84+
const isValid = (await isDocumentValid(stateMachineData, updatedTextDocument)) && !yamlErrors.length
85+
86+
const webview = this.getWebview()
87+
if (this.isPanelDisposed || !webview) {
88+
return
89+
}
90+
91+
logger.debug('Sending update message to webview.')
92+
93+
webview.postMessage({
94+
command: 'update',
95+
stateMachineData,
96+
isValid,
97+
errors: yamlErrors,
98+
})
99+
}
100+
54101
private setupWebviewPanel(textDocument: vscode.TextDocument): vscode.WebviewPanel {
55102
const documentUri = textDocument.uri
56103
const logger: Logger = getLogger()
@@ -78,7 +125,7 @@ export class AslVisualization {
78125
this.disposables.push(
79126
vscode.workspace.onDidSaveTextDocument(async savedTextDocument => {
80127
if (savedTextDocument && savedTextDocument.uri.path === documentUri.path) {
81-
await sendUpdateMessage(savedTextDocument)
128+
await this.sendUpdateMessage(savedTextDocument)
82129
}
83130
})
84131
)
@@ -98,22 +145,7 @@ export class AslVisualization {
98145
})
99146
)
100147

101-
const sendUpdateMessage = async (updatedTextDocument: vscode.TextDocument) => {
102-
const isValid = await isDocumentValid(updatedTextDocument)
103-
const webview = this.getWebview()
104-
if (this.isPanelDisposed || !webview) {
105-
return
106-
}
107-
108-
logger.debug('Sending update message to webview.')
109-
110-
webview.postMessage({
111-
command: 'update',
112-
stateMachineData: updatedTextDocument.getText(),
113-
isValid,
114-
})
115-
}
116-
const debouncedUpdate = debounce(sendUpdateMessage.bind(this), 500)
148+
const debouncedUpdate = debounce(this.sendUpdateMessage.bind(this), 500)
117149

118150
this.disposables.push(
119151
vscode.workspace.onDidChangeTextDocument(async textDocumentEvent => {
@@ -136,7 +168,7 @@ export class AslVisualization {
136168
case 'webviewRendered': {
137169
// Webview has finished rendering, so now we can give it our
138170
// initial state machine definition.
139-
await sendUpdateMessage(textDocument)
171+
await this.sendUpdateMessage(textDocument)
140172
break
141173
}
142174

src/stepFunctions/utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,11 @@ export function isStepFunctionsRole(role: IAM.Role): boolean {
183183
return !!assumeRolePolicyDocument?.includes(STEP_FUNCTIONS_SEVICE_PRINCIPAL)
184184
}
185185

186-
export async function isDocumentValid(textDocument?: vscode.TextDocument): Promise<boolean> {
187-
if (!textDocument) {
186+
export async function isDocumentValid(text: string, textDocument?: vscode.TextDocument): Promise<boolean> {
187+
if (!textDocument || !text) {
188188
return false
189189
}
190190

191-
const text = textDocument.getText()
192191
const doc = ASLTextDocument.create(textDocument.uri.path, textDocument.languageId, textDocument.version, text)
193192
// tslint:disable-next-line: no-inferred-empty-object-type
194193
const jsonDocument = languageService.parseJSONDocument(doc)

src/test/stepFunctions/commands/visualizeStateMachine.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,78 @@ const mockTextDocumentTwo: vscode.TextDocument = {
9191
validateRange: sinon.spy(),
9292
}
9393

94+
const mockUriThree: vscode.Uri = {
95+
authority: 'MockAuthorityYaml',
96+
fragment: 'MockFragmentYaml',
97+
fsPath: 'MockFSPathYaml',
98+
query: 'MockQueryYaml',
99+
path: 'MockPathYaml',
100+
scheme: 'MockSchemeYaml',
101+
with: sinon.spy(),
102+
toJSON: sinon.spy(),
103+
}
104+
105+
const mockDataJson =
106+
'{"Comment":"A Hello World example of the Amazon States Language using Pass states","StartAt":"Hello","States":{"Hello":{"Type":"Pass","Result":"Hello","Next":"World"},"World":{"Type":"Pass","Result":"${Text}","End":true}}}'
107+
108+
const mockDataYaml = `
109+
Comment: "A Hello World example of the Amazon States Language using Pass states"
110+
StartAt: Hello
111+
States:
112+
Hello:
113+
Type: Pass
114+
Result: Hello
115+
Next: World
116+
World:
117+
Type: Pass
118+
Result: \$\{Text\}
119+
End: true
120+
`
121+
122+
const mockTextDocumentYaml: vscode.TextDocument = {
123+
eol: 1,
124+
fileName: 'MockFileNameYaml',
125+
isClosed: false,
126+
isDirty: false,
127+
isUntitled: false,
128+
languageId: 'yaml',
129+
lineCount: 0,
130+
uri: mockUriThree,
131+
version: 0,
132+
getText: () => {
133+
return mockDataYaml
134+
},
135+
getWordRangeAtPosition: sinon.spy(),
136+
lineAt: sinon.spy(),
137+
offsetAt: sinon.spy(),
138+
positionAt: sinon.spy(),
139+
save: sinon.spy(),
140+
validatePosition: sinon.spy(),
141+
validateRange: sinon.spy(),
142+
}
143+
144+
const mockTextDocumentJson: vscode.TextDocument = {
145+
eol: 1,
146+
fileName: 'MockFileNameJson',
147+
isClosed: false,
148+
isDirty: false,
149+
isUntitled: false,
150+
languageId: 'asl',
151+
lineCount: 0,
152+
uri: mockUriThree,
153+
version: 0,
154+
getText: () => {
155+
return mockDataJson
156+
},
157+
getWordRangeAtPosition: sinon.spy(),
158+
lineAt: sinon.spy(),
159+
offsetAt: sinon.spy(),
160+
positionAt: sinon.spy(),
161+
save: sinon.spy(),
162+
validatePosition: sinon.spy(),
163+
validateRange: sinon.spy(),
164+
}
165+
94166
const mockPosition: vscode.Position = {
95167
line: 0,
96168
character: 0,
@@ -339,6 +411,52 @@ describe('StepFunctions VisualizeStateMachine', async () => {
339411

340412
assert.strictEqual(aslVisualizationManager.getManagedVisualizations().size, 1)
341413
})
414+
415+
it('Test AslVisualisation sendUpdateMessage posts a correct update message for YAML files', async () => {
416+
const postMessage = sinon.spy()
417+
class MockAslVisualizationYaml extends AslVisualization {
418+
public getWebview(): vscode.Webview | undefined {
419+
return ({ postMessage } as unknown) as vscode.Webview
420+
}
421+
}
422+
423+
const visualisation = new MockAslVisualizationYaml(mockTextDocumentYaml)
424+
425+
await visualisation.sendUpdateMessage(mockTextDocumentYaml)
426+
427+
const message = {
428+
command: 'update',
429+
stateMachineData: mockDataJson,
430+
isValid: true,
431+
errors: [],
432+
}
433+
434+
assert.ok(postMessage.calledOnce)
435+
assert.deepEqual(postMessage.firstCall.args, [message])
436+
})
437+
438+
it('Test AslVisualisation sendUpdateMessage posts a correct update message for ASL files', async () => {
439+
const postMessage = sinon.spy()
440+
class MockAslVisualizationJson extends AslVisualization {
441+
public getWebview(): vscode.Webview | undefined {
442+
return ({ postMessage } as unknown) as vscode.Webview
443+
}
444+
}
445+
446+
const visualisation = new MockAslVisualizationJson(mockTextDocumentJson)
447+
448+
await visualisation.sendUpdateMessage(mockTextDocumentJson)
449+
450+
const message = {
451+
command: 'update',
452+
stateMachineData: mockDataJson,
453+
isValid: true,
454+
errors: [],
455+
}
456+
457+
assert.ok(postMessage.calledOnce)
458+
assert.deepEqual(postMessage.firstCall.args, [message])
459+
})
342460
})
343461

344462
class MockAslVisualization extends AslVisualization {

src/test/stepFunctions/utils.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,8 @@ describe('isDocumentValid', async () => {
234234
} `
235235

236236
let textDocument = await vscode.workspace.openTextDocument({ language: 'asl' })
237-
textDocument = Object.assign({}, textDocument, { getText: () => aslText })
238237

239-
const isValid = await isDocumentValid(textDocument)
238+
const isValid = await isDocumentValid(aslText, textDocument)
240239
assert.ok(isValid)
241240
})
242241

@@ -254,9 +253,8 @@ describe('isDocumentValid', async () => {
254253
} `
255254

256255
let textDocument = await vscode.workspace.openTextDocument({ language: 'asl' })
257-
textDocument = Object.assign({}, textDocument, { getText: () => aslText })
258256

259-
const isValid = await isDocumentValid(textDocument)
257+
const isValid = await isDocumentValid(aslText, textDocument)
260258
assert.ok(isValid)
261259
})
262260

@@ -274,9 +272,8 @@ describe('isDocumentValid', async () => {
274272
} `
275273

276274
let textDocument = await vscode.workspace.openTextDocument({ language: 'asl' })
277-
textDocument = Object.assign({}, textDocument, { getText: () => aslText })
278275

279-
const isValid = await isDocumentValid(textDocument)
276+
const isValid = await isDocumentValid(aslText, textDocument)
280277

281278
assert.ok(!isValid)
282279
})

0 commit comments

Comments
 (0)