Skip to content

Commit 885f3f0

Browse files
committed
Show notification banner
1 parent 09729c7 commit 885f3f0

File tree

12 files changed

+337
-65
lines changed

12 files changed

+337
-65
lines changed

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

Lines changed: 77 additions & 29 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,20 +22,31 @@ 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.NotificationFollowupActions
26+
import software.aws.toolkits.jetbrains.core.notifications.ShowCriticalNotificationBannerListener
27+
import software.aws.toolkits.jetbrains.core.notifications.getBannerActionList
2528
import software.aws.toolkits.jetbrains.core.webview.BrowserState
2629
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
2730
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
2831
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.openMeetQPage
2932
import software.aws.toolkits.jetbrains.utils.isQConnected
3033
import software.aws.toolkits.jetbrains.utils.isQExpired
3134
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
35+
import software.aws.toolkits.jetbrains.utils.notifyInfo
3236
import software.aws.toolkits.resources.message
3337
import software.aws.toolkits.telemetry.FeatureId
3438
import java.awt.event.ComponentAdapter
3539
import java.awt.event.ComponentEvent
40+
import javax.swing.JComponent
3641

3742
class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
43+
private val notificationPanel = NotificationPanel()
44+
private val qPanel = QPanel()
3845
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
46+
val mainPanel = BorderLayoutPanel()
47+
mainPanel.addToTop(notificationPanel)
48+
mainPanel.add(qPanel)
49+
3950
if (toolWindow is ToolWindowEx) {
4051
val actionManager = ActionManager.getInstance()
4152
toolWindow.setTitleActions(listOf(actionManager.getAction("aws.q.toolwindow.titleBar")))
@@ -46,7 +57,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
4657
ToolkitConnectionManagerListener.TOPIC,
4758
object : ToolkitConnectionManagerListener {
4859
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
49-
onConnectionChanged(project, newConnection, toolWindow)
60+
onConnectionChanged(project, newConnection)
5061
}
5162
}
5263
)
@@ -56,55 +67,62 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
5667
object : RefreshQChatPanelButtonPressedListener {
5768
override fun onRefresh() {
5869
runInEdt {
59-
contentManager.removeAllContents(true)
60-
prepareChatContent(project, contentManager)
70+
prepareChatContent(project)
6171
}
6272
}
6373
}
6474
)
6575

76+
project.messageBus.connect().subscribe(
77+
ShowCriticalNotificationBannerListener.TOPIC,
78+
object : ShowCriticalNotificationBannerListener {
79+
override fun onReceiveEmergencyNotification(title: String, message: String, actions: List<NotificationFollowupActions>?) {
80+
notificationPanel.updateNotificationPanel(title, message, actions)
81+
}
82+
}
83+
)
84+
6685
project.messageBus.connect().subscribe(
6786
BearerTokenProviderListener.TOPIC,
6887
object : BearerTokenProviderListener {
6988
override fun onChange(providerId: String, newScopes: List<String>?) {
7089
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
71-
val content = contentManager.factory.createContent(AmazonQToolWindow.getInstance(project).component, null, false).also {
90+
// change this
91+
val qComponent = AmazonQToolWindow.getInstance(project).component
92+
93+
val content = contentManager.factory.createContent(qComponent, null, false).also {
7294
it.isCloseable = true
7395
it.isPinnable = true
7496
}
7597

7698
runInEdt {
77-
contentManager.removeAllContents(true)
78-
contentManager.addContent(content)
99+
qPanel.updateQPanel(qComponent)
79100
}
80101
}
81102
}
82103
}
83104
)
84105

85-
val content = prepareChatContent(project, contentManager)
106+
prepareChatContent(project)
86107

108+
val content = contentManager.factory.createContent(mainPanel, null, false).also {
109+
it.isCloseable = true
110+
it.isPinnable = true
111+
}
87112
toolWindow.activate(null)
88-
contentManager.setSelectedContent(content)
113+
contentManager.addContent(content)
89114
}
90115

91116
private fun prepareChatContent(
92117
project: Project,
93-
contentManager: ContentManager,
94-
): Content {
118+
) {
95119
val component = if (isQConnected(project) && !isQExpired(project)) {
96120
AmazonQToolWindow.getInstance(project).component
97121
} else {
98122
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.Q))
99123
QWebviewPanel.getInstance(project).component
100124
}
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
125+
qPanel.updateQPanel(component)
108126
}
109127

