-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: show/allow setting decoded base64 values in Secrets, ConfigMaps (…
…#663) Signed-off-by: Andre Dietisheim <[email protected]>
- Loading branch information
Showing
8 changed files
with
665 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 193 additions & 0 deletions
193
src/main/kotlin/com/redhat/devtools/intellij/kubernetes/balloon/StringInputBalloon.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/******************************************************************************* | ||
* 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.balloon | ||
|
||
import com.intellij.openapi.editor.Editor | ||
import com.intellij.openapi.ui.ComponentValidator | ||
import com.intellij.openapi.ui.ValidationInfo | ||
import com.intellij.openapi.ui.popup.Balloon | ||
import com.intellij.openapi.ui.popup.JBPopupFactory | ||
import com.intellij.openapi.ui.popup.JBPopupListener | ||
import com.intellij.openapi.ui.popup.LightweightWindowEvent | ||
import com.intellij.openapi.util.Disposer | ||
import com.intellij.openapi.util.ExpirableRunnable | ||
import com.intellij.openapi.util.text.StringUtil | ||
import com.intellij.openapi.wm.IdeFocusManager | ||
import com.intellij.ui.ScrollPaneFactory | ||
import com.intellij.ui.awt.RelativePoint | ||
import com.intellij.ui.components.JBLabel | ||
import com.intellij.ui.components.JBTextArea | ||
import com.intellij.ui.components.JBTextField | ||
import com.intellij.util.ui.JBUI | ||
import java.awt.BorderLayout | ||
import java.awt.Dimension | ||
import java.awt.event.KeyAdapter | ||
import java.awt.event.KeyEvent | ||
import java.awt.event.KeyListener | ||
import java.awt.event.MouseEvent | ||
import java.util.function.Supplier | ||
import javax.swing.JPanel | ||
import javax.swing.JTextArea | ||
import javax.swing.text.JTextComponent | ||
import kotlin.math.max | ||
|
||
class StringInputBalloon( | ||
private val value: String, | ||
private val onValidValue: (String) -> Unit, | ||
private val editor: Editor | ||
) { | ||
|
||
companion object { | ||
private const val MAX_WIDTH = 220.0 | ||
private const val MAX_CHARACTERS = 64 | ||
} | ||
|
||
private var isValid = false | ||
|
||
fun show(event: MouseEvent) { | ||
val (field, balloon) = create() | ||
balloon.show(RelativePoint(event), Balloon.Position.above) | ||
val focusManager = IdeFocusManager.getInstance(editor.project) | ||
focusManager.doWhenFocusSettlesDown(onFocused(focusManager, field)) | ||
} | ||
|
||
private fun create(): Pair<JTextComponent, Balloon> { | ||
val panel = JPanel(BorderLayout()) | ||
val textComponent = if (value.contains('\n')) { | ||
createTextArea(panel) | ||
} else { | ||
createTextField(panel) | ||
} | ||
val balloon = createBalloon(panel) | ||
val disposable = Disposer.newDisposable() | ||
Disposer.register(balloon, disposable) | ||
ComponentValidator(disposable) | ||
.withValidator(ValueValidator(textComponent)) | ||
.installOn(textComponent) | ||
.andRegisterOnDocumentListener(textComponent) | ||
.revalidate() | ||
val keyListener = onKeyPressed(textComponent, balloon) | ||
textComponent.addKeyListener(keyListener) | ||
balloon.addListener(onClosed(textComponent, keyListener)) | ||
return Pair(textComponent, balloon) | ||
} | ||
|
||
private fun createTextField(panel: JPanel): JBTextField { | ||
val label = JBLabel("Value:") | ||
label.border = JBUI.Borders.empty(0, 3, 0, 1) | ||
panel.add(label, BorderLayout.WEST) | ||
val field = JBTextField(value) | ||
field.preferredSize = Dimension( | ||
max(MAX_WIDTH, field.preferredSize.width.toDouble()).toInt(), | ||
field.preferredSize.height | ||
) | ||
panel.add(field, BorderLayout.CENTER) | ||
return field | ||
} | ||
|
||
private fun createTextArea(panel: JPanel): JTextArea { | ||
val label = JBLabel("Value:") | ||
label.border = JBUI.Borders.empty(0, 3, 4, 0) | ||
panel.add(label, BorderLayout.NORTH) | ||
val textArea = JBTextArea( | ||
value, | ||
value.length.floorDiv(MAX_CHARACTERS - 1) + 2, // textarea has text lines + 1 | ||
MAX_CHARACTERS - 1 | ||
) | ||
val scrolled = ScrollPaneFactory.createScrollPane(textArea, true) | ||
panel.add(scrolled, BorderLayout.CENTER) | ||
return textArea | ||
} | ||
|
||
private fun createBalloon(panel: JPanel): Balloon { | ||
return JBPopupFactory.getInstance() | ||
.createBalloonBuilder(panel) | ||
.setCloseButtonEnabled(true) | ||
.setBlockClicksThroughBalloon(true) | ||
.setAnimationCycle(0) | ||
.setHideOnKeyOutside(true) | ||
.setHideOnClickOutside(true) | ||
.setFillColor(panel.background) | ||
.setHideOnAction(false) // allow user to Ctrl+A & Ctrl+C | ||
.createBalloon() | ||
} | ||
|
||
private fun onClosed(field: JTextComponent, keyListener: KeyListener): JBPopupListener { | ||
return object : JBPopupListener { | ||
override fun beforeShown(event: LightweightWindowEvent) { | ||
// do nothing | ||
} | ||
|
||
override fun onClosed(event: LightweightWindowEvent) { | ||
field.removeKeyListener(keyListener) | ||
} | ||
} | ||
} | ||
|
||
private fun onKeyPressed(textComponent: JTextComponent, balloon: Balloon): KeyListener { | ||
return object : KeyAdapter() { | ||
override fun keyPressed(e: KeyEvent) { | ||
when (e.keyCode) { | ||
KeyEvent.VK_ESCAPE -> | ||
balloon.hide() | ||
|
||
KeyEvent.VK_ENTER -> | ||
if (isValid) { | ||
balloon.hide() | ||
onValidValue.invoke(textComponent.text) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun onFocused(focusManager: IdeFocusManager, field: JTextComponent): ExpirableRunnable { | ||
return object : ExpirableRunnable { | ||
|
||
override fun run() { | ||
focusManager.requestFocus(field, true) | ||
field.selectAll() | ||
} | ||
|
||
override fun isExpired(): Boolean { | ||
return false | ||
} | ||
} | ||
} | ||
|
||
private inner class ValueValidator(private val textComponent: JTextComponent) : Supplier<ValidationInfo?> { | ||
|
||
override fun get(): ValidationInfo? { | ||
if (!textComponent.isEnabled | ||
|| !textComponent.isVisible | ||
) { | ||
return null | ||
} | ||
return validate(textComponent.text) | ||
} | ||
|
||
private fun validate(newValue: String): ValidationInfo? { | ||
val validation = when { | ||
StringUtil.isEmptyOrSpaces(newValue) -> | ||
ValidationInfo("Provide a value", textComponent).asWarning() | ||
|
||
value == newValue -> | ||
ValidationInfo("Provide new value", textComponent).asWarning() | ||
|
||
else -> | ||
null | ||
} | ||
this@StringInputBalloon.isValid = (validation == null) | ||
return validation | ||
} | ||
} | ||
|
||
} |
151 changes: 151 additions & 0 deletions
151
...kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsFactory.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
/******************************************************************************* | ||
* 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 | ||
******************************************************************************/ | ||
@file:Suppress("UnstableApiUsage") | ||
package com.redhat.devtools.intellij.kubernetes.editor.inlay | ||
|
||
import com.intellij.codeInsight.hints.InlayHintsSink | ||
import com.intellij.codeInsight.hints.presentation.InlayPresentation | ||
import com.intellij.codeInsight.hints.presentation.PresentationFactory | ||
import com.intellij.openapi.command.WriteCommandAction | ||
import com.intellij.openapi.editor.Editor | ||
import com.intellij.openapi.editor.impl.EditorImpl | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.psi.PsiElement | ||
import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo | ||
import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon | ||
import com.redhat.devtools.intellij.kubernetes.editor.util.getBinaryData | ||
import com.redhat.devtools.intellij.kubernetes.editor.util.getData | ||
import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource | ||
import com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis | ||
import org.jetbrains.concurrency.runAsync | ||
import java.awt.event.MouseEvent | ||
|
||
class Base64PresentationsFactory { | ||
|
||
companion object { | ||
private const val SECRET_RESOURCE_KIND = "Secret" | ||
private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap" | ||
} | ||
|
||
fun create(content: PsiElement, info: KubernetesResourceInfo, editor: Editor, sink: InlayHintsSink): InlayPresentationsFactory? { | ||
return when { | ||
isKubernetesResource(SECRET_RESOURCE_KIND, info) -> | ||
StringPresentationsFactory(content, editor, sink) | ||
|
||
isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> | ||
BinaryPresentationsFactory(content, editor, sink) | ||
|
||
else -> null | ||
} | ||
|
||
} | ||
|
||
abstract class InlayPresentationsFactory( | ||
private val element: PsiElement, | ||
private val editor: Editor, | ||
private val sink: InlayHintsSink | ||
) { | ||
|
||
companion object { | ||
const val INLAY_HINT_MAX_WIDTH = 50 | ||
} | ||
|
||
fun create(): Collection<InlayPresentation>? { | ||
return getChildren(element)?.children?.mapNotNull { child -> | ||
val adapter = Base64ValueAdapter(child) | ||
create(adapter, editor, sink) | ||
} | ||
} | ||
|
||
protected abstract fun getChildren(element: PsiElement): PsiElement? | ||
|
||
protected abstract fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? | ||
|
||
} | ||
|
||
class StringPresentationsFactory(element: PsiElement, editor: Editor, sink: InlayHintsSink) | ||
: InlayPresentationsFactory(element, editor, sink) { | ||
|
||
override fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? { | ||
val decoded = adapter.getDecoded() ?: return null | ||
val offset = adapter.getStartOffset() ?: return null | ||
val onClick = StringInputBalloon( | ||
decoded, | ||
onValidValue(adapter::set, editor.project), | ||
editor | ||
)::show | ||
val presentation = create(decoded, onClick, editor) ?: return null | ||
sink.addInlineElement(offset, false, presentation, false) | ||
return presentation | ||
} | ||
|
||
private fun create(text: String, onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? { | ||
val factory = PresentationFactory(editor as EditorImpl) | ||
val trimmed = trimWithEllipsis(text, INLAY_HINT_MAX_WIDTH) ?: return null | ||
val textPresentation = factory.smallText(trimmed) | ||
val hoverPresentation = factory.referenceOnHover(textPresentation) { event, _ -> | ||
onClick.invoke(event) | ||
} | ||
val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation) | ||
val roundPresentation = factory.roundWithBackground(tooltipPresentation) | ||
return roundPresentation | ||
} | ||
|
||
private fun onValidValue(setter: (value: String, project: Project?) -> Unit, project: Project?) | ||
: (value: String) -> Unit { | ||
return { value -> | ||
runAsync { | ||
WriteCommandAction.runWriteCommandAction(project) { | ||
setter.invoke(value, project) | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun getChildren(element: PsiElement): PsiElement? { | ||
return getData(element) | ||
} | ||
|
||
} | ||
|
||
class BinaryPresentationsFactory(element: PsiElement, editor: Editor, sink: InlayHintsSink) | ||
: InlayPresentationsFactory(element, editor, sink) { | ||
|
||
override fun create(adapter: Base64ValueAdapter, editor: Editor, sink: InlayHintsSink): InlayPresentation? { | ||
val decoded = adapter.getDecodedBytes() ?: return null | ||
val offset = adapter.getStartOffset() ?: return null | ||
val presentation = create(decoded, editor) ?: return null | ||
sink.addInlineElement(offset, false, presentation, false) | ||
return presentation | ||
} | ||
|
||
private fun create(bytes: ByteArray, editor: Editor): InlayPresentation? { | ||
val factory = PresentationFactory(editor as EditorImpl) | ||
val hex = toHexString(bytes) ?: return null | ||
val trimmed = trimWithEllipsis(hex, INLAY_HINT_MAX_WIDTH) ?: return null | ||
return factory.roundWithBackground(factory.smallText(trimmed)) | ||
} | ||
|
||
private fun toHexString(bytes: ByteArray): String? { | ||
return try { | ||
bytes.joinToString(separator = " ") { byte -> | ||
Integer.toHexString(byte.toInt()) | ||
} | ||
} catch (e: Exception) { | ||
null | ||
} | ||
} | ||
|
||
override fun getChildren(element: PsiElement): PsiElement? { | ||
return getBinaryData(element) | ||
} | ||
} | ||
} |
Oops, something went wrong.