Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c3cc2bf
Display toast notifications with actions
manodnyab Nov 13, 2024
09729c7
Condition matcher for displaying notifications
manodnyab Nov 15, 2024
885f3f0
Show notification banner
manodnyab Nov 18, 2024
f0aca38
feedback 1
manodnyab Nov 19, 2024
e909a0e
Modified deserialization cases and added tests
manodnyab Nov 20, 2024
5a84e39
Merge branch 'feature/ideNotifs' into manodnyb/checkRulesForNotificat…
manodnyab Nov 20, 2024
56c1888
not required file change
manodnyab Nov 20, 2024
a981227
Merge remote-tracking branch 'origin/manodnyb/checkRulesForNotificati…
manodnyab Nov 20, 2024
682897f
feedback 1
manodnyab Nov 20, 2024
adb6ec7
resolved conflicts
manodnyab Nov 20, 2024
dbfcefc
feedback 1
manodnyab Nov 20, 2024
823ea7f
Merge remote-tracking branch 'origin/feature/ideNotifs' into manodnyb…
manodnyab Nov 20, 2024
218c8ed
modified the base class
manodnyab Nov 20, 2024
b684a01
merge conflicts resolved
manodnyab Nov 21, 2024
368fd25
merge conflicts resolved
manodnyab Nov 21, 2024
57fb2ef
rearranged call site
manodnyab Nov 21, 2024
a5126d1
show notifications when panel is opened
manodnyab Nov 21, 2024
22f9438
resolved merge conflicts
manodnyab Nov 21, 2024
87ec1d9
fixed tests
manodnyab Nov 21, 2024
24a2f65
Merge branch 'feature/ideNotifs' into manodnyb/addNotificationBanner
manodnyab Nov 21, 2024
78f8c6b
detekt
manodnyab Nov 21, 2024
bfc1077
Merge remote-tracking branch 'origin/manodnyb/addNotificationBanner' …
manodnyab Nov 21, 2024
1a83a40
feedback
manodnyab Nov 22, 2024
9e8b0af
Merge branch 'feature/ideNotifs' into manodnyb/addNotificationBanner
manodnyab Nov 22, 2024
1b12854
convert panels into wrappers
manodnyab Nov 22, 2024
e7d8594
Merge remote-tracking branch 'origin/manodnyb/addNotificationBanner' …
manodnyab Nov 22, 2024
bbf6779
fixed test
manodnyab Nov 22, 2024
a8fa482
Merge branch 'feature/ideNotifs' into manodnyb/addNotificationBanner
manodnyab Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.openapi.wm.ex.ToolWindowEx
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentManager
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
Expand All @@ -22,6 +21,8 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.notifications.NotificationActionList
import software.aws.toolkits.jetbrains.core.notifications.ShowCriticalNotificationBannerListener
import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
Expand All @@ -35,7 +36,14 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent

class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {

override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
val mainPanel = BorderLayoutPanel()
val qPanel = OuterAmazonQPanel.getInstance(project)
val notificationPanel = NotificationPanel.getInstance(project)
mainPanel.addToTop(notificationPanel)
mainPanel.add(qPanel)

if (toolWindow is ToolWindowEx) {
val actionManager = ActionManager.getInstance()
toolWindow.setTitleActions(listOf(actionManager.getAction("aws.q.toolwindow.titleBar")))
Expand All @@ -46,7 +54,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
ToolkitConnectionManagerListener.TOPIC,
object : ToolkitConnectionManagerListener {
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
onConnectionChanged(project, newConnection, toolWindow)
onConnectionChanged(project, newConnection)
}
}
)
Expand All @@ -56,8 +64,18 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
object : RefreshQChatPanelButtonPressedListener {
override fun onRefresh() {
runInEdt {
contentManager.removeAllContents(true)
prepareChatContent(project, contentManager)
prepareChatContent(project)
}
}
}
)

