mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 03:59:41 +00:00
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:
@@ -12,6 +12,11 @@ vi.mock('@/context/LicenseContext', () => ({
|
||||
useLicense: () => useLicenseMock(),
|
||||
}));
|
||||
|
||||
const useExperimentalMock = vi.fn(() => ({ experimental: true, experimentalReady: true }));
|
||||
vi.mock('@/hooks/useExperimental', () => ({
|
||||
useExperimental: () => useExperimentalMock(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/lib/utils')>('@/lib/utils');
|
||||
return {
|
||||
@@ -39,6 +44,8 @@ function statusJson(status: number, payload: unknown = {}): Response {
|
||||
beforeEach(() => {
|
||||
apiFetchMock.mockReset();
|
||||
useLicenseMock.mockReset();
|
||||
useExperimentalMock.mockReset();
|
||||
useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -91,4 +98,24 @@ describe('useMeshDataPlane', () => {
|
||||
expect(result.current.status).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
it('does not fetch when paid but experimental discovery is off', async () => {
|
||||
useLicenseMock.mockReturnValue({ isPaid: true });
|
||||
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
|
||||
const { result } = renderHook(() => useMeshDataPlane());
|
||||
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
|
||||
|
||||
expect(apiFetchMock).not.toHaveBeenCalled();
|
||||
expect(result.current.status).toBeNull();
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('does not fetch while experimental metadata is still loading', async () => {
|
||||
useLicenseMock.mockReturnValue({ isPaid: true });
|
||||
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: false });
|
||||
const { result } = renderHook(() => useMeshDataPlane());
|
||||
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
|
||||
|
||||
expect(apiFetchMock).not.toHaveBeenCalled();
|
||||
expect(result.current.status).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useLicense } from '@/context/LicenseContext';
|
||||
import { useExperimental } from '@/hooks/useExperimental';
|
||||
import { visibilityInterval } from '@/lib/utils';
|
||||
import type { MeshDataPlaneStatus } from '@/types/mesh';
|
||||
|
||||
@@ -11,14 +12,17 @@ export interface MeshDataPlaneResult {
|
||||
|
||||
/**
|
||||
* Poll `/mesh/status` for the local data-plane health so dashboard surfaces
|
||||
* can flag a down mesh without opening the Routing tab. The endpoint is
|
||||
* paid-gated, so the hook short-circuits on the free tier (no request
|
||||
* fired, no banner rendered). On the rare 403 from a paid tier (token
|
||||
* race during downgrade) we leave `status` at null. 30 s cadence matches
|
||||
* `useFleetHeartbeat` so the dashboard refresh feel is consistent.
|
||||
* can flag a down mesh without opening the Routing tab. Discovery requires
|
||||
* SENCHO_EXPERIMENTAL and an Admiral license; the hook short-circuits when
|
||||
* either gate is off (no request fired, no banner rendered). On the rare
|
||||
* 403 from a paid tier (token race during downgrade) we leave `status` at
|
||||
* null. 30 s cadence matches `useFleetHeartbeat` so the dashboard refresh
|
||||
* feel is consistent.
|
||||
*/
|
||||
export function useMeshDataPlane(): MeshDataPlaneResult {
|
||||
const { isPaid } = useLicense();
|
||||
const { experimental, experimentalReady } = useExperimental();
|
||||
const canDiscover = experimentalReady && experimental && isPaid;
|
||||
const [status, setStatus] = useState<MeshDataPlaneStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -41,14 +45,14 @@ export function useMeshDataPlane(): MeshDataPlaneResult {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPaid) {
|
||||
if (!canDiscover) {
|
||||
setStatus(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
void fetchStatus();
|
||||
return visibilityInterval(() => { void fetchStatus(); }, 30_000);
|
||||
}, [isPaid, fetchStatus]);
|
||||
}, [canDiscover, fetchStatus]);
|
||||
|
||||
return { status, loading };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user