Values editing (#17)

* Refactorings

* save

* REdesigning install dialog

* Display values editor

* Reconfigure flow

* Status handler and version

* error reporting
This commit is contained in:
Andrey Pokhilko
2022-10-07 17:01:19 +01:00
committed by GitHub
parent 3c4d73665e
commit d8afa3861d
13 changed files with 239 additions and 103 deletions

View File

@@ -76,10 +76,8 @@ and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real
### Further Ideas
- solve umbrella-chart case
- use `--dry-run` instead of `template`
- Have cleaner idea on the web API structure
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
- Recognise the revisions that are rollbacks by their description and mark in timeline
#### Topic "Validating Manifests"

View File

@@ -22,7 +22,7 @@ func main() {
os.Exit(0)
}
address, webServerDone := dashboard.StartServer()
address, webServerDone := dashboard.StartServer(version)
if os.Getenv("HD_NOBROWSER") == "" {
log.Infof("Opening web UI: %s", address)

View File

@@ -33,7 +33,7 @@ func errorHandler(c *gin.Context) {
}
}
func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
func NewRouter(abortWeb ControlChan, data *DataLayer, version string) *gin.Engine {
var api *gin.Engine
if os.Getenv("DEBUG") == "" {
api = gin.New()
@@ -47,15 +47,20 @@ func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
api.Use(errorHandler)
configureStatic(api)
configureRoutes(abortWeb, data, api)
configureRoutes(abortWeb, data, api, version)
return api
}
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine, version string) {
// server shutdown handler
api.DELETE("/", func(c *gin.Context) {
abortWeb <- struct{}{}
c.Status(http.StatusAccepted)
})
api.GET("/status", func(c *gin.Context) {
c.String(http.StatusOK, version)
})
configureHelms(api.Group("/api/helm"), data)
@@ -71,7 +76,7 @@ func configureHelms(api *gin.RouterGroup, data *DataLayer) {
api.GET("/charts/resources", h.Resources)
api.GET("/repo/search", h.RepoSearch)
api.POST("/repo/update", h.RepoUpdate)
api.GET("/charts/install", h.InstallPreview)
api.GET("/repo/values", h.RepoValues)
api.POST("/charts/install", h.Install)
api.GET("/charts/:section", h.GetInfoSection)
}

View File

@@ -11,7 +11,6 @@ import (
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
"io/ioutil"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"os"
"os/exec"
@@ -234,7 +233,7 @@ func (d *DataLayer) RevisionManifests(namespace string, chartName string, revisi
return out, nil
}
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*GenericResource, error) {
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
out, err := d.RevisionManifests(namespace, chartName, revision, false)
if err != nil {
return nil, err
@@ -242,7 +241,7 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
res := make([]*GenericResource, 0)
res := make([]*v1.Carp, 0)
var tmp interface{}
for dec.Decode(&tmp) == nil {
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
@@ -252,7 +251,7 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
return nil, err
}
var doc GenericResource
var doc v1.Carp
err = json.Unmarshal(jsoned, &doc)
if err != nil {
return nil, err
@@ -289,11 +288,11 @@ func (d *DataLayer) RevisionValues(namespace string, chartName string, revision
return out, nil
}
func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*GenericResource, error) {
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
return &GenericResource{
return &v1.Carp{
Status: v1.CarpStatus{
Phase: "NotFound",
Message: err.Error(),
@@ -305,7 +304,7 @@ func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*Generi
}
}
var res GenericResource
var res v1.Carp
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
@@ -367,24 +366,22 @@ func (d *DataLayer) ChartRepoUpdate(name string) error {
return nil
}
func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string, version string, justTemplate bool) (string, error) {
oldVals, err := d.RevisionValues(namespace, name, 0, false)
func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string, version string, justTemplate bool, values string) (string, error) {
if values == "" {
oldVals, err := d.RevisionValues(namespace, name, 0, true)
if err != nil {
return "", err
}
values = oldVals
}
oldValsFile, close1, err := tempFile(values)
defer close1()
if err != nil {
return "", err
}
file, err := ioutil.TempFile("", "helm_vals_")
if err != nil {
return "", err
}
defer os.Remove(file.Name())
err = ioutil.WriteFile(file.Name(), []byte(oldVals), 0600)
if err != nil {
return "", err
}
cmd := []string{"upgrade", name, repoChart, "--version", version, "--namespace", namespace, "--values", file.Name(), "--output", "json"}
cmd := []string{"upgrade", name, repoChart, "--version", version, "--namespace", namespace, "--values", oldValsFile, "--output", "json"}
if justTemplate {
cmd = append(cmd, "--dry-run")
}
@@ -406,11 +403,22 @@ func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string
return "", err
}
out = getDiff(strings.TrimSpace(manifests), strings.TrimSpace(res.Manifest), "current.yaml", "upgraded.yaml")
} else {
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
_ = res
}
return out, nil
}
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
return d.runCommandHelm("show", "values", chart, "--version", ver)
}
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
@@ -438,5 +446,3 @@ func getDiff(text1 string, text2 string, name1 string, name2 string) string {
log.Debugf("The diff is: %s", diff)
return diff
}
type GenericResource = v1.Carp

View File

@@ -3,6 +3,7 @@ package dashboard
import (
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"sync"
"testing"
)
@@ -63,7 +64,7 @@ func TestFlow(t *testing.T) {
_ = manifests
var wg sync.WaitGroup
res := make([]*GenericResource, 0)
res := make([]*v1.Carp, 0)
for _, m := range manifests {
wg.Add(1)
mc := m // fix the clojure

View File

@@ -1,10 +1,8 @@
package dashboard
import (
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"helm.sh/helm/v3/pkg/release"
"net/http"
"strconv"
)
@@ -113,30 +111,25 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
c.Status(http.StatusNoContent)
}
func (h *HelmHandler) InstallPreview(c *gin.Context) {
out, err := chartInstall(c, h.Data, true)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, out)
}
func (h *HelmHandler) Install(c *gin.Context) {
out, err := chartInstall(c, h.Data, false)
qp, err := getQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
justTemplate := c.Query("flag") != "true"
out, err := h.Data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
if !justTemplate {
c.Header("Content-Type", "application/json")
}
c.IndentedJSON(http.StatusAccepted, res)
c.String(http.StatusAccepted, out)
}
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
@@ -156,17 +149,13 @@ func (h *HelmHandler) GetInfoSection(c *gin.Context) {
c.String(http.StatusOK, res)
}
func chartInstall(c *gin.Context, data *DataLayer, justTemplate bool) (string, error) {
qp, err := getQueryProps(c, false)
func (h *HelmHandler) RepoValues(c *gin.Context) {
out, err := h.Data.ShowValues(c.Query("chart"), c.Query("version"))
if err != nil {
return "", err
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
out, err := data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate)
if err != nil {
return "", err
}
return out, nil
c.String(http.StatusOK, out)
}
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) {

View File

@@ -27,7 +27,7 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
return
}
res, err := h.Data.GetResource(qp.Namespace, &GenericResource{
res, err := h.Data.GetResource(qp.Namespace, &v12.Carp{
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
})

View File

@@ -8,7 +8,7 @@ import (
"os"
)
func StartServer() (string, ControlChan) {
func StartServer(version string) (string, ControlChan) {
data := DataLayer{}
err := data.CheckConnectivity()
if err != nil {
@@ -28,7 +28,7 @@ func StartServer() (string, ControlChan) {
}
abort := make(ControlChan)
api := NewRouter(abort, &data)
api := NewRouter(abort, &data, version)
done := startBackgroundServer(address, api, abort)
return "http://" + address, done

View File

@@ -4,7 +4,7 @@ $("#btnUpgradeCheck").click(function () {
self.find(".spinner-border").show()
const repoName = self.data("repo")
$("#btnUpgrade span").text("Checking...")
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-check-circle").addClass("bi-hourglass-split")
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass")
$.post("/api/helm/repo/update?name=" + repoName).fail(function (xhr) {
reportError("Failed to update chart repo", xhr)
}).done(function () {
@@ -29,24 +29,30 @@ function checkUpgradeable(name) {
return
}
$('#upgradeModalLabel select').empty()
const verCur = $("#specRev").data("last-chart-ver");
$('#upgradeModal select').empty()
for (let i = 0; i < data.length; i++) {
$('#upgradeModalLabel select').append("<option value='" + data[i].version + "'>" + data[i].version + "</option>")
const opt = $("<option value='" + data[i].version + "'></option>");
if (data[i].version === verCur) {
opt.html(data[i].version + " &middot;")
} else {
opt.html(data[i].version)
}
$('#upgradeModal select').append(opt)
}
const elm = data[0]
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
const verCur = $("#specRev").data("last-chart-ver");
const canUpgrade = isNewerVersion(verCur, elm.version);
$("#btnUpgradeCheck").prop("disabled", false)
if (canUpgrade) {
$("#btnUpgrade span").text("Upgrade to " + elm.version)
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-arrow-up")
} else {
$("#btnUpgrade span").text("Up-to-date")
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-check-circle")
$("#btnUpgrade span").text("Reconfigure")
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-pencil")
}
$("#btnUpgrade").off("click").click(function () {
@@ -58,12 +64,12 @@ function checkUpgradeable(name) {
function popUpUpgrade(self, verCur, elm) {
const name = getHashParam("chart");
let url = "/api/helm/charts/install?namespace=" + getHashParam("namespace") + "&name=" + name + "&chart=" + elm.name;
$('#upgradeModalLabel select').data("url", url)
$('#upgradeModal select').data("url", url).data("chart", elm.name)
$("#upgradeModalLabel .name").text(name)
$("#upgradeModalLabel .ver-old").text(verCur)
$("#upgradeModal .ver-old").text(verCur)
$('#upgradeModalLabel select').val(elm.version).trigger("change")
$('#upgradeModal select').val(elm.version).trigger("change")
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
myModal.show()
@@ -72,26 +78,86 @@ function popUpUpgrade(self, verCur, elm) {
btnConfirm.prop("disabled", true).off('click').click(function () {
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({
url: url + "&version=" + $('#upgradeModalLabel select').val(),
type: 'POST',
url: url + "&version=" + $('#upgradeModal select').val() + "&flag=true",
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
}).fail(function (xhr) {
reportError("Failed to upgrade the chart", xhr)
}).done(function (data) {
setHashParam("revision", data.version)
window.location.reload()
console.log(data)
if (data.version) {
setHashParam("revision", data.version)
window.location.reload()
} else {
reportError("Failed to get new revision number")
}
})
})
// fill current values
const lastRev = $("#specRev").data("last-rev")
$.get("/api/helm/charts/values?namespace=" + getHashParam("namespace") + "&revision=" + lastRev + "&name=" + getHashParam("chart") + "&flag=true").fail(function (xhr) {
reportError("Failed to get charts values info", xhr)
}).done(function (data) {
$("#upgradeModal textarea").val(data).data("dirty", false)
})
}
$('#upgradeModalLabel select').change(function () {
let reconfigTimeout = null;
$("#upgradeModal textarea").keyup(function () {
const self = $(this);
self.data("dirty", true)
if (reconfigTimeout) {
window.clearTimeout(reconfigTimeout)
}
reconfigTimeout = window.setTimeout(function () {
requestChangeDiff()
}, 500)
})
$('#upgradeModal select').change(function () {
const self = $(this)
$("#upgradeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$("#upgradeModal .btn-confirm").prop("disabled", true)
$.get(self.data("url") + "&version=" + self.val()).fail(function (xhr) {
requestChangeDiff()
// fill reference values
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.get("/api/helm/repo/values?chart=" + self.data("chart") + "&version=" + self.val()).fail(function (xhr) {
reportError("Failed to get upgrade info", xhr)
}).done(function (data) {
$("#upgradeModalBody").empty();
data = hljs.highlight(data, {language: 'yaml'}).value
$("#upgradeModal .ref-vals").html(data)
})
})
function requestChangeDiff() {
const self = $('#upgradeModal select');
const diffBody = $("#upgradeModalBody");
diffBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Calculating diff...')
$("#upgradeModal .btn-confirm").prop("disabled", true)
let values = null;
if ($("#upgradeModal textarea").data("dirty")) {
$("#upgradeModal .invalid-feedback").hide()
values = $("#upgradeModal form").serialize()
try {
jsyaml.load($("#upgradeModal textarea").val())
} catch (e) {
$("#upgradeModal .invalid-feedback").text("YAML parse error: "+e.message).show()
$("#upgradeModalBody").html("Invalid values YAML")
return
}
}
$.ajax({
type: "POST",
url: self.data("url") + "&version=" + self.val(),
data: values,
}).fail(function (xhr) {
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: "+ xhr.responseText+"</p>")
}).done(function (data) {
diffBody.empty();
$("#upgradeModal .btn-confirm").prop("disabled", false)
const targetElement = document.getElementById('upgradeModalBody');
@@ -101,12 +167,11 @@ $('#upgradeModalLabel select').change(function () {
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
diff2htmlUi.draw()
$("#upgradeModalBody").prepend("<p>Following changes will happen to cluster:</p>")
if (!data) {
$("#upgradeModalBody").html("No changes will happen to cluster")
diffBody.html("No changes will happen to the cluster")
}
})
})
}
const btnConfirm = $("#confirmModal .btn-confirm");
$("#btnUninstall").click(function () {

View File

@@ -4,15 +4,22 @@
n=o.getElementsByTagName(u)[0];n.parentNode.insertBefore(d,n)
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
DD_RUM.onReady(function() {
DD_RUM.init({
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
site: 'datadoghq.com',
service: 'helm-dashboard',
version: 'v0.0.0',
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask'
})
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
DD_RUM.init({
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
site: 'datadoghq.com',
service: 'helm-dashboard',
version: xhr.responseText,
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask'
})
}
}
xhr.open('GET', '/status', true);
xhr.send(null);
})

View File

@@ -16,7 +16,18 @@
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
<link href="static/styles.css" rel="stylesheet">
<script type="text/javascript">
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
window.heap = window.heap || [], heap.load = function (e, t) {
window.heap.appid = e, window.heap.config = t = t || {};
var r = document.createElement("script");
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
var a = document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(r, a);
for (var n = function (e) {
return function () {
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
};
heap.load("3615793373");
</script>
</head>
@@ -52,9 +63,11 @@
</ul>
<div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" alt="komodor.io" style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" alt="komodor.io"
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
<a class="btn me-2 text-muted" href="https://github.com/komodorio/helm-dashboard" title="Project page on GitHub"><i class="bi-github"></i></a>
<a class="btn me-2 text-muted" href="https://github.com/komodorio/helm-dashboard"
title="Project page on GitHub"><i class="bi-github"></i></a>
</div>
<div class="separator-vertical"><span></span></div>
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
@@ -222,7 +235,7 @@
<!-- Modals -->
<div id="errorAlert"
<div id="errorAlert" style="z-index: 2000"
class="display-none alert alert-sm alert-danger alert-dismissible position-absolute position-absolute top-0 start-50 translate-middle-x mt-3 border-danger"
role="alert">
<h4 class="alert-heading"><i class="bi-exclamation-triangle-fill"></i> <span></span></h4>
@@ -268,15 +281,42 @@
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="upgradeModalLabel">
Upgrade <b class='text-success name'></b> from version <b class='text-success ver-old'></b> to
<select class='fw-bold text-success ver-new'></select>
</h5>
<h4 class="modal-title" id="upgradeModalLabel">
Upgrade <b class='text-success name'></b>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="upgradeModalBody">
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
<div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark">Version to install: <select
class='fw-bold text-success ver-new'></select></label> (current version is <span
class='text-success ver-old ms-1'>0.0.0</span>)
</div>
<div class="row">
<div class="col-6 pe-3">
<label class="form-label">User-Defined Values:</label>
</div>
<div class="col-6 ps-3">
<label class="form-label">Chart Values Reference:</label>
</div>
</div>
<div class="row">
<div class="col-6 pe-3">
<textarea name="values" class="form-control w-100 h-100" rows="5"></textarea>
</div>
<div class="col-6 ps-3">
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
</div>
</div>
</div>
<div class="row">
<div class="col-6 pe-3">
<span class="invalid-feedback small mb-3"> (wrong YAML)</span>
</div>
</div>
<label class="form-label mt-5">Manifest changes:</label>
<div id="upgradeModalBody" class="small"></div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-confirm">Confirm Upgrade</button>
</div>
@@ -293,6 +333,9 @@
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.0.3/build/global/luxon.min.js"
integrity="sha256-RH4TKnKcKyde0s2jc5BW3pXZl/5annY3fcZI9VrV5WQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="static/list-view.js"></script>
<script src="static/revisions-view.js"></script>
<script src="static/details-view.js"></script>

View File

@@ -22,6 +22,12 @@ $(function () {
})
const myAlert = document.getElementById('errorAlert')
myAlert.addEventListener('close.bs.alert', event => {
event.preventDefault()
$("#errorAlert").hide()
})
function reportError(err, xhr) {
$("#errorAlert h4 span").text(err)
if (xhr) {

View File

@@ -2,6 +2,8 @@ package dashboard
import (
"errors"
"io/ioutil"
"os"
"strings"
)
@@ -14,4 +16,18 @@ func chartAndVersion(x string) (string, string, error) {
}
return x[:lastInd], x[lastInd+1:], nil
}
}
func tempFile(txt string) (string, func(), error) {
file, err := ioutil.TempFile("", "helm_vals_")
if err != nil {
return "", nil, err
}
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
if err != nil {
return "", nil, err
}
return file.Name(), func() { os.Remove(file.Name()) }, nil
}