@@ -21,6 +21,9 @@ import { getEnvironmentSpecificMemento } from '../shared/utilities/mementos'
21
21
import { setContext } from '../shared'
22
22
import { telemetry } from '../shared/telemetry'
23
23
import { getSessionId } from '../shared/telemetry/util'
24
+ import { NotificationsController } from '../notifications/controller'
25
+ import { DevNotificationsState } from '../notifications/types'
26
+ import { QuickPickItem } from 'vscode'
24
27
25
28
interface MenuOption {
26
29
readonly label : string
@@ -34,21 +37,25 @@ export type DevFunction =
34
37
| 'openTerminal'
35
38
| 'deleteDevEnv'
36
39
| 'editStorage'
40
+ | 'resetState'
37
41
| 'showEnvVars'
38
42
| 'deleteSsoConnections'
39
43
| 'expireSsoConnections'
40
44
| 'editAuthConnections'
45
+ | 'notificationsSend'
41
46
| 'forceIdeCrash'
42
47
43
48
export type DevOptions = {
44
49
context : vscode . ExtensionContext
45
- auth : Auth
50
+ auth : ( ) => Auth
51
+ notificationsController : ( ) => NotificationsController
46
52
menuOptions ?: DevFunction [ ]
47
53
}
48
54
49
55
let targetContext : vscode . ExtensionContext
50
56
let globalState : vscode . Memento
51
57
let targetAuth : Auth
58
+ let targetNotificationsController : NotificationsController
52
59
53
60
/**
54
61
* Defines AWS Toolkit developer tools.
@@ -83,6 +90,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
83
90
detail : 'Shows all globalState values, or edit a globalState/secret item' ,
84
91
executor : openStorageFromInput ,
85
92
} ,
93
+ resetState : {
94
+ label : 'Reset feature state' ,
95
+ detail : 'Quick reset the state of extension components or features' ,
96
+ executor : resetState ,
97
+ } ,
86
98
showEnvVars : {
87
99
label : 'Show Environment Variables' ,
88
100
description : 'AWS Toolkit' ,
@@ -104,6 +116,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
104
116
detail : 'Opens editor to all Auth Connections the extension is using.' ,
105
117
executor : editSsoConnections ,
106
118
} ,
119
+ notificationsSend : {
120
+ label : 'Notifications: Send Notifications' ,
121
+ detail : 'Send JSON notifications for testing.' ,
122
+ executor : editNotifications ,
123
+ } ,
107
124
forceIdeCrash : {
108
125
label : 'Crash: Force IDE ExtHost Crash' ,
109
126
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> {
156
173
vscode . workspace . registerTextDocumentContentProvider ( 'aws-dev2' , new DevDocumentProvider ( ) ) ,
157
174
// "AWS (Developer): Open Developer Menu"
158
175
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
+ } )
160
181
} ) ,
161
182
// Internal command to open dev menu for a specific context and options
162
183
vscode . commands . registerCommand ( '_aws.dev.invokeMenu' , ( opts : DevOptions ) => {
163
184
targetContext = opts . context
164
185
// eslint-disable-next-line aws-toolkits/no-banned-usages
165
186
globalState = targetContext . globalState
166
- targetAuth = opts . auth
187
+ targetAuth = opts . auth ( )
188
+ targetNotificationsController = opts . notificationsController ( )
167
189
const options = menuOptions ( )
168
190
void openMenu (
169
191
entries ( options )
@@ -302,7 +324,7 @@ class ObjectEditor {
302
324
vscode . workspace . registerFileSystemProvider ( ObjectEditor . scheme , this . fs )
303
325
}
304
326
305
- public async openStorage ( type : 'globalsView' | 'globals' | 'secrets' | 'auth' , key : string ) : Promise < void > {
327
+ public async openStorage ( type : 'globalsView' | 'globals' | 'secrets' | 'auth' , key : string ) {
306
328
switch ( type ) {
307
329
case 'globalsView' :
308
330
return showState ( 'globalstate' )
@@ -316,17 +338,19 @@ class ObjectEditor {
316
338
}
317
339
}
318
340
319
- private async openState ( storage : vscode . Memento | vscode . SecretStorage , key : string ) : Promise < void > {
341
+ private async openState ( storage : vscode . Memento | vscode . SecretStorage , key : string ) {
320
342
const uri = this . uriFromKey ( key , storage )
321
343
const tab = this . tabs . get ( this . fs . uriToKey ( uri ) )
322
344
323
345
if ( tab ) {
324
346
tab . virtualFile . refresh ( )
325
347
await vscode . window . showTextDocument ( tab . editor . document )
348
+ return tab . virtualFile
326
349
} else {
327
350
const newTab = await this . createTab ( storage , key )
328
351
const newKey = this . fs . uriToKey ( newTab . editor . document . uri )
329
352
this . tabs . set ( newKey , newTab )
353
+ return newTab . virtualFile
330
354
}
331
355
}
332
356
@@ -417,6 +441,62 @@ async function openStorageFromInput() {
417
441
}
418
442
}
419
443
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
+
420
500
async function editSsoConnections ( ) {
421
501
void openStorageCommand . execute ( 'auth' , 'auth.profiles' )
422
502
}
@@ -460,3 +540,41 @@ export const openStorageCommand = Commands.from(ObjectEditor).declareOpenStorage
460
540
export async function updateDevMode ( ) {
461
541
await setContext ( 'aws.isDevMode' , DevSettings . instance . isDevMode ( ) )
462
542
}
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