project.messageBus.connect().subscribe(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
project.messageBus.connect().subscribe(
project.messageBus.connect(disposable).subscribe(

ShowCriticalNotificationBannerListener.TOPIC,
object : ShowCriticalNotificationBannerListener {
override fun onReceiveEmergencyNotification(title: String, message: String, notificationActionList: List<NotificationActionList>) {
runInEdt {
notificationPanel.updateNotificationPanel(title, message, notificationActionList)
}
}
}
Expand All @@ -68,43 +86,40 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
object : BearerTokenProviderListener {
override fun onChange(providerId: String, newScopes: List<String>?) {
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
val content = contentManager.factory.createContent(AmazonQToolWindow.getInstance(project).component, null, false).also {
it.isCloseable = true
it.isPinnable = true
}
val qComponent = AmazonQToolWindow.getInstance(project).component

runInEdt {
contentManager.removeAllContents(true)
contentManager.addContent(content)
qPanel.updateQPanel(qComponent)
}
}
}
}
)

val content = prepareChatContent(project, contentManager)
prepareChatContent(project)

val content = contentManager.factory.createContent(mainPanel, null, false).also {
it.isCloseable = true
it.isPinnable = true
}
toolWindow.activate(null)
contentManager.setSelectedContent(content)
contentManager.addContent(content)
}

