From 07be1c5399831f1a0ad7a79ad0fe45b22b7ef607 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 13:23:35 -0700 Subject: [PATCH] Show the first-boot wizard again The console shows the wizard when the signed-in account can see no layout. Two links broke that for the bootstrap administrator, because a custom component with no rule of its own counted as visible to everyone: - the Settings Overview, which the console adds itself (since 1052700), kept the Settings layout visible on every server; - a page the server lists but this console doesn't know did the same. With the 2026.9.28 image against server 2026.9.29 that was the Compliance overview. Either way the bootstrap administrator got an empty console instead of the wizard, and couldn't finish setup (inbuxa-server#135). The Overview is now visible when some other settings page is, and a custom component this console has no rule for is hidden, since it could only draw "Unknown component". --- src/lib/layout.test.ts | 74 ++++++++++++++++++++++++++++++++++++++++++ src/lib/layout.ts | 20 +++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/lib/layout.test.ts diff --git a/src/lib/layout.test.ts b/src/lib/layout.test.ts new file mode 100644 index 0000000..83f3075 --- /dev/null +++ b/src/lib/layout.test.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, it, expect } from 'vitest'; +import { isLinkVisible, visibleLayouts } from './layout'; +import { withRegroupedSettings, SETTINGS_OVERVIEW_VIEW } from './settingsLayout'; +import type { Schema } from '@/types/schema'; + +// The shape a server in bootstrap mode serves: every layout present, and an +// administrator who may only read and write x:Bootstrap. +const served = { + objects: { + 'x:Bootstrap': { type: 'singleton', description: '', permissionPrefix: 'sysBootstrap' }, + 'x:Domain': { type: 'object', description: '', permissionPrefix: 'sysDomain' }, + 'x:MtaStageEhlo': { type: 'singleton', description: '', permissionPrefix: 'sysMtaStageEhlo' }, + }, + schemas: {}, + fields: {}, + forms: {}, + lists: {}, + enums: {}, + dashboards: [], + layouts: [ + { + name: 'Management', + icon: 'server', + items: [ + { link: { name: 'Domains', icon: 'globe', viewName: 'x:Domain' } }, + { link: { name: 'Compliance', icon: 'scale', viewName: 'CustomComponent/ComplianceOverview' } }, + { link: { name: 'Coming soon', icon: 'sparkles', viewName: 'CustomComponent/NotInThisConsole' } }, + ], + }, + { + name: 'Settings', + icon: 'sliders-horizontal', + items: [ + { + container: { + name: 'MTA', + icon: 'route', + items: [{ type: 'link', name: 'EHLO Stage', viewName: 'x:MtaStageEhlo' }], + }, + }, + ], + }, + ], +} as unknown as Schema; + +const schema = withRegroupedSettings(served); +const canGetFrom = (perms: string[]) => (prefix: string) => perms.includes(`${prefix}Get`); +const hasPermFrom = (perms: string[]) => (perm: string) => perms.includes(perm); + +describe('layout visibility', () => { + it('leaves the bootstrap administrator no layout, so the wizard shows (inbuxa-server#135)', () => { + const perms = ['sysBootstrapGet', 'sysBootstrapUpdate']; + expect(visibleLayouts(schema, 'enterprise', canGetFrom(perms), hasPermFrom(perms))).toEqual([]); + }); + + it('shows the Settings overview only beside a settings page the account may see', () => { + const none = canGetFrom([]); + const ehlo = canGetFrom(['sysMtaStageEhloGet']); + expect(isLinkVisible(schema, SETTINGS_OVERVIEW_VIEW, 'enterprise', none)).toBe(false); + expect(isLinkVisible(schema, SETTINGS_OVERVIEW_VIEW, 'enterprise', ehlo)).toBe(true); + expect(visibleLayouts(schema, 'enterprise', ehlo).map((l) => l.name)).toEqual(['Settings']); + }); + + it('hides a page this console cannot draw', () => { + const all = () => true; + expect(isLinkVisible(schema, 'CustomComponent/NotInThisConsole', 'enterprise', all)).toBe(false); + }); +}); diff --git a/src/lib/layout.ts b/src/lib/layout.ts index 79c7734..45e1117 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -9,6 +9,7 @@ import type { Layout, LayoutItem, LayoutSubItem, Schema } from '@/types/schema'; import { resolveObject } from '@/lib/schemaResolver'; +import { SETTINGS_LAYOUT_NAME, SETTINGS_OVERVIEW_VIEW } from '@/lib/settingsLayout'; function findFirstSubLink(items: LayoutSubItem[]): string | null { for (const item of items) { @@ -111,8 +112,11 @@ function checkSpecialLink( return { visible: canGet ? canGet('sysAccountLock') : true, enterprise: false }; } + // INBUXA: a page this console can't draw is hidden. A server newer than the + // console lists pages the console doesn't know yet, and counting them as + // visible hid the first-boot wizard (inbuxa-server#135). if (viewName.startsWith('CustomComponent/')) { - return { visible: true, enterprise: false }; + return { visible: false, enterprise: false }; } // INBUXA: guided jobs are open to whoever may manage what they change. @@ -148,6 +152,20 @@ export function isLinkVisible( canGet: CanGet, hasPerm?: HasPermission, ): boolean { + // INBUXA: the Settings overview is the console's own page, there when any + // settings page is. Visible on its own, it made the Settings layout visible + // to the bootstrap administrator, and the wizard never showed. + if (viewName === SETTINGS_OVERVIEW_VIEW) { + const settings = schema.layouts.find((layout) => layout.name === SETTINGS_LAYOUT_NAME); + return ( + settings?.items.some( + (item) => + 'container' in item && + findFirstVisibleSubLink(schema, item.container.items, edition, canGet, hasPerm) !== null, + ) ?? false + ); + } + const special = checkSpecialLink(viewName, edition, hasPerm, canGet); if (special !== null) return special.visible; -- 2.54.0