Skip to content

Commit 6672f77

Browse files
committed
Merge remote-tracking branch 'upstream/main' into browser
2 parents 1f209f7 + 330f565 commit 6672f77

File tree

8 files changed

+247
-151
lines changed

8 files changed

+247
-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
@@ -222,6 +222,10 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
222222
)
223223
}
224224

225+
is BrowserMessage.ListProfiles -> {
226+
handleListProfilesMessage()
227+
}
228+
225229
is BrowserMessage.PublishWebviewTelemetry -> {
226230
publishTelemetry(message)
227231
}
@@ -268,60 +272,41 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
268272
writeValueAsString(it)
269273
}
270274

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

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

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

324-
runInEdt {
325310
executeJS("window.ideClient.prepareUi($jsonData)")
326311
}
327312
}
@@ -336,6 +321,52 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
336321
jcefBrowser.loadHTML(getWebviewHTML(webScriptUri, query))
337322
}
338323

324+
private fun handleListProfilesMessage() {
325+
ApplicationManager.getApplication().executeOnPooledThread {
326+
var errorMessage = ""
327+
val profiles = try {
328+
QRegionProfileManager.getInstance().listRegionProfiles(project)
329+
} catch (e: Exception) {
330+
e.message?.let {
331+
errorMessage = it
332+
}
333+
LOG.warn { "Failed to call listRegionProfiles API: $errorMessage" }
334+
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
335+
Telemetry.amazonq.didSelectProfile.use { span ->
336+
span.source(QProfileSwitchIntent.Auth.value)
337+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
338+
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
339+
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
340+
.result(MetricResult.Failed)
341+
.reason(e.message)
342+
}
343+
344+
null
345+
}
346+
347+
// auto-select the profile if users only have 1 and don't show the UI
348+
if (profiles?.size == 1) {
349+
LOG.debug { "User only have access to 1 Q profile, auto-selecting profile ${profiles.first().profileName} for ${project.name}" }
350+
QRegionProfileManager.getInstance().switchProfile(project, profiles.first(), QProfileSwitchIntent.Update)
351+
return@executeOnPooledThread
352+
}
353+
354+
// required EDT as this entire block is executed on thread pool
355+
runInEdt {
356+
val jsonData = """
357+
{
358+
stage: 'PROFILE_SELECT',
359+
status: '${if (profiles != null) "succeeded" else "failed"}',
360+
profiles: ${writeValueAsString(profiles ?: "")},
361+
errorMessage: '$errorMessage'
362+
}
363+
""".trimIndent()
364+
365+
executeJS("window.ideClient.prepareUi($jsonData)")
366+
}
367+
}
368+
}
369+
339370
companion object {
340371
private val LOG = getLogger<QWebviewBrowser>()
341372
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
@@ -28,7 +28,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
2828
JsonSubTypes.Type(value = BrowserMessage.SendUiClickTelemetry::class, name = "sendUiClickTelemetry"),
2929
JsonSubTypes.Type(value = BrowserMessage.SwitchProfile::class, name = "switchProfile"),
3030
JsonSubTypes.Type(value = BrowserMessage.PublishWebviewTelemetry::class, name = "webviewTelemetry"),
31-
JsonSubTypes.Type(value = BrowserMessage.OpenUrl::class, name = "openUrl")
31+
JsonSubTypes.Type(value = BrowserMessage.OpenUrl::class, name = "openUrl"),
32+
JsonSubTypes.Type(value = BrowserMessage.ListProfiles::class, name = "listProfiles")
3233
)
3334
sealed interface BrowserMessage {
3435

@@ -69,6 +70,8 @@ sealed interface BrowserMessage {
6970
val arn: String,
7071
) : BrowserMessage
7172

73+
object ListProfiles : BrowserMessage
74+
7275
data class SendUiClickTelemetry(val signInOptionClicked: String?) : BrowserMessage
7376

7477
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)