Skip to content

Commit

Permalink
feat: show/allow setting decoded base64 values in Secrets, ConfigMaps (
Browse files Browse the repository at this point in the history
…#663)

Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Feb 14, 2024
1 parent 0a05c6a commit 797b07e
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*******************************************************************************
* 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.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.JBUI
import org.jetbrains.annotations.NotNull
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import java.util.function.BiConsumer
import java.util.function.Supplier
import javax.swing.JPanel
import javax.swing.JTextField
import kotlin.math.max


class StringInputBalloon(@NotNull private val onValidValue: (String) -> Unit, @NotNull private val editor: Editor) {

companion object {
private const val MAX_WIDTH = 220.0
}

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<JBTextField, Balloon> {
val panel = JPanel(BorderLayout())
val label = JBLabel("Value:")
label.border = JBUI.Borders.empty(0, 3, 0, 1)
panel.add(label, BorderLayout.WEST)
val field = JBTextField()
field.preferredSize = Dimension(
max(MAX_WIDTH, field.preferredSize.width.toDouble()).toInt(),
field.preferredSize.height
)
panel.add(field, BorderLayout.CENTER)
val balloon = createBalloon(panel)
val disposable = Disposer.newDisposable()
Disposer.register(balloon, disposable)
ComponentValidator(disposable)
.withValidator(ValueValidator(field))
.installOn(field)
.andRegisterOnDocumentListener(field)
.revalidate()
val keyListener = onKeyPressed(field, balloon)
field.addKeyListener(keyListener)
balloon.addListener(onClosed(field, keyListener))
return Pair(field, balloon)
}

private fun createBalloon(panel: JPanel): Balloon {
return JBPopupFactory.getInstance()
.createBalloonBuilder(panel)
.setCloseButtonEnabled(false)
.setBlockClicksThroughBalloon(true)
.setAnimationCycle(0)
.setHideOnKeyOutside(true)
.setHideOnClickOutside(true)
.setFillColor(panel.background)
.createBalloon()
}

private fun onClosed(field: JBTextField, keyListener: KeyAdapter): JBPopupListener {
return object : JBPopupListener {
override fun beforeShown(event: LightweightWindowEvent) {}
override fun onClosed(event: LightweightWindowEvent) {
field.removeKeyListener(keyListener)
}
}
}

private fun onKeyPressed(field: JTextField, balloon: Balloon) = object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
when (e.keyCode) {
KeyEvent.VK_ESCAPE ->
balloon.hide()
KeyEvent.VK_ENTER ->
if (isValid) {
balloon.hide()
onValidValue.invoke(field.text)
}
}
}
}

private fun onFocused(focusManager: IdeFocusManager, field: JBTextField): 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 field: JTextField) : Supplier<ValidationInfo?> {

override fun get(): ValidationInfo? {
if (!field.isEnabled
|| !field.isVisible
) {
return null
}
return validate(field.text)
}

private fun validate(name: String): ValidationInfo? {
val validation = if (StringUtil.isEmptyOrSpaces(name)) {
ValidationInfo("Provide a value", field).asWarning()
} else {
null
}
this@StringInputBalloon.isValid = (validation == null)
return validation
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*******************************************************************************
* 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.ChangeListener
import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
import com.intellij.codeInsight.hints.ImmediateConfigurable
import com.intellij.codeInsight.hints.InlayHintsCollector
import com.intellij.codeInsight.hints.InlayHintsProvider
import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.NoSettings
import com.intellij.codeInsight.hints.SettingsKey
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.json.psi.JsonProperty
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import com.intellij.ui.dsl.builder.panel
import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64
import com.redhat.devtools.intellij.kubernetes.editor.util.getContent
import com.redhat.devtools.intellij.kubernetes.editor.util.getData
import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesResourceInfo
import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource
import com.redhat.devtools.intellij.kubernetes.editor.util.setValue
import org.jetbrains.concurrency.runAsync
import org.jetbrains.yaml.psi.YAMLKeyValue
import java.awt.event.MouseEvent
import javax.swing.JComponent


internal class Base64ValueInlayHintsProvider : InlayHintsProvider<NoSettings> {

override val key: SettingsKey<NoSettings> = SettingsKey("KubernetesResource.hints")
override val name: String = "Kubernetes"
override val previewText: String = "Preview"

override fun createSettings(): NoSettings {
return NoSettings()
}

override fun createConfigurable(settings: NoSettings): ImmediateConfigurable {
return object : ImmediateConfigurable {
override fun createComponent(listener: ChangeListener): JComponent = panel {}

override val mainCheckboxText: String = "Show hints for:"

override val cases: List<ImmediateConfigurable.Case> = emptyList()
}
}

override fun getCollectorFor(
file: PsiFile,
editor: Editor,
settings: NoSettings,
sink: InlayHintsSink
): InlayHintsCollector? {
val project = editor.project ?: return null
val virtualFile = file.virtualFile ?: return null
val info = getKubernetesResourceInfo(virtualFile, project)
if (!isKubernetesResource("Secret", info)
&& !isKubernetesResource("ConfigMap", info)
) {
return null
}
return Collector(editor)
}

private class Collector(editor: Editor) : FactoryInlayHintsCollector(editor) {

override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
if (!element.isValid) {
return true
}
val content = getContent(element) ?: return true
val data = getData(content) ?: return true
data.children.toList()
.forEach { child ->
val factory = Base64InlineHintFactory(child)
if (!factory.isSupported()) {
return true
}
val onClick = StringInputBalloon(onValidValue(factory::setValue, editor), editor)::show
return factory.addInlineHint(onClick, editor, sink)
}

return true
}

fun onValidValue(setter: (value: String, editor: Editor) -> Unit, editor: Editor): (value: String) -> Unit {
return { value ->
runAsync {
WriteCommandAction.runWriteCommandAction(editor.project) {
setter.invoke(value, editor)
}
}
}
}
}

private class Base64InlineHintFactory(private val element: PsiElement) {

fun isSupported(): Boolean {
return getDecoded() != null
}

fun addInlineHint(onClick: (event: MouseEvent) -> Unit, editor: Editor, sink: InlayHintsSink): Boolean {
val offset = getValueStartOffset() ?: return true
val presentation = createPresentation(onClick, editor) ?: return true
sink.addInlineElement(offset, false, presentation, false)
return false
}

fun setValue(value: String, editor: Editor) {
val encoded = encodeBase64(value) ?: return
setValue(encoded, element, editor.project)
}

private fun getText(): String? {
return when (element) {
is YAMLKeyValue -> element.value?.text
is JsonProperty -> element.value?.text
else -> null
}
}

private fun getValueStartOffset(): Int? {
return when (element) {
is YAMLKeyValue -> element.value?.startOffset
is JsonProperty -> element.value?.startOffset
else -> null
}
}

private fun createPresentation(onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? {
val factory = PresentationFactory(editor as EditorImpl)
val decoded = getDecoded() ?: return null
val textPresentation = factory.smallText(decoded)
val hoverPresentation = factory.referenceOnHover(textPresentation) { event, translated ->
onClick.invoke(event)
}
val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation)
val roundPresentation = factory.roundWithBackground(tooltipPresentation)
return roundPresentation
}

private fun getDecoded(): String? {
return decodeBase64(getText())
}

}
}
Loading

0 comments on commit 797b07e

Please sign in to comment.