private fun prepareChatContent(
project: Project,
contentManager: ContentManager,
): Content {
) {
val component = if (isQConnected(project) && !isQExpired(project)) {
AmazonQToolWindow.getInstance(project).component
} else {
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
QWebviewPanel.getInstance(project).component
}

val content = contentManager.factory.createContent(component, null, false).also {
it.isCloseable = true
it.isPinnable = true
runInEdt {
val qPanel = OuterAmazonQPanel.getInstance(project)
qPanel.updateQPanel(component)
}
contentManager.addContent(content)
return content
}

override fun init(toolWindow: ToolWindow) {
Expand All @@ -125,8 +140,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {

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

private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, toolWindow: ToolWindow) {
val contentManager = toolWindow.contentManager
private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?) {
val isNewConnectionForQ = newConnection?.let {
(it as? AwsBearerTokenConnection)?.let { conn ->
val scopeShouldHave = Q_SCOPES
Expand All @@ -151,15 +165,9 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
LOG.debug { "returning login window; no Q connection found" }
QWebviewPanel.getInstance(project).component
}

val content = contentManager.factory.createContent(component, null, false).also {
it.isCloseable = true
it.isPinnable = true
}

runInEdt {
contentManager.removeAllContents(true)
contentManager.addContent(content)
val qPanel = OuterAmazonQPanel.getInstance(project)
qPanel.updateQPanel(component)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.toolwindow

import com.intellij.icons.AllIcons
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.jetbrains.core.notifications.NotificationActionList
import software.aws.toolkits.jetbrains.core.notifications.NotificationManager
import software.aws.toolkits.resources.AwsCoreBundle

@Service(Service.Level.PROJECT)
class NotificationPanel : BorderLayoutPanel() {
private val wrapper = Wrapper()
init {
isOpaque = false
addToCenter(wrapper)
}

private fun removeNotificationPanel() = runInEdt {
wrapper.removeAll()
}

fun updateNotificationPanel(title: String, message: String, notificationActionList: List<NotificationActionList>) {
val panel = EditorNotificationPanel()
panel.text = title
panel.icon(AllIcons.General.Error)
val panelWithActions = NotificationManager.buildBannerPanel(panel, notificationActionList)
panelWithActions.createActionLabel(AwsCoreBundle.message("general.dismiss")) {
removeNotificationPanel()
}

wrapper.setContent(panelWithActions)
}

companion object {
fun getInstance(project: Project): NotificationPanel = project.service()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.toolwindow

import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.core.utils.getLogger
import javax.swing.JComponent

@Service(Service.Level.PROJECT)
class OuterAmazonQPanel : BorderLayoutPanel() {
private val wrapper = Wrapper()
init {
isOpaque = false
addToCenter(wrapper)
}

fun updateQPanel(content: JComponent) {

Check notice

Code scanning / QDJVMC

Class member can have 'private' visibility Note

Function 'updateQPanel' could be private
try {
wrapper.setContent(content)
} catch (e: Exception) {
getLogger<OuterAmazonQPanel>().error("Error while creating window")
}
}

companion object {
fun getInstance(project: Project): OuterAmazonQPanel = project.service()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ object AwsToolkit {

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

data class PluginInfo(val id: String, val name: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages
import com.intellij.ui.EditorNotificationPanel
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.resources.AwsCoreBundle

fun checkSeverity(notificationSeverity: String): NotificationSeverity = when (notificationSeverity) {
"Critical" -> NotificationSeverity.CRITICAL
"Warning" -> NotificationSeverity.WARNING
"Info" -> NotificationSeverity.INFO
else -> NotificationSeverity.INFO
}

object NotificationManager {
fun createActions(
followupActions: List<NotificationFollowupActions>?,
message: String,
title: String,

): List<NotificationActionList> = buildList {
add(
NotificationActionList(AwsCoreBundle.message("notification.expand")) {
Messages.showYesNoDialog(
null,
message,
title,
AwsCoreBundle.message("general.ok"),
AwsCoreBundle.message("general.cancel"),
AllIcons.General.Error
)
}
)

followupActions?.forEach { action ->
if (action.type == "ShowUrl") {
add(
NotificationActionList(AwsCoreBundle.message("notification.learn_more")) {
action.content.locale.url?.let { url -> BrowserUtil.browse(url) }
}
)
}

if (action.type == "UpdateExtension") {
add(
NotificationActionList(AwsCoreBundle.message("notification.update")) {
// TODO: Add update logic
}
)
}

if (action.type == "OpenChangelog") {
add(
NotificationActionList(AwsCoreBundle.message("notification.changelog")) {
BrowserUtil.browse(AwsToolkit.GITHUB_CHANGELOG)
}
)
}
}
}

fun buildNotificationActions(actions: List<NotificationActionList>): List<AnAction> = actions.map { (title, block) ->
object : AnAction(title) {
override fun actionPerformed(e: AnActionEvent) {
block()
}
}
}

fun buildBannerPanel(panel: EditorNotificationPanel, actions: List<NotificationActionList>): EditorNotificationPanel {
actions.forEach { (actionTitle, block) ->
panel.createActionLabel(actionTitle) {
block()
}
}

return panel
}
}

data class NotificationActionList(
val title: String,
val blockToExecute: () -> Unit,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.utils.notifyStickyWithData

object DisplayToastNotifications {
fun showToast(title: String, message: String, action: List<AnAction>, notificationType: NotificationSeverity, notificationId: String) {

Check notice on line 12 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/DisplayToastNotifications.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'showToast' could be private
val notifyType = when (notificationType) {
NotificationSeverity.CRITICAL -> NotificationType.ERROR
NotificationSeverity.WARNING -> NotificationType.WARNING
NotificationSeverity.INFO -> NotificationType.INFORMATION
}
notifyStickyWithData(notifyType, title, message, null, action, notificationId)
}

fun shouldShowNotification(project: Project, notificationData: NotificationData) {

Check warning on line 21 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/DisplayToastNotifications.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Function "shouldShowNotification" is never used
if (RulesEngine.displayNotification(notificationData, project)) {
val notificationContent = notificationData.content.locale
val severity = notificationData.severity
val followupActions = NotificationManager.createActions(notificationData.actions, notificationContent.description, notificationContent.title)
showToast(
notificationContent.title,
notificationContent.description,
NotificationManager.buildNotificationActions(followupActions),
checkSeverity(severity),
notificationData.id
)

if (severity == "Critical") {
ShowCriticalNotificationBannerListener.showBanner(notificationContent.title, notificationContent.description, followupActions)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.util.registry.Registry

class DummyNotificationAction : AnAction("Show notif") {

Check warning on line 10 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/DummyNotificationAction.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Component/Action not registered

Action is not registered in plugin.xml

Check warning on line 10 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/DummyNotificationAction.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Incorrect string capitalization

String 'Show notif' is not properly capitalized. It should have title capitalization
override fun actionPerformed(e: AnActionEvent) {
if (!Registry.`is`("aws.toolkit.developerMode")) return
ShowCriticalNotificationBannerListener.showBanner(
"hello hello",
"This is a bug",
NotificationManager.createActions(emptyList(), "This is a bug", "hello hello")
)
}
}
Loading
Loading