From eb2c69bda04067eba78d8a3adb9c52ecfc951e3a Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Mon, 30 Sep 2024 16:10:44 +0200 Subject: [PATCH] feat: allow to disable editor notifications (#763) Signed-off-by: Andre Dietisheim --- .../kubernetes/editor/EditorResource.kt | 5 +- .../kubernetes/editor/ResourceEditor.kt | 195 ++++------- .../editor/notification/Notifications.kt | 164 +++++++++ .../intellij/kubernetes/settings/Settings.kt | 59 ++++ .../settings/SettingsChangeListener.kt | 24 ++ .../kubernetes/settings/SettingsComponent.kt | 47 +++ .../settings/SettingsConfigurable.kt | 61 ++++ src/main/resources/META-INF/plugin.xml | 4 + .../kubernetes/editor/ResourceEditorTest.kt | 200 +++-------- .../editor/notification/NotificationsTest.kt | 318 ++++++++++++++++++ 10 files changed, 784 insertions(+), 293 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/Notifications.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/Settings.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsChangeListener.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsComponent.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsConfigurable.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/NotificationsTest.kt diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt index 9947b3447..ff645eada 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt @@ -33,7 +33,10 @@ open class EditorResource( private val resourceModel: IResourceModel, private val resourceChangedListener: IResourceModelListener?, // for mocking purposes - private val clusterResourceFactory: (resource: HasMetadata, context: IActiveContext?) -> ClusterResource? = + private val clusterResourceFactory: ( + resource: HasMetadata, + context: IActiveContext? + ) -> ClusterResource? = ClusterResource.Factory::create ) { var disposed: Boolean = false diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt index 16ac04059..52feb5318 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt @@ -23,14 +23,10 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager +import com.intellij.util.messages.MessageBusConnection import com.redhat.devtools.intellij.common.utils.MetadataClutter import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo -import com.redhat.devtools.intellij.kubernetes.editor.notification.DeletedNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.ErrorNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PullNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PulledNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PushNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PushedNotification +import com.redhat.devtools.intellij.kubernetes.editor.notification.Notifications import com.redhat.devtools.intellij.kubernetes.editor.util.getDocument import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource import com.redhat.devtools.intellij.kubernetes.model.IResourceModel @@ -39,6 +35,9 @@ import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException import com.redhat.devtools.intellij.kubernetes.model.util.toMessage import com.redhat.devtools.intellij.kubernetes.model.util.toTitle +import com.redhat.devtools.intellij.kubernetes.settings.Settings +import com.redhat.devtools.intellij.kubernetes.settings.Settings.Companion.PROP_EDITOR_SYNC_ENABLED +import com.redhat.devtools.intellij.kubernetes.settings.SettingsChangeListener import io.fabric8.kubernetes.api.model.HasMetadata import java.util.concurrent.CompletableFuture @@ -57,18 +56,7 @@ open class ResourceEditor( // for mocking purposes private val createResourceFileForVirtual: (file: VirtualFile?) -> ResourceFile? = ResourceFile.Factory::create, - // for mocking purposes - private val pushNotification: PushNotification = PushNotification(editor, project), - // for mocking purposes - private val pushedNotification: PushedNotification = PushedNotification(editor, project), - // for mocking purposes - private val pullNotification: PullNotification = PullNotification(editor, project), - // for mocking purposes - private val pulledNotification: PulledNotification = PulledNotification(editor, project), - // for mocking purposes - private val deletedNotification: DeletedNotification = DeletedNotification(editor, project), - // for mocking purposes - private val errorNotification: ErrorNotification = ErrorNotification(editor, project), + private val notifications: Notifications = Notifications(editor, project), // for mocking purposes private val getDocument: (FileEditor) -> Document? = ::getDocument, // for mocking purposes @@ -81,14 +69,19 @@ open class ResourceEditor( // for mocking purposes private val diff: ResourceDiff = ResourceDiff(project), // for mocking purposes - protected val editorResources: EditorResources = EditorResources(resourceModel) + protected val editorResources: EditorResources = EditorResources(resourceModel), + private val settings: Settings = Settings.getInstance(), + private val connection: MessageBusConnection = ApplicationManager.getApplication().messageBus.connect() ) : Disposable { init { Disposer.register(editor, this) editorResources.resourceChangedListener = onResourceChanged() resourceModel.addListener(onNamespaceOrContextChanged()) - runAsync { enableEditingNonProjectFile() } + runAsync { + enableEditingNonProjectFile() + connection.subscribe(SettingsChangeListener.CHANGED, onSettingsChanged()) + } } companion object { @@ -113,19 +106,11 @@ open class ResourceEditor( resourceModel.getCurrentNamespace() ) val editorResources = editorResources.setResources(resources) - - if (editorResources.size == 1) { - // show notification for 1 resource - val editorResource = editorResources.firstOrNull() ?: return@runAsync - showNotification(editorResource) - } else if (editorResources.size > 1) { - // show notification for multiple resources - showNotification(editorResources) - } + showNotifications(editorResources) } catch (e: Exception) { runInUI { - hideNotifications() - errorNotification.show( + notifications.hideAll() + notifications.showError( toTitle(e), toMessage(e.cause) ) @@ -134,106 +119,24 @@ open class ResourceEditor( } } - open protected fun updateDeleted(deleted: HasMetadata?) { - if (deleted == null) { - return + private fun showNotifications(editorResources: Collection) { + val syncEnabled = Settings.getInstance().isEditorSyncEnabled() + if (editorResources.size == 1) { + // show notification for 1 resource + val editorResource = editorResources.firstOrNull() ?: return + notifications.show(editorResource, syncEnabled) + } else if (editorResources.size > 1) { + // show notification for multiple resources + notifications.show(editorResources, syncEnabled) } - editorResources.setDeleted(deleted) - update() } - private fun showNotification(editorResource: EditorResource) { - val state = editorResource.getState() - val resource = editorResource.getResource() - when (state) { - is Error -> - showErrorNotification(state.title, state.message) - is Pushed -> - showPushedNotification(listOf(editorResource)) - is DeletedOnCluster -> - showDeletedNotification(resource) -/** - * avoid too many notifications, don't notify outdated - is Outdated -> - showPullNotification(resource) - */ - is Modified -> - showPushNotification(true, listOf(editorResource)) - - else -> - runInUI { - hideNotifications() - } - } - } - - private fun showNotification(editorResources: Collection) { - val toPush = editorResources.filter(FILTER_TO_PUSH) - if (toPush.isNotEmpty()) { - showPushNotification(false, toPush) + protected open fun updateDeleted(deleted: HasMetadata?) { + if (deleted == null) { return } - val inError = editorResources.filter(FILTER_ERROR) - if (inError.isNotEmpty()) { - showErrorNotification(inError) - } else { - runInUI { - hideNotifications() - } - } - } - - private fun showErrorNotification(title: String, message: String?) { - runInUI { - hideNotifications() - errorNotification.show(title, message) - } - } - - private fun showErrorNotification(editorResources: Collection) { - val inError = editorResources.filter(FILTER_ERROR) - val toDisplay = inError.firstOrNull()?.getState() as? Error ?: return - showErrorNotification(toDisplay.title, toDisplay.message) - } - - private fun showPushNotification(showPull: Boolean, editorResources: Collection) { - runInUI { - // hide & show in the same UI thread runnable avoid flickering - hideNotifications() - pushNotification.show(showPull, editorResources) - } - } - - private fun showPushedNotification(editorResources: Collection) { - runInUI { - // hide & show in the same UI thread runnable avoid flickering - hideNotifications() - pushedNotification.show(editorResources) - } - } - - private fun showPullNotification(resource: HasMetadata) { - runInUI { - hideNotifications() - pullNotification.show(resource) - } - } - - private fun showDeletedNotification(resource: HasMetadata) { - runInUI { - // hide & show in the same UI thread runnable avoid flickering - hideNotifications() - deletedNotification.show(resource) - } - } - - private fun hideNotifications() { - errorNotification.hide() - pullNotification.hide() - deletedNotification.hide() - pushNotification.hide() - pushedNotification.hide() - pulledNotification.hide() + editorResources.setDeleted(deleted) + update() } /** @@ -248,7 +151,7 @@ open class ResourceEditor( } runInUI { // hide before running pull. Pull may take quite some time on remote cluster - hideNotifications() + notifications.hideAll() } val resource = resources.firstOrNull() ?: return @@ -293,7 +196,7 @@ open class ResourceEditor( fun push(all: Boolean) { runInUI { // hide before running push. Push may take quite some time on remote cluster - hideNotifications() + notifications.hideAll() } runAsync { if (all) { @@ -355,7 +258,9 @@ open class ResourceEditor( } fun startWatch(): ResourceEditor { - editorResources.watchAll() + if (settings.isEditorSyncEnabled()) { + editorResources.watchAll() + } return this } @@ -370,7 +275,7 @@ open class ResourceEditor( } runInUI { replaceDocument(resources) - hideNotifications() + notifications.hideAll() } } @@ -404,11 +309,15 @@ open class ResourceEditor( } override fun removed(removed: Any) { - updateDeleted(removed as? HasMetadata) + if (settings.isEditorSyncEnabled()) { + updateDeleted(removed as? HasMetadata) + } } override fun modified(modified: Any) { - update() + if (settings.isEditorSyncEnabled()) { + update() + } } } } @@ -431,6 +340,27 @@ open class ResourceEditor( } } + protected open fun onSettingsChanged(): SettingsChangeListener { + return object : SettingsChangeListener { + override fun changed(property: String, value: String?) { + if (value == null) { + return + } + + if (PROP_EDITOR_SYNC_ENABLED == property) { + val enabled = value.toBoolean() + if (enabled) { + editorResources.watchAll() + update() + } else { + editorResources.stopWatchAll() + notifications.hideSyncNotifications() + } + } + } + } + } + /** * Closes this instance and cleans up references to it. * - Removes the resource model listener, @@ -496,5 +426,6 @@ open class ResourceEditor( override fun dispose() { resourceModel.removeListener(onNamespaceContextChanged) editorResources.dispose() + connection.dispose() } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/Notifications.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/Notifications.kt new file mode 100644 index 000000000..6ef6331cb --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/Notifications.kt @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.notification + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.project.Project +import com.redhat.devtools.intellij.kubernetes.editor.DeletedOnCluster +import com.redhat.devtools.intellij.kubernetes.editor.EditorResource +import com.redhat.devtools.intellij.kubernetes.editor.Error +import com.redhat.devtools.intellij.kubernetes.editor.FILTER_ERROR +import com.redhat.devtools.intellij.kubernetes.editor.FILTER_TO_PUSH +import com.redhat.devtools.intellij.kubernetes.editor.Modified +import com.redhat.devtools.intellij.kubernetes.editor.Pushed +import io.fabric8.kubernetes.api.model.HasMetadata + +open class Notifications( + editor: FileEditor, + project: Project, + // for mocking purposes + private val pushNotification: PushNotification = PushNotification(editor, project), + // for mocking purposes + private val pushedNotification: PushedNotification = PushedNotification(editor, project), + // for mocking purposes + private val pullNotification: PullNotification = PullNotification(editor, project), + // for mocking purposes + private val pulledNotification: PulledNotification = PulledNotification(editor, project), + // for mocking purposes + private val deletedNotification: DeletedNotification = DeletedNotification(editor, project), + // for mocking purposes + private val errorNotification: ErrorNotification = ErrorNotification(editor, project), +) { + + fun show(editorResource: EditorResource) { + show(editorResource, true) + } + + fun show(editorResource: EditorResource, showSyncNotifications: Boolean) { + val state = editorResource.getState() + val resource = editorResource.getResource() + when { + state is Error -> + showError(state.title, state.message) + + state is Pushed -> + showPushed(listOf(editorResource)) + + state is DeletedOnCluster + && showSyncNotifications -> + showDeleted(resource) + + /** + * avoid too many notifications, don't notify outdated + state is Outdated && showSyncNotification -> + showPullNotification(resource) + */ + + state is Modified + && showSyncNotifications -> + showPush(true, listOf(editorResource)) + + else -> + hideAll() + } + } + + fun show(editorResources: Collection) { + show(editorResources, true) + } + + fun show(editorResources: Collection, showSyncNotifications: Boolean) { + val toPush = editorResources.filter(FILTER_TO_PUSH) + if (toPush.isNotEmpty() + && showSyncNotifications) { + showPush(false, toPush) + return + } + val inError = editorResources.filter(FILTER_ERROR) + if (inError.isNotEmpty()) { + showError(inError) + } else { + hideAll() + } + } + + fun showError(title: String, message: String?) { + runInUI { + hideAll() + errorNotification.show(title, message) + } + } + + private fun showError(editorResources: Collection) { + val inError = editorResources.filter(FILTER_ERROR) + val toDisplay = inError.firstOrNull()?.getState() as? Error ?: return + showError(toDisplay.title, toDisplay.message) + } + + private fun showPush(showPull: Boolean, editorResources: Collection) { + runInUI { + // hide & show in the same UI thread runnable avoid flickering + hideAll() + pushNotification.show(showPull, editorResources) + } + } + + private fun showPushed(editorResources: Collection) { + runInUI { + // hide & show in the same UI thread runnable avoid flickering + hideAll() + pushedNotification.show(editorResources) + } + } + + private fun showPull(resource: HasMetadata) { + runInUI { + hideAll() + pullNotification.show(resource) + } + } + + private fun showDeleted(resource: HasMetadata) { + runInUI { + // hide & show in the same UI thread runnable avoid flickering + hideAll() + deletedNotification.show(resource) + } + } + + fun hideSyncNotifications() { + runInUI { + pushNotification.hide() + pullNotification.hide() + deletedNotification.hide() + } + } + + fun hideAll() { + runInUI { + pushNotification.hide() + pushedNotification.hide() + pullNotification.hide() + deletedNotification.hide() + pulledNotification.hide() + errorNotification.hide() + } + } + + protected open fun runInUI(runnable: () -> Unit) { + if (ApplicationManager.getApplication().isDispatchThread) { + runnable.invoke() + } else { + ApplicationManager.getApplication().invokeLater(runnable) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/Settings.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/Settings.kt new file mode 100644 index 000000000..9c7cc1e0c --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/Settings.kt @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package com.redhat.devtools.intellij.kubernetes.settings + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.redhat.devtools.intellij.kubernetes.settings.Settings.SettingsState + +@Service +@State( + name = "com.redhat.devtools.intellij.kubernetes.Settings", + storages = [Storage(value = "intellij-kubernetes-settings.xml")] +) +class Settings: SimplePersistentStateComponent(SettingsState()) { + + companion object { + const val PROP_EDITOR_SYNC_ENABLED: String = "com.redhat.devtools.intellij.kubernetes.settings.editor.notifications" + + private const val EDITOR_SYNC_ENABLED_DEFAULT = true + + fun getInstance(): Settings { + return ApplicationManager.getApplication().service() + } + } + + fun setEditorSyncEnabled(enabled: Boolean) { + val wasEnabled = state.editorSyncEnabled + if (wasEnabled != enabled) { + state.editorSyncEnabled = enabled + notifyListeners(PROP_EDITOR_SYNC_ENABLED, enabled.toString()) + } + } + + fun isEditorSyncEnabled(): Boolean { + return state.editorSyncEnabled + } + + private fun notifyListeners(property: String, value: String?) { + val listener = ApplicationManager.getApplication().messageBus.syncPublisher(SettingsChangeListener.CHANGED) + listener?.changed(property, value) + } + + class SettingsState: BaseState() { + var editorSyncEnabled: Boolean by property(EDITOR_SYNC_ENABLED_DEFAULT) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsChangeListener.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsChangeListener.kt new file mode 100644 index 000000000..a0016dd8a --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsChangeListener.kt @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.settings + +import com.intellij.util.messages.Topic + +interface SettingsChangeListener { + + companion object { + @Topic.AppLevel + val CHANGED = Topic.create("Kubernetes Settings Changed", SettingsChangeListener::class.java) + } + + fun changed(property: String, value: String?) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsComponent.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsComponent.kt new file mode 100644 index 000000000..2c48e0529 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsComponent.kt @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.settings + +import com.intellij.openapi.observable.properties.AtomicBooleanProperty +import com.intellij.openapi.options.BoundConfigurable +import com.intellij.openapi.options.SearchableConfigurable +import com.intellij.openapi.ui.DialogPanel +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import org.jetbrains.annotations.NonNls + +class SettingsComponent(editorSyncEnabled: Boolean): BoundConfigurable("Editor"), SearchableConfigurable { + + private var editorSyncEnabled = AtomicBooleanProperty(editorSyncEnabled) + + override fun createPanel(): DialogPanel { + return panel { + row { + checkBox("Sync editor with cluster") + .bindSelected(editorSyncEnabled) + .comment("If unchecked, no local or remote changes are notified in the editor.") + } + } + } + + fun setEditorSyncDisabled(enabled: Boolean) { + editorSyncEnabled.set(enabled) + } + + fun isEditorSyncDisabled(): Boolean { + return editorSyncEnabled.get() + } + + override fun getId(): @NonNls String { + return "kubernetes.editor" + } +} + diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsConfigurable.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsConfigurable.kt new file mode 100644 index 000000000..646a26ecb --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/settings/SettingsConfigurable.kt @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.settings + +import com.intellij.openapi.options.SearchableConfigurable +import org.jetbrains.annotations.Nls +import javax.swing.JComponent + +internal class SettingsConfigurable: SearchableConfigurable { + + companion object { + /* plugin.xml > applicationConfigurable > id */ + const val ID: String = "tools.settings.redhat.kubernetes" + } + + private lateinit var component: SettingsComponent + + @Nls(capitalization = Nls.Capitalization.Title) + override fun getDisplayName(): String = "Red Hat Kubernetes" + + override fun getPreferredFocusedComponent(): JComponent? { + return component.preferredFocusedComponent + } + + override fun createComponent(): JComponent? { + this.component = SettingsComponent(isEditorSyncEnabled()) + return this.component.createComponent() + } + + override fun isModified(): Boolean { + return component.isEditorSyncDisabled() != isEditorSyncEnabled() + } + + override fun apply() { + val editorNotificationsDisabled = component.isEditorSyncDisabled() + setEditorSyncEnabled(editorNotificationsDisabled) + } + + override fun reset() { + component.setEditorSyncDisabled(isEditorSyncEnabled()) + } + + override fun getId(): String = ID + + private fun isEditorSyncEnabled(): Boolean { + return Settings.getInstance().isEditorSyncEnabled() + } + + private fun setEditorSyncEnabled(enabled: Boolean) { + Settings.getInstance().setEditorSyncEnabled(enabled) + } + +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index aa8174013..58f857e5b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -225,6 +225,10 @@ + diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTest.kt index e853f3e6a..be886f812 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTest.kt @@ -19,6 +19,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import com.intellij.psi.util.PsiUtilCore +import com.intellij.util.messages.MessageBusConnection import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.argWhere @@ -34,17 +35,13 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo -import com.redhat.devtools.intellij.kubernetes.editor.notification.DeletedNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.ErrorNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PullNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PulledNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PushNotification -import com.redhat.devtools.intellij.kubernetes.editor.notification.PushedNotification +import com.redhat.devtools.intellij.kubernetes.editor.notification.Notifications import com.redhat.devtools.intellij.kubernetes.model.IResourceModel import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesResourceInfo import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException +import com.redhat.devtools.intellij.kubernetes.settings.Settings import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.api.model.PodBuilder import io.fabric8.kubernetes.client.KubernetesClient @@ -126,12 +123,7 @@ class ResourceEditorTest { EditorResourceSerialization.serialize(resources, YAMLFileType.YML) }.whenever(this).invoke(any(), any()) } - private val pushNotification: PushNotification = mock() - private val pushedNotification: PushedNotification = mock() - private val pullNotification: PullNotification = mock() - private val pulledNotification: PulledNotification = mock() - private val deletedNotification: DeletedNotification = mock() - private val errorNotification: ErrorNotification = mock() + private val notifications: Notifications = mock() private val document: Document = mock() private val getDocument: (FileEditor) -> Document? = { document } // using a mock of PsiFile made tests fail with a NoClassDefFoundError on github @@ -146,7 +138,10 @@ class ResourceEditorTest { kubernetesResourceInfo } private val diff: ResourceDiff = mock() - + private val settings: Settings = mock { + on { isEditorSyncEnabled() } doReturn true + } + private val connection: MessageBusConnection = mock() private val editor = TestableResourceEditor( fileEditor, @@ -155,17 +150,14 @@ class ResourceEditorTest { createResources, serialize, createResourceFileForVirtual, - pushNotification, - pushedNotification, - pullNotification, - pulledNotification, - deletedNotification, - errorNotification, + notifications, getDocument, getPsiDocumentManager, getKubernetesResourceInfo, diff, - editorResources + editorResources, + settings, + connection ) @Before @@ -177,7 +169,7 @@ class ResourceEditorTest { } @Test - fun `#constructor should add listener to resourceModel when created`() { + fun `#constructor should add listener to resourceModel`() { // given // when // then @@ -211,25 +203,12 @@ class ResourceEditorTest { } @Test - fun `#update should hide all notifications when resource on cluster is in error`() { + fun `#dispose should dispose notifications`() { // given - givenResources(mapOf(GARGAMEL to Error("disturbance in the force"))) // when - editor.update() - // then - verifyHideAllNotifications() - } - - @Test - fun `#update should show error notification when resource on cluster is in error`() { - // given - val title = "disturbance in the force" - val message = "need to meditate more" - givenResources(mapOf(GARGAMEL to Error(title, message))) - // when - editor.update() + editor.dispose() // then - verify(errorNotification).show(title, message) + verify(connection).dispose() } @Test @@ -240,103 +219,8 @@ class ResourceEditorTest { // when editor.update() // then - verify(errorNotification).show(any(), argWhere { it.contains("client error") }) - verifyHideAllNotifications() - } - - @Test - fun `#update should hide all notifications when resource on cluster is deleted`() { - // given - givenResources(mapOf(GARGAMEL to DeletedOnCluster())) - // when - editor.update() - // then - verifyHideAllNotifications() - } - - @Test - fun `#update should show deleted notification when resource on cluster is deleted`() { - // given - givenResources(mapOf(GARGAMEL to DeletedOnCluster())) - // when - editor.update() - // then - verify(deletedNotification).show(GARGAMEL) - } - - @Test - fun `#update should show push notification when there are several deleted resources`() { - // given - givenResources(mapOf( - GARGAMEL to Identical(), - GARGAMEL_WITH_LABEL to DeletedOnCluster(), - GARGAMEL_V2 to DeletedOnCluster())) - // when - editor.update() - // then - verify(pushNotification).show(eq(false), argWhere> { editorResources -> - editorResources.size == 2 - && editorResources.map { it.getResource() } - .containsAll(listOf(GARGAMEL_WITH_LABEL, GARGAMEL_V2)) - }) - } - - @Test - fun `#update should show push notifications when resource is modified`() { - // given - givenResources(mapOf(GARGAMEL to Modified(true, true))) - // when - editor.update() - // then - verify(pushNotification).show(eq(true), argWhere { resources -> - resources.size == 1 - && resources.first().getResource() == GARGAMEL - }) - } - - @Test - fun `#update should show push notification when there are several resources and one is modified`() { - // given - givenResources(mapOf( - GARGAMEL to Identical(), - GARGAMEL_WITH_LABEL to Modified(true, false))) - // when - editor.update() - // then - verify(pushNotification).show(any(), argWhere> { editorResources -> - editorResources.size == 1 - && editorResources.first().getResource() == GARGAMEL_WITH_LABEL - }) - } - - @Test - fun `#update should hide all notifications when resource on cluster is modified`() { - // given - givenResources(mapOf(GARGAMEL to Modified(true, true))) - // when - editor.update() - // then - verifyHideAllNotifications() - } - - @Test - fun `#update should hide all notifications when resource is outdated`() { - // given - givenResources(mapOf(GARGAMEL to Outdated())) - // when - editor.update() - // then - verifyHideAllNotifications() - } - - @Test - fun `#update should hide all notifications when editor resource is identical`() { - // given - givenResources(mapOf(GARGAMEL to Identical())) - // when - editor.update() - // then - verifyHideAllNotifications() + verify(notifications).showError(any(), argWhere { it.contains("client error") }) + verify(notifications).hideAll() } @Test @@ -470,7 +354,7 @@ class ResourceEditorTest { // when editor.pull() // then - verifyHideAllNotifications() + verify(notifications).hideAll() } @Test @@ -481,7 +365,7 @@ class ResourceEditorTest { editor.push(true) // then // hides 1) before pushing, 2) after pushing by calling #update - verifyHideAllNotifications(2) + verify(notifications, times(2)).hideAll() } @Test @@ -557,6 +441,17 @@ class ResourceEditorTest { verify(editorResources).watchAll() } + @Test + fun `#startWatch should NOT start watching all editor resources if editorSync is NOT Enabled`() { + // given + whenever(settings.isEditorSyncEnabled()) + .thenReturn(false) + // when + editor.startWatch() + // then + verify(editorResources, never()).watchAll() + } + @Test fun `#stopWatch should stop watching all editor resources`() { // given @@ -650,7 +545,7 @@ class ResourceEditorTest { // when editor.removeClutter() // then - verifyHideAllNotifications() + verify(notifications).hideAll() } @Test fun `#isEditing should return true if there is an EditorResource with the given resource`() { @@ -712,15 +607,6 @@ class ResourceEditorTest { verify(fileEditor).putUserData(ResourceEditor.KEY_RESOURCE_EDITOR, null) } - private fun verifyHideAllNotifications(times: Int = 1) { - verify(errorNotification, times(times)).hide() - verify(pullNotification, times(times)).hide() - verify(pulledNotification, times(times)).hide() - verify(deletedNotification, times(times)).hide() - verify(pushNotification, times(times)).hide() - verify(pushedNotification, times(times)).hide() - } - private fun givenResources(editorResourceByResource: Map) { val resources = editorResourceByResource.keys.toList() doReturn(resources) @@ -746,17 +632,14 @@ class ResourceEditorTest { createResources: (string: String?, fileType: FileType?, currentNamespace: String?) -> List, serialize: (resources: Collection, fileType: FileType?) -> String?, resourceFileForVirtual: (file: VirtualFile?) -> ResourceFile?, - pushNotification: PushNotification, - pushedNotification: PushedNotification, - pullNotification: PullNotification, - pulledNotification: PulledNotification, - deletedNotification: DeletedNotification, - errorNotification: ErrorNotification, + notifications: Notifications, documentProvider: (FileEditor) -> Document?, psiDocumentManagerProvider: (Project) -> PsiDocumentManager, getKubernetesResourceInfo: (VirtualFile?, Project) -> KubernetesResourceInfo, diff: ResourceDiff, - editorResources: EditorResources + editorResources: EditorResources, + settings: Settings, + connection: MessageBusConnection ) : ResourceEditor( editor, resourceModel, @@ -764,17 +647,14 @@ class ResourceEditorTest { createResources, serialize, resourceFileForVirtual, - pushNotification, - pushedNotification, - pullNotification, - pulledNotification, - deletedNotification, - errorNotification, + notifications, documentProvider, psiDocumentManagerProvider, getKubernetesResourceInfo, diff, - editorResources + editorResources, + settings, + connection ) { public override fun updateDeleted(deleted: HasMetadata?) { diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/NotificationsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/NotificationsTest.kt new file mode 100644 index 000000000..7c44106db --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/notification/NotificationsTest.kt @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor.notification + +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.project.Project +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.argWhere +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.redhat.devtools.intellij.kubernetes.editor.Created +import com.redhat.devtools.intellij.kubernetes.editor.DeletedOnCluster +import com.redhat.devtools.intellij.kubernetes.editor.EditorResource +import com.redhat.devtools.intellij.kubernetes.editor.EditorResourceState +import com.redhat.devtools.intellij.kubernetes.editor.Error +import com.redhat.devtools.intellij.kubernetes.editor.Identical +import com.redhat.devtools.intellij.kubernetes.editor.Modified +import com.redhat.devtools.intellij.kubernetes.editor.Outdated +import com.redhat.devtools.intellij.kubernetes.editor.Updated +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.Pod +import org.junit.Test + +class NotificationsTest { + private val pushNotification: PushNotification = mock() + private val pushedNotification: PushedNotification = mock() + private val pullNotification: PullNotification = mock() + private val pulledNotification: PulledNotification = mock() + private val deletedNotification: DeletedNotification = mock() + private val errorNotification: ErrorNotification = mock() + + private val notifications = TestableNotifications( + mock(), mock(), + pushNotification, + pushedNotification, + pullNotification, + pulledNotification, + deletedNotification, + errorNotification + ) + + @Test + fun `#show should hide all notifications if resource is outdated`() { + // given + val resource = editorResource(resource("darth vader"), Outdated()) + // when + notifications.show(resource) + // then + verifyHideAllNotifications() + } + + @Test + fun `#show should hide all notifications if resource is identical`() { + // given + val resource = editorResource(resource("obiwan"), Identical()) + // when + notifications.show(resource) + // then + verifyHideAllNotifications() + } + + @Test + fun `#show should hide all notifications if resource is modified`() { + // given + val resource = editorResource(resource("luke"), Modified(true, true)) + // when + notifications.show(resource) + // then + verifyHideAllNotifications() + } + + @Test + fun `#show should show push notification if resource is modified`() { + // given + val darthVader = resource("darth vader") + val resources = listOf( + editorResource(darthVader, Modified(true, true)) + ) + // when + notifications.show(resources) + // then + verify(pushNotification).show(eq(false), containsAll(darthVader)) + } + + @Test + fun `#show should NOT show push notification if resource is modified but showSyncNotifications is false`() { + // given + val darthVader = resource("darth vader") + val resources = listOf( + editorResource(darthVader, Modified(true, true)) + ) + // when + notifications.show(resources, false) + // then + verify(pushNotification, never()).show(any(), any()) + } + + @Test + fun `#show should show push notification with modified resource if there are several resources and one is modified`() { + // given + val obiwan = resource("obiwan") + val resources = listOf( + editorResource(resource("luke"), Identical()), + editorResource(obiwan, Modified(true, true)) + ) + // when + notifications.show(resources) + // then + verify(pushNotification).show(eq(false), containsAll(obiwan)) + } + + @Test + fun `#show should NOT show push notification if there are several resources and one is modified but showSyncNotifications is false`() { + // given + val obiwan = resource("obiwan") + val resources = listOf( + editorResource(resource("luke"), Identical()), + editorResource(obiwan, Modified(true, true)) + ) + // when + notifications.show(resources, false) + // then + verify(pushNotification, never()).show(any(), any()) + } + + @Test + fun `#show should hide all notifications if resource is deleted`() { + // given + val resource = editorResource(resource("darth vader"), DeletedOnCluster()) + // when + notifications.show(resource) + // then + verifyHideAllNotifications() + } + + @Test + fun `#show should show deleted notification if resource is deleted`() { + // given + val darthVader = resource("darth vader") + val resource = editorResource(darthVader, DeletedOnCluster()) + // when + notifications.show(resource) + // then + verify(deletedNotification).show(darthVader) + } + + @Test + fun `#show should NOT show deleted notification if resource is deleted but showSyncNotifications is false`() { + // given + val darthVader = resource("darth vader") + val resource = editorResource(darthVader, DeletedOnCluster()) + // when + notifications.show(resource, false) + // then + verify(deletedNotification, never()).show(any()) + } + + @Test + fun `#show should NOT show push notification if resource is deleted but showSyncNotifications is false`() { + // given + val darthVader = resource("darth vader") + val resources = listOf( + editorResource(darthVader, DeletedOnCluster()) + ) + // when + notifications.show(resources, false) + // then + verify(pushNotification, never()).show(any(), any()) + } + + @Test + fun `#show should show push notification with deleted resources if there are several resources and several are deleted`() { + // given + val darthVader = resource("darth vader") + val emperor = resource("emperor") + val resources = listOf( + editorResource(emperor, DeletedOnCluster()), + editorResource(resource("leia"), Identical()), + editorResource(darthVader, DeletedOnCluster()) + ) + // when + notifications.show(resources) + // then + verify(pushNotification).show(eq(false), containsAll(emperor, darthVader)) + } + + @Test + fun `#show should hide all notifications if resource is in error`() { + // given + val resource = editorResource(resource("darth vader"), Error("disturbance in the force")) + // when + notifications.show(resource) + // then + verifyHideAllNotifications() + } + + @Test + fun `#show should show error notification if resource is in error`() { + // given + val title = "disturbance in the force" + val message = "need to meditate more" + val darthVader = resource("darth vader") + val resources = listOf( + editorResource(darthVader, Error(title, message)) + ) + // when + notifications.show(resources) + // then + verify(errorNotification).show(title, message) + } + + @Test + fun `#show should show pushed notification if resource was created`() { + // given + val luke = resource("luke") + val resource = editorResource(luke, Created()) + // when + notifications.show(resource, true) + // then + verify(pushedNotification).show(containsAll(luke)) + } + + @Test + fun `#show should show pushed notification if resource was created even if showSyncNotifications is false`() { + // given + val luke = resource("luke") + val resource = editorResource(luke, Created()) + // when + notifications.show(resource, false) + // then + verify(pushedNotification).show(containsAll(luke)) + } + + @Test + fun `#show should show pushed notification if resource was updated`() { + // given + val r2d2 = resource("r2d2") + val resource = editorResource(r2d2, Updated()) + // when + notifications.show(resource, true) + // then + verify(pushedNotification).show(containsAll(r2d2)) + } + + @Test + fun `#show should show pushed notification if resource was updated even if showSyncNotifications is false`() { + // given + val r2d2 = resource("r2d2") + val resource = editorResource(r2d2, Updated()) + // when + notifications.show(resource, false) + // then + verify(pushedNotification).show(containsAll(r2d2)) + } + + private fun editorResource(resource: HasMetadata, state: EditorResourceState): EditorResource { + return mock { + on { getResource() } doReturn resource + on { getState() } doReturn state + } + } + + private fun containsAll(vararg resources: HasMetadata): List { + return argWhere> { editorResources -> + editorResources.size == resources.size + && editorResources.map { it.getResource() } + .containsAll(resources.toList()) + } + } + + private fun verifyHideAllNotifications() { + verify(errorNotification).hide() + verify(pullNotification).hide() + verify(deletedNotification).hide() + verify(pushNotification).hide() + verify(pushedNotification).hide() + verify(pulledNotification).hide() + } + + private class TestableNotifications( + editor: FileEditor, + project: Project, + pushNotification: PushNotification, + pushedNotification: PushedNotification, + pullNotification: PullNotification, + pulledNotification: PulledNotification, + deletedNotification: DeletedNotification, + errorNotification: ErrorNotification + ) : Notifications( + editor, + project, + pushNotification, + pushedNotification, + pullNotification, + pulledNotification, + deletedNotification, + errorNotification + ) { + override fun runInUI(runnable: () -> Unit) { + // run directly + runnable.invoke() + } + } +} + +