Skip to content

Commit 5f97d38

Browse files
authored
Merge branch 'main' into regionExpansion_fixlaunch
2 parents d3c76bd + 330f565 commit 5f97d38

File tree

11 files changed

+333
-212
lines changed

11 files changed

+333
-212
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/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
action.q.hello.description=Hello description
21
amazonqInlineChat.hint.edit = Edit
32
amazonqInlineChat.popup.accept=Accept \u23CE
43
amazonqInlineChat.popup.cancel=Cancel \u238B
@@ -10,9 +9,9 @@ amazonqInlineChat.popup.title=Enter Instructions for Q
109
amazonq.refresh.panel=Refresh Chat Session
1110
amazonq.title=Amazon Q
1211
amazonq.workspace.settings.open.prompt=Workspace index is now enabled. You can disable it from Amazon Q settings.
13-
action.q.profile.usage.text=You changed profile
14-
action.q.profile.usage=You're using the '<b>{0}</b>' profile for Amazon Q.
15-
action.q.switchProfiles.text=Change profile
12+
action.q.profile.usage.text=You changed your profile
13+
action.q.profile.usage=You''re using the ''<b>{0}</b>'' profile for Amazon Q.
14+
action.q.switchProfiles.text=Change Profile
1615
action.q.switchProfiles.dialog.text=Amazon Q Developer Profile
1716
action.q.switchProfiles.dialog.account.label=Account: {0}
1817
action.q.switchProfiles.dialog.panel.text=Change your Q Developer profile

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/actions/QSwitchProfilesAction.kt

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,10 @@ import com.intellij.icons.AllIcons
77
import com.intellij.openapi.actionSystem.ActionUpdateThread
88
import com.intellij.openapi.actionSystem.AnAction
99
import com.intellij.openapi.actionSystem.AnActionEvent
10-
import com.intellij.openapi.application.ApplicationManager
1110
import com.intellij.openapi.project.DumbAware
12-
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
13-
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
14-
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
15-
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
1611
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileDialog
1712
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
1813
import software.aws.toolkits.resources.AmazonQBundle.message
19-
import software.aws.toolkits.telemetry.MetricResult
20-
import software.aws.toolkits.telemetry.Telemetry
2114

