Skip to content

Commit

Permalink
Display startup and emergency notifications (#5143)
Browse files Browse the repository at this point in the history
* Add framework for processing notifications (#5112)

* Add deserialization for notification messages retrieved from the notification file and criteria on whether it should be displayed (#5093)

* Display toast notifications with actions

* Condition matcher for displaying notifications

* Modified deserialization cases and added tests

* not required file change

* feedback 1

* modified the base class

* modified test instance lifecycle

* 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

* Adding update/restart action to notifications (#5136)

* Poll for new notifications (#5119)

* initial commit

* run on startup

* detekt

* move vals

* remote resource implementation

* comments

* detekt

* Validate file before saving

* cache path

* observer implementation

* deserialize notifs from file

* detekt

* remove unused interface

* internal class

* Fix observer

* etag singleton state component

* add telemetry

* atomicBoolean

* initialize once per IDE startup

* code scan

* Omit (Unit)

* specify etag storage location

* detekt

* fix detekt issues

* basic tests

* no star imports

* coroutine scope delay instead of thread.sleep

* feedback fixes

* test fix

* Application Exists for tests

* endpoint object

* detekt

* detekt fixes

* boolean flag

* boolean flag

* update tests

* move startup flag handling to processBase

* fix delay

* fix delay

* Notification dismissal state tracking  (#5129)

* split notifications into separated lists.

* add persistent notification dismissal state logic

* boolean changes

* group persistant states

* comments

* Service initialized automatically

* isStartup global

* Deserialized notification schedule type

* tests

* persistent state syntax

* convert to light services

* Remove state from companion object

* detekt

* endpoint as registryKey

* detekt

* fix startup issues

* Expiry issues

* Add logging info to IDE notification polling and processing (#5138)

* add logs for polling and processing notifs

* redundant

* finish log

* fix isFirstPoll not setting to false on first pass

---------

Co-authored-by: aws-toolkit-automation <[email protected]>
Co-authored-by: manodnyab <[email protected]>
Co-authored-by: Bryce Ito <[email protected]>
  • Loading branch information
4 people authored Nov 29, 2024
1 parent 07ccd11 commit 1a5f1c9
Show file tree
Hide file tree
Showing 34 changed files with 2,385 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -35,7 +37,21 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent

class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {

Check warning on line 39 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Extension class should be final and non-public

Extension class should not be public

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")))
Expand All @@ -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)
}
}
)
Expand All @@ -56,8 +72,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
object : RefreshQChatPanelButtonPressedListener {
override fun onRefresh() {
runInEdt {
contentManager.removeAllContents(true)
prepareChatContent(project, contentManager)
prepareChatContent(project, qPanel)
}
}
}
Expand All @@ -68,43 +83,37 @@ 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.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) {
Expand All @@ -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
Expand All @@ -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)
}
}

Expand Down
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.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<OuterAmazonQPanel>().error { "Error while creating window" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ class DefaultRemoteResourceResolver(
private fun internalResolve(resource: RemoteResource): Path {
val expectedLocation = cacheBasePath.resolve(resource.name)
val current = expectedLocation.existsOrNull()
if (current != null && !isExpired(current, resource)) {
LOG.debug { "Existing file ($current) for ${resource.name} is present and not expired - using it." }
return current
if (resource.name != "notifications.json") {
if ((current != null && !isExpired(current, resource))) {
LOG.debug { "Existing file ($current) for ${resource.name} is present and not expired - using it." }
return current
}
}

LOG.debug { "Current file for ${resource.name} does not exist or is expired. Attempting to fetch from ${resource.urls}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@

<postStartupActivity implementation="software.aws.toolkits.jetbrains.core.plugin.PluginAutoUpdater"/>
<postStartupActivity implementation="software.aws.toolkits.jetbrains.core.AwsTelemetryPrompter"/>
<postStartupActivity implementation="software.aws.toolkits.jetbrains.services.telemetry.AwsToolkitStartupMetrics"/>

<registryKey key="aws.dev.useDAG" description="True if DAG should be used instead of authorization_grant with PKCE"
defaultValue="false" restartRequired="false"/>
Expand All @@ -77,6 +78,9 @@
restartRequired="true"/>
<registryKey key="aws.toolkit.developerMode" description="Enables features to facilitate development of the toolkit" restartRequired="false"
defaultValue="false"/>
<registryKey key="aws.toolkit.notification.endpoint" description="Endpoint for AWS Toolkit notifications"
defaultValue="https://idetoolkits-hostedfiles.amazonaws.com/Notifications/Jetbrains/emergency/1.x.json" restartRequired="true"/>


<notificationGroup id="aws.plugin.version.mismatch" displayType="STICKY_BALLOON" key="aws.settings.title"/>

Expand Down
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,132 @@
// 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.application.runInEdt
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.ui.EditorNotificationPanel
import software.aws.toolkits.jetbrains.AwsPlugin
import software.aws.toolkits.jetbrains.AwsToolkit
import software.aws.toolkits.jetbrains.core.plugin.PluginUpdateManager
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(
project: Project,
followupActions: List<NotificationFollowupActions>?,
message: String,
title: String,

): List<NotificationActionList> = 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")) {
updatePlugins()
}
)
}

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<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
}
private fun updatePlugins() {
val pluginUpdateManager = PluginUpdateManager()
runInEdt {
ProgressManager.getInstance().run(object : Task.Backgroundable(
null,
AwsCoreBundle.message("aws.settings.auto_update.progress.message")
) {
override fun run(indicator: ProgressIndicator) {
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.CORE)
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.TOOLKIT)
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.Q)
}
})
}
}
}

data class NotificationActionList(
val title: String,
val blockToExecute: () -> Unit,
)

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

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

object DisplayToastNotifications
Loading

0 comments on commit 1a5f1c9

Please sign in to comment.