Skip to content

Commit a6debbf

Browse files
committed
profile loading ux
1 parent 36ee79e commit a6debbf

File tree

7 files changed

+265
-157
lines changed

7 files changed

+265
-157
lines changed

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

Lines changed: 75 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,16 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
182182
ToolkitConnectionManager.getInstance(project)
183183
.activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
184184
)?.let { connection ->
185-
runInEdt {
186-
SsoLogoutAction(connection).actionPerformed(
187-
AnActionEvent.createFromDataContext(
188-
"qBrowser",
189-
null,
190-
DataContext.EMPTY_CONTEXT
185+
runInEdt {
186+
SsoLogoutAction(connection).actionPerformed(
187+
AnActionEvent.createFromDataContext(
188+
"qBrowser",
189+
null,
190+
DataContext.EMPTY_CONTEXT
191+
)
191192
)
192-
)
193+
}
193194
}
194-
}
195195
}
196196

197197
is BrowserMessage.Reauth -> {
@@ -261,56 +261,79 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
261261
writeValueAsString(it)
262262
}
263263

264-
// TODO: pass "REAUTH" if connection expires
265-
// Perform the potentially blocking AWS call outside the EDT to fetch available region profiles.
266-
ApplicationManager.getApplication().executeOnPooledThread {
267-
val stage = if (isQExpired(project)) {
268-
"REAUTH"
269-
} else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
270-
"PROFILE_SELECT"
271-
} else {
272-
"START"
273-
}
264+
val stage = if (isQExpired(project)) {
265+
"REAUTH"
266+
} else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
267+
"PROFILE_SELECT"
268+
} else {
269+
"START"
270+
}
271+
272+
when (stage) {
273+
"PROFILE_SELECT" -> {
274+
ApplicationManager.getApplication().executeOnPooledThread {
275+
var errorMessage: String = ""
276+
val profiles = try {
277+
QRegionProfileManager.getInstance().listRegionProfiles(project)
278+
} catch (e: Exception) {
279+
e.message?.let {
280+
errorMessage = it
281+
}
282+
LOG.warn { "Failed to call listRegionProfiles API: $errorMessage" }
283+
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
284+
Telemetry.amazonq.didSelectProfile.use { span ->
285+
span.source(QProfileSwitchIntent.Auth.value)
286+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
287+
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
288+
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
289+
.result(MetricResult.Failed)
290+
.reason(e.message)
291+
}
274292

275-
var errorMessage: String? = null
276-
var profiles: List<QRegionProfile> = emptyList()
277-
278-
if (stage == "PROFILE_SELECT") {
279-
try {
280-
profiles = QRegionProfileManager.getInstance().listRegionProfiles(project).orEmpty()
281-
} catch (e: Exception) {
282-
errorMessage = e.message
283-
LOG.warn { "Failed to call listRegionProfiles API" }
284-
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
285-
Telemetry.amazonq.didSelectProfile.use { span ->
286-
span.source(QProfileSwitchIntent.Auth.value)
287-
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
288-
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
289-
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
290-
.result(MetricResult.Failed)
291-
.reason(e.message)
293+
null
294+
}
295+
296+
// required EDT as this entire block is executed on thread pool
297+
runInEdt {
298+
val jsonData = """
299+
{
300+
stage: '$stage',
301+
status: '${if (profiles != null) "succeeded" else "failed"}',
302+
profiles: ${writeValueAsString(profiles ?: "")},
303+
errorMessage: '$errorMessage'
304+
}
305+
""".trimIndent()
306+
307+
println(jsonData)
308+
executeJS("window.ideClient.prepareUi($jsonData)")
292309
}
293310
}
294-
}
295311

296-
val jsonData = """
297-
{
298-
stage: '$stage',
299-
regions: $regions,
300-
idcInfo: {
301-
profileName: '${lastLoginIdcInfo.profileName}',
302-
startUrl: '${lastLoginIdcInfo.startUrl}',
303-
region: '${lastLoginIdcInfo.region}'
304-
},
305-
cancellable: ${state.browserCancellable},
306-
feature: '${state.feature}',
307-
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
308-
profiles: ${writeValueAsString(profiles)},
309-
errorMessage: ${errorMessage?.let { "\"$it\"" } ?: "null"}
312+
val jsonData = """
313+
{
314+
stage: '$stage',
315+
status: 'pending'
316+
}
317+
""".trimIndent()
318+
executeJS("window.ideClient.prepareUi($jsonData)")
310319
}
311-
""".trimIndent()
312320

313-
runInEdt {
321+
else -> {
322+
val jsonData = """
323+
{
324+
stage: '$stage',
325+
regions: $regions,
326+
idcInfo: {
327+
profileName: '${lastLoginIdcInfo.profileName}',
328+
startUrl: '${lastLoginIdcInfo.startUrl}',
329+
region: '${lastLoginIdcInfo.region}'
330+
},
331+
cancellable: ${state.browserCancellable},
332+
feature: '${state.feature}',
333+
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
334+
}
335+
""".trimIndent()
336+
314337
executeJS("window.ideClient.prepareUi($jsonData)")
315338
}
316339
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
2929
JsonSubTypes.Type(value = BrowserMessage.SwitchProfile::class, name = "switchProfile"),
3030
JsonSubTypes.Type(value = BrowserMessage.PublishWebviewTelemetry::class, name = "webviewTelemetry")
3131
)
32+
// Webview to IDE
3233
sealed interface BrowserMessage {
33-
3434
// FIX_WHEN_MIN_IS_233: data objects are not stable until Kotlin 1.9
3535
// https://kotlinlang.org/docs/whatsnew19.html#stable-data-objects-for-symmetry-with-data-classes
3636
object PrepareUi : BrowserMessage

plugins/core/webview/src/ideClient.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,63 @@
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: any) {
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)
32+
break
33+
34+
default:
35+
this.handleAuthSetupMessage(state)
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+
console.log(result)
53+
this.store.commit('setProfilesResult', result)
54+
}
55+
56+
private handleAuthSetupMessage(msg: AuthSetupMessageFromIde) {
57+
this.store.commit('setSsoRegions', msg.regions)
58+
this.updateLastLoginIdcInfo(msg.idcInfo)
59+
this.store.commit("setCancellable", msg.cancellable)
60+
this.store.commit("setFeature", msg.feature)
61+
const existConnections = msg.existConnections.map(it => {
2762
return {
2863
sessionName: it.sessionName,
2964
startUrl: it.startUrl,
@@ -37,13 +72,6 @@ export class IdeClient {
3772
this.updateAuthorization(undefined)
3873
}
3974

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-
4775
updateAuthorization(code: string | undefined) {
4876
this.store.commit('setAuthorizationCode', code)
4977
// TODO: mutage stage to AUTHing here probably makes life easier
@@ -60,6 +88,6 @@ export class IdeClient {
6088
cancelLogin(): void {
6189
// this.reset()
6290
this.store.commit('setStage', 'START')
63-
window.ideApi.postMessage({ command: 'cancelLogin' })
91+
window.ideApi.postMessage({command: 'cancelLogin'})
6492
}
6593
}

plugins/core/webview/src/model.ts

Lines changed: 37 additions & 5 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,33 @@ 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+
75+
constructor(readonly profiles: Profile[]) {
76+
}
77+
}
78+
79+
export class ListProfileFailureResult implements ListProfileResult {
80+
status: 'failed' = 'failed'
81+
82+
constructor(readonly errorMessage: string) {
83+
}
84+
}
85+
86+
export class ListProfilePendingResult implements ListProfileResult {
87+
status: 'pending' = 'pending'
88+
89+
constructor() {
90+
}
6091
}
6192

6293
export enum LoginIdentifier {
@@ -115,7 +146,8 @@ export class BuilderId implements LoginOption {
115146
export class ExistConnection implements LoginOption {
116147
id: LoginIdentifier = LoginIdentifier.EXISTING_LOGINS
117148

118-
constructor(readonly pluginConnectionId: string) {}
149+
constructor(readonly pluginConnectionId: string) {
150+
}
119151

120152
// this case only happens for bearer connection for now
121153
requiresBrowser(): boolean {

0 commit comments

Comments
 (0)