Repository-related functions (#19)

* Roadmap item

* Start building repo view

* Section switcher

* Show repo list

* Adding chart repo works

* Showing the pane

* Couple of buttons

* Listing items

* Styling

* Enriching repo view

* Navigate from repo to installed

* Tuning install popup

* Working on install

* Cosmetics
This commit is contained in:
Andrey Pokhilko
2022-10-23 13:41:45 +01:00
committed by GitHub
parent 0141eecef1
commit 0de0b5d0cb
12 changed files with 552 additions and 144 deletions

View File

@@ -30,17 +30,6 @@ function checkUpgradeable(name) {
}
const verCur = $("#specRev").data("last-chart-ver");
$('#upgradeModal select').empty()
for (let i = 0; i < data.length; i++) {
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())
@@ -56,55 +45,86 @@ function checkUpgradeable(name) {
}
$("#btnUpgrade").off("click").click(function () {
popUpUpgrade($(this), verCur, elm)
popUpUpgrade(elm, getHashParam("namespace"), getHashParam("chart"), verCur, $("#specRev").data("last-rev"))
})
})
}
function popUpUpgrade(self, verCur, elm) {
const name = getHashParam("chart");
const qstr = "?namespace=" + getHashParam("namespace") + "&name=" + name + "&chart=" + elm.name;
let url = "/api/helm/charts/install" + qstr
$('#upgradeModal select').data("qstr", qstr).data("url", url).data("chart", elm.name)
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
$("#upgradeModal .btn-confirm").prop("disabled", true)
$("#upgradeModalLabel .name").text(name)
$("#upgradeModal .ver-old").text(verCur)
$('#upgradeModal').data("chart", elm.name).data("initial", !verCur)
$('#upgradeModal select').val(elm.version).trigger("change")
$("#upgradeModalLabel .name").text(elm.name)
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
myModal.show()
if (verCur) {
$("#upgradeModal .ver-old").show().find("span").text(verCur)
$("#upgradeModal .rel-name").prop("disabled", true).val(name)
$("#upgradeModal .rel-ns").prop("disabled", true).val(ns)
} else {
$("#upgradeModal .ver-old").hide()
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
$("#upgradeModal .rel-ns").prop("disabled", false).val("")
}
const btnConfirm = $("#upgradeModal .btn-confirm");
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({
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) {
if (data.version) {
setHashParam("revision", data.version)
window.location.reload()
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (vers) {
// fill versions
$('#upgradeModal select').empty()
for (let i = 0; i < vers.length; i++) {
const opt = $("<option value='" + vers[i].version + "'></option>");
if (vers[i].version === verCur) {
opt.html(vers[i].version + " &middot;")
} else {
reportError("Failed to get new revision number")
opt.html(vers[i].version)
}
})
})
$('#upgradeModal select').append(opt)
}
// 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)
$('#upgradeModal select').val(elm.version).trigger("change")
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
myModal.show()
if (verCur) {
// fill current values
$.get("/api/helm/charts/values?namespace=" + ns + "&revision=" + lastRev + "&name=" + name + "&flag=true").fail(function (xhr) {
reportError("Failed to get charts values info", xhr)
}).done(function (data) {
$("#upgradeModal textarea").val(data).data("dirty", false)
})
} else {
$("#upgradeModal textarea").val("").data("dirty", true)
}
})
}
$("#upgradeModal .btn-confirm").click(function () {
const btnConfirm = $("#upgradeModal .btn-confirm")
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({
type: 'POST',
url: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true",
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
}).fail(function (xhr) {
reportError("Failed to upgrade the chart", xhr)
}).done(function (data) {
if (data.version) {
setHashParam("section", null)
setHashParam("namespace", $("#upgradeModal .rel-ns").val())
setHashParam("chart", $("#upgradeModal .rel-name").val())
setHashParam("revision", data.version)
window.location.reload()
} else {
reportError("Failed to get new revision number")
}
})
})
let reconfigTimeout = null;
$("#upgradeModal textarea").keyup(function () {
function changeTimer() {
const self = $(this);
self.data("dirty", true)
if (reconfigTimeout) {
@@ -113,7 +133,11 @@ $("#upgradeModal textarea").keyup(function () {
reconfigTimeout = window.setTimeout(function () {
requestChangeDiff()
}, 500)
})
}
$("#upgradeModal textarea").keyup(changeTimer)
$("#upgradeModal .rel-name").keyup(changeTimer)
$("#upgradeModal .rel-ns").keyup(changeTimer)
$('#upgradeModal select').change(function () {
const self = $(this)
@@ -122,7 +146,7 @@ $('#upgradeModal select').change(function () {
// 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) {
$.get("/api/helm/repo/values?chart=" + $("#upgradeModal").data("chart") + "&version=" + self.val()).fail(function (xhr) {
reportError("Failed to get upgrade info", xhr)
}).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value
@@ -134,10 +158,9 @@ $('#upgradeModal .btn-scan').click(function () {
const self = $(this)
self.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
const qstr = $('#upgradeModal select').data("qstr")
$.ajax({
type: "POST",
url: "/api/scanners/manifests" + qstr + "&version=" + $('#upgradeModal select').val(),
url: "/api/scanners/manifests" + upgradeModalQstr(),
data: $("#upgradeModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to scan the manifest", xhr)
@@ -186,7 +209,7 @@ function requestChangeDiff() {
$.ajax({
type: "POST",
url: self.data("url") + "&version=" + self.val(),
url: "/api/helm/charts/install" + upgradeModalQstr(),
data: values,
}).fail(function (xhr) {
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
@@ -207,6 +230,20 @@ function requestChangeDiff() {
})
}
function upgradeModalQstr() {
let qstr = "?" +
"namespace=" + $("#upgradeModal .rel-ns").val() +
"&name=" + $("#upgradeModal .rel-name").val() +
"&chart=" + $("#upgradeModal").data("chart") +
"&version=" + $('#upgradeModal select').val()
if ($("#upgradeModal").data("initial")) {
qstr += "&initial=true"
}
return qstr
}
const btnConfirm = $("#confirmModal .btn-confirm");
$("#btnUninstall").click(function () {
const chart = getHashParam('chart');

View File

@@ -48,18 +48,11 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item mx-2">
<a class="nav-link px-3 active" aria-current="page" href="/">Installed</a>
<a class="nav-link px-3 section-installed">Installed</a>
</li>
<!-- TODO
<li class="nav-item mx-2">
<a href="#" class="nav-link px-3">Repository</a>
<a class="nav-link px-3 section-repo">Repository</a>
</li>
-->
<!-- TODO
<li class="nav-item">
<a class="nav-link disabled">Provisional Charts</a>
</li>
-->
</ul>
<div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
@@ -75,7 +68,48 @@
</nav>
<!-- /TOP BAR -->
<div class="row mt-3 pt-3 me-5" id="sectionList" style="display: none">
<!--REPO SECTION-->
<div class="row mt-3 pt-3 me-5 section" id="sectionRepo" style="display: none">
<div class="col-3 ps-4 repo-list">
<div class="p-2 bg-white rounded-1 b-shadow">
<h4 class="fs-6">Repositories</h4>
<ul class="list-unstyled p-2">
</ul>
<button class="btn btn-sm border-secondary text-muted">
<i class="bi-plus-lg"></i> Add Repository
</button>
</div>
</div>
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
<div class="float-end">
<button class="me-2 btn btn-sm btn-light bg-white border border-secondary btn-update">
<i class="bi-arrow-repeat"></i> Update
</button>
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
<i class="bi-trash3"></i> Remove
</button>
</div>
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
<h2 class="mb-3">name-of-repo</h2>
<div class="mb-5">
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
</div>
<div class="float-end">
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
</div>
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
style="text-transform: uppercase">
<div class="col-3">Chart Name</div>
<div class="col">Description</div>
<div class="col-1">Version</div>
<div class="col-1"></div>
</div>
<ul class="list-unstyled mt-4"></ul>
</div>
</div>
<div class="row mt-3 pt-3 me-5 section" id="sectionList" style="display: none">
<div class="col-2 ms-3">
<!-- FILTER BLOCK -->
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
@@ -117,7 +151,7 @@
<!-- /INSTALLED LIST -->
</div>
<div class="row flex-nowrap pt-0 mx-0" id="sectionDetails" style="display: none">
<div class="row flex-nowrap pt-0 mx-0 section" id="sectionDetails" style="display: none">
<div class="col-2 px-4 py-4 pe-3 rev-list">
<h3 class="fw-bold small">Revisions</h3>
<ul class="list-unstyled">
@@ -270,21 +304,48 @@
</div>
</div>
<div class="modal" id="repoAddModal" tabindex="-1">
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Chart Repository</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form enctype="application/x-www-form-urlencoded">
<label class="form-label">Name: <input class="form-control" name="name"></label>
<label class="form-label">URL: <input class="form-control" name="url"></label>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-confirm">Add Repository</button>
</div>
</div>
</div>
</div>
<div class="modal" id="upgradeModal" tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
<div class="modal" id="upgradeModal" tabindex="-1">
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="upgradeModalLabel">
Upgrade <b class='text-success name'></b>
Install <b class='text-success name'></b>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<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>)
class='fw-bold text-success ver-new'></select></label> <span class="ver-old">(current version is <span
class='text-success ms-1'>0.0.0</span>)</span>
</div>
<div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark">
Release Name: <input class="form-control rel-name">
</label>
<label class="form-label me-4 text-dark">
Namespace (optional): <input class="form-control rel-ns">
</label>
</div>
<div class="row">
<div class="col-6 pe-3">
@@ -296,7 +357,8 @@
</div>
<div class="row">
<div class="col-6 pe-3">
<textarea name="values" class="form-control w-100 h-100" rows="5" style="font-family: monospace"></textarea>
<textarea name="values" class="form-control w-100 h-100" rows="5"
style="font-family: monospace"></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>
@@ -313,7 +375,7 @@
</form>
<div class="modal-footer d-flex">
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
<button type="button" class="btn btn-primary btn-confirm">Confirm Upgrade</button>
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
</div>
</div>
</div>
@@ -332,6 +394,7 @@
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="static/repo.js"></script>
<script src="static/list-view.js"></script>
<script src="static/revisions-view.js"></script>
<script src="static/details-view.js"></script>

View File

@@ -15,7 +15,6 @@ function loadChartsList() {
})
}
function buildChartCard(elm) {
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start">
<div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>

View File

@@ -0,0 +1,120 @@
function loadRepoView() {
$("#sectionRepo .repo-details").hide()
$("#sectionRepo").show()
$.getJSON("/api/helm/repo").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr)
}).done(function (data) {
const items = $("#sectionRepo .repo-list ul").empty()
data.forEach(function (elm) {
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', elm.url)
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
opt.find("span").text(elm.name)
items.append(opt)
})
if (!data.length) {
items.text("No repositories found, try adding one")
}
items.find("input").click(function () {
const self = $(this)
const elm = self.data("item");
setHashParam("repo", elm.name)
$("#sectionRepo .repo-details").show()
$("#sectionRepo .repo-details h2").text(elm.name)
$("#sectionRepo .repo-details .url").text(elm.url)
$("#sectionRepo .repo-details ul").html('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
$.getJSON("/api/helm/repo/charts?name=" + elm.name).fail(function (xhr) {
reportError("Failed to get list of charts in repo", xhr)
}).done(function (data) {
$("#sectionRepo .repo-details ul").empty()
data.forEach(function (elm) {
const li = $(`<li class="row p-2 rounded">
<h6 class="col-3 py-2">` + elm.name.split('/').pop() + `</h6>
<div class="col py-2">` + elm.description + `</div>
<div class="col-1 py-2">` + elm.version + `</div>
<div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div>
</li>`)
li.data("item", elm)
if (elm.installed_namespace) {
li.find("button").text("View").addClass("btn-success").removeClass("bg-white")
li.find(".action").prepend("<i class='bi-check-circle-fill me-1 text-success' title='Already installed'></i>")
}
li.click(repoChartClicked)
$("#sectionRepo .repo-details ul").append(li)
})
})
})
if (getHashParam("repo")) {
items.find("input[value='" + getHashParam("repo") + "']").click()
} else {
items.find("input").first().click()
}
})
}
$("#sectionRepo .repo-list .btn").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
myModal.show()
})
$("#repoAddModal .btn-confirm").click(function () {
$("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
$.ajax({
type: 'POST',
url: "/api/helm/repo",
data: $("#repoAddModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
setHashParam("repo", $("#repoAddModal form input[name=name]").val())
window.location.reload()
})
})
$("#sectionRepo .btn-remove").click(function () {
if (confirm("Confirm removing repository?")) {
$.ajax({
type: 'DELETE',
url: "/api/helm/repo?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
setHashParam("repo", null)
window.location.reload()
})
}
})
$("#sectionRepo .btn-update").click(function () {
$("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({
type: 'POST',
url: "/api/helm/repo/update?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
window.location.reload()
})
})
function repoChartClicked() {
const self = $(this)
const elm = self.data("item")
if (elm.installed_namespace) {
setHashParam("section", null)
setHashParam("namespace", elm.installed_namespace)
setHashParam("chart", elm.installed_name)
window.location.reload()
} else {
popUpUpgrade(elm)
}
}

View File

@@ -11,6 +11,27 @@ $(function () {
const context = getHashParam("context")
fillClusterList(data, context);
initView(); // can only do it after loading cluster list
})
$.getJSON("/api/scanners").fail(function (xhr) {
reportError("Failed to get list of scanners", xhr)
}).done(function (data) {
if (!data.length) {
$("#upgradeModal .btn-scan").hide()
}
})
})
function initView() {
$(".section").hide()
const section = getHashParam("section")
if (section === "repository") {
$("#topNav ul a.section-repo").addClass("active")
loadRepoView()
} else {
$("#topNav ul a.section-installed").addClass("active")
const namespace = getHashParam("namespace")
const chart = getHashParam("chart")
if (!chart) {
@@ -18,27 +39,27 @@ $(function () {
} else {
loadChartHistory(namespace, chart)
}
})
}
}
$.getJSON("/api/scanners").fail(function (xhr) {
reportError("Failed to get list of scanners", xhr)
}).done(function (data) {
for (let n = 0; n < data.length; n++) {
const item = $(`
<label class="form-check-label me-4">
<input class="form-check-input me-1" type="checkbox" checked name="scanner" value="` + data[n] + `"> ` + data[n] + `
</label>`)
$("#topNav ul a").click(function () {
const self = $(this)
$("#nav-scanners form span").prepend(item)
}
$("#topNav ul a").removeClass("active")
if (!data.length) {
$("#upgradeModal .btn-scan").hide()
}
})
const ctx = getHashParam("context")
setHashParam(null, null)
setHashParam("context", ctx)
if (self.hasClass("section-repo")) {
setHashParam("section", "repository")
} else {
setHashParam("section", null)
}
initView()
})
const myAlert = document.getElementById('errorAlert')
myAlert.addEventListener('close.bs.alert', event => {
event.preventDefault()
@@ -60,8 +81,14 @@ function getHashParam(name) {
}
function setHashParam(name, val) {
const params = new URLSearchParams(window.location.hash.substring(1))
params.set(name, val)
let params = new URLSearchParams(window.location.hash.substring(1))
if (!name) {
params = new URLSearchParams()
} else if (!val) {
params.delete(name)
} else {
params.set(name, val)
}
window.location.hash = new URLSearchParams(params).toString()
}
@@ -86,14 +113,14 @@ function statusStyle(status, card, txt) {
}
function getCleanClusterName(rawClusterName) {
if (rawClusterName.indexOf('arn') == 0) {
if (rawClusterName.indexOf('arn') === 0) {
// AWS cluster
clusterSplit = rawClusterName.split(':')
clusterName = clusterSplit.at(-1).split("/").at(-1)
region = clusterSplit.at(-3)
const clusterSplit = rawClusterName.split(':')
const clusterName = clusterSplit.at(-1).split("/").at(-1)
const region = clusterSplit.at(-3)
return region + "/" + clusterName + ' [AWS]'
}
if (rawClusterName.indexOf('gke') == 0) {
if (rawClusterName.indexOf('gke') === 0) {
// GKE cluster
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
}
@@ -102,13 +129,11 @@ function getCleanClusterName(rawClusterName) {
function fillClusterList(data, context) {
data.forEach(function (elm) {
// aws CLI uses complicated context names, the suffix does not work well
// maybe we should have an `if` statement here
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
let label = getCleanClusterName(elm.Name)
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', label)
opt.attr('title', elm.Name)
opt.find("input").val(elm.Name).text(label)
opt.find("span").text(getCleanClusterName(label))
opt.find("span").text(label)
if (elm.IsCurrent && !context) {
opt.find("input").prop("checked", true)
setCurrentContext(elm.Name)

View File

@@ -1,3 +1,7 @@
.link, .nav-link {
cursor: pointer;
}
.strike {
text-decoration: line-through;
}
@@ -325,7 +329,7 @@ span.link {
}
#nav-resources .bg-secondary {
background-color: #E6E7EB!important;
background-color: #E6E7EB !important;
}
.res-actions .btn-sm {
@@ -349,4 +353,16 @@ span.link {
#describeModalBody pre {
font-size: 1rem;
}
#sectionRepo .repo-details ul .row .btn {
visibility: hidden;
}
#sectionRepo .repo-details ul .row:hover {
background-color: #F4F7FA !important;
}
#sectionRepo .repo-details ul .row:hover .btn {
visibility: visible;
}