chore(ui): hide Mesh, Fleet Secrets, and Host Console behind experimental discovery (#1624)

Gate Routing, Secrets, Host Console, and Mesh dashboard/settings surfaces on the existing useExperimental readiness flag so immature operator surfaces stay out of the default UI while paid and admin backend gates remain unchanged.
This commit is contained in:
Anso
2026-07-13 11:55:50 -04:00
committed by GitHub
parent 9b3f5c5a90
commit 0cd03c6f87
23 changed files with 578 additions and 62 deletions
+39 -1
View File
@@ -2,6 +2,8 @@ import { describe, it, expect } from 'vitest';
import {
authzReady,
isViewHidden,
isFleetTabHidden,
isSettingsSectionHidden,
normalizeHiddenView,
type ReachabilityContext,
} from './reachability';
@@ -16,6 +18,8 @@ function ctx(over: Partial<ReachabilityContext> = {}): ReachabilityContext {
containerLabelsEnabled: true,
permissionsStatus: 'ready',
licenseStatus: 'ready',
experimental: false,
experimentalReady: true,
...over,
};
}
@@ -46,7 +50,41 @@ describe('reachability', () => {
});
it('preserves paid views when license metadata failed', () => {
const licenseError = ctx({ licenseStatus: 'error' });
const licenseError = ctx({ licenseStatus: 'error', experimental: true });
expect(isViewHidden('host-console', licenseError)).toBe(false);
});
it('does not apply experimental hide to host-console until experimentalReady', () => {
const loading = ctx({ experimental: false, experimentalReady: false, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', loading)).toBe(false);
});
it('hides host-console when experimental is ready and off even for paid admin', () => {
const off = ctx({ experimental: false, experimentalReady: true, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', off)).toBe(true);
expect(normalizeHiddenView('host-console', off)).toBe('dashboard');
});
it('keeps host-console when experimental is on for paid admin', () => {
const on = ctx({ experimental: true, experimentalReady: true, isPaid: true, isAdmin: true });
expect(isViewHidden('host-console', on)).toBe(false);
});
it('hides routing and secrets fleet tabs only after experimentalReady when off', () => {
const loading = ctx({ experimental: false, experimentalReady: false });
expect(isFleetTabHidden('routing', loading)).toBe(false);
expect(isFleetTabHidden('secrets', loading)).toBe(false);
const off = ctx({ experimental: false, experimentalReady: true });
expect(isFleetTabHidden('routing', off)).toBe(true);
expect(isFleetTabHidden('secrets', off)).toBe(true);
expect(isFleetTabHidden('deployments', off)).toBe(false);
expect(isFleetTabHidden('federation', off)).toBe(false);
expect(isFleetTabHidden('actions', off)).toBe(false);
});
it('does not hide fleet-mesh settings for experimental off', () => {
const off = ctx({ experimental: false, experimentalReady: true, isAdmin: true });
expect(isSettingsSectionHidden('fleet-mesh', off)).toBe(false);
});
});
+27 -2
View File
@@ -15,6 +15,10 @@ export interface ReachabilityContext {
containerLabelsEnabled: boolean;
permissionsStatus: ReadinessStatus;
licenseStatus: ReadinessStatus;
/** Gateway SENCHO_EXPERIMENTAL discovery flag. */
experimental: boolean;
/** True once /meta experimental has settled (success or fail-closed). */
experimentalReady: boolean;
}
/** RBAC/tier gates apply only when permission and license metadata are ready. */
@@ -22,6 +26,15 @@ export function authzReady(ctx: ReachabilityContext): boolean {
return ctx.permissionsStatus === 'ready' && ctx.licenseStatus === 'ready';
}
/**
* Experimental discovery gates apply only after /meta settles. Before that,
* treat surfaces as not-yet-hidden so URL sync does not rewrite enabled
* deep links during cold load.
*/
export function experimentalDiscoveryReady(ctx: ReachabilityContext): boolean {
return ctx.experimentalReady;
}
/** Role/tier hidden views normalize away only when permission and license metadata are ready. */
export function isViewHidden(view: ActiveView, ctx: ReachabilityContext): boolean {
if (!authzReady(ctx)) return false;
@@ -29,11 +42,17 @@ export function isViewHidden(view: ActiveView, ctx: ReachabilityContext): boolea
if (!ctx.isAdmin && view === 'global-observability') return true;
if (!ctx.isAdmin && (view === 'auto-updates' || view === 'scheduled-ops')) return true;
if (!ctx.can('node:read') && view === 'fleet') return true;
if (view === 'host-console') {
// Defer experimental hide until ready so enabled deep links survive cold load.
if (experimentalDiscoveryReady(ctx) && !ctx.experimental) return true;
if (!ctx.isPaid) return true;
if (!ctx.isAdmin) return true;
return false;
}
if (!ctx.isPaid) {
if (view === 'host-console' || view === 'audit-log') return true;
if (view === 'audit-log') return true;
} else {
if (view === 'audit-log' && !ctx.can('system:audit')) return true;
if (view === 'host-console' && !ctx.isAdmin) return true;
}
return false;
}
@@ -48,6 +67,10 @@ export function isViewCapabilityLocked(view: ActiveView, ctx: ReachabilityContex
export function isFleetTabHidden(tab: FleetTab, ctx: ReachabilityContext): boolean {
if (!authzReady(ctx)) return false;
if (tab === 'container-labels' && !ctx.containerLabelsEnabled) return true;
// Defer experimental hide until ready (same cold-load contract as host-console).
if ((tab === 'routing' || tab === 'secrets') && experimentalDiscoveryReady(ctx) && !ctx.experimental) {
return true;
}
return false;
}
@@ -58,6 +81,8 @@ export function isSettingsSectionHidden(section: SectionId, ctx: ReachabilityCon
if (ctx.isRemote && item.hiddenOnRemote) return true;
if (item.adminOnly && !ctx.isAdmin) return true;
if (item.tier === 'paid' && !ctx.isPaid) return true;
// fleet-mesh stays reachable: snapshot_documentation lives there even when
// Mesh discovery is off.
return false;
}