Skip to content

Commit 01dc7a8

Browse files
Merge master into feature/cwltail
2 parents eb064ff + 4158d67 commit 01dc7a8

File tree

15 files changed

+119
-49
lines changed

15 files changed

+119
-49
lines changed

packages/core/src/auth/auth.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ export interface ConnectionStateChangeEvent {
126126
readonly state: ProfileMetadata['connectionState']
127127
}
128128

129-
export type AuthType = Auth
130-
131129
export type DeletedConnection = { connId: Connection['id']; storedProfile?: StoredProfile }
132130
type DeclaredConnection = Pick<SsoProfile, 'ssoRegion' | 'startUrl'> & { source: string }
133131

packages/core/src/auth/connection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const warnOnce = onceChanged((s: string, url: string) => {
2424
void showMessageWithUrl(s, url, undefined, 'warn')
2525
})
2626

27+
// TODO: Refactor all scopes to a central file with minimal dependencies.
2728
export const scopesCodeCatalyst = ['codecatalyst:read_write']
2829
export const scopesSsoAccountAccess = ['sso:account:access']
2930
/** These are the non-chat scopes for CW. */
@@ -39,6 +40,11 @@ type SsoType =
3940
| 'idc' // AWS Identity Center
4041
| 'builderId'
4142

43+
// TODO: This type is not centralized and there are many routines in the codebase that use some
44+
// variation for these for validation, telemetry, UX, etc. A refactor is needed to align these
45+
// string types.
46+
export type AuthType = 'credentials' | 'builderId' | 'identityCenter' | 'unknown'
47+
4248
export const isIamConnection = (conn?: Connection): conn is IamConnection => conn?.type === 'iam'
4349
export const isSsoConnection = (conn?: Connection, type: SsoType = 'any'): conn is SsoConnection => {
4450
if (conn?.type !== 'sso') {

packages/core/src/auth/sso/validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import * as vscode from 'vscode'
66
import { UnknownError } from '../../shared/errors'
7-
import { AuthType } from '../auth'
7+
import { Auth } from '../auth'
88
import { SsoConnection, hasScopes, isAnySsoConnection } from '../connection'
99
import { ssoUrlFormatMessage, ssoUrlFormatRegex } from './constants'
1010

@@ -19,7 +19,7 @@ export function validateSsoUrlFormat(url: string) {
1919
}
2020

2121
export async function validateIsNewSsoUrlAsync(
22-
auth: AuthType,
22+
auth: Auth,
2323
url: string,
2424
requiredScopes?: string[]
2525
): Promise<string | undefined> {

packages/core/src/codewhisperer/util/supplementalContext/rankBm25.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
export interface BM25Document {
99
content: string
10-
/** The score that the document recieves. */
10+
/** The score that the document receives. */
1111
score: number
1212

1313
index: number

packages/core/src/notifications/activation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { RuleEngine, getRuleContext } from './rules'
1111
import globals from '../shared/extensionGlobals'
1212
import { AuthState } from './types'
1313
import { getLogger } from '../shared/logger/logger'
14+
import { oneMinute } from '../shared/datetime'
1415

1516
/** Time in MS to poll for emergency notifications */
16-
const emergencyPollTime = 1000 * 10 * 60
17+
const emergencyPollTime = oneMinute * 10
1718

1819
/**
1920
* Activate the in-IDE notifications module and begin receiving notifications.

packages/core/src/notifications/controller.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import globals from '../shared/extensionGlobals'
99
import { globalKey } from '../shared/globalState'
1010
import {
1111
getNotificationTelemetryId,
12+
Notifications,
1213
NotificationsState,
1314
NotificationsStateConstructor,
1415
NotificationType,
@@ -56,12 +57,7 @@ export class NotificationsController {
5657
}
5758
NotificationsController.#instance = this
5859

59-
this.state = globals.globalState.tryGet<NotificationsState>(this.storageKey, NotificationsStateConstructor, {
60-
startUp: {},
61-
emergency: {},
62-
dismissed: [],
63-
newlyReceived: [],
64-
})
60+
this.state = this.getDefaultState()
6561
}
6662

6763
public pollForStartUp(ruleEngine: RuleEngine) {
@@ -74,6 +70,9 @@ export class NotificationsController {
7470

7571
private async poll(ruleEngine: RuleEngine, category: NotificationType) {
7672
try {
73+
// Get latest state in case it was modified by other windows.
74+
// It is a minimal read to avoid race conditions.
75+
this.readState()
7776
await this.fetchNotifications(category)
7877
} catch (err: any) {
7978
getLogger('notifications').error(`Unable to fetch %s notifications: %s`, category, err)
@@ -139,7 +138,7 @@ export class NotificationsController {
139138
return
140139
}
141140
// Parse the notifications
142-
const newPayload = JSON.parse(response.content)
141+
const newPayload: Notifications = JSON.parse(response.content)
143142
const newNotifications = newPayload.notifications ?? []
144143

145144
// Get the current notifications
@@ -188,6 +187,33 @@ export class NotificationsController {
188187
await globals.globalState.update(this.storageKey, this.state)
189188
}
190189

190+
/**
191+
* Read relevant values from the latest global state to memory. Useful to bring changes from other windows.
192+
*
193+
* Currently only brings dismissed, so users with multiple vscode instances open do not have issues with
194+
* dismissing notifications multiple times. Otherwise, each instance has an independent session for
195+
* displaying the notifications (e.g. multiple windows can be blocked in critical emergencies).
196+
*
197+
* Note: This sort of pattern (reading back and forth from global state in async functions) is prone to
198+
* race conditions, which is why we limit the read to the fairly inconsequential `dismissed` property.
199+
*/
200+
private readState() {
201+
const state = this.getDefaultState()
202+
this.state.dismissed = state.dismissed
203+
}
204+
205+
/**
206+
* Returns stored notification state, or a default state object if it is invalid or undefined.
207+
*/
208+
private getDefaultState() {
209+
return globals.globalState.tryGet<NotificationsState>(this.storageKey, NotificationsStateConstructor, {
210+
startUp: {},
211+
emergency: {},
212+
dismissed: [],
213+
newlyReceived: [],
214+
})
215+
}
216+
191217
static get instance() {
192218
if (this.#instance === undefined) {
193219
throw new ToolkitError('NotificationsController was accessed before it has been initialized.')

packages/core/src/notifications/panelNode.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ export class NotificationsNode implements TreeNode {
159159
* instructions included in the notification. See {@link ToolkitNotification.uiRenderInstructions}.
160160
*/
161161
public async openNotification(notification: ToolkitNotification) {
162-
switch (notification.uiRenderInstructions.onClick.type) {
162+
const onClickType = notification.uiRenderInstructions.onClick.type
163+
switch (onClickType) {
163164
case 'modal':
164165
// Render blocking modal
165166
logger.verbose(`rendering modal for notificaiton: ${notification.id} ...`)
@@ -180,7 +181,7 @@ export class NotificationsNode implements TreeNode {
180181
// Display read-only txt document
181182
logger.verbose(`showing txt document for notification: ${notification.id} ...`)
182183
await telemetry.toolkit_invokeAction.run(async () => {
183-
telemetry.record({ source: getNotificationTelemetryId(notification), action: 'openTxt' })
184+
telemetry.record({ source: getNotificationTelemetryId(notification), action: onClickType })
184185
await readonlyDocument.show(
185186
notification.uiRenderInstructions.content['en-US'].description,
186187
`Notification: ${notification.id}`
@@ -230,7 +231,7 @@ export class NotificationsNode implements TreeNode {
230231
// Different button options
231232
if (selectedButton) {
232233
switch (selectedButton.type) {
233-
case 'openTxt':
234+
case 'openTextDocument':
234235
await readonlyDocument.show(
235236
notification.uiRenderInstructions.content['en-US'].description,
236237
`Notification: ${notification.id}`
@@ -260,7 +261,7 @@ export class NotificationsNode implements TreeNode {
260261

261262
public async onReceiveNotifications(notifications: ToolkitNotification[]) {
262263
for (const notification of notifications) {
263-
void this.showInformationWindow(notification, notification.uiRenderInstructions.onRecieve, true)
264+
void this.showInformationWindow(notification, notification.uiRenderInstructions.onReceive, true)
264265
}
265266
}
266267

packages/core/src/notifications/rules.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNo
1010
import { getComputeEnvType, getOperatingSystem } from '../shared/telemetry/util'
1111
import { AuthFormId } from '../login/webview/vue/types'
1212
import { getLogger } from '../shared/logger/logger'
13+
import { ToolkitError } from '../shared/errors'
1314

1415
const logger = getLogger('notifications')
1516
/**
@@ -158,7 +159,8 @@ export class RuleEngine {
158159
case 'ActiveExtensions':
159160
return isSuperSetOfExpected(this.context.activeExtensions)
160161
default:
161-
throw new Error(`Unknown criteria type: ${criteria.type}`)
162+
logger.error('Unknown criteria passed to RuleEngine: %O', criteria)
163+
throw new ToolkitError(`Unknown criteria type: ${(criteria as any).type}`)
162164
}
163165
}
164166
}

packages/core/src/notifications/types.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,55 @@ import * as vscode from 'vscode'
77
import { EnvType, OperatingSystem } from '../shared/telemetry/util'
88
import { TypeConstructor } from '../shared/utilities/typeConstructors'
99
import { AuthUserState, AuthStatus } from '../shared/telemetry/telemetry.gen'
10+
import { AuthType } from '../auth/connection'
1011

1112
/** Types of information that we can use to determine whether to show a notification or not. */
12-
export type Criteria = 'OS' | 'ComputeEnv' | 'AuthType' | 'AuthRegion' | 'AuthState' | 'AuthScopes' | 'ActiveExtensions'
1313

14-
/** Generic condition where the type determines how the values are evaluated. */
15-
export interface CriteriaCondition {
16-
readonly type: Criteria
17-
readonly values: string[]
14+
type OsCriteria = {
15+
type: 'OS'
16+
values: OperatingSystem[]
17+
}
18+
19+
type ComputeEnvCriteria = {
20+
type: 'ComputeEnv'
21+
values: EnvType[]
22+
}
23+
24+
type AuthTypeCriteria = {
25+
type: 'AuthType'
26+
values: AuthType[]
27+
}
28+
29+
type AuthRegionCriteria = {
30+
type: 'AuthRegion'
31+
values: string[]
32+
}
33+
34+
type AuthStateCriteria = {
35+
type: 'AuthState'
36+
values: AuthStatus[]
37+
}
38+
39+
type AuthScopesCriteria = {
40+
type: 'AuthScopes'
41+
values: string[] // TODO: Scopes should be typed. Could import here, but don't want to import too much.
42+
}
43+
44+
type ActiveExtensionsCriteria = {
45+
type: 'ActiveExtensions'
46+
values: string[]
1847
}
1948

49+
/** Generic condition where the type determines how the values are evaluated. */
50+
export type CriteriaCondition =
51+
| OsCriteria
52+
| ComputeEnvCriteria
53+
| AuthTypeCriteria
54+
| AuthRegionCriteria
55+
| AuthStateCriteria
56+
| AuthScopesCriteria
57+
| ActiveExtensionsCriteria
58+
2059
/** One of the subconditions (clauses) must match to be valid. */
2160
export interface OR {
2261
readonly type: 'or'
@@ -39,8 +78,8 @@ export interface ExactMatch {
3978
export type ConditionalClause = Range | ExactMatch | OR
4079

4180
export type OnReceiveType = 'toast' | 'modal'
42-
export type OnClickType = 'modal' | 'openTextDocument' | 'openUrl'
43-
export type ActionType = 'openUrl' | 'updateAndReload' | 'openTxt'
81+
export type OnClickType = { type: 'modal' } | { type: 'openTextDocument' } | { type: 'openUrl'; url: string }
82+
export type ActionType = 'openUrl' | 'updateAndReload' | 'openTextDocument'
4483

4584
/** How to display the notification. */
4685
export interface UIRenderInstructions {
@@ -51,11 +90,8 @@ export interface UIRenderInstructions {
5190
toastPreview?: string // optional property for toast
5291
}
5392
}
54-
onRecieve: OnReceiveType // TODO: typo
55-
onClick: {
56-
type: OnClickType
57-
url?: string // optional property for 'openUrl'
58-
}
93+
onReceive: OnReceiveType
94+
onClick: OnClickType
5995
actions?: Array<{
6096
type: ActionType
6197
displayText: {
@@ -124,7 +160,7 @@ export interface RuleContext {
124160
readonly extensionVersion: string
125161
readonly os: OperatingSystem
126162
readonly computeEnv: EnvType
127-
readonly authTypes: ('credentials' | 'builderId' | 'identityCenter' | 'unknown')[]
163+
readonly authTypes: AuthType[]
128164
readonly authRegions: string[]
129165
readonly authStates: AuthStatus[]
130166
readonly authScopes: string[]

packages/core/src/test/awsService/ec2/model.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Ec2ConnectClient', function () {
2424
})
2525

2626
describe('getAttachedIamRole', async function () {
27-
it('only returns role if recieves ARN from instance profile', async function () {
27+
it('only returns role if receives ARN from instance profile', async function () {
2828
let role: IAM.Role | undefined
2929
const getInstanceProfileStub = sinon.stub(Ec2Client.prototype, 'getAttachedIamInstanceProfile')
3030

0 commit comments

Comments
 (0)