Skip to content

Commit 70ba83f

Browse files
authored
feat(amazonq): show all customizations across different profiles (#7060)
## Problem followup of #7049 as mentioned in #7049 ``` Before we introduced QProfile customization is bound to a specific idc instance After customization is bound to a specific Q profile and an idc instance can have multi profiles in other words, each Q profile will have access to different sets of customization ``` ## Solution Product team wants us to show all customizations across profiles instead of the connected one only. By that mean, when users select a customization, it might implicitly change the profile for users in the selected customization is not accessible by the current profile. The purpose is to reduce the churn users might be lost what profile has access to what customization. ## user story 1. click "select customization" button from the menu -> should show "all" customizations across profiles 2. select a customization under different profile -> should change the profile to the one owning the newly selected customization 3. select a profile which doesn't have access to the selected customization -> should fallback to "default" and prompt ui saying you don't have access to the customization ### Implicity change profile when user selects a customization under a different profile https://github.com/user-attachments/assets/49bfe61f-04a7-4d07-aaff-1e3c284fb710 ### Change profile should validate if the selected customization is under the new profile or not https://github.com/user-attachments/assets/414f85dd-50a1-4d28-9b33-e59691f25a0c Note that customization will have only 1 profile owner, so there wont be duplicate customization across profiles --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 7afbc71 commit 70ba83f

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)