110128
override fun init(toolWindow: ToolWindow) {
@@ -125,8 +143,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
125143

126144
override fun shouldBeAvailable(project: Project): Boolean = isQWebviewsAvailable()
127145

128-
private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, toolWindow: ToolWindow) {
129-
val contentManager = toolWindow.contentManager
146+
private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?) {
130147
val isNewConnectionForQ = newConnection?.let {
131148
(it as? AwsBearerTokenConnection)?.let { conn ->
132149
val scopeShouldHave = Q_SCOPES
@@ -151,15 +168,8 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
151168
LOG.debug { "returning login window; no Q connection found" }
152169
QWebviewPanel.getInstance(project).component
153170
}
154-
155-
val content = contentManager.factory.createContent(component, null, false).also {
156-
it.isCloseable = true
157-
it.isPinnable = true
158-
}
159-
160171
runInEdt {
161-
contentManager.removeAllContents(true)
162-
contentManager.addContent(content)
172+
qPanel.updateQPanel(component)
163173
}
164174
}
165175

@@ -169,3 +179,41 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
169179
private const val MINIMUM_TOOLWINDOW_WIDTH = 325
170180
}
171181
}
182+
183+
class NotificationPanel : BorderLayoutPanel() {
184+
private val wrapper = Wrapper()
185+
init {
186+
isOpaque = false
187+
addToCenter(wrapper)
188+
}
189+
190+
private fun removeNotificationPanel() = runInEdt {
191+
wrapper.removeAll()
192+
}
193+
194+
fun updateNotificationPanel(title: String, message: String, actions: List<NotificationFollowupActions>?) {
195+
val editorNotificationPanel = getBannerActionList(actions, title, message)
196+
197+
editorNotificationPanel.createActionLabel("Dismiss") {
198+
removeNotificationPanel()
199+
}
200+
201+
wrapper.setContent(editorNotificationPanel)
202+
}
203+
}
204+
205+
class QPanel : BorderLayoutPanel() {
206+
private val wrapper = Wrapper()
207+
init {
208+
isOpaque = false
209+
addToCenter(wrapper)
210+
}
211+
212+
fun updateQPanel(content: JComponent) {
213+
try {
214+
wrapper.setContent(content)
215+
} catch (e: Exception) {
216+
notifyInfo("Error while creating window")
217+
}
218+
}
219+
}

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

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,97 @@
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.ui.Messages
11+
import com.intellij.ui.EditorNotificationPanel
12+
613
fun checkSeverity(notificationSeverity: String): NotificationSeverity = when (notificationSeverity) {
714
"Critical" -> NotificationSeverity.CRITICAL
815
"Warning" -> NotificationSeverity.WARNING
916
"Info" -> NotificationSeverity.INFO
1017
else -> NotificationSeverity.INFO
1118
}
1219

