Skip to content

Commit

Permalink
feat(stepfunctions): handle launching with invalid JSON file, refacto…
Browse files Browse the repository at this point in the history
…r save metric
  • Loading branch information
Vlad Nikolaenko committed Nov 21, 2024
1 parent f48080f commit 3c166d7
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 43 deletions.
6 changes: 6 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3653,6 +3653,12 @@
"key": "ctrl+shift+v",
"mac": "cmd+shift+v",
"when": "editorTextFocus && editorLangId == asl || editorTextFocus && editorLangId == asl-yaml"
},
{
"command": "noop",
"key": "ctrl+z",
"mac": "cmd+z",
"when": "aws.stepFunctions.isWorkflowStudioFocused"
}
],
"grammars": [
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"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",
"AWS.stepFunctions.workflowStudio.actions.saveSuccessMessage": "{0} has been saved",
"AWS.stepFunctions.workflowStudio.actions.invalidJson": "The Workflow Studio editor was not opened because the JSON in the file is invalid. To access Workflow Studio, please fix the JSON and manually reopen the integration.",
"AWS.configuration.description.awssam.debug.api": "API Gateway configuration",
"AWS.configuration.description.awssam.debug.api.clientCertId": "The API Gateway client certificate ID",
"AWS.configuration.description.awssam.debug.api.headers": "Additional HTTP headers",
Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/shared/telemetry/vscodeTelemetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
"allowedValues": ["Create", "Update"],
"description": "SSM Publish Document operation type"
},
{
"name": "stepFunctionsSyncType",
"type": "string",
"allowedValues": ["manualSaveWFS", "manualSaveVSCode", "autoSyncWFS"],
"description": "The action type that triggered the save or sync event between SFN Workflow Studio and the local source file."
},
{
"name": "isInvalidJson",
"type": "boolean",
"description": "Indicates whether the message contains an invalid JSON definition."
},
{
"name": "starterTemplate",
"type": "string",
Expand Down Expand Up @@ -527,20 +538,19 @@
]
},
{
"name": "stepfunctions_saveFile",
"description": "Called after the user saves local ASL file (inlcuding autosave) from VSCode editor or Workflow Studio",
"name": "stepfunctions_syncFile",
"description": "Triggered when Workflow Studio auto-syncs to the local file or when unsaved local changes are saved from Workflow Studio or VSCode.",
"metadata": [
{
"type": "id",
"required": true
},
{
"type": "saveType",
"type": "stepFunctionsSyncType",
"required": true
},
{
"type": "source",
"required": true
"type": "isInvalidJson"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/shared/vscode/setContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type contextKey =
| 'aws.explorer.showAuthView'
| 'aws.toolkit.amazonq.dismissed'
| 'aws.toolkit.amazonqInstall.dismissed'
| 'aws.stepFunctions.isWorkflowStudioFocused'
// Deprecated/legacy names. New keys should start with "aws.".
| 'codewhisperer.activeLine'
| 'gumby.isPlanAvailable'
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/stepFunctions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,32 @@ export async function isDocumentValid(text: string, textDocument?: vscode.TextDo
return isValid
}

/**
* Checks if the JSON content in a text document is invalid.
* Returns `true` for invalid JSON; `false` for valid JSON, empty content, or non-JSON files.
*
* @param textDocument - The text document to check.
* @returns `true` if invalid; `false` otherwise.
*/
export const isInvalidJsonFile = (textDocument: vscode.TextDocument): boolean => {
const fileExtension = textDocument.fileName.split('.').pop()?.toLowerCase()

if (fileExtension === 'json') {
const jsonFileContent = textDocument.getText().trim()
// An empty file or whitespace-only text is considered valid JSON for our use case
if (!jsonFileContent) {
return false
}
try {
JSON.parse(jsonFileContent)
return false
} catch {
return true
}
}
return false
}

const descriptor = {
maxItemsComputed: (v: unknown) => Math.trunc(Math.max(0, Number(v))),
['format.enable']: Boolean,
Expand Down
40 changes: 17 additions & 23 deletions packages/core/src/stepFunctions/workflowStudio/handleMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
InitResponseMessage,
FileChangedMessage,
FileChangeEventTrigger,
SyncFileRequestMessage,
} from './types'
import { submitFeedback } from '../../feedback/vue/submitFeedback'
import { placeholder } from '../../shared/vscode/commands2'
Expand All @@ -38,8 +39,8 @@ export async function handleMessage(message: Message, context: WebviewContext) {
case Command.SAVE_FILE:
void saveFileMessageHandler(message as SaveFileRequestMessage, context)
break
case Command.AUTO_SAVE_FILE:
void autoSaveFileMessageHandler(message as SaveFileRequestMessage, context)
case Command.AUTO_SYNC_FILE:
void autoSyncFileMessageHandler(message as SyncFileRequestMessage, context)
break
case Command.CLOSE_WFS:
void closeCustomEditorMessageHandler(context)
Expand Down Expand Up @@ -131,17 +132,15 @@ export function closeCustomEditorMessageHandler(context: WebviewContext) {
* @param context The webview context containing the necessary information for saving the file.
*/
async function saveFileMessageHandler(request: SaveFileRequestMessage, context: WebviewContext) {
await telemetry.stepfunctions_saveFile.run(async (span) => {
await telemetry.stepfunctions_syncFile.run(async (span) => {
span.record({
id: context.fileId,
saveType: 'MANUAL_SAVE',
source: 'WORKFLOW_STUDIO',
stepFunctionsSyncType: 'manualSaveWFS',
isInvalidJson: request.isInvalidJson,
})

try {
await saveWorkspace(context, request.fileContents)
await context.textDocument.save()

void vscode.window.showInformationMessage(
localize(
'AWS.stepFunctions.workflowStudio.actions.saveSuccessMessage',
Expand All @@ -161,29 +160,24 @@ async function saveFileMessageHandler(request: SaveFileRequestMessage, context:
* @param request The request message containing the file contents.
* @param context The webview context containing the necessary information for saving the file.
*/
async function autoSaveFileMessageHandler(request: SaveFileRequestMessage, context: WebviewContext) {
await telemetry.stepfunctions_saveFile.run(async (span) => {
async function autoSyncFileMessageHandler(request: SyncFileRequestMessage, context: WebviewContext) {
await telemetry.stepfunctions_syncFile.run(async (span) => {
span.record({
id: context.fileId,
saveType: 'AUTO_SAVE',
source: 'WORKFLOW_STUDIO',
stepFunctionsSyncType: 'autoSyncWFS',
isInvalidJson: request.isInvalidJson,
})

try {
await saveWorkspace(context, request.fileContents)
const edit = new vscode.WorkspaceEdit()
edit.replace(
context.textDocument.uri,
new vscode.Range(0, 0, context.textDocument.lineCount, 0),
request.fileContents
)
await vscode.workspace.applyEdit(edit)
} catch (err) {
throw ToolkitError.chain(err, 'Could not autosave asl file.', { code: 'AutoSaveFailed' })
}
})
}

/**
* Saves to the workspace with the provided file contents.
* @param context The webview context containing the necessary information for saving the file.
* @param fileContents The file contents to save.
*/
async function saveWorkspace(context: WebviewContext, fileContents: string) {
const edit = new vscode.WorkspaceEdit()
edit.replace(context.textDocument.uri, new vscode.Range(0, 0, context.textDocument.lineCount, 0), fileContents)
await vscode.workspace.applyEdit(edit)
}
12 changes: 5 additions & 7 deletions packages/core/src/stepFunctions/workflowStudio/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export enum MessageType {
export enum Command {
INIT = 'INIT',
SAVE_FILE = 'SAVE_FILE',
AUTO_SAVE_FILE = 'AUTO_SAVE_FILE',
AUTO_SYNC_FILE = 'AUTO_SYNC_FILE',
FILE_CHANGED = 'FILE_CHANGED',
LOAD_STAGE = 'LOAD_STAGE',
OPEN_FEEDBACK = 'OPEN_FEEDBACK',
Expand All @@ -45,12 +45,6 @@ export type FileWatchInfo = {
fileContents: string
}

export enum SaveCompleteSubType {
SAVED = 'SAVED',
SAVE_SKIPPED_SAME_CONTENT = 'SAVE_SKIPPED_SAME_CONTENT',
SAVE_FAILED = 'SAVE_FAILED',
}

export interface Message {
command: Command
messageType: MessageType
Expand All @@ -71,5 +65,9 @@ export interface InitResponseMessage extends Omit<FileChangedMessage, 'trigger'>
}

export interface SaveFileRequestMessage extends Message {
isInvalidJson: boolean
}

export interface SyncFileRequestMessage extends SaveFileRequestMessage {
fileContents: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { broadcastFileChange } from './handleMessage'
import { FileWatchInfo, WebviewContext } from './types'
import { CancellationError } from '../../shared/utilities/timeoutUtils'
import { handleMessage } from './handleMessage'
import { isInvalidJsonFile } from '../utils'
import { setContext } from '../../shared/vscode/setContext'

/**
* The main class for Workflow Studio Editor. This class handles the creation and management
Expand Down Expand Up @@ -139,11 +141,11 @@ export class WorkflowStudioEditor {
// The text document acts as our model, thus we send and event to the webview on file save to trigger update
contextObject.disposables.push(
vscode.workspace.onDidSaveTextDocument(async () => {
await telemetry.stepfunctions_saveFile.run(async (span) => {
await telemetry.stepfunctions_syncFile.run(async (span) => {
span.record({
id: contextObject.fileId,
saveType: 'MANUAL_SAVE',
source: 'VSCODE',
stepFunctionsSyncType: 'manualSaveVSCode',
isInvalidJson: isInvalidJsonFile(contextObject.textDocument),
})
await broadcastFileChange(contextObject, 'MANUAL_SAVE')
})
Expand All @@ -152,18 +154,31 @@ export class WorkflowStudioEditor {

// Handle messages from the webview
this.disposables.push(
this.webviewPanel.webview.onDidReceiveMessage((message) =>
handleMessage(message, contextObject)
)
this.webviewPanel.webview.onDidReceiveMessage(async (message) => {
await handleMessage(message, contextObject)
})
)

// Track webview focus to suppress VSCode's default undo, as WFS has its own
await setContext('aws.stepFunctions.isWorkflowStudioFocused', true)
this.disposables.push(
this.webviewPanel.onDidChangeViewState(async (event) => {
if (event.webviewPanel.active) {
await setContext('aws.stepFunctions.isWorkflowStudioFocused', true)
} else {
await setContext('aws.stepFunctions.isWorkflowStudioFocused', false)
}
})
)

// When the panel is closed, dispose of any disposables/remove subscriptions
this.disposables.push(
this.webviewPanel.onDidDispose(() => {
this.webviewPanel.onDidDispose(async () => {
if (this.isPanelDisposed) {
return
}

await setContext('aws.stepFunctions.isWorkflowStudioFocused', false)
this.isPanelDisposed = true
resolve()
this.onVisualizationDisposeEmitter.fire()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import globals from '../../shared/extensionGlobals'
import { getRandomString, getStringHash } from '../../shared/utilities/textUtilities'
import { ToolkitError } from '../../shared/errors'
import { WorkflowStudioEditor } from './workflowStudioEditor'
import { i18n } from '../../shared/i18n-helper'
import { isInvalidJsonFile } from '../utils'

// TODO: switch to production mode: change isLocalDev to false and add CDN link
const isLocalDev = true
Expand All @@ -30,7 +32,7 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
public static readonly viewType = 'workflowStudio.asl'

/**
* Registers a new custom editor provider for `.tc.json` files.
* Registers a new custom editor provider for asl files.
* @remarks This should only be called once per extension.
* @param context The extension context
*/
Expand Down Expand Up @@ -115,6 +117,21 @@ export class WorkflowStudioEditorProvider implements vscode.CustomTextEditorProv
_token: vscode.CancellationToken
): Promise<void> {
await telemetry.stepfunctions_openWorkflowStudio.run(async () => {
// For invalid JSON, open default editor and show warning message
if (isInvalidJsonFile(document)) {
await vscode.commands.executeCommand('vscode.openWith', document.uri, 'default')
webviewPanel.dispose()
void vscode.window.showWarningMessage(i18n('AWS.stepFunctions.workflowStudio.actions.invalidJson'))

throw ToolkitError.chain(
'Invalid JSON file',
'The Workflow Studio editor was not opened because the JSON in the file is invalid',
{
code: 'InvalidJSONContent',
}
)
}

if (!this.webviewHtml) {
await this.fetchWebviewHtml()
}
Expand Down

0 comments on commit 3c166d7

Please sign in to comment.