Skip to content

Commit f98622b

Browse files
authored
feat(notifications): add misc rule, log, type, and test changes (#5989)
- Add types to some RuleContext fields - Add some informative log statements for debugging - Add a test for getRuleContext() - Add case-insenstive rule checks - Fix getRuleContext to properly capture authTypes - Remove a few unneeded lines of code - Add red icon for emergency notifications --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 4235561 commit f98622b

File tree

5 files changed

+161
-19
lines changed

5 files changed

+161
-19
lines changed

packages/core/src/login/webview/vue/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,4 @@ export type AuthFormId =
8181
| 'identityCenterCodeWhisperer'
8282
| 'identityCenterCodeCatalyst'
8383
| 'identityCenterExplorer'
84-
| 'aggregateExplorer'
8584
| 'unknown'

packages/core/src/notifications/panelNode.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode'
77
import { ResourceTreeDataProvider, TreeNode } from '../shared/treeview/resourceTreeDataProvider'
88
import { Command, Commands } from '../shared/vscode/commands2'
9-
import { getIcon } from '../shared/icons'
9+
import { Icon, IconPath, getIcon } from '../shared/icons'
1010
import { contextKey, setContext } from '../shared/vscode/setContext'
1111
import { NotificationType, ToolkitNotification, getNotificationTelemetryId } from './types'
1212
import { ToolkitError } from '../shared/errors'
@@ -78,10 +78,15 @@ export class NotificationsNode implements TreeNode {
7878

7979
public getChildren() {
8080
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') }
8185
return this.openNotificationCmd.build(n).asTreeNode({
8286
label: n.uiRenderInstructions.content['en-US'].title,
83-
iconPath: type === 'startUp' ? getIcon('vscode-question') : getIcon('vscode-alert'),
87+
iconPath: icon,
8488
contextValue: type === 'startUp' ? this.startUpNodeContext : this.emergencyNodeContext,
89+
tooltip: 'Click to open',
8590
})
8691
}
8792

packages/core/src/notifications/rules.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as semver from 'semver'
88
import globals from '../shared/extensionGlobals'
99
import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification, AuthState } from './types'
1010
import { getComputeEnvType, getOperatingSystem } from '../shared/telemetry/util'
11+
import { AuthFormId } from '../login/webview/vue/types'
12+
import { getLogger } from '../shared/logger/logger'
1113

1214
/**
1315
* Evaluates if a given version fits into the parameters specified by a notification, e.g:
@@ -26,8 +28,14 @@ import { getComputeEnvType, getOperatingSystem } from '../shared/telemetry/util'
2628
function isValidVersion(version: string, condition: ConditionalClause): boolean {
2729
switch (condition.type) {
2830
case 'range': {
29-
const lowerConstraint = !condition.lowerInclusive || semver.gte(version, condition.lowerInclusive)
30-
const upperConstraint = !condition.upperExclusive || semver.lt(version, condition.upperExclusive)
31+
const lowerConstraint =
32+
!condition.lowerInclusive ||
33+
condition.lowerInclusive === '-inf' ||
34+
semver.gte(version, condition.lowerInclusive)
35+
const upperConstraint =
36+
!condition.upperExclusive ||
37+
condition.upperExclusive === '+inf' ||
38+
semver.lt(version, condition.upperExclusive)
3139
return lowerConstraint && upperConstraint
3240
}
3341
case 'exactMatch':
@@ -99,7 +107,6 @@ export class RuleEngine {
99107
const expected = criteria.values
100108
const expectedSet = new Set(expected)
101109

102-
const isExpected = (i: string) => expectedSet.has(i)
103110
const hasAnyOfExpected = (i: string[]) => i.some((v) => expectedSet.has(v))
104111
const isSuperSetOfExpected = (i: string[]) => {
105112
const s = new Set(i)
@@ -116,10 +123,9 @@ export class RuleEngine {
116123
// So... YAGNI
117124
switch (criteria.type) {
118125
case 'OS':
119-
// todo: allow lowercase?
120-
return isExpected(this.context.os)
126+
return hasAnyOfExpected([this.context.os])
121127
case 'ComputeEnv':
122-
return isExpected(this.context.computeEnv)
128+
return hasAnyOfExpected([this.context.computeEnv])
123129
case 'AuthType':
124130
return hasAnyOfExpected(this.context.authTypes)
125131
case 'AuthRegion':
@@ -139,16 +145,40 @@ export class RuleEngine {
139145
}
140146

141147
export async function getRuleContext(context: vscode.ExtensionContext, authState: AuthState): Promise<RuleContext> {
142-
return {
148+
const authTypes =
149+
authState.authEnabledConnections === ''
150+
? []
151+
: // TODO: There is a large disconnect in the codebase with how auth "types" are stored, displayed, sent to telemetry, etc.
152+
// For now we have this "hack" (more of an inefficiency) until auth code can be properly refactored.
153+
(authState.authEnabledConnections.split(',') as AuthFormId[]).map(
154+
(id): RuleContext['authTypes'][number] => {
155+
if (id.includes('builderId')) {
156+
return 'builderId'
157+
} else if (id.includes('identityCenter')) {
158+
return 'identityCenter'
159+
} else if (id.includes('credentials')) {
160+
return 'credentials'
161+
}
162+
return 'unknown'
163+
}
164+
)
165+
const ruleContext = {
143166
ideVersion: vscode.version,
144167
extensionVersion: context.extension.packageJSON.version,
145168
os: getOperatingSystem(),
146169
computeEnv: await getComputeEnvType(),
147-
authTypes: authState.authEnabledConnections.split(','),
148-
authRegions: authState.awsRegion ? [authState.awsRegion] : [],
149-
authStates: [authState.authStatus],
170+
authTypes: [...new Set(authTypes)],
150171
authScopes: authState.authScopes ? authState.authScopes?.split(',') : [],
151172
installedExtensions: vscode.extensions.all.map((e) => e.id),
152173
activeExtensions: vscode.extensions.all.filter((e) => e.isActive).map((e) => e.id),
174+
175+
// Toolkit (and eventually Q?) may have multiple connections with different regions and states.
176+
// However, this granularity does not seem useful at this time- only the active connection is considered.
177+
authRegions: authState.awsRegion ? [authState.awsRegion] : [],
178+
authStates: [authState.authStatus],
153179
}
180+
const { activeExtensions, installedExtensions, ...loggableRuleContext } = ruleContext
181+
getLogger('notifications').debug('getRuleContext() determined rule context: %O', loggableRuleContext)
182+
183+
return ruleContext
154184
}

packages/core/src/notifications/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode'
77
import { EnvType, OperatingSystem } from '../shared/telemetry/util'
88
import { TypeConstructor } from '../shared/utilities/typeConstructors'
9-
import { AuthUserState } from '../shared/telemetry/telemetry.gen'
9+
import { AuthUserState, AuthStatus } from '../shared/telemetry/telemetry.gen'
1010

1111
/** Types of information that we can use to determine whether to show a notification or not. */
1212
export type Criteria =
@@ -128,9 +128,9 @@ export interface RuleContext {
128128
readonly extensionVersion: string
129129
readonly os: OperatingSystem
130130
readonly computeEnv: EnvType
131-
readonly authTypes: string[]
131+
readonly authTypes: ('credentials' | 'builderId' | 'identityCenter' | 'unknown')[]
132132
readonly authRegions: string[]
133-
readonly authStates: string[]
133+
readonly authStates: AuthStatus[]
134134
readonly authScopes: string[]
135135
readonly installedExtensions: string[]
136136
readonly activeExtensions: string[]

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

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import * as vscode from 'vscode'
67
import assert from 'assert'
7-
import { RuleEngine } from '../../notifications/rules'
8+
import { RuleEngine, getRuleContext } from '../../notifications/rules'
89
import { DisplayIf, ToolkitNotification, RuleContext } from '../../notifications/types'
9-
import { globals } from '../../shared'
10+
import globals from '../../shared/extensionGlobals'
11+
import { Connection, scopesCodeCatalyst } from '../../auth/connection'
12+
import { getOperatingSystem } from '../../shared/telemetry/util'
13+
import { getAuthFormIdsFromConnection } from '../../auth/utils'
14+
import { builderIdStartUrl } from '../../auth/sso/model'
15+
import { amazonQScopes } from '../../codewhisperer'
1016

1117
describe('Notifications Rule Engine', function () {
1218
const context: RuleContext = {
@@ -126,6 +132,18 @@ describe('Notifications Rule Engine', function () {
126132
),
127133
true
128134
)
135+
assert.equal(
136+
ruleEngine.shouldDisplayNotification(
137+
buildNotification({
138+
extensionVersion: {
139+
type: 'range',
140+
lowerInclusive: '-inf',
141+
upperExclusive: '1.23.0',
142+
},
143+
})
144+
),
145+
true
146+
)
129147

130148
assert.equal(
131149
ruleEngine.shouldDisplayNotification(
@@ -138,6 +156,31 @@ describe('Notifications Rule Engine', function () {
138156
),
139157
true
140158
)
159+
assert.equal(
160+
ruleEngine.shouldDisplayNotification(
161+
buildNotification({
162+
extensionVersion: {
163+
type: 'range',
164+
lowerInclusive: '1.0.0',
165+
upperExclusive: '+inf',
166+
},
167+
})
168+
),
169+
true
170+
)
171+
172+
assert.equal(
173+
ruleEngine.shouldDisplayNotification(
174+
buildNotification({
175+
extensionVersion: {
176+
type: 'range',
177+
lowerInclusive: '-inf',
178+
upperExclusive: '+inf',
179+
},
180+
})
181+
),
182+
true
183+
)
141184
})
142185

143186
it('should NOT display notification with invalid version range criteria', function () {
@@ -153,6 +196,18 @@ describe('Notifications Rule Engine', function () {
153196
),
154197
false
155198
)
199+
assert.equal(
200+
ruleEngine.shouldDisplayNotification(
201+
buildNotification({
202+
extensionVersion: {
203+
type: 'range',
204+
lowerInclusive: '-inf',
205+
upperExclusive: '1.20.0',
206+
},
207+
})
208+
),
209+
false
210+
)
156211
})
157212

158213
it('should display notification with version OR criteria', function () {
@@ -365,7 +420,7 @@ describe('Notifications Rule Engine', function () {
365420
assert.equal(
366421
ruleEngine.shouldDisplayNotification(
367422
buildNotification({
368-
additionalCriteria: [{ type: 'InstalledExtensions', values: ['ext1', 'ext2', 'unkownExtension'] }],
423+
additionalCriteria: [{ type: 'InstalledExtensions', values: ['ext1', 'ext2', 'unknownExtension'] }],
369424
})
370425
),
371426
false
@@ -473,3 +528,56 @@ describe('Notifications Rule Engine', function () {
473528
)
474529
})
475530
})
531+
532+
describe('Notifications getRuleContext()', function () {
533+
it('should return the correct rule context', async function () {
534+
const context = globals.context
535+
context.extension.packageJSON.version = '0.0.1'
536+
const authEnabledConnections = (
537+
[
538+
{
539+
type: 'iam',
540+
},
541+
{
542+
type: 'sso',
543+
scopes: amazonQScopes,
544+
startUrl: builderIdStartUrl,
545+
},
546+
{
547+
type: 'sso',
548+
scopes: scopesCodeCatalyst,
549+
startUrl: builderIdStartUrl,
550+
},
551+
{
552+
type: 'sso',
553+
scopes: amazonQScopes,
554+
startUrl: 'something',
555+
},
556+
{
557+
type: 'unknown',
558+
},
559+
] as Connection[]
560+
)
561+
.map((c) => getAuthFormIdsFromConnection(c))
562+
.join(',')
563+
564+
const ruleContext = await getRuleContext(context, {
565+
authEnabledConnections,
566+
authScopes: amazonQScopes.join(','),
567+
authStatus: 'connected',
568+
awsRegion: 'us-east-1',
569+
})
570+
assert.deepStrictEqual(ruleContext, {
571+
ideVersion: vscode.version,
572+
extensionVersion: '0.0.1',
573+
os: getOperatingSystem(),
574+
computeEnv: 'test',
575+
authTypes: ['credentials', 'builderId', 'identityCenter', 'unknown'],
576+
authRegions: ['us-east-1'],
577+
authStates: ['connected'],
578+
authScopes: amazonQScopes,
579+
installedExtensions: vscode.extensions.all.map((e) => e.id),
580+
activeExtensions: vscode.extensions.all.filter((e) => e.isActive).map((e) => e.id),
581+
})
582+
})
583+
})

0 commit comments

Comments
 (0)