13-
// TODO: Add actions that can be performed from the notifications here
20+
fun getNotificationActionList(
21+
notificationFollowupActions: List<NotificationFollowupActions>?,
22+
notificationTitle: String,
23+
notificationMessage: String,
24+
): List<AnAction> {
25+
val actionList = mutableListOf<AnAction>()
26+
actionList.add(
27+
getNotifAction("Expand") {
28+
Messages.showYesNoDialog(null, notificationMessage, notificationTitle, "OK", "Cancel", AllIcons.General.Error)
29+
}
30+
)
31+
if (notificationFollowupActions.isNullOrEmpty()) return actionList
32+
notificationFollowupActions.forEach { notificationAction ->
33+
if (notificationAction.type == "ShowUrl") {
34+
actionList.add(
35+
getNotifAction("Learn more") {
36+
notificationAction.content.locale.url?.let { url -> BrowserUtil.browse(url) }
37+
}
38+
)
39+
}
40+
41+
if (notificationAction.type == "UpdateExtension") {
42+
actionList.add(
43+
getNotifAction("Update") {
44+
// add update logic
45+
}
46+
)
47+
}
48+
49+
if (notificationAction.type == "openChangelog") {
50+
actionList.add(
51+
getNotifAction("Changelog") {
52+
BrowserUtil.browse("https://github.com/aws/aws-toolkit-jetbrains/blob/main/CHANGELOG.md")
53+
}
54+
)
55+
}
56+
}
57+
return actionList
58+
}
59+
60+
fun getNotifAction(title: String, block: () -> Unit): AnAction = object : AnAction(title) {
61+
override fun actionPerformed(e: AnActionEvent) {
62+
block()
63+
}
64+
}
65+
66+
fun getBannerActionList(
67+
notificationFollowupActions: List<NotificationFollowupActions>?,
68+
notificationTitle: String,
69+
notificationMessage: String,
70+
): EditorNotificationPanel {
71+
val panel = EditorNotificationPanel()
72+
73+
panel.text = notificationTitle
74+
panel.icon(AllIcons.General.Error)
75+
panel.createActionLabel("Expand") {
76+
Messages.showYesNoDialog(null, notificationMessage, notificationTitle, "OK", "Cancel", AllIcons.General.Error)
77+
}
78+
if (notificationFollowupActions.isNullOrEmpty()) return panel
79+
notificationFollowupActions.forEach { notificationAction ->
80+
if (notificationAction.type == "ShowUrl") {
81+
panel.createActionLabel("Learn more") {
82+
notificationAction.content.locale.url?.let { url -> BrowserUtil.browse(url) }
83+
}
84+
}
85+
86+
if (notificationAction.type == "UpdateExtension") {
87+
panel.createActionLabel("Update") {
88+
// add update logic
89+
}
90+
}
91+
92+
if (notificationAction.type == "openChangelog") {
93+
panel.createActionLabel("Changelog") {
94+
BrowserUtil.browse("https://github.com/aws/aws-toolkit-jetbrains/blob/main/CHANGELOG.md")
95+
}
96+
}
97+
}
98+
return panel
99+
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,33 @@ package software.aws.toolkits.jetbrains.core.notifications
66
import com.intellij.notification.NotificationType
77
import com.intellij.openapi.actionSystem.AnAction
88
import com.intellij.openapi.project.Project
9-
import software.aws.toolkits.jetbrains.utils.notifySticky
9+
import software.aws.toolkits.jetbrains.utils.notifyStickyWithData
1010

1111
object DisplayToastNotifications {
12-
fun show(title: String, message: String, action: List<AnAction>, notificationType: NotificationSeverity) {
12+
fun showToast(title: String, message: String, action: List<AnAction>, notificationType: NotificationSeverity, notificationId: String) {
1313
val notifyType = when (notificationType) {
1414
NotificationSeverity.CRITICAL -> NotificationType.ERROR
1515
NotificationSeverity.WARNING -> NotificationType.WARNING
1616
NotificationSeverity.INFO -> NotificationType.INFORMATION
1717
}
18-
notifySticky(notifyType, title, message, null, action)
18+
notifyStickyWithData(notifyType, title, message, null, action, notificationId)
1919
}
2020

21-
fun shouldShow(project: Project, notificationData: NotificationData) {
21+
fun shouldShowNotification(project: Project, notificationData: NotificationData) {
2222
if (RulesEngine.displayNotification(notificationData, project)) {
2323
val notificationContent = notificationData.content.locale
2424
val severity = notificationData.severity
25-
show(notificationContent.title, notificationContent.description, emptyList(), checkSeverity(severity))
25+
showToast(
26+
notificationContent.title,
27+
notificationContent.description,
28+
getNotificationActionList(notificationData.actions, notificationContent.title, notificationContent.description),
29+
checkSeverity(severity),
30+
notificationData.id
31+
)
32+
33+
if (severity == "Critical") {
34+
ShowCriticalNotificationBannerListener.showBanner(notificationContent.title, notificationContent.description, notificationData.actions)
35+
}
2636
}
2737
}
2838
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.openapi.application.ApplicationManager
7+
import com.intellij.util.messages.Topic
8+
import java.util.EventListener
9+
10+
interface ShowCriticalNotificationBannerListener : EventListener {
11+
fun onReceiveEmergencyNotification(title: String, message: String, actions: List<NotificationFollowupActions>?) {}
12+
13+
companion object {
14+
@Topic.AppLevel
15+
val TOPIC = Topic.create("Show critical banner", ShowCriticalNotificationBannerListener::class.java)
16+
17+
fun showBanner(title: String, message: String, actions: List<NotificationFollowupActions>?) {
18+
ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onReceiveEmergencyNotification(title, message, actions)
19+
}
20+
}
21+
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,35 @@ private fun notify(type: NotificationType, title: String, content: String = "",
4848
notify(notification, project)
4949
}
5050

51-
fun notifySticky(type: NotificationType, title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>) {
51+
fun notifyStickyWithData(
52+
type: NotificationType,
53+
title: String,
54+
content: String = "",
55+
project: Project? = null,
56+
notificationActions: Collection<AnAction>,
57+
id: String,
58+
) {
59+
val notification = Notification(GROUP_DISPLAY_ID_STICKY, title, content, type)
60+
notificationActions.forEach {
61+
notification.addAction(it)
62+
}
63+
64+
notification.addAction(
65+
createNotificationExpiringAction(
66+
object : AnAction("Dismiss") {
67+
override fun actionPerformed(e: AnActionEvent) {
68+
id
69+
// TODO: add id to dismissed notification list
70+
}
71+
}
72+
)
73+
74+
)
75+
76+
notify(notification, project)
77+
}
78+
79+
private fun notifySticky(type: NotificationType, title: String, content: String = "", project: Project? = null, notificationActions: Collection<AnAction>) {
5280
val notification = Notification(GROUP_DISPLAY_ID_STICKY, title, content, type)
5381
notificationActions.forEach {
5482
notification.addAction(if (it !is NotificationAction) createNotificationExpiringAction(it) else it)

0 commit comments

Comments
 (0)