Skip to content

Commit f1cdbeb

Browse files
authored
Merge pull request #6004 from hayemaxi/notifications-3
feat(notification): misc logging, types, fixes
2 parents 3d46b7b + 2f07377 commit f1cdbeb

File tree

10 files changed

+155
-91
lines changed

10 files changed

+155
-91
lines changed

packages/core/src/notifications/activation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export async function activate(
3838
const controller = new NotificationsController(panelNode)
3939
const engine = new RuleEngine(await getRuleContext(context, initialState))
4040

41-
void controller.pollForStartUp(engine)
42-
void controller.pollForEmergencies(engine)
41+
await controller.pollForStartUp(engine)
42+
await controller.pollForEmergencies(engine)
4343

4444
globals.clock.setInterval(async () => {
4545
const ruleContext = await getRuleContext(context, await authStateFn())

packages/core/src/notifications/controller.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,22 @@ import { telemetry } from '../shared/telemetry/telemetry'
4040
export class NotificationsController {
4141
public static readonly suggestedPollIntervalMs = 1000 * 60 * 10 // 10 minutes
4242

43-
public readonly storageKey: globalKey
44-
4543
/** Internal memory state that is written to global state upon modification. */
4644
private readonly state: NotificationsState
4745

4846
static #instance: NotificationsController | undefined
4947

5048
constructor(
5149
private readonly notificationsNode: NotificationsNode,
52-
private readonly fetcher: NotificationFetcher = new RemoteFetcher()
50+
private readonly fetcher: NotificationFetcher = new RemoteFetcher(),
51+
public readonly storageKey: globalKey = 'aws.notifications'
5352
) {
5453
if (!NotificationsController.#instance) {
5554
// Register on first creation only.
5655
registerDismissCommand()
5756
}
5857
NotificationsController.#instance = this
5958

60-
this.storageKey = 'aws.notifications'
6159
this.state = globals.globalState.tryGet<NotificationsState>(this.storageKey, NotificationsStateConstructor, {
6260
startUp: {},
6361
emergency: {},
@@ -94,7 +92,7 @@ export class NotificationsController {
9492
ruleEngine.shouldDisplayNotification(n)
9593
)
9694

97-
NotificationsNode.instance.setNotifications(startUp, emergency)
95+
await NotificationsNode.instance.setNotifications(startUp, emergency)
9896

9997
// Emergency notifications can't be dismissed, but if the user minimizes the panel then
10098
// we don't want to focus it each time we set the notification nodes.
@@ -128,7 +126,7 @@ export class NotificationsController {
128126
this.state.dismissed.push(notificationId)
129127
await this.writeState()
130128

131-
NotificationsNode.instance.dismissStartUpNotification(notificationId)
129+
await NotificationsNode.instance.dismissStartUpNotification(notificationId)
132130
}
133131

134132
/**

packages/core/src/notifications/panelNode.ts

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,31 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import * as nls from 'vscode-nls'
78
import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceTreeDataProvider'
89
import { Command, Commands } from '../shared/vscode/commands2'
9-
import { Icon, IconPath, getIcon } from '../shared/icons'
10+
import { Icon, getIcon } from '../shared/icons'
1011
import { contextKey, setContext } from '../shared/vscode/setContext'
11-
import { NotificationType, ToolkitNotification, getNotificationTelemetryId } from './types'
12+
import { NotificationType, OnReceiveType, ToolkitNotification, getNotificationTelemetryId } from './types'
1213
import { ToolkitError } from '../shared/errors'
1314
import { isAmazonQ } from '../shared/extensionUtilities'
1415
import { getLogger } from '../shared/logger/logger'
1516
import { registerToolView } from '../awsexplorer/activationShared'
1617
import { readonlyDocument } from '../shared/utilities/textDocumentUtilities'
1718
import { openUrl } from '../shared/utilities/vsCodeUtils'
1819
import { telemetry } from '../shared/telemetry/telemetry'
20+
import { globals } from '../shared'
21+
22+
const localize = nls.loadMessageBundle()
23+
const logger = getLogger('notifications')
1924

2025
/**
2126
* Controls the "Notifications" side panel/tree in each extension. It takes purely UX actions
2227
* and does not determine what notifications to dispaly or how to fetch and store them.
2328
*/
2429
export class NotificationsNode implements TreeNode {
30+
public static readonly title = localize('AWS.notifications.title', 'Notifications')
31+
2532
public readonly id = 'notifications'
2633
public readonly resource = this
2734
public provider?: ResourceTreeDataProvider
@@ -34,6 +41,7 @@ export class NotificationsNode implements TreeNode {
3441
private readonly showContextStr: contextKey
3542
private readonly startUpNodeContext: string
3643
private readonly emergencyNodeContext: string
44+
private view: vscode.TreeView<TreeNode> | undefined
3745

3846
static #instance: NotificationsNode
3947

@@ -62,31 +70,49 @@ export class NotificationsNode implements TreeNode {
6270
}
6371

6472
public getTreeItem() {
65-
const item = new vscode.TreeItem('Notifications')
73+
const item = new vscode.TreeItem(NotificationsNode.title)
6674
item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
6775
item.contextValue = 'notifications'
6876

6977
return item
7078
}
7179

72-
public refresh(): void {
73-
const hasNotifications = this.startUpNotifications.length > 0 || this.emergencyNotifications.length > 0
74-
void setContext(this.showContextStr, hasNotifications)
80+
public refresh() {
81+
const totalNotifications = this.notificationCount()
82+
if (this.view) {
83+
if (totalNotifications > 0) {
84+
this.view.badge = {
85+
tooltip: `${totalNotifications} notification${totalNotifications > 1 ? 's' : ''}`,
86+
value: totalNotifications,
87+
}
88+
this.view.title = `${NotificationsNode.title} (${totalNotifications})`
89+
} else {
90+
this.view.badge = undefined
91+
this.view.title = NotificationsNode.title
92+
}
93+
} else {
94+
logger.warn('NotificationsNode was refreshed but the view was not initialized!')
95+
}
7596

7697
this.provider?.refresh()
98+
return setContext(this.showContextStr, totalNotifications > 0)
7799
}
78100

79101
public getChildren() {
80102
const buildNode = (n: ToolkitNotification, type: NotificationType) => {
81-
const icon: Icon | IconPath =
82-
type === 'startUp'
83-
? getIcon('vscode-question')
84-
: { ...getIcon('vscode-alert'), color: new vscode.ThemeColor('errorForeground') }
103+
const icon: Icon =
104+
type === 'emergency'
105+
? Object.assign(getIcon('vscode-alert') as Icon, {
106+
color: new vscode.ThemeColor('errorForeground'),
107+
})
108+
: (getIcon('vscode-question') as Icon)
109+
110+
const title = n.uiRenderInstructions.content['en-US'].title
85111
return this.openNotificationCmd.build(n).asTreeNode({
86-
label: n.uiRenderInstructions.content['en-US'].title,
112+
label: title,
113+
tooltip: title,
87114
iconPath: icon,
88115
contextValue: type === 'startUp' ? this.startUpNodeContext : this.emergencyNodeContext,
89-
tooltip: 'Click to open',
90116
})
91117
}
92118

@@ -100,10 +126,10 @@ export class NotificationsNode implements TreeNode {
100126
* Sets the current list of notifications. Nodes are generated for each notification.
101127
* No other processing is done, see NotificationController.
102128
*/
103-
public setNotifications(startUp: ToolkitNotification[], emergency: ToolkitNotification[]) {
129+
public async setNotifications(startUp: ToolkitNotification[], emergency: ToolkitNotification[]) {
104130
this.startUpNotifications = startUp
105131
this.emergencyNotifications = emergency
106-
this.refresh()
132+
await this.refresh()
107133
}
108134

109135
/**
@@ -112,9 +138,9 @@ export class NotificationsNode implements TreeNode {
112138
*
113139
* Only dismisses startup notifications.
114140
*/
115-
public dismissStartUpNotification(id: string) {
141+
public async dismissStartUpNotification(id: string) {
116142
this.startUpNotifications = this.startUpNotifications.filter((n) => n.id !== id)
117-
this.refresh()
143+
await this.refresh()
118144
}
119145

120146
/**
@@ -124,6 +150,10 @@ export class NotificationsNode implements TreeNode {
124150
return vscode.commands.executeCommand(this.focusCmdStr)
125151
}
126152

153+
private notificationCount() {
154+
return this.startUpNotifications.length + this.emergencyNotifications.length
155+
}
156+
127157
/**
128158
* Fired when a notification is clicked on in the panel. It will run any rendering
129159
* instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}.
@@ -132,23 +162,23 @@ export class NotificationsNode implements TreeNode {
132162
switch (notification.uiRenderInstructions.onClick.type) {
133163
case 'modal':
134164
// Render blocking modal
135-
getLogger('notifications').verbose(`rendering modal for notificaiton: ${notification.id} ...`)
165+
logger.verbose(`rendering modal for notificaiton: ${notification.id} ...`)
136166
await this.showInformationWindow(notification, 'modal', false)
137167
break
138168
case 'openUrl':
139169
// Show open url option
140170
if (!notification.uiRenderInstructions.onClick.url) {
141171
throw new ToolkitError('No url provided for onclick open url')
142172
}
143-
getLogger('notifications').verbose(`opening url for notification: ${notification.id} ...`)
173+
logger.verbose(`opening url for notification: ${notification.id} ...`)
144174
await openUrl(
145175
vscode.Uri.parse(notification.uiRenderInstructions.onClick.url),
146176
getNotificationTelemetryId(notification)
147177
)
148178
break
149179
case 'openTextDocument':
150180
// Display read-only txt document
151-
getLogger('notifications').verbose(`showing txt document for notification: ${notification.id} ...`)
181+
logger.verbose(`showing txt document for notification: ${notification.id} ...`)
152182
await telemetry.toolkit_invokeAction.run(async () => {
153183
telemetry.record({ source: getNotificationTelemetryId(notification), action: 'openTxt' })
154184
await readonlyDocument.show(
@@ -165,7 +195,11 @@ export class NotificationsNode implements TreeNode {
165195
* Can be either a blocking modal or a bottom-right corner toast
166196
* Handles the button click actions based on the button type.
167197
*/
168-
private showInformationWindow(notification: ToolkitNotification, type: string = 'toast', passive: boolean = false) {
198+
private showInformationWindow(
199+
notification: ToolkitNotification,
200+
type: OnReceiveType = 'toast',
201+
passive: boolean = false
202+
) {
169203
const isModal = type === 'modal'
170204

171205
// modal has to have defined actions (buttons)
@@ -203,7 +237,10 @@ export class NotificationsNode implements TreeNode {
203237
)
204238
break
205239
case 'updateAndReload':
206-
await this.updateAndReload(notification.displayIf.extensionId)
240+
// Give things time to finish executing.
241+
globals.clock.setTimeout(() => {
242+
return this.updateAndReload(notification.displayIf.extensionId)
243+
}, 1000)
207244
break
208245
case 'openUrl':
209246
if (selectedButton.url) {
@@ -228,7 +265,11 @@ export class NotificationsNode implements TreeNode {
228265
}
229266

230267
private async updateAndReload(id: string) {
231-
getLogger('notifications').verbose('Updating and reloading the extension...')
268+
logger.verbose('Updating and reloading the extension...')
269+
270+
// Publish pending telemetry before it is lost to the window reload.
271+
await globals.telemetry.flushRecords()
272+
232273
await vscode.commands.executeCommand('workbench.extensions.installExtension', id)
233274
await vscode.commands.executeCommand('workbench.action.reloadWindow')
234275
}
@@ -258,14 +299,13 @@ export class NotificationsNode implements TreeNode {
258299
}
259300

260301
registerView(context: vscode.ExtensionContext) {
261-
const view = registerToolView(
302+
this.view = registerToolView(
262303
{
263304
nodes: [this],
264305
view: isAmazonQ() ? 'aws.amazonq.notifications' : 'aws.toolkit.notifications',
265306
refreshCommands: [(provider: ResourceTreeDataProvider) => this.registerProvider(provider)],
266307
},
267308
context
268309
)
269-
view.message = `New feature announcements and emergency notifications for ${isAmazonQ() ? 'Amazon Q' : 'AWS Toolkit'} will appear here.`
270310
}
271311
}

packages/core/src/notifications/rules.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getComputeEnvType, getOperatingSystem } from '../shared/telemetry/util'
1111
import { AuthFormId } from '../login/webview/vue/types'
1212
import { getLogger } from '../shared/logger/logger'
1313

14+
const logger = getLogger('notifications')
1415
/**
1516
* Evaluates if a given version fits into the parameters specified by a notification, e.g:
1617
*
@@ -72,31 +73,51 @@ export class RuleEngine {
7273
constructor(private readonly context: RuleContext) {}
7374

7475
public shouldDisplayNotification(payload: ToolkitNotification) {
75-
return this.evaluate(payload.displayIf)
76+
return this.evaluate(payload.id, payload.displayIf)
7677
}
7778

78-
private evaluate(condition: DisplayIf): boolean {
79+
private evaluate(id: string, condition: DisplayIf): boolean {
7980
const currentExt = globals.context.extension.id
8081
if (condition.extensionId !== currentExt) {
82+
logger.verbose(
83+
'notification id: (%s) did NOT pass extension id check, actual ext id: (%s), expected ext id: (%s)',
84+
id,
85+
currentExt,
86+
condition.extensionId
87+
)
8188
return false
8289
}
8390

8491
if (condition.ideVersion) {
8592
if (!isValidVersion(this.context.ideVersion, condition.ideVersion)) {
93+
logger.verbose(
94+
'notification id: (%s) did NOT pass IDE version check, actual version: (%s), expected version: (%s)',
95+
id,
96+
this.context.ideVersion,
97+
condition.ideVersion
98+
)
8699
return false
87100
}
88101
}
89102
if (condition.extensionVersion) {
90103
if (!isValidVersion(this.context.extensionVersion, condition.extensionVersion)) {
104+
logger.verbose(
105+
'notification id: (%s) did NOT pass extension version check, actual ext version: (%s), expected ext version: (%s)',
106+
id,
107+
this.context.extensionVersion,
108+
condition.extensionVersion
109+
)
91110
return false
92111
}
93112
}
94113

95114
if (condition.additionalCriteria) {
96115
for (const criteria of condition.additionalCriteria) {
97116
if (!this.evaluateRule(criteria)) {
117+
logger.verbose('notification id: (%s) did NOT pass criteria check: %O', id, criteria)
98118
return false
99119
}
120+
logger.debug('notification id: (%s) passed criteria check: %O', id, criteria)
100121
}
101122
}
102123

@@ -134,8 +155,6 @@ export class RuleEngine {
134155
return hasAnyOfExpected(this.context.authStates)
135156
case 'AuthScopes':
136157
return isEqualSetToExpected(this.context.authScopes)
137-
case 'InstalledExtensions':
138-
return isSuperSetOfExpected(this.context.installedExtensions)
139158
case 'ActiveExtensions':
140159
return isSuperSetOfExpected(this.context.activeExtensions)
141160
default:
@@ -169,16 +188,16 @@ export async function getRuleContext(context: vscode.ExtensionContext, authState
169188
computeEnv: await getComputeEnvType(),
170189
authTypes: [...new Set(authTypes)],
171190
authScopes: authState.authScopes ? authState.authScopes?.split(',') : [],
172-
installedExtensions: vscode.extensions.all.map((e) => e.id),
173191
activeExtensions: vscode.extensions.all.filter((e) => e.isActive).map((e) => e.id),
174192

175193
// Toolkit (and eventually Q?) may have multiple connections with different regions and states.
176194
// However, this granularity does not seem useful at this time- only the active connection is considered.
177195
authRegions: authState.awsRegion ? [authState.awsRegion] : [],
178196
authStates: [authState.authStatus],
179197
}
180-
const { activeExtensions, installedExtensions, ...loggableRuleContext } = ruleContext
181-
getLogger('notifications').debug('getRuleContext() determined rule context: %O', loggableRuleContext)
198+
199+
const { activeExtensions, ...loggableRuleContext } = ruleContext
200+
logger.debug('getRuleContext() determined rule context: %O', loggableRuleContext)
182201

183202
return ruleContext
184203
}

0 commit comments

Comments
 (0)