Skip to content

Commit dfe18e9

Browse files
authored
Show toasts for notifications and notification banner on critical notifications(#5097)
* Display toast notifications with actions * Condition matcher for displaying notifications * Show notification banner * feedback 1 * Modified deserialization cases and added tests * not required file change * feedback 1 * feedback 1 * modified the base class * merge conflicts resolved * rearranged call site * show notifications when panel is opened * fixed tests * detekt * feedback * convert panels into wrappers * fixed test
1 parent 87bbbce commit dfe18e9

File tree

17 files changed

+449
-71
lines changed

17 files changed

+449
-71
lines changed

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

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import com.intellij.openapi.project.Project
1010
import com.intellij.openapi.wm.ToolWindow
1111
import com.intellij.openapi.wm.ToolWindowFactory
1212
import com.intellij.openapi.wm.ex.ToolWindowEx
13-
import com.intellij.ui.content.Content
14-
import com.intellij.ui.content.ContentManager
13+
import com.intellij.ui.components.panels.Wrapper
14+
import com.intellij.util.ui.components.BorderLayoutPanel
1515
import software.aws.toolkits.core.utils.debug
1616
import software.aws.toolkits.core.utils.getLogger
1717
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
@@ -22,6 +22,8 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
2222
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
2323
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
2424
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
25+
import software.aws.toolkits.jetbrains.core.notifications.NotificationPanel
26+
import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase
2527
import software.aws.toolkits.jetbrains.core.webview.BrowserState
2628
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
2729
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
@@ -35,7 +37,21 @@ import java.awt.event.ComponentAdapter
3537
import java.awt.event.ComponentEvent
3638

3739
class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
40+
3841
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
42+
val mainPanel = BorderLayoutPanel()
43+
val qPanel = Wrapper()
44+
val notificationPanel = NotificationPanel()
45+
46+
mainPanel.addToTop(notificationPanel)
47+
mainPanel.add(qPanel)
48+
val notifListener = ProcessNotificationsBase.getInstance(project)
49+
notifListener.addListenerForNotification { bannerContent ->
50+
runInEdt {
51+
notificationPanel.updateNotificationPanel(bannerContent)
52+
}
53+
}
54+
3955
if (toolWindow is ToolWindowEx) {
4056
val actionManager = ActionManager.getInstance()
4157
toolWindow.setTitleActions(listOf(actionManager.getAction("aws.q.toolwindow.titleBar")))
@@ -46,7 +62,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
4662
ToolkitConnectionManagerListener.TOPIC,
4763
object : ToolkitConnectionManagerListener {
4864
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
49-
onConnectionChanged(project, newConnection, toolWindow)
65+
onConnectionChanged(project, newConnection, qPanel)
5066
}
5167
}
5268
)
@@ -56,8 +72,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
5672
object : RefreshQChatPanelButtonPressedListener {
5773
override fun onRefresh() {
5874
runInEdt {
59-
contentManager.removeAllContents(true)
60-
prepareChatContent(project, contentManager)
75+
prepareChatContent(project, qPanel)
6176
}
6277
}
6378
}
@@ -68,43 +83,37 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
6883
object : BearerTokenProviderListener {
6984
override fun onChange(providerId: String, newScopes: List<String>?) {
7085
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
71-
val content = contentManager.factory.createContent(AmazonQToolWindow.getInstance(project).component, null, false).also {
72-
it.isCloseable = true
73-
it.isPinnable = true
74-
}
86+
val qComponent = AmazonQToolWindow.getInstance(project).component
7587

7688
runInEdt {
77-
contentManager.removeAllContents(true)
78-
contentManager.addContent(content)
89+
qPanel.setContent(qComponent)
7990
}
8091
}
8192
}
8293
}
8394
)
8495

85-
val content = prepareChatContent(project, contentManager)
96+
prepareChatContent(project, qPanel)
8697

98+
val content = contentManager.factory.createContent(mainPanel, null, false).also {
99+
it.isCloseable = true
100+
it.isPinnable = true
101+
}
87102
toolWindow.activate(null)
88-
contentManager.setSelectedContent(content)
103+
contentManager.addContent(content)
89104
}
90105

