Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -12,18 +12,20 @@ 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)) {
Comment thread
rmnbrd marked this conversation as resolved.
return (
<div className="flex min-h-page-container items-center justify-center">
<LoaderSpinner />
</div>
)
}

if (!isProfileEnabled && organizationId && clusterId) {
if (!isOperatorAttached && organizationId && clusterId) {
return (
<Navigate
to="/organization/$organizationId/cluster/$clusterId/overview"
Expand Down
7 changes: 4 additions & 3 deletions apps/console/src/routes/_authenticated/organization/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import posthog from 'posthog-js'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { type BlueprintDetailsResponse, type Cluster } from 'qovery-typescript-axios'
import { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useClusters } from '@qovery/domains/clusters/feature'
import { useClusters, useIsOperatorAttached } from '@qovery/domains/clusters/feature'
import { useEnvironment } from '@qovery/domains/environments/feature'
import { useProject } from '@qovery/domains/projects/feature'
import {
Expand Down Expand Up @@ -385,7 +385,6 @@ function useNavigationContext(): NavigationContext | null {
const pathname = location.pathname
const organizationId = typeof params.organizationId === 'string' ? params.organizationId : ''
const isAgenticWorkflowEnabled = Boolean(useFeatureFlagEnabled('argentic-workflow'))
const isEngineV2PlatformConfigurationEnabled = Boolean(useFeatureFlagEnabled('engine-v2-platform-configuration'))
const { data: service } = useServiceSummary({
environmentId: params.environmentId,
serviceId: params.serviceId,
Expand All @@ -400,6 +399,8 @@ function useNavigationContext(): NavigationContext | null {
})
const hasAlerting = clusters.some((cluster) => 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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cluster, 'id' | 'organization' | 'kubernetes'>) {
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<Cluster, 'id' | 'organization' | 'kubernetes'>) {
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')
Comment thread
rmnbrd marked this conversation as resolved.
}

export default useIsOperatorAttached
Loading