diff --git a/apps/console/src/routes/_authenticated/organization/$organizationId/cluster/$clusterId/profile/route.tsx b/apps/console/src/routes/_authenticated/organization/$organizationId/cluster/$clusterId/profile/route.tsx index 95375ee8f2d..87296f8db54 100644 --- a/apps/console/src/routes/_authenticated/organization/$organizationId/cluster/$clusterId/profile/route.tsx +++ b/apps/console/src/routes/_authenticated/organization/$organizationId/cluster/$clusterId/profile/route.tsx @@ -1,6 +1,6 @@ import { Navigate, Outlet, createFileRoute, useParams } from '@tanstack/react-router' -import { useFeatureFlagEnabled } from 'posthog-js/react' import { z } from 'zod' +import { useCluster, useIsOperatorAttached } from '@qovery/domains/clusters/feature' import { LoaderSpinner } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' @@ -12,10 +12,12 @@ export const Route = createFileRoute('/_authenticated/organization/$organization function RouteComponent() { useDocumentTitle('Cluster - Profile') const { organizationId = '', clusterId = '' } = useParams({ strict: false }) - // Undefined until PostHog has loaded the flags: redirecting then would turn away users who have access. - const isProfileEnabled = useFeatureFlagEnabled('engine-v2-platform-configuration') + const { data: cluster, isLoading: isClusterLoading } = useCluster({ organizationId, clusterId }) + // Only Engine v2 clusters, attached to the Qovery Operator, have a profile. + const isOperatorAttached = useIsOperatorAttached(cluster) - if (isProfileEnabled === undefined) { + // Redirecting before the flags, the cluster and its Operator status are loaded would turn away users who have access. + if (isClusterLoading || (cluster && isOperatorAttached === undefined)) { return (
@@ -23,7 +25,7 @@ function RouteComponent() { ) } - if (!isProfileEnabled && organizationId && clusterId) { + if (!isOperatorAttached && organizationId && clusterId) { return ( cluster.metrics_parameters?.configuration?.alerting?.enabled) const currentCluster = clusters.find((cluster) => cluster.id === environment?.cluster_id) + // Only Engine v2 clusters, attached to the Qovery Operator, have a profile. + const isProfileAvailable = Boolean(useIsOperatorAttached(clusters.find((cluster) => cluster.id === params.clusterId))) const blueprintId = service && isBlueprintService(service) && isTerraform(service) ? service.blueprint_id : '' const { data: blueprint } = useBlueprint({ blueprintId, @@ -431,7 +432,7 @@ function useNavigationContext(): NavigationContext | null { : context.type === 'organization' ? context.tabs.filter((tab) => hasAlerting || tab.id !== 'alerts') : context.type === 'cluster' - ? context.tabs.filter((tab) => isEngineV2PlatformConfigurationEnabled || tab.id !== 'profile') + ? context.tabs.filter((tab) => isProfileAvailable || tab.id !== 'profile') : context.type === 'environment' ? context.tabs.filter((tab) => isAgenticWorkflowEnabled || tab.id !== 'automation') : context.tabs diff --git a/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.spec.ts b/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.spec.ts index 63332aec01f..04c14b855e9 100644 --- a/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.spec.ts +++ b/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.spec.ts @@ -48,4 +48,32 @@ describe('useIsOperatorAttached', () => { expect(result.current).toBe(false) }) + + it('ignores self-managed clusters without Operator state', () => { + mockUseClusterOperatorStatus.mockReturnValue({ data: null }) + const { result } = renderHook(() => useIsOperatorAttached(cluster)) + + expect(result.current).toBe(false) + }) + + it('is unknown while the feature flags are loading', () => { + mockUseFeatureFlagEnabled.mockReturnValue(undefined) + const { result } = renderHook(() => useIsOperatorAttached(cluster)) + + expect(result.current).toBeUndefined() + }) + + it('is unknown while the cluster is loading', () => { + const { result } = renderHook(() => useIsOperatorAttached(undefined)) + + expect(mockUseClusterOperatorStatus).toHaveBeenCalledWith(expect.objectContaining({ enabled: false })) + expect(result.current).toBeUndefined() + }) + + it('is unknown while the Operator status is loading', () => { + mockUseClusterOperatorStatus.mockReturnValue({ data: undefined, isLoading: true }) + const { result } = renderHook(() => useIsOperatorAttached(cluster)) + + expect(result.current).toBeUndefined() + }) }) diff --git a/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.ts b/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.ts index 55821470e76..23f842bb265 100644 --- a/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.ts +++ b/libs/domains/clusters/feature/src/lib/hooks/use-is-operator-attached/use-is-operator-attached.ts @@ -4,18 +4,22 @@ import { useClusterOperatorStatus } from '../use-cluster-operator-status/use-clu // Self-managed clusters may be attached to the Qovery Operator: the Cluster model does not say it, // only the Operator status does (q-core answers 404 for clusters without Operator state). -export function useIsOperatorAttached(cluster: Pick) { - const isEngineV2Enabled = Boolean(useFeatureFlagEnabled('engine-v2-platform-configuration')) - const enabled = isEngineV2Enabled && cluster.kubernetes === 'SELF_MANAGED' - const { data: operatorStatus } = useClusterOperatorStatus({ - organizationId: cluster.organization.id, - clusterId: cluster.id, +// Undefined until it is known: while the feature flags, the cluster or its Operator status are loading. +export function useIsOperatorAttached(cluster?: Pick) { + const isEngineV2Enabled = useFeatureFlagEnabled('engine-v2-platform-configuration') + const enabled = Boolean(isEngineV2Enabled) && cluster?.kubernetes === 'SELF_MANAGED' + const { data: operatorStatus, isLoading } = useClusterOperatorStatus({ + organizationId: cluster?.organization.id ?? '', + clusterId: cluster?.id ?? '', enabled, staleTime: 60_000, }) + if (isEngineV2Enabled === undefined || !cluster) return undefined // A disabled query still exposes cached data, which other views may have fetched without the flag. - return enabled && Boolean(operatorStatus && operatorStatus.status !== 'NOT_ATTACHED') + if (!enabled) return false + if (isLoading) return undefined + return Boolean(operatorStatus && operatorStatus.status !== 'NOT_ATTACHED') } export default useIsOperatorAttached