fix(webui): refresh the Settings bus control on every org list read
E2E5 review P2. An open Settings page kept Done after the bus died
because only opening the organization reread bus health. display.js
now announces each fresh list read, org-create.js redraws the setup
steps when the selected org's bus state flips (choices kept, no draft
guard), and app.js reads the list before bus-status so a failed
bus-status read no longer skips it. Regression: healthy, Refresh down
shows Start, Refresh up hides it; red on 8a8edc44b.
This commit was merged in pull request #1881.
This commit is contained in:
1 parent
8a8edc44b6
commit
fdd6658a65
4 files changed
+46
-7
No files matched your search
@@ -486,12 +486,14 @@
|
||||
// good load, from the status `mosaic bus start` left. textContent only. Nothing here retries.
|
||||
async function busDown() {
|
||||
const box = $('bus-down');
|
||||
// E2E5 review P2: the organization list is read first and on its own, so a failed bus-status read
|
||||
// (likely when the bus is down) still refreshes the list, its banner and the Settings Start control.
|
||||
if (window.MosaicOrg) {
|
||||
const org = (await window.MosaicOrg.readList()).find(row => row.id === window.MosaicOrg.id);
|
||||
if (org?.busHealthy === false) { box.textContent = 'Bus unavailable'; box.hidden = false; return; }
|
||||
}
|
||||
try {
|
||||
const s = await api('/api/bus-status');
|
||||
if (window.MosaicOrg) {
|
||||
const org = (await window.MosaicOrg.readList()).find(row => row.id === window.MosaicOrg.id);
|
||||
if (org?.busHealthy === false) { box.textContent = 'Bus unavailable'; box.hidden = false; return; }
|
||||
}
|
||||
if (!s.down) { box.hidden = true; return; }
|
||||
const b = s.boot;
|
||||
box.replaceChildren(node('b', '', 'Bus down: organization file invalid. '), node('span', '', `${b.reason} `), node('span', 'small muted', `File ${b.file}. Refused at ${b.at}. Last good load: ${b.lastGood ?? 'none'}. Fix the file, then restart the bus.`));
|
||||
|
||||
@@ -41,7 +41,13 @@ window.MosaicLocalTime = value => Number.isFinite(Date.parse(value)) ? new Date(
|
||||
// A failed read keeps its reason, so the header can say why the list is empty.
|
||||
let listError = null;
|
||||
const readList = () => nativeFetch('/api/orgs', { cache: 'no-store', redirect: 'manual' }).then(async r => {
|
||||
if (r.ok) { const rows = await r.json(); listError = null; return Array.isArray(rows) ? rows : []; }
|
||||
if (r.ok) {
|
||||
const body = await r.json(); listError = null;
|
||||
const rows = Array.isArray(body) ? body : [];
|
||||
// E2E5 review P2: an open panel (Settings > Organizations) follows every fresh list read, not only its own.
|
||||
document.dispatchEvent(new CustomEvent('mosaic-org-list', { detail: { rows } }));
|
||||
return rows;
|
||||
}
|
||||
const body = await r.json().catch(() => ({}));
|
||||
listError = typeof body.message === 'string' && body.message ? body.message : 'Organization registry could not be read';
|
||||
return [];
|
||||
|
||||
@@ -594,8 +594,23 @@
|
||||
} catch { $('org-delete-error').textContent = 'The outcome is unknown. Inspect the receipt and resume with the CLI.'; $('org-delete-progress').innerHTML = `<p>Resume with <code>${txt(`mosaic org delete ${id} --confirm ${id}`)}</code></p>`; }
|
||||
}
|
||||
|
||||
// E2E5 review P2: a list read anywhere on the page (Refresh, the header, the bus banner) updates the
|
||||
// bus Start affordance of the open organization. Only a change re-renders, and the typed setup choices
|
||||
// are kept first, so a draft survives.
|
||||
function onOrgList(e) {
|
||||
if (!alive || !st.setup || !st.id || !Array.isArray(e.detail?.rows)) return;
|
||||
const down = e.detail.rows.find(org => org.id === st.id)?.busHealthy === false;
|
||||
if (down === Boolean(st.busDown)) return;
|
||||
for (const card of root.querySelectorAll('.org-step')) {
|
||||
if (!card.querySelector('.org-git')) continue;
|
||||
const mode = card.querySelector('input[type=radio]:checked')?.value;
|
||||
st.choice.set(card.dataset.step, { mode, remote: card.querySelector('[data-remote]')?.value ?? '', risk: !!card.querySelector('[data-risk]')?.checked });
|
||||
}
|
||||
st.busDown = down; renderSetup();
|
||||
}
|
||||
// ---- wiring -----------------------------------------------------------
|
||||
const handlers = [
|
||||
[document, 'mosaic-org-list', onOrgList],
|
||||
[$('org-archive-open'), 'click', () => { $('org-archive-open').hidden = true; $('org-archive-confirm').hidden = false; $('org-archive-confirm').focus(); }],
|
||||
[$('org-archive-cancel'), 'click', () => { resetArchive(); $('org-archive-open').focus(); }],
|
||||
[$('org-archive-final'), 'click', () => runLifecycle('archive')],
|
||||
|
||||
@@ -14,12 +14,12 @@ import { busStopForDelete } from '../../cli/src/org-bus.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
import { close } from './fixture.mjs';
|
||||
|
||||
async function fixture(t, ids = ['alpha', 'beta']) {
|
||||
async function fixture(t, ids = ['alpha', 'beta'], { health = null } = {}) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'archive-browser-'));
|
||||
const configDir = join(dir, 'config'), dataRoot = join(dir, 'data'); await mkdir(configDir, { mode: 0o700 }); await mkdir(dataRoot, { mode: 0o700 });
|
||||
const ctx = { configDir, dataRoot, instance: 'test', principal: { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' } };
|
||||
for (const id of ids) await service.createOrg(ctx, id, await service.templateRender(id));
|
||||
const busStatus = async () => ({ healthy: false });
|
||||
const busStatus = async () => ({ healthy: health ? health.up : false });
|
||||
const organizations = organizationConsole({ ctx, listOrgs: registry.listOrgs, resolveOrg: registry.resolveOrg, showOrg: service.showOrg, orgPaths, busStatus, options: ({ id }) => ({ reads: { business: async () => ({ business: { id, projects: ['work'] } }), settings: async () => ({}) }, bus: {}, send: async () => ({}) }) });
|
||||
const admin = createOrgAdmin({ service, registry, bus: { busStatus, busStopForDelete }, context: async () => ({ configDir, dataRoot, instance: 'test' }) });
|
||||
const server = await startServer({ port: 0, sso: createDevOwner(), orgAdmin: admin, organizations, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} });
|
||||
@@ -137,6 +137,22 @@ test('owner can start a down bus after setup recorded done and sees recovery gui
|
||||
assert.match(await f.b.evaluate(`document.querySelector('[data-step="bus"]').innerText`), /mosaic bus recover alpha/);
|
||||
});
|
||||
|
||||
// E2E5 review P2: the list read by Refresh, not only opening the organization, decides whether Start shows.
|
||||
test('an open Settings page offers Start when a refreshed org list reads the bus down after setup recorded done', { timeout: 45000 }, async t => {
|
||||
const health = { up: true };
|
||||
const f = await fixture(t, ['alpha', 'beta'], { health });
|
||||
await f.b.evaluate(`(()=>{const old=window.fetch;window.fetch=async (input,init={})=>{const u=String(input);if(u.endsWith('/setup') && (!init.method || init.method==='GET'))return new Response(JSON.stringify({notifications:{state:'done'},queue:{state:'done'},projects:[],bus:{state:'done',verb:'mosaic bus start alpha'}}),{status:200});return old(input,init);};})()`);
|
||||
await f.open('alpha');
|
||||
await f.wait(`document.querySelector('[data-step="bus"]')?.textContent.includes('Done')`);
|
||||
assert.equal(await f.b.evaluate(`!!document.querySelector('[data-step="bus"] [data-run="bus"]')`), false);
|
||||
health.up = false;
|
||||
await f.b.evaluate(`document.querySelector('#refresh').click()`);
|
||||
await f.wait(`!!document.querySelector('[data-step="bus"] [data-run="bus"]:not(:disabled)')`);
|
||||
health.up = true;
|
||||
await f.b.evaluate(`document.querySelector('#refresh').click()`);
|
||||
await f.wait(`!document.querySelector('[data-step="bus"] [data-run="bus"]')`);
|
||||
});
|
||||
|
||||
for (const capability of [{ user:true, orgSetup:false }, { tester:true, orgSetup:false }, { orgSetup:false }]) {
|
||||
test(`archived restore stays disabled without owner capability ${JSON.stringify(capability)}`, { timeout: 45000 }, async t => {
|
||||
const f = await fixture(t); await f.open('beta');
|
||||
|
||||
Reference in new issue
Block a user