Skip to content

Commit 330f565

Browse files
authored
fix(amazonq): confusing message during loading profiles (#5552)
1 parent f7c7687 commit 330f565

File tree

8 files changed

+256
-151
lines changed

8 files changed

+256
-151
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt

Lines changed: 80 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
220220
)
221221
}
222222

223+
is BrowserMessage.ListProfiles -> {
224+
handleListProfilesMessage()
225+
}
226+
223227
is BrowserMessage.PublishWebviewTelemetry -> {
224228
publishTelemetry(message)
225229
}
@@ -262,60 +266,41 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
262266
writeValueAsString(it)
263267
}
264268

265-
// TODO: pass "REAUTH" if connection expires
266-
// Perform the potentially blocking AWS call outside the EDT to fetch available region profiles.
267-
ApplicationManager.getApplication().executeOnPooledThread {
268-
val stage = if (isQExpired(project)) {
269-
"REAUTH"
270-
} else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
271-
"PROFILE_SELECT"
272-
} else {
273-
"START"
274-
}
275-
276-
var errorMessage: String? = null
277-
var profiles: List<QRegionProfile> = emptyList()
269+
val stage = if (isQExpired(project)) {
270+
"REAUTH"
271+
} else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
272+
"PROFILE_SELECT"
273+
} else {
274+
"START"
275+
}
278276

