Skip to content

Commit 893175e

Browse files
Merge master into feature/hybridChat
2 parents e1242e8 + 70ba83f commit 893175e

File tree

9 files changed

+249
-90
lines changed

9 files changed

+249
-90
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Support selecting customizations across all Q profiles with automatic profile switching for enterprise users"
4+
}

packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('RegionProfileManager', function () {
6565
const mockClient = {
6666
listAvailableProfiles: listProfilesStub,
6767
}
68-
const createClientStub = sinon.stub(sut, 'createQClient').resolves(mockClient)
68+
const createClientStub = sinon.stub(sut, '_createQClient').resolves(mockClient)
6969

7070
const r = await sut.listRegionProfile()
7171

@@ -234,13 +234,65 @@ describe('RegionProfileManager', function () {
234234
})
235235

236236
describe('createQClient', function () {
237+
it(`should configure the endpoint and region from a profile`, async function () {
238+
await setupConnection('idc')
239+
240+
const iadClient = await sut.createQClient({
241+
name: 'foo',
242+
region: 'us-east-1',
243+
arn: 'arn',
244+
description: 'description',
245+
})
246+
247+
assert.deepStrictEqual(iadClient.config.region, 'us-east-1')
248+
assert.deepStrictEqual(iadClient.endpoint.href, 'https://q.us-east-1.amazonaws.com/')
249+
250+
const fraClient = await sut.createQClient({
251+
name: 'bar',
252+
region: 'eu-central-1',
253+
arn: 'arn',
254+
description: 'description',
255+
})
256+
257+
assert.deepStrictEqual(fraClient.config.region, 'eu-central-1')
258+
assert.deepStrictEqual(fraClient.endpoint.href, 'https://q.eu-central-1.amazonaws.com/')
259+
})
260+
261+
it(`should throw if the region is not supported or recognizable by Q`, async function () {
262+
await setupConnection('idc')
263+
264+
await assert.rejects(
265+
async () => {
266+
await sut.createQClient({
267+
name: 'foo',
268+
region: 'ap-east-1',
269+
arn: 'arn',
270+
description: 'description',
271+
})
272+
},
273+
{ message: /trying to initiatize Q client with unrecognizable region/ }
274+
)
275+
276+
await assert.rejects(
277+
async () => {
278+
await sut.createQClient({
279+
name: 'foo',
280+
region: 'unknown-somewhere',
281+
arn: 'arn',
282+
description: 'description',
283+
})
284+
},
285+
{ message: /trying to initiatize Q client with unrecognizable region/ }
286+
)
287+
})
288+
237289
it(`should configure the endpoint and region correspondingly`, async function () {
238290
await setupConnection('idc')
239291
await sut.switchRegionProfile(profileFoo, 'user')
240292
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
241293
const conn = authUtil.conn as SsoConnection
242294

243-
const client = await sut.createQClient('eu-central-1', 'https://amazon.com/', conn)
295+
const client = await sut._createQClient('eu-central-1', 'https://amazon.com/', conn)
244296

245297
assert.deepStrictEqual(client.config.region, 'eu-central-1')
246298
assert.deepStrictEqual(client.endpoint.href, 'https://amazon.com/')

packages/core/src/codewhisperer/activation.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,7 @@ import { AuthUtil } from './util/authUtil'
7272
import { ImportAdderProvider } from './service/importAdderProvider'
7373
import { TelemetryHelper } from './util/telemetryHelper'
7474
import { openUrl } from '../shared/utilities/vsCodeUtils'
75-
import {
76-
getAvailableCustomizationsList,
77-
getSelectedCustomization,
78-
notifyNewCustomizations,
79-
switchToBaseCustomizationAndNotify,
80-
} from './util/customizationUtil'
75+
import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil'
8176
import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands'
8277
import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider'
8378
import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider'
@@ -343,26 +338,7 @@ export async function activate(context: ExtContext): Promise<void> {
343338
SecurityIssueCodeActionProvider.instance
344339
),
345340
vscode.commands.registerCommand('aws.amazonq.openEditorAtRange', openEditorAtRange),
346-
auth.regionProfileManager.onDidChangeRegionProfile(() => {
347-
// Validate user still has access to the selected customization.
348-
const selectedCustomization = getSelectedCustomization()
349-
// No need to validate base customization which has empty arn.
350-
if (selectedCustomization.arn.length > 0) {
351-
getAvailableCustomizationsList()
352-
.then(async (customizations) => {
353-
const r = customizations.find((it) => it.arn === selectedCustomization.arn)
354-
if (!r) {
355-
await switchToBaseCustomizationAndNotify()
356-
}
357-
})
358-
.catch((e) => {
359-
getLogger().error(
360-
`encounter error while validating selected customization on profile change: %s`,
361-
(e as Error).message
362-
)
363-
})
364-
}
365-
})
341+
auth.regionProfileManager.onDidChangeRegionProfile(onProfileChangedListener)
366342
)
367343

368344
// run the auth startup code with context for telemetry

packages/core/src/codewhisperer/client/codewhisperer.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,17 @@ import { AWSError, Credentials, Service } from 'aws-sdk'
77
import globals from '../../shared/extensionGlobals'
88
import * as CodeWhispererClient from './codewhispererclient'
99
import * as CodeWhispererUserClient from './codewhispereruserclient'
10-
import { ListAvailableCustomizationsResponse, SendTelemetryEventRequest } from './codewhispereruserclient'
10+
import { SendTelemetryEventRequest } from './codewhispereruserclient'
1111
import { ServiceOptions } from '../../shared/awsClientBuilder'
1212
import { hasVendedIamCredentials } from '../../auth/auth'
1313
import { CodeWhispererSettings } from '../util/codewhispererSettings'
1414
import { PromiseResult } from 'aws-sdk/lib/request'
1515
import { AuthUtil } from '../util/authUtil'
1616
import { isSsoConnection } from '../../auth/connection'
17-
import { pageableToCollection } from '../../shared/utilities/collectionUtils'
1817
import apiConfig = require('./service-2.json')
1918
import userApiConfig = require('./user-service-2.json')
2019
import { session } from '../util/codeWhispererSession'
2120
import { getLogger } from '../../shared/logger/logger'
22-
import { indent } from '../../shared/utilities/textUtilities'
2321
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
2422
import { extensionVersion, getServiceEnvVarConfig } from '../../shared/vscode/env'
2523
import { DevSettings } from '../../shared/settings'
@@ -219,28 +217,6 @@ export class DefaultCodeWhispererClient {
219217
.promise()
220218
}
221219

222-
public async listAvailableCustomizations(): Promise<ListAvailableCustomizationsResponse[]> {
223-
const client = await this.createUserSdkClient()
224-
const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile
225-
const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) =>
226-
client.listAvailableCustomizations(request).promise()
227-
return pageableToCollection(requester, { profileArn: profile?.arn }, 'nextToken')
228-
.promise()
229-
.then((resps) => {
230-
let logStr = 'amazonq: listAvailableCustomizations API request:'
231-
for (const resp of resps) {
232-
const requestId = resp.$response.requestId
233-
logStr += `\n${indent('RequestID: ', 4)}${requestId},\n${indent('Customizations:', 4)}`
234-
for (const [index, c] of resp.customizations.entries()) {
235-
const entry = `${index.toString().padStart(2, '0')}: ${c.name?.trim()}`
236-
logStr += `\n${indent(entry, 8)}`
237-
}
238-
}
239-
getLogger().debug(logStr)
240-
return resps
241-
})
242-
}
243-
244220
public async sendTelemetryEvent(request: SendTelemetryEventRequest) {
245221
const requestWithCommonFields: SendTelemetryEventRequest = {
246222
...request,

packages/core/src/codewhisperer/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,13 @@ export * as diagnosticsProvider from './service/diagnosticsProvider'
9999
export * from './ui/codeWhispererNodes'
100100
export { SecurityScanError, SecurityScanTimedOutError } from '../codewhisperer/models/errors'
101101
export * as CodeWhispererConstants from '../codewhisperer/models/constants'
102-
export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil'
102+
export {
103+
getSelectedCustomization,
104+
setSelectedCustomization,
105+
baseCustomization,
106+
onProfileChangedListener,
107+
CustomizationProvider,
108+
} from './util/customizationUtil'
103109
export { Container } from './service/serviceContainer'
104110
export * from './util/gitUtil'
105111
export * from './ui/prompters'

packages/core/src/codewhisperer/region/regionProfileManager.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,17 @@ const endpoints = createConstantMap({
4747
* 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile
4848
* 'reload' -> on plugin restart, plugin will try to reload previous selected profile
4949
*/
50-
export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload'
50+
export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload' | 'customization'
51+
52+
export type ProfileChangedEvent = {
53+
profile: RegionProfile | undefined
54+
intent: ProfileSwitchIntent
55+
}
5156

5257
export class RegionProfileManager {
5358
private static logger = getLogger()
5459
private _activeRegionProfile: RegionProfile | undefined
55-
private _onDidChangeRegionProfile = new vscode.EventEmitter<RegionProfile | undefined>()
60+
private _onDidChangeRegionProfile = new vscode.EventEmitter<ProfileChangedEvent>()
5661
public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
5762

5863
// Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result
@@ -112,7 +117,7 @@ export class RegionProfileManager {
112117
}
113118
const availableProfiles: RegionProfile[] = []
114119
for (const [region, endpoint] of endpoints.entries()) {
115-
const client = await this.createQClient(region, endpoint, conn as SsoConnection)
120+
const client = await this._createQClient(region, endpoint, conn as SsoConnection)
116121
const requester = async (request: CodeWhispererUserClient.ListAvailableProfilesRequest) =>
117122
client.listAvailableProfiles(request).promise()
118123
const request: CodeWhispererUserClient.ListAvailableProfilesRequest = {}
@@ -162,7 +167,7 @@ export class RegionProfileManager {
162167
const ssoConn = this.connectionProvider() as SsoConnection
163168

164169
// only prompt to users when users switch from A profile to B profile
165-
if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
170+
if (source !== 'customization' && this.activeRegionProfile !== undefined && regionProfile !== undefined) {
166171
const response = await showConfirmationMessage({
167172
prompt: localize(
168173
'AWS.amazonq.profile.confirmation',
@@ -204,13 +209,16 @@ export class RegionProfileManager {
204209
})
205210
}
206211

207-
await this._switchRegionProfile(regionProfile)
212+
await this._switchRegionProfile(regionProfile, source)
208213
}
209214

210-
private async _switchRegionProfile(regionProfile: RegionProfile | undefined) {
215+
private async _switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) {
211216
this._activeRegionProfile = regionProfile
212217

213-
this._onDidChangeRegionProfile.fire(regionProfile)
218+
this._onDidChangeRegionProfile.fire({
219+
profile: regionProfile,
220+
intent: source,
221+
})
214222
// dont show if it's a default (fallback)
215223
if (regionProfile && this.profiles.length > 1) {
216224
void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then()
@@ -343,7 +351,21 @@ export class RegionProfileManager {
343351
}
344352
}
345353

346-
async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise<CodeWhispererUserClient> {
354+
// TODO: Should maintain sdk client in a better way
355+
async createQClient(profile: RegionProfile): Promise<CodeWhispererUserClient> {
356+
const conn = this.connectionProvider()
357+
if (conn === undefined || !isSsoConnection(conn)) {
358+
throw new Error('No valid SSO connection')
359+
}
360+
const endpoint = endpoints.get(profile.region)
361+
if (!endpoint) {
362+
throw new Error(`trying to initiatize Q client with unrecognizable region ${profile.region}`)
363+
}
364+
return this._createQClient(profile.region, endpoint, conn)
365+
}
366+
367+
// Visible for testing only, do not use this directly, please use createQClient(profile)
368+
async _createQClient(region: string, endpoint: string, conn: SsoConnection): Promise<CodeWhispererUserClient> {
347369
const token = (await conn.getToken()).accessToken
348370
const serviceOption: ServiceOptions = {
349371
apiConfig: userApiConfig,

0 commit comments

Comments
 (0)