Skip to content

Commit 1a5f1c9

Browse files
samgst-amazonaws-toolkit-automationmanodnyabbryceitoc9
authored
Display startup and emergency notifications (#5143)
* 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]>
1 parent 07ccd11 commit 1a5f1c9

File tree

34 files changed

+2385
-72
lines changed

34 files changed

+2385
-72
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/core/src/software/aws/toolkits/core/utils/RemoteResourceResolver.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ class DefaultRemoteResourceResolver(
3232
private fun internalResolve(resource: RemoteResource): Path {
3333
val expectedLocation = cacheBasePath.resolve(resource.name)
3434
val current = expectedLocation.existsOrNull()
35-
if (current != null && !isExpired(current, resource)) {
36-
LOG.debug { "Existing file ($current) for ${resource.name} is present and not expired - using it." }
37-
return current
35+
if (resource.name != "notifications.json") {
36+
if ((current != null && !isExpired(current, resource))) {
37+
LOG.debug { "Existing file ($current) for ${resource.name} is present and not expired - using it." }
38+
return current
39+
}
3840
}
3941

4042
LOG.debug { "Current file for ${resource.name} does not exist or is expired. Attempting to fetch from ${resource.urls}" }

plugins/core/jetbrains-community/resources/META-INF/aws.toolkit.core.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
<postStartupActivity implementation="software.aws.toolkits.jetbrains.core.plugin.PluginAutoUpdater"/>
6868
<postStartupActivity implementation="software.aws.toolkits.jetbrains.core.AwsTelemetryPrompter"/>
69+
<postStartupActivity implementation="software.aws.toolkits.jetbrains.services.telemetry.AwsToolkitStartupMetrics"/>
6970

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

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

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) {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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.ide.BrowserUtil
8+
import com.intellij.openapi.actionSystem.AnAction
9+
import com.intellij.openapi.actionSystem.AnActionEvent
10+
import com.intellij.openapi.application.runInEdt
11+
import com.intellij.openapi.progress.ProgressIndicator
12+
import com.intellij.openapi.progress.ProgressManager
13+
import com.intellij.openapi.progress.Task
14+
import com.intellij.openapi.project.Project
15+
import com.intellij.openapi.ui.Messages
16+
import com.intellij.ui.EditorNotificationPanel
17+
import software.aws.toolkits.jetbrains.AwsPlugin
18+
import software.aws.toolkits.jetbrains.AwsToolkit
19+
import software.aws.toolkits.jetbrains.core.plugin.PluginUpdateManager
20+
import software.aws.toolkits.resources.AwsCoreBundle
21+
22+
fun checkSeverity(notificationSeverity: String): NotificationSeverity = when (notificationSeverity) {
23+
"Critical" -> NotificationSeverity.CRITICAL
24+
"Warning" -> NotificationSeverity.WARNING
25+
"Info" -> NotificationSeverity.INFO
26+
else -> NotificationSeverity.INFO
27+
}
28+
29+
object NotificationManager {
30+
fun createActions(
31+
project: Project,
32+
followupActions: List<NotificationFollowupActions>?,
33+
message: String,
34+
title: String,
35+
36+
): List<NotificationActionList> = buildList {
37+
var url: String? = null
38+
followupActions?.forEach { action ->
39+
if (action.type == "ShowUrl") {
40+
url = action.content.locale.url
41+
}
42+
43+
if (action.type == "UpdateExtension") {
44+
add(
45+
NotificationActionList(AwsCoreBundle.message("notification.update")) {
46+
updatePlugins()
47+
}
48+
)
49+
}
50+
51+
if (action.type == "OpenChangelog") {
52+
add(
53+
NotificationActionList(AwsCoreBundle.message("notification.changelog")) {
54+
BrowserUtil.browse(AwsToolkit.GITHUB_CHANGELOG)
55+
}
56+
)
57+
}
58+
}
59+
add(
60+
NotificationActionList(AwsCoreBundle.message("general.more_dialog")) {
61+
if (url == null) {
62+
Messages.showYesNoDialog(
63+
project,
64+
message,
65+
title,
66+
AwsCoreBundle.message("general.acknowledge"),
67+
AwsCoreBundle.message("general.cancel"),
68+
AllIcons.General.Error
69+
)
70+
} else {
71+
val link = url ?: AwsToolkit.GITHUB_URL
72+
val openLink = Messages.showYesNoDialog(
73+
project,
74+
message,
75+
title,
76+
AwsCoreBundle.message(AwsCoreBundle.message("notification.learn_more")),
77+
AwsCoreBundle.message("general.cancel"),
78+
AllIcons.General.Error
79+
)
80+
if (openLink == 0) {
81+
BrowserUtil.browse(link)
82+
}
83+
}
84+
}
85+
)
86+
}
87+
88+
fun buildNotificationActions(actions: List<NotificationActionList>): List<AnAction> = actions.map { (title, block) ->
89+
object : AnAction(title) {
90+
override fun actionPerformed(e: AnActionEvent) {
91+
block()
92+
}
93+
}
94+
}
95+
96+
fun buildBannerPanel(panel: EditorNotificationPanel, actions: List<NotificationActionList>): EditorNotificationPanel {
97+
actions.forEach { (actionTitle, block) ->
98+
panel.createActionLabel(actionTitle) {
99+
block()
100+
}
101+
}
102+
103+
return panel
104+
}
105+
private fun updatePlugins() {
106+
val pluginUpdateManager = PluginUpdateManager()
107+
runInEdt {
108+
ProgressManager.getInstance().run(object : Task.Backgroundable(
109+
null,
110+
AwsCoreBundle.message("aws.settings.auto_update.progress.message")
111+
) {
112+
override fun run(indicator: ProgressIndicator) {
113+
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.CORE)
114+
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.TOOLKIT)
115+
pluginUpdateManager.checkForUpdates(indicator, AwsPlugin.Q)
116+
}
117+
})
118+
}
119+
}
120+
}
121+
122+
data class NotificationActionList(
123+
val title: String,
124+
val blockToExecute: () -> Unit,
125+
)
126+
127+
data class BannerContent(
128+
val title: String,
129+
val message: String,
130+
val actions: List<NotificationActionList>,
131+
val id: String,
132+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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+
object DisplayToastNotifications

0 commit comments

Comments
 (0)