Show the first-boot wizard again #75

Merged
jcoffey-dev merged 1 commits from fix/bootstrap-wizard-hidden into main 2026-09-30 20:27:38 +00:00
2 changed files with 93 additions and 1 deletions

No files matched your search

+74
View File
@@ -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);
});
});
+19 -1
View File
@@ -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;