An empty organization list is now said out loud, and the page stops pretending
The organization dropdown rendered empty. Three separate defects behind it, found from the service's own request log rather than by looking at the page: - /api/uadmin/organizations returned 200 - the data was arriving - while /api/compute/nodes returned 422 on every load. ORGANIZATION_ID was only set when the account could see exactly one organization, so for anyone who can see several the node list was requested with no organization and the compute routes refused, as they are designed to. The list is now fetched when a choice is made, not at load. - The page had no case for *no* organizations. A node belongs to exactly one, so there is nowhere to put it: the form is now disabled and a banner explains why, with a link to the organizations screen. A dropdown with nothing in it and no explanation is the same as a broken page. - A 401 - an expired session - was indistinguishable from an empty list. It now says the session ended, which is the one failure the operator can act on. Also removed eight CSS classes I had invented rather than looked up: card, card-body, card-note, field, form-actions, table-wrap, alert--warn and button--quiet. None exist in styles.css, so the page was rendering unstyled. Replaced with what the working console page uses - admin-card, form-field, modal-actions, admin-table-wrap, admin-banner-- warning, button--ghost. Every class on the page is now verified present in the stylesheet rather than assumed, and the markup is checked as balanced by a parser rather than by counting tags by eye. 0005 was applied to the deployment to unblock startup: the tenant-schema contract check runs at boot and correctly refused to start a service whose schema it could not find. That is the check working. Backup taken and checksum-verified first; three tenant schemas created, one per organization, with users=2 orgs=3 nodes=9 audit=46 unchanged. 547 passed, 29 skipped.
This commit is contained in:
@@ -402,6 +402,19 @@
|
||||
form.addEventListener('submit', onAddNode);
|
||||
}
|
||||
|
||||
// Choosing an organization is what makes the node list meaningful, so the list is
|
||||
// fetched here rather than at load time. Fetching it before the choice is made is
|
||||
// what produced a 422 on every page load.
|
||||
var organizationField = query('#node-organization');
|
||||
if (organizationField) {
|
||||
organizationField.addEventListener('change', function () {
|
||||
ORGANIZATION_ID = String(organizationField.value || '');
|
||||
if (ORGANIZATION_ID) {
|
||||
loadNodes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var doneButton = query('#pairing-done');
|
||||
if (doneButton) {
|
||||
doneButton.addEventListener('click', function () {
|
||||
@@ -462,23 +475,90 @@
|
||||
return;
|
||||
}
|
||||
clear(select);
|
||||
var placeholder = el('option', { value: '', text: 'Choose an organization…' });
|
||||
select.appendChild(placeholder);
|
||||
select.appendChild(el('option', { value: '', text: 'Choose an organization…' }));
|
||||
items.forEach(function (org) {
|
||||
select.appendChild(el('option', { value: org.id, text: org.name }));
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
// A node belongs to exactly one organization. With none, there is nothing to
|
||||
// add a node to, and the form cannot be completed - so say that rather than
|
||||
// presenting a dropdown with nothing in it, which is what this did.
|
||||
showNoOrganizations();
|
||||
return;
|
||||
}
|
||||
hideNoOrganizations();
|
||||
|
||||
if (items.length === 1) {
|
||||
// An organization administrator has exactly one. Choosing it for them is not
|
||||
// a convenience that hides a decision - there is no decision.
|
||||
select.value = items[0].id;
|
||||
ORGANIZATION_ID = items[0].id;
|
||||
return loadNodes();
|
||||
}
|
||||
return loadNodes();
|
||||
// More than one: the choice is real and has not been made yet. The node list is
|
||||
// not fetched until it is, because the compute routes refuse to guess - they
|
||||
// answer 422 organization_required, which used to arrive as a silent failure to
|
||||
// load anything at all.
|
||||
setNodeListHint(
|
||||
'Choose an organization to see its nodes. This account can see ' +
|
||||
items.length +
|
||||
' organizations.'
|
||||
);
|
||||
return undefined;
|
||||
},
|
||||
function (error) {
|
||||
if (error && error.status === 401) {
|
||||
// Distinguished from every other failure because it is the one the operator
|
||||
// can fix: the session ended, and the page looks broken rather than expired.
|
||||
setNodeListHint(
|
||||
'Your session has ended. Sign in again and reload this page.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
say(api.describeError(error), 'error');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function showNoOrganizations() {
|
||||
var banner = query('#no-organizations');
|
||||
if (banner) {
|
||||
banner.hidden = false;
|
||||
}
|
||||
var form = query('#add-node-form');
|
||||
if (form) {
|
||||
// Disabled rather than merely described: every field in it is meaningless
|
||||
// without a tenant to add the node to.
|
||||
Array.prototype.forEach.call(
|
||||
form.querySelectorAll('input, select, button'),
|
||||
function (field) {
|
||||
field.disabled = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
var button = query('#open-add-node');
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function hideNoOrganizations() {
|
||||
var banner = query('#no-organizations');
|
||||
if (banner) {
|
||||
banner.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeListHint(text) {
|
||||
var list = query('#node-list');
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
clear(list);
|
||||
list.appendChild(el('p', { class: 'empty-state', text: text }));
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', boot);
|
||||
} else {
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<header class="admin-topbar">
|
||||
<div class="admin-topbar-inner">
|
||||
<a class="admin-brand" href="./index.html">
|
||||
<img src="../assets/umbratools-emblem.png" alt="" width="28" height="28" onerror="this.style.visibility='hidden'">
|
||||
<div class="admin-layout">
|
||||
<a class="brand-lockup" href="../index.html">
|
||||
<img class="brand-image" src="../assets/umbratrans-emblem.png" alt="">
|
||||
<span>UmbraTrans</span>
|
||||
</a>
|
||||
<div class="admin-identity">
|
||||
<div class="admin-context">
|
||||
<strong id="admin-display-name">Checking session…</strong>
|
||||
<small id="admin-identity">No session</small>
|
||||
</div>
|
||||
<a class="button button--quiet" href="../settings.html">Sign out</a>
|
||||
<a class="button button--ghost" href="../settings.html">Sign out</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -50,17 +50,38 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="add-node-card" hidden>
|
||||
<div class="card-body">
|
||||
<!-- Shown only when the account can see no organizations at all. A node belongs to
|
||||
exactly one, so there is nothing to add it to and the form below is disabled
|
||||
rather than merely described. -->
|
||||
<div class="admin-banners">
|
||||
<p class="admin-banner admin-banner--warning" id="no-organizations" hidden>
|
||||
<strong>There are no organizations yet.</strong>
|
||||
<p>
|
||||
A node belongs to exactly one organization, so there is nowhere to put this one
|
||||
until an organization exists. An organization is the outermost boundary in the
|
||||
system: everything else lives inside one.
|
||||
</p>
|
||||
<p>
|
||||
<a class="button button--primary" href="./index.html#organizations">
|
||||
Go to organizations and create one
|
||||
</a>
|
||||
<a class="button button--ghost" href="./index.html#organizations">
|
||||
Open the organizations screen
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-card" id="add-node-card" hidden>
|
||||
<h2>Add a node</h2>
|
||||
<p class="card-note">
|
||||
<p class="otp-panel-target">
|
||||
This produces a one-line command and a six-digit code. Run the command on the
|
||||
machine; it asks for the code, pairs with the controller, installs the agent
|
||||
and registers it as a service.
|
||||
</p>
|
||||
|
||||
<form id="add-node-form" novalidate>
|
||||
<div class="field">
|
||||
<div class="form-field">
|
||||
<label for="node-organization">Organization</label>
|
||||
<select id="node-organization" name="organization_id" required
|
||||
aria-describedby="node-organization-hint">
|
||||
@@ -74,7 +95,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="form-field">
|
||||
<label for="node-name">Node name</label>
|
||||
<input id="node-name" name="node_name" type="text" autocomplete="off"
|
||||
maxlength="120" required
|
||||
@@ -86,7 +107,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="form-field">
|
||||
<label for="node-locality">Where it is</label>
|
||||
<input id="node-locality" name="locality" type="text" autocomplete="off"
|
||||
maxlength="120" placeholder="192.168.123.238 or a name you will recognise">
|
||||
@@ -94,7 +115,7 @@
|
||||
report at once.</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="form-field">
|
||||
<label for="node-trust-domain">Trust domain</label>
|
||||
<select id="node-trust-domain" name="trust_domain">
|
||||
<option value="local">local — on my own network</option>
|
||||
@@ -106,7 +127,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="form-field">
|
||||
<label for="node-controller">Controller address</label>
|
||||
<input id="node-controller" name="controller_url" type="text" autocomplete="off"
|
||||
value="http://192.168.123.14:8442">
|
||||
@@ -116,7 +137,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="field field--check">
|
||||
<div class="form-field">
|
||||
<input id="node-as-root" name="as_root" type="checkbox">
|
||||
<label for="node-as-root">
|
||||
Expect to run as root on an exclusive machine
|
||||
@@ -131,22 +152,20 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<div class="modal-actions">
|
||||
<button class="button button--primary" type="submit" id="add-node-submit">
|
||||
Create the pairing code
|
||||
</button>
|
||||
<button class="button button--quiet" type="button" id="add-node-cancel">
|
||||
<button class="button button--ghost" type="button" id="add-node-cancel">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="pairing-result-card" hidden>
|
||||
<div class="card-body">
|
||||
<div class="admin-card" id="pairing-result-card" hidden>
|
||||
<h2>Run this on the machine</h2>
|
||||
<p class="card-note" id="pairing-node-name-note"></p>
|
||||
<p class="otp-panel-target" id="pairing-node-name-note"></p>
|
||||
|
||||
<div class="otp-panel">
|
||||
<div class="otp-panel-head">
|
||||
@@ -184,26 +203,25 @@
|
||||
<p class="otp-hint" id="pairing-code-hint" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert--warn" id="pairing-warning" hidden>
|
||||
<div class="admin-banners">
|
||||
<p class="admin-banner admin-banner--warning" id="pairing-warning" hidden>
|
||||
<strong>Write this down now.</strong>
|
||||
<span id="pairing-warning-text"></span>
|
||||
If you lose it, create a new one — a lost code is not a problem, but a code
|
||||
that has expired is a wasted trip.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="button button--quiet" type="button" id="pairing-done">
|
||||
<div class="modal-actions">
|
||||
<button class="button button--ghost" type="button" id="pairing-done">
|
||||
Done — clear this from the page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="node-list" class="table-wrap" aria-live="polite"></div>
|
||||
<div class="admin-card">
|
||||
<div id="node-list" class="admin-table-wrap" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user