Skip to content

Commit 4961ec5

Browse files
committed
feat: show/allow setting decoded base64 values in Secrets, ConfigMaps (redhat-developer#663)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent 0a05c6a commit 4961ec5

File tree

4 files changed

+339
-1
lines changed

4 files changed

+339
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.balloon
12+
13+
import com.intellij.openapi.editor.Editor
14+
import com.intellij.openapi.ui.ComponentValidator
15+
import com.intellij.openapi.ui.ValidationInfo
16+
import com.intellij.openapi.ui.popup.Balloon
17+
import com.intellij.openapi.ui.popup.JBPopupFactory
18+
import com.intellij.openapi.ui.popup.JBPopupListener
19+
import com.intellij.openapi.ui.popup.LightweightWindowEvent
20+
import com.intellij.openapi.util.Disposer
21+
import com.intellij.openapi.util.ExpirableRunnable
22+
import com.intellij.openapi.util.text.StringUtil
23+
import com.intellij.openapi.wm.IdeFocusManager
24+
import com.intellij.ui.awt.RelativePoint
25+
import com.intellij.ui.components.JBLabel
26+
import com.intellij.ui.components.JBTextField
27+
import com.intellij.util.ui.JBUI
28+
import org.jetbrains.annotations.NotNull
29+
import java.awt.BorderLayout
30+
import java.awt.Dimension
31+
import java.awt.event.KeyAdapter
32+
import java.awt.event.KeyEvent
33+
import java.awt.event.MouseEvent
34+
import java.util.function.BiConsumer
35+
import java.util.function.Supplier
36+
import javax.swing.JPanel
37+
import javax.swing.JTextField
38+
import kotlin.math.max
39+
40+
41+
class StringInputBalloon(@NotNull private val onValidValue: (String) -> Unit, @NotNull private val editor: Editor) {
42+
43+
companion object {
44+
private const val MAX_WIDTH = 220.0
45+
}
46+
47+
private var isValid = false
48+
49+
fun show(event: MouseEvent) {
50+
val (field, balloon) = create()
51+
balloon.show(RelativePoint(event), Balloon.Position.above)
52+
val focusManager = IdeFocusManager.getInstance(editor.project)
53+
focusManager.doWhenFocusSettlesDown(onFocused(focusManager, field))
54+
}
55+
56+
private fun create(): Pair<JBTextField, Balloon> {
57+
val panel = JPanel(BorderLayout())
58+
val label = JBLabel("Value:")
59+
label.border = JBUI.Borders.empty(0, 3, 0, 1)
60+
panel.add(label, BorderLayout.WEST)
61+
val field = JBTextField()
62+
field.preferredSize = Dimension(
63+
max(MAX_WIDTH, field.preferredSize.width.toDouble()).toInt(),
64+
field.preferredSize.height
65+
)
66+
panel.add(field, BorderLayout.CENTER)
67+
val balloon = createBalloon(panel)
68+
val disposable = Disposer.newDisposable()
69+
Disposer.register(balloon, disposable)
70+
ComponentValidator(disposable)
71+
.withValidator(ValueValidator(field))
72+
.installOn(field)
73+
.andRegisterOnDocumentListener(field)
74+
.revalidate()
75+
val keyListener = onKeyPressed(field, balloon)
76+
field.addKeyListener(keyListener)
77+
balloon.addListener(onClosed(field, keyListener))
78+
return Pair(field, balloon)
79+
}
80+
81+
private fun createBalloon(panel: JPanel): Balloon {
82+
return JBPopupFactory.getInstance()
83+
.createBalloonBuilder(panel)
84+
.setCloseButtonEnabled(false)
85+
.setBlockClicksThroughBalloon(true)
86+
.setAnimationCycle(0)
87+
.setHideOnKeyOutside(true)
88+
.setHideOnClickOutside(true)
89+
.setFillColor(panel.background)
90+
.createBalloon()
91+
}
92+
93+
private fun onClosed(field: JBTextField, keyListener: KeyAdapter): JBPopupListener {
94+
return object : JBPopupListener {
95+
override fun beforeShown(event: LightweightWindowEvent) {}
96+
override fun onClosed(event: LightweightWindowEvent) {
97+
field.removeKeyListener(keyListener)
98+
}
99+
}
100+
}
101+
102+
private fun onKeyPressed(field: JTextField, balloon: Balloon) = object : KeyAdapter() {
103+
override fun keyPressed(e: KeyEvent) {
104+
when (e.keyCode) {
105+
KeyEvent.VK_ESCAPE ->
106+
balloon.hide()
107+
KeyEvent.VK_ENTER ->
108+
if (isValid) {
109+
onValidValue.invoke(field.text)
110+
}
111+
}
112+
}
113+
}
114+
115+
private fun onFocused(focusManager: IdeFocusManager, field: JBTextField): ExpirableRunnable {
116+
return object : ExpirableRunnable {
117+
118+
override fun run() {
119+
focusManager.requestFocus(field, true)
120+
field.selectAll()
121+
}
122+
123+
override fun isExpired(): Boolean {
124+
return false
125+
}
126+
}
127+
}
128+
129+
private inner class ValueValidator(private val field: JTextField) : Supplier<ValidationInfo?> {
130+
131+
override fun get(): ValidationInfo? {
132+
if (!field.isEnabled
133+
|| !field.isVisible
134+
) {
135+
return null
136+
}
137+
return validate(field.text)
138+
}
139+
140+
private fun validate(name: String): ValidationInfo? {
141+
val validation = if (StringUtil.isEmptyOrSpaces(name)) {
142+
ValidationInfo("Provide a value", field).asWarning()
143+
} else {
144+
null
145+
}
146+
this@StringInputBalloon.isValid = (validation == null)
147+
return validation
148+
}
149+
}
150+
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
@file:Suppress("UnstableApiUsage")
2+
3+
package com.redhat.devtools.intellij.kubernetes.editor.inlay
4+
5+
import com.intellij.codeInsight.hints.ChangeListener
6+
import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
7+
import com.intellij.codeInsight.hints.ImmediateConfigurable
8+
import com.intellij.codeInsight.hints.InlayHintsCollector
9+
import com.intellij.codeInsight.hints.InlayHintsProvider
10+
import com.intellij.codeInsight.hints.InlayHintsSink
11+
import com.intellij.codeInsight.hints.NoSettings
12+
import com.intellij.codeInsight.hints.SettingsKey
13+
import com.intellij.codeInsight.hints.presentation.PresentationFactory
14+
import com.intellij.json.psi.JsonProperty
15+
import com.intellij.openapi.application.ApplicationManager
16+
import com.intellij.openapi.application.WriteAction
17+
import com.intellij.openapi.command.WriteCommandAction
18+
import com.intellij.openapi.editor.Editor
19+
import com.intellij.openapi.editor.impl.EditorImpl
20+
import com.intellij.openapi.progress.ModalTaskOwner.project
21+
import com.intellij.psi.PsiElement
22+
import com.intellij.psi.PsiFile
23+
import com.intellij.refactoring.suggested.startOffset
24+
import com.intellij.ui.dsl.builder.panel
25+
import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon
26+
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
27+
import com.redhat.devtools.intellij.kubernetes.editor.util.getContent
28+
import com.redhat.devtools.intellij.kubernetes.editor.util.getData
29+
import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesResourceInfo
30+
import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource
31+
import com.redhat.devtools.intellij.kubernetes.editor.util.setValue
32+
import org.jetbrains.yaml.psi.YAMLKeyValue
33+
import javax.swing.JComponent
34+
35+
36+
internal class Base64InlayHintsProvider : InlayHintsProvider<NoSettings> {
37+
38+
override val key: SettingsKey<NoSettings> = SettingsKey("LSP.hints")
39+
override val name: String = "Kubernetes"
40+
override val previewText: String = "Preview"
41+
42+
override fun createSettings(): NoSettings {
43+
return NoSettings()
44+
}
45+
46+
override fun createConfigurable(settings: NoSettings): ImmediateConfigurable {
47+
return object : ImmediateConfigurable {
48+
override fun createComponent(listener: ChangeListener): JComponent = panel {}
49+
50+
override val mainCheckboxText: String = "Show hints for:"
51+
52+
override val cases: List<ImmediateConfigurable.Case> = emptyList()
53+
}
54+
}
55+
56+
override fun getCollectorFor(
57+
file: PsiFile,
58+
editor: Editor,
59+
settings: NoSettings,
60+
sink: InlayHintsSink
61+
): InlayHintsCollector? {
62+
val project = editor.project ?: return null
63+
val virtualFile = file.virtualFile ?: return null
64+
val info = getKubernetesResourceInfo(virtualFile, project)
65+
if (!isKubernetesResource("Secret", info)
66+
&& !isKubernetesResource("ConfigMap", info)
67+
) {
68+
return null
69+
}
70+
return Collector(editor)
71+
}
72+
73+
private class Collector(editor: Editor) : FactoryInlayHintsCollector(editor) {
74+
75+
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
76+
if (!element.isValid) {
77+
return true
78+
}
79+
val content = getContent(element) ?: return true
80+
val data = getData(content) ?: return true
81+
data.children.toList()
82+
.forEach { child ->
83+
val value = when (child) {
84+
is YAMLKeyValue -> child.value?.text
85+
is JsonProperty -> child.value?.text
86+
else -> null
87+
}
88+
val decoded = decodeBase64(value)
89+
val offset = when (child) {
90+
is YAMLKeyValue -> child.value?.startOffset
91+
is JsonProperty -> child.value?.startOffset
92+
else -> null
93+
} ?: return true
94+
if (decoded != null) {
95+
val factory = PresentationFactory(editor as EditorImpl)
96+
val text = factory.smallText(decoded)
97+
val hover = factory.referenceOnHover(text) { event, translated -> StringInputBalloon(onValidValue(child, editor), editor).show(event) }
98+
val tooltip = factory.withTooltip("Click to change value", hover)
99+
val round = factory.roundWithBackground(tooltip)
100+
sink.addInlineElement(offset, false, round, false)
101+
}
102+
}
103+
104+
return true
105+
}
106+
107+
fun onValidValue(child: PsiElement, editor: Editor): (value: String) -> Unit {
108+
return { value ->
109+
ApplicationManager.getApplication().invokeLater {
110+
WriteCommandAction.runWriteCommandAction(editor.project) {
111+
setValue(value, child, editor.project)
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
119+
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt

+66-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
******************************************************************************/
1111
package com.redhat.devtools.intellij.kubernetes.editor.util
1212

13+
import com.intellij.json.psi.JsonElement
1314
import com.intellij.json.psi.JsonElementGenerator
1415
import com.intellij.json.psi.JsonFile
1516
import com.intellij.json.psi.JsonProperty
1617
import com.intellij.json.psi.JsonValue
1718
import com.intellij.openapi.application.ReadAction
1819
import com.intellij.openapi.editor.Document
1920
import com.intellij.openapi.project.Project
21+
import com.intellij.openapi.util.text.Strings
2022
import com.intellij.openapi.vfs.VirtualFile
2123
import com.intellij.psi.PsiDocumentManager
2224
import com.intellij.psi.PsiElement
@@ -27,9 +29,13 @@ import org.jetbrains.yaml.YAMLElementGenerator
2729
import org.jetbrains.yaml.YAMLUtil
2830
import org.jetbrains.yaml.psi.YAMLFile
2931
import org.jetbrains.yaml.psi.YAMLKeyValue
32+
import org.jetbrains.yaml.psi.YAMLPsiElement
3033
import org.jetbrains.yaml.psi.YAMLValue
34+
import org.jetbrains.yaml.psi.impl.YAMLKeyValueKeyManipulator
35+
import java.util.Base64
3136

3237
private const val KEY_METADATA = "metadata"
38+
private const val KEY_DATA = "data"
3339
private const val KEY_RESOURCE_VERSION = "resourceVersion"
3440

3541
/**
@@ -59,6 +65,11 @@ fun isKubernetesResource(resourceInfo: KubernetesResourceInfo?): Boolean {
5965
&& resourceInfo?.typeInfo?.kind?.isNotBlank() ?: false
6066
}
6167

68+
fun isKubernetesResource(kind: String, resourceInfo: KubernetesResourceInfo?): Boolean {
69+
return resourceInfo?.typeInfo?.apiGroup?.isNotBlank() ?: false
70+
&& kind == resourceInfo?.typeInfo?.kind
71+
}
72+
6273
/**
6374
* Returns [KubernetesResourceInfo] for the given file and project. Returns `null` if it could not be retrieved.
6475
*
@@ -134,6 +145,13 @@ private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: YAM
134145
}
135146
}
136147

148+
fun getContent(element: PsiElement): PsiElement? {
149+
if (element !is PsiFile) {
150+
return null
151+
}
152+
return getContent(element)
153+
}
154+
137155
private fun getContent(file: PsiFile): PsiElement? {
138156
return when (file) {
139157
is YAMLFile -> {
@@ -154,7 +172,7 @@ fun getMetadata(document: Document?, psi: PsiDocumentManager): PsiElement? {
154172
}
155173
val file = psi.getPsiFile(document) ?: return null
156174
val content = getContent(file) ?: return null
157-
return getMetadata(content) ?: return null
175+
return getMetadata(content)
158176
}
159177

160178
private fun getMetadata(content: PsiElement): PsiElement? {
@@ -172,6 +190,53 @@ private fun getMetadata(content: PsiElement): PsiElement? {
172190
}
173191
}
174192

193+
fun getData(element: PsiElement): PsiElement? {
194+
return when (element) {
195+
is YAMLPsiElement ->
196+
element.children
197+
.filterIsInstance(YAMLKeyValue::class.java)
198+
.find { it.name == KEY_DATA }
199+
?.value
200+
is JsonElement ->
201+
element.children.toList()
202+
.filterIsInstance(JsonProperty::class.java)
203+
.find { it.name == KEY_DATA }
204+
?.value
205+
else ->
206+
null
207+
}
208+
}
209+
210+
fun decodeBase64(value: String?): String? {
211+
if (Strings.isEmptyOrSpaces(value)) {
212+
return null
213+
}
214+
return try {
215+
val bytes = Base64.getDecoder().decode(value)
216+
String(bytes)
217+
} catch (e: IllegalArgumentException) {
218+
null
219+
}
220+
}
221+
222+
fun setValue(value: String, element: PsiElement, project: Project?) {
223+
if (project == null) {
224+
return
225+
}
226+
when (element) {
227+
is YAMLKeyValue -> {
228+
val textElement = YAMLElementGenerator.getInstance(project).createYamlDoubleQuotedString()
229+
textElement.updateText(value)
230+
element.setValue(textElement)
231+
}
232+
is JsonProperty -> {
233+
val textElement = JsonElementGenerator(project).createProperty(element.name, value)
234+
element.parent.addAfter(element, textElement)
235+
element.delete()
236+
}
237+
}
238+
}
239+
175240
private fun getResourceVersion(metadata: YAMLKeyValue): YAMLKeyValue? {
176241
return metadata.value?.children
177242
?.filterIsInstance(YAMLKeyValue::class.java)

0 commit comments

Comments
 (0)