279-
if (stage == "PROFILE_SELECT") {
280-
try {
281-
profiles = QRegionProfileManager.getInstance().listRegionProfiles(project).orEmpty()
282-
if (profiles.size == 1) {
283-
LOG.debug { "User only have access to 1 Q profile, auto-selecting profile ${profiles.first().profileName} for ${project.name}" }
284-
QRegionProfileManager.getInstance().switchProfile(project, profiles.first(), QProfileSwitchIntent.Update)
285-
}
286-
} catch (e: Exception) {
287-
errorMessage = e.message
288-
LOG.warn { "Failed to call listRegionProfiles API" }
289-
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
290-
Telemetry.amazonq.didSelectProfile.use { span ->
291-
span.source(QProfileSwitchIntent.Auth.value)
292-
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
293-
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
294-
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
295-
.result(MetricResult.Failed)
296-
.reason(e.message)
277+
when (stage) {
278+
"PROFILE_SELECT" -> {
279+
val jsonData = """
280+
{
281+
stage: '$stage',
282+
status: 'pending'
297283
}
298-
}
284+
""".trimIndent()
285+
executeJS("window.ideClient.prepareUi($jsonData)")
299286
}
300287

301-
val jsonData = """
302-
{
303-
stage: '$stage',
304-
regions: $regions,
305-
idcInfo: {
306-
profileName: '${lastLoginIdcInfo.profileName}',
307-
startUrl: '${lastLoginIdcInfo.startUrl}',
308-
region: '${lastLoginIdcInfo.region}'
309-
},
310-
cancellable: ${state.browserCancellable},
311-
feature: '${state.feature}',
312-
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
313-
profiles: ${writeValueAsString(profiles)},
314-
errorMessage: ${errorMessage?.let { "\"$it\"" } ?: "null"}
315-
}
316-
""".trimIndent()
288+
else -> {
289+
val jsonData = """
290+
{
291+
stage: '$stage',
292+
regions: $regions,
293+
idcInfo: {
294+
profileName: '${lastLoginIdcInfo.profileName}',
295+
startUrl: '${lastLoginIdcInfo.startUrl}',
296+
region: '${lastLoginIdcInfo.region}'
297+
},
298+
cancellable: ${state.browserCancellable},
299+
feature: '${state.feature}',
300+
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
301+
}
302+
""".trimIndent()
317303

318-
runInEdt {
319304
executeJS("window.ideClient.prepareUi($jsonData)")
320305
}
321306
}
@@ -330,6 +315,52 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
330315
jcefBrowser.loadHTML(getWebviewHTML(webScriptUri, query))
331316
}
332317

318+
private fun handleListProfilesMessage() {
319+
ApplicationManager.getApplication().executeOnPooledThread {
320+
var errorMessage = ""
321+
val profiles = try {
322+
QRegionProfileManager.getInstance().listRegionProfiles(project)
323+
} catch (e: Exception) {
324+
e.message?.let {
325+
errorMessage = it
326+
}
327+
LOG.warn { "Failed to call listRegionProfiles API: $errorMessage" }
328+
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
329+
Telemetry.amazonq.didSelectProfile.use { span ->
330+
span.source(QProfileSwitchIntent.Auth.value)
331+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
332+
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
333+
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
334+
.result(MetricResult.Failed)
335+
.reason(e.message)
336+
}
337+
338+
null
339+
}
340+
341+
// auto-select the profile if users only have 1 and don't show the UI
342+
if (profiles?.size == 1) {
343+
LOG.debug { "User only have access to 1 Q profile, auto-selecting profile ${profiles.first().profileName} for ${project.name}" }
344+
QRegionProfileManager.getInstance().switchProfile(project, profiles.first(), QProfileSwitchIntent.Update)
345+
return@executeOnPooledThread
346+
}
347+
348+
// required EDT as this entire block is executed on thread pool
349+
runInEdt {
350+
val jsonData = """
351+
{
352+
stage: 'PROFILE_SELECT',
353+
status: '${if (profiles != null) "succeeded" else "failed"}',
354+
profiles: ${writeValueAsString(profiles ?: "")},
355+
errorMessage: '$errorMessage'
356+
}
357+
""".trimIndent()
358+
359+
executeJS("window.ideClient.prepareUi($jsonData)")
360+
}
361+
}
362+
}
363+
333364
companion object {
334365
private val LOG = getLogger<QWebviewBrowser>()
335366
private const val WEB_SCRIPT_URI = "http://webview/js/getStart.js"

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/webview/BrowserMessage.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
2727
JsonSubTypes.Type(value = BrowserMessage.Reauth::class, name = "reauth"),
2828
JsonSubTypes.Type(value = BrowserMessage.SendUiClickTelemetry::class, name = "sendUiClickTelemetry"),
2929
JsonSubTypes.Type(value = BrowserMessage.SwitchProfile::class, name = "switchProfile"),
30-
JsonSubTypes.Type(value = BrowserMessage.PublishWebviewTelemetry::class, name = "webviewTelemetry")
30+
JsonSubTypes.Type(value = BrowserMessage.PublishWebviewTelemetry::class, name = "webviewTelemetry"),
31+
JsonSubTypes.Type(value = BrowserMessage.ListProfiles::class, name = "listProfiles")
3132
)
3233
sealed interface BrowserMessage {
3334

@@ -66,6 +67,8 @@ sealed interface BrowserMessage {
6667
val arn: String,
6768
) : BrowserMessage
6869

70+
object ListProfiles : BrowserMessage
71+
6972
data class SendUiClickTelemetry(val signInOptionClicked: String?) : BrowserMessage
7073

7174
data class PublishWebviewTelemetry(val event: String) : BrowserMessage

plugins/core/webview/src/ideClient.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,62 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import {Store} from "vuex";
5-
import {IdcInfo, Region, Stage, State, BrowserSetupData, AwsBearerTokenConnection, Profile} from "./model";
5+
import {
6+
IdcInfo,
7+
State,
8+
AuthSetupMessageFromIde,
9+
ListProfileResult,
10+
ListProfileSuccessResult,
11+
ListProfileFailureResult, ListProfilePendingResult, ListProfilesMessageFromIde
12+
} from "./model";
613
import {WebviewTelemetry} from './webviewTelemetry'
714

815
export class IdeClient {
916
constructor(private readonly store: Store<State>) {}
1017

1118
// TODO: design and improve the API here
1219

13-
prepareUi(state: BrowserSetupData) {
20+
prepareUi(state: AuthSetupMessageFromIde | ListProfilesMessageFromIde) {
1421
WebviewTelemetry.instance.reset()
1522
console.log('browser is preparing UI with state ', state)
16-
this.store.commit('setStage', state.stage)
1723
// hack as window.onerror don't have access to vuex store
1824
void ((window as any).uiState = state.stage)
1925
WebviewTelemetry.instance.willShowPage(state.stage)
20-
this.store.commit('setSsoRegions', state.regions)
21-
this.updateLastLoginIdcInfo(state.idcInfo)
22-
this.store.commit("setCancellable", state.cancellable)
23-
this.store.commit("setFeature", state.feature)
24-
this.store.commit('setProfiles', state.profiles);
25-
this.store.commit("setErrorMessage", state.errorMessage)
26-
const existConnections = state.existConnections.map(it => {
26+
27+
this.store.commit('setStage', state.stage)
28+
29+
switch (state.stage) {
30+
case "PROFILE_SELECT":
31+
this.handleProfileSelectMessage(state as ListProfilesMessageFromIde)
32+
break
33+
34+
default:
35+
this.handleAuthSetupMessage(state as AuthSetupMessageFromIde)
36+
}
37+
}
38+
39+
private handleProfileSelectMessage(msg: ListProfilesMessageFromIde) {
40+
let result: ListProfileResult | undefined
41+
switch (msg.status) {
42+
case 'succeeded':
43+
result = new ListProfileSuccessResult(msg.profiles)
44+
break
45+
case 'failed':
46+
result = new ListProfileFailureResult(msg.errorMessage)
47+
break
48+
case 'pending':
49+
result = new ListProfilePendingResult()
50+
break
51+
}
52+
this.store.commit('setProfilesResult', result)
53+
}
54+
55+
private handleAuthSetupMessage(msg: AuthSetupMessageFromIde) {
56+
this.store.commit('setSsoRegions', msg.regions)
57+
this.updateLastLoginIdcInfo(msg.idcInfo)
58+
this.store.commit("setCancellable", msg.cancellable)
59+
this.store.commit("setFeature", msg.feature)
60+
const existConnections = msg.existConnections.map(it => {
2761
return {
2862
sessionName: it.sessionName,
2963
startUrl: it.startUrl,
@@ -37,13 +71,6 @@ export class IdeClient {
3771
this.updateAuthorization(undefined)
3872
}
3973

40-
handleProfiles(profilesData: { profiles: Profile[] }) {
41-
this.store.commit('setStage', 'PROFILE_SELECT')
42-
console.debug("received profile data")
43-
const availableProfiles: Profile[] = profilesData.profiles;
44-
this.store.commit('setProfiles', availableProfiles);
45-
}
46-
4774
updateAuthorization(code: string | undefined) {
4875
this.store.commit('setAuthorizationCode', code)
4976
// TODO: mutage stage to AUTHing here probably makes life easier

plugins/core/webview/src/model.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
export type BrowserSetupData = {
4+
export type AuthSetupMessageFromIde = {
55
stage: Stage,
66
regions: Region[],
77
idcInfo: IdcInfo,
88
cancellable: boolean,
99
feature: string,
1010
existConnections: AwsBearerTokenConnection[],
11+
}
12+
13+
export type ListProfilesMessageFromIde = {
14+
stage: Stage,
15+
status: 'succeeded' | 'failed' | 'pending',
1116
profiles: Profile[],
1217
errorMessage: string
1318
}
1419

20+
1521
// plugin interface [AwsBearerTokenConnection]
1622
export interface AwsBearerTokenConnection {
1723
sessionName: string,
@@ -20,6 +26,7 @@ export interface AwsBearerTokenConnection {
2026
scopes: string[],
2127
id: string
2228
}
29+
2330
export const SONO_URL = "https://view.awsapps.com/start"
2431

2532
export type Stage =
@@ -54,9 +61,27 @@ export interface State {
5461
feature: Feature,
5562
cancellable: boolean,
5663
existingConnections: AwsBearerTokenConnection[],
57-
profiles: Profile[],
58-
selectedProfile: Profile | undefined,
59-
errorMessage: string | undefined
64+
listProfilesResult: ListProfileResult | undefined,
65+
selectedProfile: Profile | undefined
66+
}
67+
68+
export interface ListProfileResult {
69+
status: 'succeeded' | 'failed' | 'pending'
70+
}
71+
72+
export class ListProfileSuccessResult implements ListProfileResult {
73+
status: 'succeeded' = 'succeeded'
74+
constructor(readonly profiles: Profile[]) {}
75+
}
76+
77+
export class ListProfileFailureResult implements ListProfileResult {
78+
status: 'failed' = 'failed'
79+
constructor(readonly errorMessage: string) {}
80+
}
81+
82+
export class ListProfilePendingResult implements ListProfileResult {
83+
status: 'pending' = 'pending'
84+
constructor() {}
6085
}
6186

6287
export enum LoginIdentifier {

0 commit comments

Comments
 (0)