@@ -21,6 +21,9 @@ import { getEnvironmentSpecificMemento } from '../shared/utilities/mementos'
2121import { setContext } from '../shared'
2222import { telemetry } from '../shared/telemetry'
2323import { getSessionId } from '../shared/telemetry/util'
24+ import { NotificationsController } from '../notifications/controller'
25+ import { DevNotificationsState } from '../notifications/types'
26+ import { QuickPickItem } from 'vscode'
2427
2528interface MenuOption {
2629 readonly label : string
@@ -34,21 +37,25 @@ export type DevFunction =
3437 | 'openTerminal'
3538 | 'deleteDevEnv'
3639 | 'editStorage'
40+ | 'resetState'
3741 | 'showEnvVars'
3842 | 'deleteSsoConnections'
3943 | 'expireSsoConnections'
4044 | 'editAuthConnections'
45+ | 'notificationsSend'
4146 | 'forceIdeCrash'
4247
4348export type DevOptions = {
4449 context : vscode . ExtensionContext
45- auth : Auth
50+ auth : ( ) => Auth
51+ notificationsController : ( ) => NotificationsController
4652 menuOptions ?: DevFunction [ ]
4753}
4854
4955let targetContext : vscode . ExtensionContext
5056let globalState : vscode . Memento
5157let targetAuth : Auth
58+ let targetNotificationsController : NotificationsController
5259
5360/**
5461 * Defines AWS Toolkit developer tools.
@@ -83,6 +90,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
8390 detail : 'Shows all globalState values, or edit a globalState/secret item' ,
8491 executor : openStorageFromInput ,
8592 } ,
93+ resetState : {
94+ label : 'Reset feature state' ,
95+ detail : 'Quick reset the state of extension components or features' ,
96+ executor : resetState ,
97+ } ,
8698 showEnvVars : {
8799 label : 'Show Environment Variables' ,
88100 description : 'AWS Toolkit' ,
@@ -104,6 +116,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
104116 detail : 'Opens editor to all Auth Connections the extension is using.' ,
105117 executor : editSsoConnections ,
106118 } ,
119+ notificationsSend : {
120+ label : 'Notifications: Send Notifications' ,
121+ detail : 'Send JSON notifications for testing.' ,
122+ executor : editNotifications ,
123+ } ,
107124 forceIdeCrash : {
108125 label : 'Crash: Force IDE ExtHost Crash' ,
109126 detail : `Will SIGKILL ExtHost, { pid: ${ process . pid } , sessionId: '${ getSessionId ( ) . slice ( 0 , 8 ) } -...' }, but the IDE itself will not crash.` ,
@@ -156,14 +173,19 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
156173 vscode . workspace . registerTextDocumentContentProvider ( 'aws-dev2' , new DevDocumentProvider ( ) ) ,
157174 // "AWS (Developer): Open Developer Menu"
158175 vscode . commands . registerCommand ( 'aws.dev.openMenu' , async ( ) => {
159- await vscode . commands . executeCommand ( '_aws.dev.invokeMenu' , { context : ctx , auth : Auth . instance } )
176+ await vscode . commands . executeCommand ( '_aws.dev.invokeMenu' , {
177+ context : ctx ,
178+ auth : ( ) => Auth . instance ,
179+ notificationsController : ( ) => NotificationsController . instance ,
180+ } )
160181 } ) ,
161182 // Internal command to open dev menu for a specific context and options
162183 vscode . commands . registerCommand ( '_aws.dev.invokeMenu' , ( opts : DevOptions ) => {
163184 targetContext = opts . context
164185 // eslint-disable-next-line aws-toolkits/no-banned-usages
165186 globalState = targetContext . globalState
166- targetAuth = opts . auth
187+ targetAuth = opts . auth ( )
188+ targetNotificationsController = opts . notificationsController ( )
167189 const options = menuOptions ( )
168190 void openMenu (
169191 entries ( options )
@@ -302,7 +324,7 @@ class ObjectEditor {
302324 vscode . workspace . registerFileSystemProvider ( ObjectEditor . scheme , this . fs )
303325 }
304326
305- public async openStorage ( type : 'globalsView' | 'globals' | 'secrets' | 'auth' , key : string ) : Promise < void > {
327+ public async openStorage ( type : 'globalsView' | 'globals' | 'secrets' | 'auth' , key : string ) {
306328 switch ( type ) {
307329 case 'globalsView' :
308330 return showState ( 'globalstate' )
@@ -316,17 +338,19 @@ class ObjectEditor {
316338 }
317339 }
318340
319- private async openState ( storage : vscode . Memento | vscode . SecretStorage , key : string ) : Promise < void > {
341+ private async openState ( storage : vscode . Memento | vscode . SecretStorage , key : string ) {
320342 const uri = this . uriFromKey ( key , storage )
321343 const tab = this . tabs . get ( this . fs . uriToKey ( uri ) )
322344
323345 if ( tab ) {
324346 tab . virtualFile . refresh ( )
325347 await vscode . window . showTextDocument ( tab . editor . document )
348+ return tab . virtualFile
326349 } else {
327350 const newTab = await this . createTab ( storage , key )
328351 const newKey = this . fs . uriToKey ( newTab . editor . document . uri )
329352 this . tabs . set ( newKey , newTab )
353+ return newTab . virtualFile
330354 }
331355 }
332356
@@ -417,6 +441,62 @@ async function openStorageFromInput() {
417441 }
418442}
419443
444+ type ResettableFeature = {
445+ name : string
446+ executor : ( ) => Promise < void > | void
447+ } & QuickPickItem
448+
449+ /**
450+ * Extend this array with features that may need state resets often for
451+ * testing purposes. It will appear as an entry in the "Reset feature state" menu.
452+ */
453+ const resettableFeatures : readonly ResettableFeature [ ] = [
454+ {
455+ name : 'notifications' ,
456+ label : 'Notifications' ,
457+ detail : 'Resets memory/global state for the notifications panel (includes dismissed, onReceive).' ,
458+ executor : resetNotificationsState ,
459+ } ,
460+ ] as const
461+
462+ // TODO this is *somewhat* similar to `openStorageFromInput`. If we need another
463+ // one of these prompters, can we make it generic?
464+ async function resetState ( ) {
465+ const wizard = new ( class extends Wizard < { target : string ; key : string } > {
466+ constructor ( ) {
467+ super ( )
468+
469+ this . form . target . bindPrompter ( ( ) =>
470+ createQuickPick (
471+ resettableFeatures . map ( ( f ) => {
472+ return {
473+ data : f . name ,
474+ label : f . label ,
475+ detail : f . detail ,
476+ }
477+ } ) ,
478+ {
479+ title : 'Select a feature/component to reset' ,
480+ }
481+ )
482+ )
483+
484+ this . form . key . bindPrompter ( ( { target } ) => {
485+ if ( target && resettableFeatures . some ( ( f ) => f . name === target ) ) {
486+ return new SkipPrompter ( '' )
487+ }
488+ throw new Error ( 'invalid feature target' )
489+ } )
490+ }
491+ } ) ( )
492+
493+ const response = await wizard . run ( )
494+
495+ if ( response ) {
496+ return resettableFeatures . find ( ( f ) => f . name === response . target ) ?. executor ( )
497+ }
498+ }
499+
420500async function editSsoConnections ( ) {
421501 void openStorageCommand . execute ( 'auth' , 'auth.profiles' )
422502}
@@ -460,3 +540,41 @@ export const openStorageCommand = Commands.from(ObjectEditor).declareOpenStorage
460540export async function updateDevMode ( ) {
461541 await setContext ( 'aws.isDevMode' , DevSettings . instance . isDevMode ( ) )
462542}
543+
544+ async function resetNotificationsState ( ) {
545+ await targetNotificationsController . reset ( )
546+ }
547+
548+ async function editNotifications ( ) {
549+ const storageKey = 'aws.notifications.dev'
550+ const current = globalState . get ( storageKey ) ?? { }
551+ const isValid = ( item : any ) => {
552+ if ( typeof item !== 'object' || ! Array . isArray ( item . startUp ) || ! Array . isArray ( item . emergency ) ) {
553+ return false
554+ }
555+ return true
556+ }
557+ if ( ! isValid ( current ) ) {
558+ // Set a default state if the developer does not have it or it's malformed.
559+ await globalState . update ( storageKey , { startUp : [ ] , emergency : [ ] } as DevNotificationsState )
560+ }
561+
562+ // Monitor for when the global state is updated.
563+ // A notification will be sent based on the contents.
564+ const virtualFile = await openStorageCommand . execute ( 'globals' , storageKey )
565+ virtualFile ?. onDidChange ( async ( ) => {
566+ const val = globalState . get ( storageKey ) as DevNotificationsState
567+ if ( ! isValid ( val ) ) {
568+ void vscode . window . showErrorMessage (
569+ 'Dev mode: invalid notification object provided. State data must take the form: { "startUp": ToolkitNotification[], "emergency": ToolkitNotification[] }'
570+ )
571+ return
572+ }
573+
574+ // This relies on the controller being built with DevFetcher, as opposed to
575+ // the default RemoteFetcher. DevFetcher will check for notifications in the
576+ // global state, which was just modified.
577+ await targetNotificationsController . pollForStartUp ( )
578+ await targetNotificationsController . pollForEmergencies ( )
579+ } )
580+ }
0 commit comments