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