diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt index e31a74e57d9..f540bfeca8f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt @@ -10,8 +10,8 @@ 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.ui.components.panels.Wrapper +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 @@ -22,6 +22,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.NotificationPanel +import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener @@ -35,7 +37,21 @@ 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 = Wrapper() + val notificationPanel = NotificationPanel() + + mainPanel.addToTop(notificationPanel) + mainPanel.add(qPanel) + val notifListener = ProcessNotificationsBase.getInstance(project) + notifListener.addListenerForNotification { bannerContent -> + runInEdt { + notificationPanel.updateNotificationPanel(bannerContent) + } + } + if (toolWindow is ToolWindowEx) { val actionManager = ActionManager.getInstance() toolWindow.setTitleActions(listOf(actionManager.getAction("aws.q.toolwindow.titleBar"))) @@ -46,7 +62,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { override fun activeConnectionChanged(newConnection: ToolkitConnection?) { - onConnectionChanged(project, newConnection, toolWindow) + onConnectionChanged(project, newConnection, qPanel) } } ) @@ -56,8 +72,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { object : RefreshQChatPanelButtonPressedListener { override fun onRefresh() { runInEdt { - contentManager.removeAllContents(true) - prepareChatContent(project, contentManager) + prepareChatContent(project, qPanel) } } } @@ -68,43 +83,37 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware { object : BearerTokenProviderListener { override fun onChange(providerId: String, newScopes: List?) { 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.setContent(qComponent) } } } } ) - val content = prepareChatContent(project, contentManager) + prepareChatContent(project, qPanel) + 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 { + qPanel: Wrapper, + ) { 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 - } - contentManager.addContent(content) - return content + qPanel.setContent(component) } override fun init(toolWindow: ToolWindow) { @@ -125,8 +134,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?, qPanel: Wrapper) { val isNewConnectionForQ = newConnection?.let { (it as? AwsBearerTokenConnection)?.let { conn -> val scopeShouldHave = Q_SCOPES @@ -151,15 +159,8 @@ 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) + qPanel.setContent(component) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt new file mode 100644 index 00000000000..72524e49b27 --- /dev/null +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/OuterAmazonQPanel.kt @@ -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.services.amazonq.toolwindow + +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.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.core.webview.BrowserState +import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel +import software.aws.toolkits.jetbrains.utils.isQConnected +import software.aws.toolkits.jetbrains.utils.isQExpired +import software.aws.toolkits.telemetry.FeatureId +import javax.swing.JComponent + +class OuterAmazonQPanel(val project: Project) : BorderLayoutPanel() { + private val wrapper = Wrapper() + init { + isOpaque = false + addToCenter(wrapper) + val component = if (isQConnected(project) && !isQExpired(project)) { + AmazonQToolWindow.getInstance(project).component + } else { + QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ)) + QWebviewPanel.getInstance(project).component + } + updateQPanel(component) + } + + fun updateQPanel(content: JComponent) { + try { + wrapper.setContent(content) + } catch (e: Exception) { + getLogger().error { "Error while creating window" } + } + } +} diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt index 05d1e21e378..883c64a9051 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt @@ -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) { diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/CustomizeNotificationsUi.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/CustomizeNotificationsUi.kt index c9deddcb60d..e9c87b76f28 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/CustomizeNotificationsUi.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/CustomizeNotificationsUi.kt @@ -3,6 +3,16 @@ 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.project.Project +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 @@ -10,4 +20,92 @@ fun checkSeverity(notificationSeverity: String): NotificationSeverity = when (no else -> NotificationSeverity.INFO } -// TODO: Add actions that can be performed from the notifications here +object NotificationManager { + fun createActions( + project: Project, + followupActions: List?, + message: String, + title: String, + + ): List = buildList { + var url: String? = null + followupActions?.forEach { action -> + if (action.type == "ShowUrl") { + url = action.content.locale.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) + } + ) + } + } + add( + NotificationActionList(AwsCoreBundle.message("general.more_dialog")) { + if (url == null) { + Messages.showYesNoDialog( + project, + message, + title, + AwsCoreBundle.message("general.acknowledge"), + AwsCoreBundle.message("general.cancel"), + AllIcons.General.Error + ) + } else { + val link = url ?: AwsToolkit.GITHUB_URL + val openLink = Messages.showYesNoDialog( + project, + message, + title, + AwsCoreBundle.message(AwsCoreBundle.message("notification.learn_more")), + AwsCoreBundle.message("general.cancel"), + AllIcons.General.Error + ) + if (openLink == 0) { + BrowserUtil.browse(link) + } + } + } + ) + } + + fun buildNotificationActions(actions: List): List = actions.map { (title, block) -> + object : AnAction(title) { + override fun actionPerformed(e: AnActionEvent) { + block() + } + } + } + + fun buildBannerPanel(panel: EditorNotificationPanel, actions: List): EditorNotificationPanel { + actions.forEach { (actionTitle, block) -> + panel.createActionLabel(actionTitle) { + block() + } + } + + return panel + } +} + +data class NotificationActionList( + val title: String, + val blockToExecute: () -> Unit, +) + +data class BannerContent( + val title: String, + val message: String, + val actions: List, + val id: String, +) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPanel.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPanel.kt new file mode 100644 index 00000000000..8658d67df27 --- /dev/null +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPanel.kt @@ -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.icons.AllIcons +import com.intellij.openapi.application.runInEdt +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.components.panels.Wrapper +import com.intellij.util.ui.components.BorderLayoutPanel +import software.aws.toolkits.resources.AwsCoreBundle + +class NotificationPanel : BorderLayoutPanel() { + private val wrapper = Wrapper() + init { + isOpaque = false + addToCenter(wrapper) + ProcessNotificationsBase.showBannerNotification.forEach { + updateNotificationPanel(it.value) + } + } + + private fun removeNotificationPanel(notificationId: String) = runInEdt { + ProcessNotificationsBase.showBannerNotification.remove(notificationId) // TODO: add id to dismissed notification list + wrapper.removeAll() + } + + fun updateNotificationPanel(bannerContent: BannerContent) { + val panel = EditorNotificationPanel() + panel.text = bannerContent.title + panel.icon(AllIcons.General.Error) + val panelWithActions = NotificationManager.buildBannerPanel(panel, bannerContent.actions) + panelWithActions.createActionLabel(AwsCoreBundle.message("general.dismiss")) { + removeNotificationPanel(bannerContent.id) + } + + wrapper.setContent(panelWithActions) + } +} diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt index cd11ba4ef2e..8911c79db76 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/ProcessNotificationsBase.kt @@ -3,9 +3,16 @@ package software.aws.toolkits.jetbrains.core.notifications +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.utils.notifyStickyWithData +@Service(Service.Level.PROJECT) class ProcessNotificationsBase { + private val notifListener = mutableListOf() init { // TODO: install a listener for the polling class } @@ -22,13 +29,49 @@ class ProcessNotificationsBase { fun processNotification(project: Project, notificationData: NotificationData) { val shouldShow = RulesEngine.displayNotification(project, notificationData) if (shouldShow) { - // TODO: notifies listeners + val notificationContent = notificationData.content.locale + val severity = notificationData.severity + val followupActions = NotificationManager.createActions( + project, + notificationData.actions, + notificationContent.description, + notificationContent.title + ) + showToast( + notificationContent.title, + notificationContent.description, + NotificationManager.buildNotificationActions(followupActions), + checkSeverity(severity), + notificationData.id + ) + if (severity == "Critical") { + val bannerContent = BannerContent(notificationContent.title, notificationContent.description, followupActions, notificationData.id) + showBannerNotification[notificationData.id] = bannerContent + notifyListenerForNotification(bannerContent) + } } } - fun notifyListenerForNotification() { + private fun showToast(title: String, message: String, action: List, notificationType: NotificationSeverity, notificationId: String) { + val notifyType = when (notificationType) { + NotificationSeverity.CRITICAL -> NotificationType.ERROR + NotificationSeverity.WARNING -> NotificationType.WARNING + NotificationSeverity.INFO -> NotificationType.INFORMATION + } + notifyStickyWithData(notifyType, title, message, null, action, notificationId) } - fun addListenerForNotification() { + fun notifyListenerForNotification(bannerContent: BannerContent) = + notifListener.forEach { it(bannerContent) } + + fun addListenerForNotification(newNotifListener: NotifListener) = + notifListener.add(newNotifListener) + + companion object { + fun getInstance(project: Project): ProcessNotificationsBase = project.service() + + val showBannerNotification = mutableMapOf() } } + +typealias NotifListener = (bannerContent: BannerContent) -> Unit diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/NotificationUtils.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/NotificationUtils.kt index 0bd81b96227..12377a83792 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/NotificationUtils.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/NotificationUtils.kt @@ -17,6 +17,7 @@ import com.intellij.ui.ScrollPaneFactory import org.slf4j.LoggerFactory import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.help.HelpIds +import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase import software.aws.toolkits.resources.AwsCoreBundle import javax.swing.JLabel import javax.swing.JTextArea @@ -48,6 +49,34 @@ private fun notify(type: NotificationType, title: String, content: String = "", notify(notification, project) } +fun notifyStickyWithData( + type: NotificationType, + title: String, + content: String = "", + project: Project? = null, + notificationActions: Collection, + id: String, +) { + val notification = Notification(GROUP_DISPLAY_ID_STICKY, title, content, type) + notificationActions.forEach { + notification.addAction(it) + } + + notification.addAction( + createNotificationExpiringAction( + object : AnAction("Dismiss") { + override fun actionPerformed(e: AnActionEvent) { + ProcessNotificationsBase.showBannerNotification.remove(id) + // TODO: add id to dismissed notification list + } + } + ) + + ) + + notify(notification, project) +} + private fun notifySticky(type: NotificationType, title: String, content: String = "", project: Project? = null, notificationActions: Collection) { val notification = Notification(GROUP_DISPLAY_ID_STICKY, title, content, type) notificationActions.forEach { diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/notifications/NotificationManagerTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/notifications/NotificationManagerTest.kt new file mode 100644 index 00000000000..3e112a23055 --- /dev/null +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/core/notifications/NotificationManagerTest.kt @@ -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.core.notifications + +import com.intellij.testFramework.ApplicationExtension +import com.intellij.testFramework.ProjectRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extension +import org.junit.jupiter.api.extension.RegisterExtension + +@ExtendWith(ApplicationExtension::class) +class NotificationManagerTest { + + val projectRule = ProjectRule() + + @JvmField + @RegisterExtension + val testExtension = object : Extension { + fun getProject() = projectRule.project + } + + @Test + fun `If no follow-up actions, expand action is present`() { + val sut = NotificationManager.createActions(projectRule.project, listOf(), "Dummy Test Action", "Dummy title") + assertThat(sut).isNotNull + assertThat(sut).hasSize(1) + assertThat(sut.first().title).isEqualTo("More...") + } + + @Test + fun `Show Url action shows the option to learn more`() { + val followupActions = NotificationFollowupActions( + "UpdateExtension", + NotificationFollowupActionsContent(NotificationActionDescription("title", null)) + ) + val sut = NotificationManager.createActions(projectRule.project, listOf(followupActions), "Dummy Test Action", "Dummy title") + assertThat(sut).isNotNull + assertThat(sut).hasSize(2) + assertThat(sut.first().title).isEqualTo("Update") + assertThat(sut[1].title).isEqualTo("More...") + } +} diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index b823b0507f3..19152fb03da 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -1257,6 +1257,7 @@ gateway.connection.workflow.start_ide=Start IDE gateway.connection.workflow.step_failed=\nStep failed exceptionally\n gateway.connection.workflow.step_skipped=Step skipped gateway.connection.workflow.step_successful=\nStep completed successfully\n +general.acknowledge=Acknowledge general.add.another=Add another general.auth.reauthenticate=Reauthenticate general.cancel=Cancel @@ -1271,6 +1272,7 @@ general.default=Default general.delete=Delete general.delete_accessible_name=Delete confirmation box general.details=(details) +general.dismiss=Dismiss general.execute_button=Execute general.execution.canceled=canceled general.execution.cli_error=Command did not exit successfully, exit code: {0}\n @@ -1284,10 +1286,12 @@ general.in_progress_button=In progress general.logs=Logs general.message=Message general.more=More +general.more_dialog=More... general.name.label=Name: general.no_changes=No changes were provided general.notification.action.hide_forever=Don't show again general.notification.action.hide_once=Dismiss +general.ok=OK general.open.in.progress=Opening... general.open_in_aws_console=Open in AWS Console general.open_in_aws_console.error=Failed to open link in browser @@ -1530,6 +1534,10 @@ lambda.workflow.update_code.wait_for_updatable=Waiting for function to transitio loading_resource.failed=Failed loading resources loading_resource.loading=Loading... loading_resource.still_loading=Resources are still loading +notification.changelog=Changelog +notification.expand=Expand +notification.learn_more=Learn more +notification.update=Update plugin.incompatible.fix=Disable incompatible plugins and restart IDE plugin.incompatible.message=The plugin versions for Amazon Q, AWS Toolkit, and AWS Toolkit Core must match or conflicts may occur. plugin.incompatible.title=AWS Plugin Incompatibility diff --git a/plugins/core/src/main/resources/META-INF/plugin.xml b/plugins/core/src/main/resources/META-INF/plugin.xml index dbc7e6b3972..86639d85020 100644 --- a/plugins/core/src/main/resources/META-INF/plugin.xml +++ b/plugins/core/src/main/resources/META-INF/plugin.xml @@ -26,4 +26,5 @@ + diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/ExplorerNewConnectionAction.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/ExplorerNewConnectionAction.kt index 0f05448be91..1f67c8c2fb0 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/ExplorerNewConnectionAction.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/ExplorerNewConnectionAction.kt @@ -7,7 +7,7 @@ import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.DumbAwareAction -import software.aws.toolkits.jetbrains.core.explorer.showWebview +import software.aws.toolkits.jetbrains.core.explorer.ShowToolkitListener import software.aws.toolkits.jetbrains.core.explorer.webview.ToolkitWebviewPanel import software.aws.toolkits.jetbrains.core.gettingstarted.editor.GettingStartedPanel import software.aws.toolkits.jetbrains.core.webview.BrowserState @@ -25,7 +25,7 @@ class ExplorerNewConnectionAction : DumbAwareAction(AllIcons.General.Add) { GettingStartedPanel.openPanel(it) } else { ToolkitWebviewPanel.getInstance(it).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer, true)) - showWebview(it) + ShowToolkitListener.showWebview(it) } UiTelemetry.click(it, "devtools_connectToAws") } diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/NewConnectionAction.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/NewConnectionAction.kt index 67be9bcf010..b0a1c8601d7 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/NewConnectionAction.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/actions/NewConnectionAction.kt @@ -6,7 +6,7 @@ package software.aws.toolkits.jetbrains.core.credentials.actions import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.DumbAwareAction -import software.aws.toolkits.jetbrains.core.explorer.showWebview +import software.aws.toolkits.jetbrains.core.explorer.ShowToolkitListener import software.aws.toolkits.jetbrains.core.explorer.webview.ToolkitWebviewPanel import software.aws.toolkits.jetbrains.core.gettingstarted.editor.GettingStartedPanel import software.aws.toolkits.jetbrains.core.webview.BrowserState @@ -22,7 +22,7 @@ class NewConnectionAction : DumbAwareAction() { GettingStartedPanel.openPanel(it, connectionInitiatedFromExplorer = true) } else { ToolkitWebviewPanel.getInstance(it).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer, true)) - showWebview(it) + ShowToolkitListener.showWebview(it) } UiTelemetry.click(e.project, "auth_gettingstarted_explorermenu") } diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/AwsToolkitExplorerFactory.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/AwsToolkitExplorerFactory.kt index 0174aef2e08..6179672b8c8 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/AwsToolkitExplorerFactory.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/AwsToolkitExplorerFactory.kt @@ -6,12 +6,16 @@ package software.aws.toolkits.jetbrains.core.explorer import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.DumbAware 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.components.panels.Wrapper +import com.intellij.util.messages.Topic +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.AwsToolkit @@ -32,15 +36,30 @@ import software.aws.toolkits.jetbrains.core.experiments.ExperimentsActionGroup import software.aws.toolkits.jetbrains.core.explorer.webview.ToolkitWebviewPanel import software.aws.toolkits.jetbrains.core.explorer.webview.shouldPromptToolkitReauth import software.aws.toolkits.jetbrains.core.help.HelpIds +import software.aws.toolkits.jetbrains.core.notifications.NotificationPanel +import software.aws.toolkits.jetbrains.core.notifications.ProcessNotificationsBase import software.aws.toolkits.jetbrains.core.webview.BrowserState import software.aws.toolkits.jetbrains.utils.actions.OpenBrowserAction import software.aws.toolkits.jetbrains.utils.isTookitConnected import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.FeatureId +import java.util.EventListener import javax.swing.JComponent class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val notificationPanel = NotificationPanel() + val toolkitPanel = Wrapper() + val mainPanel = BorderLayoutPanel() + mainPanel.addToTop(notificationPanel) + mainPanel.add(toolkitPanel) + val notifListener = ProcessNotificationsBase.getInstance(project) + notifListener.addListenerForNotification { bannerContent -> + runInEdt { + notificationPanel.updateNotificationPanel(bannerContent) + } + } toolWindow.helpId = HelpIds.EXPLORER_WINDOW.id if (toolWindow is ToolWindowEx) { @@ -83,7 +102,9 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { AwsToolkitExplorerToolWindow.getInstance(project) } - val content = contentManager.factory.createContent(component, null, false).also { + toolkitPanel.setContent(component) + + val content = contentManager.factory.createContent(mainPanel, null, false).also { it.isCloseable = true it.isPinnable = true } @@ -95,7 +116,7 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { ToolkitConnectionManagerListener.TOPIC, object : ToolkitConnectionManagerListener { override fun activeConnectionChanged(newConnection: ToolkitConnection?) { - connectionChanged(project, newConnection) + connectionChanged(project, newConnection, toolkitPanel) } } ) @@ -104,7 +125,7 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { AwsConnectionManager.CONNECTION_SETTINGS_STATE_CHANGED, object : ConnectionSettingsStateChangeNotifier { override fun settingsStateChanged(newState: ConnectionState) { - settingsStateChanged(project, newState) + settingsStateChanged(project, newState, toolkitPanel) } } ) @@ -116,18 +137,31 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { if (ToolkitConnectionManager.getInstance(project) .connectionStateForFeature(CodeCatalystConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED ) { - showExplorerTree(project) + loadContent(AwsToolkitExplorerToolWindow.getInstance(project), toolkitPanel) } } } ) + + project.messageBus.connect().subscribe( + ShowToolkitListener.TOPIC, + object : ShowToolkitListener { + override fun showWebview(project: Project) { + loadContent(ToolkitWebviewPanel.getInstance(project).component, toolkitPanel) + } + + override fun showExplorerTree(project: Project) { + loadContent(AwsToolkitExplorerToolWindow.getInstance(project), toolkitPanel) + } + } + ) } override fun init(toolWindow: ToolWindow) { toolWindow.stripeTitle = message("aws.notification.title") } - private fun connectionChanged(project: Project, newConnection: ToolkitConnection?) { + private fun connectionChanged(project: Project, newConnection: ToolkitConnection?, toolkitPanel: Wrapper) { val isNewConnToolkitConnection = when (newConnection) { is AwsConnectionManagerConnection -> { LOG.debug { "IAM connection" } @@ -148,16 +182,16 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { } if (isNewConnToolkitConnection) { - showExplorerTree(project) + loadContent(AwsToolkitExplorerToolWindow.getInstance(project), toolkitPanel) } else if (!isTookitConnected(project) || shouldPromptToolkitReauth(project)) { ToolkitWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer)) - showWebview(project) + loadContent(ToolkitWebviewPanel.getInstance(project).component, toolkitPanel) } else { - showExplorerTree(project) + loadContent(AwsToolkitExplorerToolWindow.getInstance(project), toolkitPanel) } } - private fun settingsStateChanged(project: Project, newState: ConnectionState) { + private fun settingsStateChanged(project: Project, newState: ConnectionState, toolkitPanel: Wrapper) { val isToolkitConnected = if (newState is ConnectionState.ValidConnection) { true } else { @@ -168,9 +202,15 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { if (!isToolkitConnected || shouldPromptToolkitReauth(project)) { ToolkitWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer)) - showWebview(project) + loadContent(ToolkitWebviewPanel.getInstance(project).component, toolkitPanel) } else { - showExplorerTree(project) + loadContent(AwsToolkitExplorerToolWindow.getInstance(project), toolkitPanel) + } + } + + private fun loadContent(component: JComponent, toolkitPanel: Wrapper) { + runInEdt { + toolkitPanel.setContent(component) } } @@ -180,22 +220,20 @@ class AwsToolkitExplorerFactory : ToolWindowFactory, DumbAware { } } -fun showWebview(project: Project) { - AwsToolkitExplorerToolWindow.toolWindow(project).loadContent(ToolkitWebviewPanel.getInstance(project).component) -} +interface ShowToolkitListener : EventListener { + fun showExplorerTree(project: Project) + fun showWebview(project: Project) -fun showExplorerTree(project: Project) { - AwsToolkitExplorerToolWindow.toolWindow(project).loadContent(AwsToolkitExplorerToolWindow.getInstance(project)) -} + companion object { + @Topic.AppLevel + val TOPIC = Topic.create("Show Explorer contents", ShowToolkitListener::class.java) -private fun ToolWindow.loadContent(component: JComponent) { - val content = contentManager.factory.createContent(component, null, false).also { - it.isCloseable = true - it.isPinnable = true - } + fun showExplorerTree(project: Project) { + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).showExplorerTree(project) + } - runInEdt { - contentManager.removeAllContents(true) - contentManager.addContent(content) + fun showWebview(project: Project) { + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).showWebview(project) + } } } diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/ExplorerToolWindow.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/ExplorerToolWindow.kt index a3f3f3a4562..c670548e7c1 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/ExplorerToolWindow.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/ExplorerToolWindow.kt @@ -211,7 +211,7 @@ class ExplorerToolWindow(private val project: Project) : GettingStartedPanel.openPanel(project) } else { ToolkitWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer, true)) - showWebview(project) + ShowToolkitListener.showWebview(project) } } } diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/OuterToolkitPanel.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/OuterToolkitPanel.kt new file mode 100644 index 00000000000..82717b337dc --- /dev/null +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/OuterToolkitPanel.kt @@ -0,0 +1,36 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.core.explorer.webview + +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.error +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.jetbrains.core.explorer.AwsToolkitExplorerToolWindow +import software.aws.toolkits.jetbrains.utils.isTookitConnected +import javax.swing.JComponent + +class OuterToolkitPanel(val project: Project) : BorderLayoutPanel() { + private val wrapper = Wrapper() + init { + isOpaque = false + addToCenter(wrapper) + val component = if (!isTookitConnected(project) || shouldPromptToolkitReauth(project)) { + ToolkitWebviewPanel.getInstance(project).component + } else { + AwsToolkitExplorerToolWindow.getInstance(project) + } + + updateToolkitPanel(component) + } + + fun updateToolkitPanel(content: JComponent) { + try { + wrapper.setContent(content) + } catch (e: Exception) { + getLogger().error { "Error while creating window" } + } + } +} diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/ToolkitLoginWebview.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/ToolkitLoginWebview.kt index 481e84e7f9d..8ea17eceaa0 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/ToolkitLoginWebview.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/explorer/webview/ToolkitLoginWebview.kt @@ -46,7 +46,7 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeCatalystConn import software.aws.toolkits.jetbrains.core.credentials.sono.CODECATALYST_SCOPES import software.aws.toolkits.jetbrains.core.credentials.sono.IDENTITY_CENTER_ROLE_ACCESS_SCOPE import software.aws.toolkits.jetbrains.core.credentials.sono.isSono -import software.aws.toolkits.jetbrains.core.explorer.showExplorerTree +import software.aws.toolkits.jetbrains.core.explorer.ShowToolkitListener import software.aws.toolkits.jetbrains.core.gettingstarted.IdcRolePopup import software.aws.toolkits.jetbrains.core.gettingstarted.IdcRolePopupState import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider @@ -211,7 +211,7 @@ class ToolkitWebviewBrowser(val project: Project, private val parentDisposable: } is BrowserMessage.ToggleBrowser -> { - showExplorerTree(project) + ShowToolkitListener.showExplorerTree(project) } is BrowserMessage.CancelLogin -> { diff --git a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/gettingstarted/ToolkitGettingStartedAuthUtils.kt b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/gettingstarted/ToolkitGettingStartedAuthUtils.kt index 98da68929aa..53b5479b47f 100644 --- a/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/gettingstarted/ToolkitGettingStartedAuthUtils.kt +++ b/plugins/toolkit/jetbrains-core/src/software/aws/toolkits/jetbrains/core/gettingstarted/ToolkitGettingStartedAuthUtils.kt @@ -6,7 +6,7 @@ package software.aws.toolkits.jetbrains.core.gettingstarted import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.project.Project import software.aws.toolkits.jetbrains.core.credentials.sono.CODECATALYST_SCOPES -import software.aws.toolkits.jetbrains.core.explorer.showWebview +import software.aws.toolkits.jetbrains.core.explorer.ShowToolkitListener import software.aws.toolkits.jetbrains.core.explorer.webview.ToolkitWebviewPanel import software.aws.toolkits.jetbrains.core.gettingstarted.editor.SourceOfEntry import software.aws.toolkits.jetbrains.core.gettingstarted.editor.getConnectionCount @@ -32,7 +32,7 @@ fun requestCredentialsForCodeCatalyst( ): Boolean? { if (isQWebviewsAvailable() && project != null) { ToolkitWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.Codecatalyst, true)) // TODO: consume data - showWebview(project) + ShowToolkitListener.showWebview(project) return null } @@ -128,7 +128,7 @@ fun requestCredentialsForExplorer( ): Boolean? { if (isQWebviewsAvailable()) { ToolkitWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AwsExplorer, true)) // TODO: consume data - showWebview(project) + ShowToolkitListener.showWebview(project) return null }