91106
private fun prepareChatContent(
92107
project: Project,
93-
contentManager: ContentManager,
94-
): Content {
108+
qPanel: Wrapper,
109+
) {
95110
val component = if (isQConnected(project) && !isQExpired(project)) {
96111
AmazonQToolWindow.getInstance(project).component
97112
} else {
98113
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
99114
QWebviewPanel.getInstance(project).component
100115
}
101-
102-
val content = contentManager.factory.createContent(component, null, false).also {
103-
it.isCloseable = true
104-
it.isPinnable = true
105-
}
106-
contentManager.addContent(content)
107-
return content
116+
qPanel.setContent(component)
108117
}
109118

110119
override fun init(toolWindow: ToolWindow) {
@@ -125,8 +134,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
125134

126135
override fun shouldBeAvailable(project: Project): Boolean = isQWebviewsAvailable()
127136

128-
private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, toolWindow: ToolWindow) {
129-
val contentManager = toolWindow.contentManager
137+
private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, qPanel: Wrapper) {
130138
val isNewConnectionForQ = newConnection?.let {
131139
(it as? AwsBearerTokenConnection)?.let { conn ->
132140
val scopeShouldHave = Q_SCOPES
@@ -151,15 +159,8 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
151159
LOG.debug { "returning login window; no Q connection found" }
152160
QWebviewPanel.getInstance(project).component
153161
}
154-
155-
val content = contentManager.factory.createContent(component, null, false).also {
156-
it.isCloseable = true
157-
it.isPinnable = true
158-
}
159-
160162
runInEdt {
161-
contentManager.removeAllContents(true)
162-
contentManager.addContent(content)
163+
qPanel.setContent(component)
163164
}
164165
}
165166

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.toolwindow
5+
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.ui.components.panels.Wrapper
8+
import com.intellij.util.ui.components.BorderLayoutPanel
9+
import software.aws.toolkits.core.utils.error
10+
import software.aws.toolkits.core.utils.getLogger
11+
import software.aws.toolkits.jetbrains.core.webview.BrowserState
12+
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
13+
import software.aws.toolkits.jetbrains.utils.isQConnected
14+
import software.aws.toolkits.jetbrains.utils.isQExpired
15+
import software.aws.toolkits.telemetry.FeatureId
16+
import javax.swing.JComponent
17+
18+
class OuterAmazonQPanel(val project: Project) : BorderLayoutPanel() {
19+
private val wrapper = Wrapper()
20+
init {
21+
isOpaque = false
22+
addToCenter(wrapper)
23+
val component = if (isQConnected(project) && !isQExpired(project)) {
24+
AmazonQToolWindow.getInstance(project).component
25+
} else {
26+
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
27+
QWebviewPanel.getInstance(project).component
28+
}
29+
updateQPanel(component)
30+
}
31+
32+
fun updateQPanel(content: JComponent) {
33+
try {
34+
wrapper.setContent(content)
35+
} catch (e: Exception) {
36+
getLogger<OuterAmazonQPanel>().error { "Error while creating window" }
37+
}
38+
}
39+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object AwsToolkit {
2828

2929
const val GITHUB_URL = "https://github.com/aws/aws-toolkit-jetbrains"
3030
const val AWS_DOCS_URL = "https://docs.aws.amazon.com/console/toolkit-for-jetbrains"
31+
const val GITHUB_CHANGELOG = "https://github.com/aws/aws-toolkit-jetbrains/blob/main/CHANGELOG.md"
3132
}
3233

3334
data class PluginInfo(val id: String, val name: String) {

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/CustomizeNotificationsUi.kt

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,109 @@
33

44
package software.aws.toolkits.jetbrains.core.notifications
55

6+
import com.intellij.icons.AllIcons
7+
import com.intellij.ide.BrowserUtil
8+
import com.intellij.openapi.actionSystem.AnAction
9+
import com.intellij.openapi.actionSystem.AnActionEvent
10+
import com.intellij.openapi.project.Project
11+
import com.intellij.openapi.ui.Messages
12+
import com.intellij.ui.EditorNotificationPanel
13+
import software.aws.toolkits.jetbrains.AwsToolkit
14+
import software.aws.toolkits.resources.AwsCoreBundle
15+
616
fun checkSeverity(notificationSeverity: String): NotificationSeverity = when (notificationSeverity) {
717
"Critical" -> NotificationSeverity.CRITICAL
818
"Warning" -> NotificationSeverity.WARNING
919
"Info" -> NotificationSeverity.INFO
1020
else -> NotificationSeverity.INFO
1121
}
1222

13-
// TODO: Add actions that can be performed from the notifications here
23+
object NotificationManager {
24+
fun createActions(
25+
project: Project,
26+
followupActions: List<NotificationFollowupActions>?,
27+
message: String,
28+
title: String,
29+
30+
): List<NotificationActionList> = buildList {
31+
var url: String? = null
32+
followupActions?.forEach { action ->
33+
if (action.type == "ShowUrl") {
34+
url = action.content.locale.url
35+
}
36+
37+
if (action.type == "UpdateExtension") {
38+
add(
39+
NotificationActionList(AwsCoreBundle.message("notification.update")) {
40+
// TODO: Add update logic
41+
}
42+
)
43+
}
44+
45+
if (action.type == "OpenChangelog") {
46+
add(
47+
NotificationActionList(AwsCoreBundle.message("notification.changelog")) {
48+
BrowserUtil.browse(AwsToolkit.GITHUB_CHANGELOG)
49+
}
50+
)
51+
}
52+
}
53+
add(
54+
NotificationActionList(AwsCoreBundle.message("general.more_dialog")) {
55+
if (url == null) {
56+
Messages.showYesNoDialog(
57+
project,
58+
message,
59+
title,
60+
AwsCoreBundle.message("general.acknowledge"),
61+
AwsCoreBundle.message("general.cancel"),
62+
AllIcons.General.Error
63+
)
64+
} else {
65+
val link = url ?: AwsToolkit.GITHUB_URL
66+
val openLink = Messages.showYesNoDialog(
67+
project,
68+
message,
69+
title,
70+
AwsCoreBundle.message(AwsCoreBundle.message("notification.learn_more")),
71+
AwsCoreBundle.message("general.cancel"),
72+
AllIcons.General.Error
73+
)
74+
if (openLink == 0) {
75+
BrowserUtil.browse(link)
76+
}
77+
}
78+
}
79+
)
80+
}
81+
82+
fun buildNotificationActions(actions: List<NotificationActionList>): List<AnAction> = actions.map { (title, block) ->
83+
object : AnAction(title) {
84+
override fun actionPerformed(e: AnActionEvent) {
85+
block()
86+
}
87+
}
88+
}
89+
90+
fun buildBannerPanel(panel: EditorNotificationPanel, actions: List<NotificationActionList>): EditorNotificationPanel {
91+
actions.forEach { (actionTitle, block) ->
92+
panel.createActionLabel(actionTitle) {
93+
block()
94+
}
95+
}
96+
97+
return panel
98+
}
99+
}
100+
101+
data class NotificationActionList(
102+
val title: String,
103+
val blockToExecute: () -> Unit,
104+
)
105+
106+
data class BannerContent(
107+
val title: String,
108+
val message: String,
109+
val actions: List<NotificationActionList>,
110+
val id: String,
111+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.core.notifications
5+
6+
import com.intellij.icons.AllIcons
7+
import com.intellij.openapi.application.runInEdt
8+
import com.intellij.ui.EditorNotificationPanel
9+
import com.intellij.ui.components.panels.Wrapper
10+
import com.intellij.util.ui.components.BorderLayoutPanel
11+
import software.aws.toolkits.resources.AwsCoreBundle
12+
13+
class NotificationPanel : BorderLayoutPanel() {
14+
private val wrapper = Wrapper()
15+
init {
16+
isOpaque = false
17+
addToCenter(wrapper)
18+
ProcessNotificationsBase.showBannerNotification.forEach {
19+
updateNotificationPanel(it.value)
20+
}
21+
}
22+
23+
private fun removeNotificationPanel(notificationId: String) = runInEdt {
24+
ProcessNotificationsBase.showBannerNotification.remove(notificationId) // TODO: add id to dismissed notification list
25+
wrapper.removeAll()
26+
}
27+
28+
fun updateNotificationPanel(bannerContent: BannerContent) {
29+
val panel = EditorNotificationPanel()
30+
panel.text = bannerContent.title
31+
panel.icon(AllIcons.General.Error)
32+
val panelWithActions = NotificationManager.buildBannerPanel(panel, bannerContent.actions)
33+
panelWithActions.createActionLabel(AwsCoreBundle.message("general.dismiss")) {
34+
removeNotificationPanel(bannerContent.id)
35+
}
36+
37+
wrapper.setContent(panelWithActions)
38+
}
39+
}

0 commit comments

Comments
 (0)