7f7868f7cf
---ci--- phase: 5 milestone: v0.1 status: complete requirements: covered: [REQ-01, REQ-02, REQ-03, REQ-04, REQ-05, REQ-06, REQ-07, REQ-08, REQ-09, REQ-10, REQ-11, REQ-12, REQ-13, REQ-14, REQ-15, REQ-16, REQ-17, REQ-18, REQ-19, REQ-20] partial: [] ---/ci---
271 lines
7.7 KiB
HTML
271 lines
7.7 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>App Registrations — CoreCI Broker</title>
|
|
<link rel="stylesheet" href="/ui/shared.css">
|
|
<style>
|
|
.page-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.page-sub {
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
margin: 0;
|
|
}
|
|
|
|
.table-empty {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.cell-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-danger {
|
|
color: var(--danger);
|
|
background: transparent;
|
|
border: 1px solid rgba(240, 122, 114, 0.45);
|
|
}
|
|
|
|
.badge-revoked {
|
|
color: var(--danger);
|
|
border-color: rgba(240, 122, 114, 0.45);
|
|
background: rgba(240, 122, 114, 0.08);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="nav"></div>
|
|
<main class="wrap">
|
|
<section class="card">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>App Registrations</h1>
|
|
<p class="page-sub">Tool app UUIDs allowed to register against the broker.</p>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" id="add-open">Add registration</button>
|
|
</div>
|
|
<p class="form-error" id="page-error" role="alert" hidden></p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>UUID</th>
|
|
<th>Name</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
<th>Revoked</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rows"></tbody>
|
|
</table>
|
|
</section>
|
|
</main>
|
|
<div class="modal-backdrop" id="modal" hidden role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
<div class="modal">
|
|
<h2 id="modal-title">Add registration</h2>
|
|
<form id="add-form">
|
|
<div class="field">
|
|
<label for="uuid">App UUID</label>
|
|
<input id="uuid" name="uuid" type="text" class="mono" placeholder="00000000-0000-0000-0000-000000000000" required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="name">Name (optional)</label>
|
|
<input id="name" name="name" type="text" placeholder="proxmox tool">
|
|
</div>
|
|
<p class="form-error" id="add-error" role="alert" hidden></p>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-subtle" id="add-cancel">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" id="add-submit">Add</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script src="/ui/nav.js"></script>
|
|
<script>
|
|
(function () {
|
|
var API = "/api/broker/app_registrations";
|
|
var rows = document.getElementById("rows");
|
|
var pageError = document.getElementById("page-error");
|
|
var modal = document.getElementById("modal");
|
|
var form = document.getElementById("add-form");
|
|
var uuidInput = document.getElementById("uuid");
|
|
var nameInput = document.getElementById("name");
|
|
var addError = document.getElementById("add-error");
|
|
var submitButton = document.getElementById("add-submit");
|
|
|
|
function showPageError(message) {
|
|
pageError.textContent = message;
|
|
pageError.hidden = false;
|
|
}
|
|
|
|
function clearPageError() {
|
|
pageError.hidden = true;
|
|
pageError.textContent = "";
|
|
}
|
|
|
|
function messageRow(text) {
|
|
rows.replaceChildren();
|
|
var tr = document.createElement("tr");
|
|
var td = document.createElement("td");
|
|
td.colSpan = 6;
|
|
td.className = "table-empty";
|
|
td.textContent = text;
|
|
tr.appendChild(td);
|
|
rows.appendChild(tr);
|
|
}
|
|
|
|
function textCell(value, mono) {
|
|
var td = document.createElement("td");
|
|
if (mono) {
|
|
td.className = "mono";
|
|
}
|
|
td.textContent = value === null || value === undefined || value === "" ? "—" : value;
|
|
return td;
|
|
}
|
|
|
|
function statusBadge(status) {
|
|
var span = document.createElement("span");
|
|
span.className = "badge " + (status === "active" ? "badge-active" : "badge-revoked");
|
|
span.textContent = status;
|
|
return span;
|
|
}
|
|
|
|
function actionButton(label, className, disabled, handler) {
|
|
var button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "btn " + className;
|
|
button.textContent = label;
|
|
button.disabled = disabled;
|
|
button.addEventListener("click", handler);
|
|
return button;
|
|
}
|
|
|
|
function renderRow(row) {
|
|
var tr = document.createElement("tr");
|
|
tr.appendChild(textCell(row.uuid, true));
|
|
tr.appendChild(textCell(row.name));
|
|
var statusCell = document.createElement("td");
|
|
statusCell.appendChild(statusBadge(row.status));
|
|
tr.appendChild(statusCell);
|
|
tr.appendChild(textCell(row.created_at));
|
|
tr.appendChild(textCell(row.revoked_at));
|
|
var actionsCell = document.createElement("td");
|
|
var actions = document.createElement("div");
|
|
actions.className = "cell-actions";
|
|
var key = encodeURIComponent(row.uuid);
|
|
actions.appendChild(actionButton("Revoke", "btn-danger", row.status !== "active", function () {
|
|
rowAction("DELETE", API + "/" + key, "Revoke app registration " + row.uuid + "?", "Revoke failed", row.uuid);
|
|
}));
|
|
actions.appendChild(actionButton("Reinstate", "btn-subtle", row.status === "active", function () {
|
|
rowAction("POST", API + "/" + key + "/reinstate", "Reinstate app registration " + row.uuid + "?", "Reinstate failed", row.uuid);
|
|
}));
|
|
actionsCell.appendChild(actions);
|
|
tr.appendChild(actionsCell);
|
|
return tr;
|
|
}
|
|
|
|
async function refresh() {
|
|
messageRow("Loading registrations…");
|
|
var result = await api(API);
|
|
if (!result.ok) {
|
|
messageRow(result.status === 0 ? "Broker unreachable." : "Could not load registrations.");
|
|
return;
|
|
}
|
|
var list = (result.data && result.data.registrations) || [];
|
|
if (!list.length) {
|
|
messageRow("No app registrations yet.");
|
|
return;
|
|
}
|
|
rows.replaceChildren();
|
|
list.forEach(function (row) {
|
|
rows.appendChild(renderRow(row));
|
|
});
|
|
}
|
|
|
|
async function rowAction(method, path, question, label, uuid) {
|
|
if (!confirm(question)) {
|
|
return;
|
|
}
|
|
clearPageError();
|
|
var result = await api(path, {method: method});
|
|
if (!result.ok) {
|
|
if (result.status === 0) {
|
|
showPageError(label + ": broker unreachable.");
|
|
} else {
|
|
var detail = result.data && result.data.message ? ": " + result.data.message : ".";
|
|
showPageError(label + " " + uuid + detail);
|
|
}
|
|
}
|
|
await refresh();
|
|
}
|
|
|
|
function openModal() {
|
|
form.reset();
|
|
addError.hidden = true;
|
|
addError.textContent = "";
|
|
submitButton.disabled = false;
|
|
modal.hidden = false;
|
|
uuidInput.focus();
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.hidden = true;
|
|
}
|
|
|
|
form.addEventListener("submit", async function (event) {
|
|
event.preventDefault();
|
|
addError.hidden = true;
|
|
addError.textContent = "";
|
|
submitButton.disabled = true;
|
|
var payload = {uuid: uuidInput.value.trim()};
|
|
var name = nameInput.value.trim();
|
|
if (name) {
|
|
payload.name = name;
|
|
}
|
|
var result = await api(API, {method: "POST", body: payload});
|
|
submitButton.disabled = false;
|
|
if (result.ok) {
|
|
closeModal();
|
|
clearPageError();
|
|
await refresh();
|
|
return;
|
|
}
|
|
if (result.status === 0) {
|
|
addError.textContent = "Broker unreachable.";
|
|
} else if (result.data && result.data.message) {
|
|
addError.textContent = result.data.message;
|
|
} else {
|
|
addError.textContent = "Could not add registration.";
|
|
}
|
|
addError.hidden = false;
|
|
await refresh();
|
|
});
|
|
|
|
document.getElementById("add-open").addEventListener("click", openModal);
|
|
document.getElementById("add-cancel").addEventListener("click", closeModal);
|
|
modal.addEventListener("click", function (event) {
|
|
if (event.target === modal) {
|
|
closeModal();
|
|
}
|
|
});
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.key === "Escape" && !modal.hidden) {
|
|
closeModal();
|
|
}
|
|
});
|
|
refresh();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |