Skip to content

Commit a620ccb

Browse files
committed
address some comments
1 parent 15c636c commit a620ccb

File tree

10 files changed

+115
-39
lines changed

10 files changed

+115
-39
lines changed

packages/amazonq/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@
411411
"commands": [
412412
{
413413
"command": "_aws.amazonq.notifications.dismiss",
414-
"title": "%AWS.notifications.dismiss.title%",
414+
"title": "%AWS.generic.dismiss%",
415415
"category": "%AWS.amazonq.title%",
416416
"enablement": "isCloud9 || !aws.isWebExtHost",
417417
"icon": "$(remove-close)"

packages/core/package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"AWS.amazonq.productName": "Amazon Q",
77
"AWS.codecatalyst.submenu.title": "Manage CodeCatalyst",
88
"AWS.notifications.title": "Notifications",
9-
"AWS.notifications.dismiss.title": "Dismiss",
109
"AWS.configuration.profileDescription": "The name of the credential profile to obtain credentials from.",
1110
"AWS.configuration.description.lambda.recentlyUploaded": "Recently selected Lambda upload targets.",
1211
"AWS.configuration.description.ecs.openTerminalCommand": "The command to run when starting a new interactive terminal session.",
@@ -246,6 +245,7 @@
246245
"AWS.generic.promptUpdate": "Update...",
247246
"AWS.generic.preview": "Preview",
248247
"AWS.generic.viewDocs": "View Documentation",
248+
"AWS.generic.dismiss": "Dismiss",
249249
"AWS.ssmDocument.ssm.maxItemsComputed.desc": "Controls the maximum number of problems produced by the SSM Document language server.",
250250
"AWS.walkthrough.gettingStarted.title": "Get started with AWS",
251251
"AWS.walkthrough.gettingStarted.description": "These walkthroughs help you set up the AWS Toolkit.",

packages/core/src/notifications/controller.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
77
import { ToolkitError } from '../shared/errors'
88
import globals from '../shared/extensionGlobals'
99
import { globalKey } from '../shared/globalState'
10-
import { NotificationsState, NotificationType, NotificationData, ToolkitNotification } from './types'
10+
import { NotificationsState, NotificationsStateConstructor, NotificationType, ToolkitNotification } from './types'
1111
import { HttpResourceFetcher } from '../shared/resourcefetcher/httpResourceFetcher'
1212
import { getLogger } from '../shared/logger/logger'
1313
import { NotificationsNode } from './panelNode'
@@ -16,6 +16,7 @@ import { RuleEngine } from './rules'
1616
import { TreeNode } from '../shared/treeview/resourceTreeDataProvider'
1717
import { withRetries } from '../shared/utilities/functionUtils'
1818
import { FileResourceFetcher } from '../shared/resourcefetcher/fileResourceFetcher'
19+
import { isAmazonQ } from '../shared/extensionUtilities'
1920

2021
const startUpEndpoint = 'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/startup/1.x.json'
2122
const emergencyEndpoint = 'https://idetoolkits-hostedfiles.amazonaws.com/Notifications/VSCode/emergency/1.x.json'
@@ -43,27 +44,21 @@ export class NotificationsController {
4344

4445
/** Internal memory state that is written to global state upon modification. */
4546
private readonly state: NotificationsState
46-
private readonly notificationsNode: NotificationsNode
4747

4848
static #instance: NotificationsController | undefined
4949

50-
constructor(extPrefix: 'amazonq' | 'toolkit', node: NotificationsNode) {
50+
constructor(private readonly notificationsNode: NotificationsNode) {
5151
if (!NotificationsController.#instance) {
52-
registerDismissCommand(extPrefix)
52+
registerDismissCommand()
5353
}
5454
NotificationsController.#instance = this
5555

56-
this.storageKey = `aws.${extPrefix}.notifications`
57-
this.notificationsNode = node
58-
59-
this.state = globals.globalState.get(this.storageKey) ?? {
60-
startUp: {} as NotificationData,
61-
emergency: {} as NotificationData,
56+
this.storageKey = 'aws.notifications'
57+
this.state = globals.globalState.tryGet<NotificationsState>(this.storageKey, NotificationsStateConstructor, {
58+
startUp: {},
59+
emergency: {},
6260
dismissed: [],
63-
}
64-
this.state.startUp = this.state.startUp ?? {}
65-
this.state.emergency = this.state.emergency ?? {}
66-
this.state.dismissed = this.state.dismissed ?? []
61+
})
6762
}
6863

6964
public pollForStartUp(ruleEngine: RuleEngine) {
@@ -78,7 +73,7 @@ export class NotificationsController {
7873
try {
7974
await this.fetchNotifications(category)
8075
} catch (err: any) {
81-
getLogger().error(`Unable to fetch %s notifications: %s`, category, err)
76+
getLogger('notifications').error(`Unable to fetch %s notifications: %s`, category, err)
8277
}
8378

8479
await this.displayNotifications(ruleEngine)
@@ -113,7 +108,7 @@ export class NotificationsController {
113108
* hide all notifications.
114109
*/
115110
public async dismissNotification(notificationId: string) {
116-
getLogger().debug('Dismissing notification: %s', notificationId)
111+
getLogger('notifications').debug('Dismissing notification: %s', notificationId)
117112
this.state.dismissed.push(notificationId)
118113
await this.writeState()
119114

@@ -126,17 +121,17 @@ export class NotificationsController {
126121
private async fetchNotifications(category: NotificationType) {
127122
const response = _useLocalFiles ? await this.fetchLocally(category) : await this.fetchRemotely(category)
128123
if (!response.content) {
129-
getLogger().verbose('No new notifications for category: %s', category)
124+
getLogger('notifications').verbose('No new notifications for category: %s', category)
130125
return
131126
}
132127

133-
getLogger().verbose('ETAG has changed for notifications category: %s', category)
128+
getLogger('notifications').verbose('ETAG has changed for notifications category: %s', category)
134129

135130
this.state[category].payload = JSON.parse(response.content)
136131
this.state[category].eTag = response.eTag
137132
await this.writeState()
138133

139-
getLogger().verbose(
134+
getLogger('notifications').verbose(
140135
"Fetched notifications JSON for category '%s' with schema version: %s. There were %d notifications.",
141136
category,
142137
this.state[category].payload?.schemaVersion,
@@ -169,7 +164,7 @@ export class NotificationsController {
169164
const uri = category === 'startUp' ? startUpLocalPath : emergencyLocalPath
170165
const content = await new FileResourceFetcher(globals.context.asAbsolutePath(uri)).get()
171166

172-
getLogger().verbose('Fetched notifications locally for category: %s at path: %s', category, uri)
167+
getLogger('notifications').verbose('Fetched notifications locally for category: %s at path: %s', category, uri)
173168
return {
174169
content,
175170
eTag: 'LOCAL_PATH',
@@ -180,7 +175,7 @@ export class NotificationsController {
180175
* Write the latest memory state to global state.
181176
*/
182177
private async writeState() {
183-
getLogger().debug('NotificationsController: Updating notifications state at %s', this.storageKey)
178+
getLogger('notifications').debug('NotificationsController: Updating notifications state at %s', this.storageKey)
184179

185180
// Clean out anything in 'dismissed' that doesn't exist anymore.
186181
const notifications = new Set(
@@ -203,8 +198,8 @@ export class NotificationsController {
203198
}
204199
}
205200

206-
function registerDismissCommand(extPrefix: string) {
207-
const name = `_aws.${extPrefix}.notifications.dismiss`
201+
function registerDismissCommand() {
202+
const name = isAmazonQ() ? '_aws.amazonq.notifications.dismiss' : '_aws.toolkit.notifications.dismiss'
208203

209204
globals.context.subscriptions.push(
210205
Commands.register(name, async (node: TreeNode) => {
@@ -216,7 +211,7 @@ function registerDismissCommand(extPrefix: string) {
216211

217212
await NotificationsController.instance.dismissNotification(notification.id)
218213
} else {
219-
getLogger().error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
214+
getLogger('notifications').error(`${name}: Cannot dismiss notification: item is not a vscode.TreeItem`)
220215
}
221216
})
222217
)
@@ -229,5 +224,6 @@ function registerDismissCommand(extPrefix: string) {
229224
const _useLocalFiles = false
230225
export const _useLocalFilesCheck = _useLocalFiles // export for testing
231226

227+
// Paths relative to current extension
232228
const startUpLocalPath = '../core/src/test/notifications/resources/startup/1.x.json'
233229
const emergencyLocalPath = '../core/src/test/notifications/resources/emergency/1.x.json'

packages/core/src/notifications/panelNode.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getIcon } from '../shared/icons'
1010
import { contextKey, setContext } from '../shared/vscode/setContext'
1111
import { NotificationType, ToolkitNotification } from './types'
1212
import { ToolkitError } from '../shared/errors'
13+
import { isAmazonQ } from '../shared/extensionUtilities'
1314

1415
/**
1516
* Controls the "Notifications" side panel/tree in each extension. It takes purely UX actions
@@ -37,17 +38,25 @@ export class NotificationsNode implements TreeNode {
3738

3839
static #instance: NotificationsNode
3940

40-
constructor(extPrefix: 'amazonq' | 'toolkit') {
41+
constructor() {
4142
NotificationsNode.#instance = this
4243

4344
this.openNotificationCmd = Commands.register(
44-
`_aws.${extPrefix}.notifications.open`,
45+
isAmazonQ() ? '_aws.amazonq.notifications.open' : '_aws.toolkit.notifications.open',
4546
async (n: ToolkitNotification) => this.openNotification(n)
4647
)
47-
this.focusCmdStr = `aws.${extPrefix}.notifications.focus`
48-
this.showContextStr = `aws.${extPrefix}.notifications.show`
49-
this.startUpNodeContext = `${extPrefix}NotificationStartUp`
50-
this.emergencyNodeContext = `${extPrefix}NotificationEmergency`
48+
49+
if (isAmazonQ()) {
50+
this.focusCmdStr = 'aws.amazonq.notifications.focus'
51+
this.showContextStr = 'aws.amazonq.notifications.show'
52+
this.startUpNodeContext = 'amazonqNotificationStartUp'
53+
this.emergencyNodeContext = 'amazonqNotificationEmergency'
54+
} else {
55+
this.focusCmdStr = 'aws.toolkit.notifications.focus'
56+
this.showContextStr = 'aws.toolkit.notifications.show'
57+
this.startUpNodeContext = 'toolkitNotificationStartUp'
58+
this.emergencyNodeContext = 'toolkitNotificationEmergency'
59+
}
5160
}
5261

5362
public getTreeItem() {
@@ -73,7 +82,7 @@ export class NotificationsNode implements TreeNode {
7382

7483
public getChildren() {
7584
const buildNode = (n: ToolkitNotification, type: NotificationType) => {
76-
return this.openNotificationCmd!.build(n).asTreeNode({
85+
return this.openNotificationCmd.build(n).asTreeNode({
7786
label: n.uiRenderInstructions.content['en-US'].title,
7887
iconPath: type === 'startUp' ? getIcon('vscode-question') : getIcon('vscode-alert'),
7988
contextValue: type === 'startUp' ? this.startUpNodeContext : this.emergencyNodeContext,
@@ -103,7 +112,6 @@ export class NotificationsNode implements TreeNode {
103112
* Only dismisses startup notifications.
104113
*/
105114
public dismissStartUpNotification(id: string) {
106-
// Only startup notifications can be dismissed.
107115
this.startUpNotifications = this.startUpNotifications.filter((n) => n.id !== id)
108116
this.refresh()
109117
}
@@ -118,6 +126,8 @@ export class NotificationsNode implements TreeNode {
118126
/**
119127
* Fired when a notification is clicked on in the panel. It will run any rendering
120128
* instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}.
129+
*
130+
* TODO: implement more rendering possibilites.
121131
*/
122132
private async openNotification(notification: ToolkitNotification) {
123133
await vscode.window.showTextDocument(

packages/core/src/notifications/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { EnvType, OperatingSystem } from '../shared/telemetry/util'
8+
import { TypeConstructor } from '../shared/utilities/typeConstructors'
89

910
/** Types of information that we can use to determine whether to show a notification or not. */
1011
export type Criteria =
@@ -88,6 +89,23 @@ export type NotificationsState = {
8889
dismissed: string[]
8990
}
9091

92+
export const NotificationsStateConstructor: TypeConstructor<NotificationsState> = (v: unknown): NotificationsState => {
93+
if (v && typeof v === 'object' && isNotificationsState(v as Partial<NotificationsState>)) {
94+
return v as NotificationsState
95+
}
96+
throw new Error('Cannot cast to NotificationsState.')
97+
}
98+
99+
function isNotificationsState(v: Partial<NotificationsState>): v is NotificationsState {
100+
const requiredKeys: (keyof NotificationsState)[] = ['startUp', 'emergency', 'dismissed']
101+
return (
102+
requiredKeys.every((key) => key in v) &&
103+
Array.isArray(v.dismissed) &&
104+
typeof v.startUp === 'object' &&
105+
typeof v.emergency === 'object'
106+
)
107+
}
108+
91109
export type NotificationType = keyof Omit<NotificationsState, 'dismissed'>
92110

93111
export interface RuleContext {

packages/core/src/shared/globalState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type globalKey =
3232
| 'aws.amazonq.hasShownWalkthrough'
3333
| 'aws.amazonq.showTryChatCodeLens'
3434
| 'aws.amazonq.notifications'
35-
| 'aws.toolkit.notifications'
35+
| 'aws.notifications'
3636
| 'aws.downloadPath'
3737
| 'aws.lastTouchedS3Folder'
3838
| 'aws.lastUploadedToS3Folder'

packages/core/src/shared/logger/toolkitLogger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ConsoleLogTransport } from './consoleLogTransport'
1515
import { isWeb } from '../extensionGlobals'
1616

1717
/* define log topics */
18-
export type LogTopic = 'unknown' | 'test' | 'crashReport'
18+
export type LogTopic = 'unknown' | 'test' | 'crashReport' | 'notifications'
1919

2020
class ErrorLog {
2121
constructor(

packages/core/src/test/notifications/controller.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { installFakeClock } from '../testUtil'
1616
import * as FakeTimers from '@sinonjs/fake-timers'
1717

1818
describe('Notifications Controller', function () {
19-
const panelNode: NotificationsNode = new NotificationsNode('toolkit')
19+
const panelNode: NotificationsNode = new NotificationsNode()
2020
let controller: NotificationsController
2121
const ruleEngine: RuleEngine = new RuleEngine({
2222
ideVersion: '1.83.0',
@@ -60,7 +60,7 @@ describe('Notifications Controller', function () {
6060

6161
beforeEach(async function () {
6262
panelNode.setNotifications([], [])
63-
controller = new NotificationsController('toolkit', panelNode)
63+
controller = new NotificationsController(panelNode)
6464

6565
sandbox = sinon.createSandbox()
6666

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import assert from 'assert'
7+
import { NotificationsState, NotificationsStateConstructor } from '../../notifications/types'
8+
9+
describe('NotificationsState type validation', function () {
10+
it('passes on valid input', async function () {
11+
const state: NotificationsState = {
12+
startUp: {},
13+
emergency: {},
14+
dismissed: [],
15+
}
16+
let ret
17+
assert.doesNotThrow(() => {
18+
ret = NotificationsStateConstructor(state)
19+
})
20+
assert.deepStrictEqual(ret, state)
21+
})
22+
23+
it('fails on invalid input', async function () {
24+
assert.throws(() => {
25+
NotificationsStateConstructor('' as unknown as NotificationsState)
26+
})
27+
assert.throws(() => {
28+
NotificationsStateConstructor({} as NotificationsState)
29+
})
30+
assert.throws(() => {
31+
NotificationsStateConstructor({
32+
startUp: {},
33+
emergency: {},
34+
dismissed: {}, // x
35+
} as NotificationsState)
36+
})
37+
assert.throws(() => {
38+
NotificationsStateConstructor({
39+
startUp: {},
40+
emergency: [], // x
41+
dismissed: [],
42+
} as NotificationsState)
43+
})
44+
assert.throws(() => {
45+
NotificationsStateConstructor({
46+
startUp: '', // x
47+
emergency: {},
48+
dismissed: [],
49+
} as NotificationsState)
50+
})
51+
})
52+
})

packages/toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2053,7 +2053,7 @@
20532053
"commands": [
20542054
{
20552055
"command": "_aws.toolkit.notifications.dismiss",
2056-
"title": "%AWS.notifications.dismiss.title%",
2056+
"title": "%AWS.generic.dismiss%",
20572057
"category": "%AWS.title%",
20582058
"enablement": "isCloud9 || !aws.isWebExtHost",
20592059
"icon": "$(remove-close)"

0 commit comments

Comments
 (0)