2215
class QSwitchProfilesAction : AnAction(message("action.q.switchProfiles.text")), DumbAware {
2316

@@ -29,30 +22,9 @@ class QSwitchProfilesAction : AnAction(message("action.q.switchProfiles.text")),
2922

3023
override fun actionPerformed(e: AnActionEvent) {
3124
val project = e.project ?: return
32-
ApplicationManager.getApplication().executeOnPooledThread {
33-
val profiles = try {
34-
QRegionProfileManager.getInstance().listRegionProfiles(project)
35-
} catch (e: Exception) {
36-
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
37-
Telemetry.amazonq.didSelectProfile.use { span ->
38-
span.source(QProfileSwitchIntent.User.value)
39-
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
40-
.ssoRegion(conn?.region)
41-
.credentialStartUrl(conn?.startUrl)
42-
.result(MetricResult.Failed)
43-
.reason(e.message)
44-
}
45-
throw e
46-
}
47-
?: error("Attempted to fetch profiles while there does not exist")
48-
val selectedProfile = QRegionProfileManager.getInstance().activeProfile(project) ?: profiles[0]
49-
ApplicationManager.getApplication().invokeLater {
50-
QRegionProfileDialog(
51-
project,
52-
profiles = profiles,
53-
selectedProfile = selectedProfile
54-
).show()
55-
}
56-
}
25+
QRegionProfileDialog(
26+
project,
27+
selectedProfile = QRegionProfileManager.getInstance().activeProfile(project)
28+
).show()
5729
}
5830
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileDialog.kt

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,61 @@ import com.intellij.icons.AllIcons
77
import com.intellij.openapi.project.Project
88
import com.intellij.openapi.ui.DialogPanel
99
import com.intellij.openapi.ui.DialogWrapper
10+
import com.intellij.ui.ColoredListCellRenderer
11+
import com.intellij.ui.SimpleTextAttributes
12+
import com.intellij.ui.dsl.builder.AlignX
1013
import com.intellij.ui.dsl.builder.BottomGap
11-
import com.intellij.ui.dsl.builder.bind
14+
import com.intellij.ui.dsl.builder.bindItem
1215
import com.intellij.ui.dsl.builder.panel
16+
import com.intellij.ui.dsl.builder.toNullableProperty
1317
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
1418
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
1519
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
1620
import software.aws.toolkits.jetbrains.core.help.HelpIds
21+
import software.aws.toolkits.jetbrains.ui.AsyncComboBox
22+
import software.aws.toolkits.jetbrains.utils.ui.selected
1723
import software.aws.toolkits.resources.AmazonQBundle.message
24+
import software.aws.toolkits.resources.AwsCoreBundle
1825
import software.aws.toolkits.telemetry.MetricResult
1926
import software.aws.toolkits.telemetry.Telemetry
2027
import javax.swing.JComponent
28+
import javax.swing.JList
29+
30+
data class QRegionProfileDialogState(
31+
var selectedProfile: QRegionProfile? = null,
32+
)
2133

2234
class QRegionProfileDialog(
2335
private var project: Project,
24-
private var profiles: List<QRegionProfile>,
25-
private var selectedProfile: QRegionProfile, // default
36+
val state: QRegionProfileDialogState = QRegionProfileDialogState(),
37+
private var selectedProfile: QRegionProfile?,
2638
) : DialogWrapper(project) {
2739

40+
private val renderer = object : ColoredListCellRenderer<QRegionProfile>() {
41+
override fun customizeCellRenderer(
42+
list: JList<out QRegionProfile>,
43+
value: QRegionProfile?,
44+
index: Int,
45+
selected: Boolean,
46+
hasFocus: Boolean,
47+
) {
48+
value?.let {
49+
append(
50+
if (it == selectedProfile) {
51+
"${it.profileName} - ${it.region} (connected)"
52+
} else {
53+
"${it.profileName} - ${it.region}"
54+
},
55+
SimpleTextAttributes.REGULAR_ATTRIBUTES
56+
)
57+
58+
append(" " + message("action.q.switchProfiles.dialog.account.label", it.accountId), SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES)
59+
}
60+
}
61+
}
62+
63+
private val combo = AsyncComboBox<QRegionProfile>(customRenderer = renderer)
64+
2865
private val panel: DialogPanel by lazy {
2966
panel {
3067
row { label(message("action.q.switchProfiles.dialog.panel.text")).bold() }
@@ -36,32 +73,40 @@ class QRegionProfileDialog(
3673
}
3774
separator().bottomGap(BottomGap.MEDIUM)
3875

39-
buttonsGroup {
40-
profiles.forEach { profile ->
41-
row {
42-
radioButton("", profile)
43-
44-
panel {
45-
val regionDisplay = if (profile == selectedProfile) {
46-
"${profile.profileName} - ${profile.region} (connected)"
47-
} else {
48-
"${profile.profileName} - ${profile.region}"
49-
}
50-
row { label(regionDisplay) }
51-
row {
52-
label(message("action.q.switchProfiles.dialog.account.label", profile.accountId)).applyToComponent {
53-
font = font.deriveFont(font.size2D - 2.0f)
54-
}
55-
}
56-
}
57-
}.bottomGap(BottomGap.MEDIUM)
76+
combo.proposeModelUpdate { model ->
77+
try {
78+
QRegionProfileManager.getInstance().listRegionProfiles(project)?.forEach {
79+
model.addElement(it)
80+
} ?: error("Attempted to fetch profiles while there does not exist")
81+
82+
model.selectedItem = selectedProfile
83+
} catch (e: Exception) {
84+
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
85+
Telemetry.amazonq.didSelectProfile.use { span ->
86+
span.source(QProfileSwitchIntent.User.value)
87+
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
88+
.ssoRegion(conn?.region)
89+
.credentialStartUrl(conn?.startUrl)
90+
.result(MetricResult.Failed)
91+
.reason(e.message)
92+
}
93+
throw e
5894
}
59-
}.bind({ selectedOption }, { selectedOption = it })
95+
}
96+
97+
row {
98+
cell(combo)
99+
.align(AlignX.FILL)
100+
.errorOnApply(AwsCoreBundle.message("gettingstarted.setup.error.not_selected")) { it.selected() == null }
101+
.bindItem(state::selectedProfile.toNullableProperty())
102+
}
60103

61104
separator().bottomGap(BottomGap.MEDIUM)
62105
}
63106
}
64-
private var selectedOption: QRegionProfile = selectedProfile // user selected
107+
108+
private val selectedOption
109+
get() = state.selectedProfile // user selected
65110

66111
init {
67112
title = message("action.q.switchProfiles.dialog.text")
@@ -87,7 +132,7 @@ class QRegionProfileDialog(
87132
Telemetry.amazonq.didSelectProfile.use { span ->
88133
span.source(QProfileSwitchIntent.User.value)
89134
.amazonQProfileRegion(profileManager.activeProfile(project)?.region ?: "not-set")
90-
.profileCount(profiles.size)
135+
.profileCount(combo.model.size)
91136
.ssoRegion(conn?.region)
92137
.credentialStartUrl(conn?.startUrl)
93138
.result(MetricResult.Cancelled)

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

0 commit comments

Comments
 (0)