From 2603657c0985a728fe06891118d85d5ba0289ad2 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Apr 2026 15:42:56 +0200 Subject: [PATCH 1/8] feat(console-migration): add ConsoleMigrationPrompt component and integrate into LayoutPage for new console redirection --- .../console-migration-prompt.tsx | 54 +++++++++ .../src/lib/ui/layout-page/layout-page.tsx | 48 +++++++- .../page-user-general-feature.spec.tsx | 20 ++++ .../page-user-general-feature.tsx | 8 +- .../page-user-general.spec.tsx | 35 ++++++ .../page-user-general/page-user-general.tsx | 26 ++++- libs/shared/iam/feature/src/index.ts | 1 + .../use-console-redirect-preference.spec.ts | 41 +++++++ .../use-console-redirect-preference.ts | 105 ++++++++++++++++++ 9 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx create mode 100644 libs/shared/iam/feature/src/lib/use-console-redirect-preference/use-console-redirect-preference.spec.ts create mode 100644 libs/shared/iam/feature/src/lib/use-console-redirect-preference/use-console-redirect-preference.ts diff --git a/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx new file mode 100644 index 00000000000..8bd34643680 --- /dev/null +++ b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx @@ -0,0 +1,54 @@ +import { AnimatePresence, motion } from 'framer-motion' +import { Button, Icon } from '@qovery/shared/ui' + +export interface ConsoleMigrationPromptProps { + open: boolean + onClose: () => void + onConfirm: () => void +} + +export function ConsoleMigrationPrompt({ open, onClose, onConfirm }: ConsoleMigrationPromptProps) { + return ( + + {open && ( +
+ +
+
+
+ +
+

+ Switch to the new console and redirect future visits automatically. +

+
+ + +
+
+
+
+
+ )} +
+ ) +} + +export default ConsoleMigrationPrompt diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx index 2c6e2921942..485a241cef4 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx @@ -1,6 +1,6 @@ -import { useFeatureFlagVariantKey } from 'posthog-js/react' +import { useFeatureFlagEnabled, useFeatureFlagVariantKey } from 'posthog-js/react' import { type Cluster, ClusterStateEnum, type Organization } from 'qovery-typescript-axios' -import { type PropsWithChildren, useMemo } from 'react' +import { type PropsWithChildren, useEffect, useMemo } from 'react' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { match } from 'ts-pattern' import { ClusterDeploymentProgressCard, useClusterStatuses } from '@qovery/domains/clusters/feature' @@ -8,7 +8,12 @@ import { useAlerts } from '@qovery/domains/observability/feature' import { FreeTrialBanner, InvoiceBanner, useOrganization } from '@qovery/domains/organizations/feature' import { AssistantTrigger } from '@qovery/shared/assistant/feature' import { DevopsCopilotButton, DevopsCopilotTrigger } from '@qovery/shared/devops-copilot/feature' -import { useUserRole } from '@qovery/shared/iam/feature' +import { + getNewConsoleUrl, + shouldBypassLegacyConsoleRedirect, + useConsoleRedirectPreference, + useUserRole, +} from '@qovery/shared/iam/feature' import { AnnouncementBanner } from '@qovery/shared/posthog/feature' import { CLUSTER_SETTINGS_CREDENTIALS_URL, @@ -17,7 +22,9 @@ import { INFRA_LOGS_URL, } from '@qovery/shared/routes' import { Banner, WarningScreenMobile } from '@qovery/shared/ui' +import { useLocalStorage } from '@qovery/shared/util-hooks' import SpotlightTrigger from '../../feature/spotlight-trigger/spotlight-trigger' +import ConsoleMigrationPrompt from '../console-migration-prompt/console-migration-prompt' import Navigation from '../navigation/navigation' import TopBar from '../top-bar/top-bar' @@ -57,10 +64,17 @@ export function LayoutPage(props: PropsWithChildren) { const { data: clusterStatuses } = useClusterStatuses({ organizationId, enabled: !!organizationId }) const { data: organization } = useOrganization({ organizationId }) const { roles, isQoveryAdminUser } = useUserRole() + const { useNewConsoleByDefault, setUseNewConsoleByDefault } = useConsoleRedirectPreference() + const isNewNavigationActivationEnabled = Boolean(useFeatureFlagEnabled('new-navigation-activation')) const isAlertingFeatureFlagEnabled = useFeatureFlagVariantKey('alerting') const isFeatureFlag = useFeatureFlagVariantKey('devops-copilot') + const [isConsoleMigrationBannerDismissed, setIsConsoleMigrationBannerDismissed] = useLocalStorage( + 'legacy-console-migration-banner-dismissed', + false + ) const isQoveryUserWithMobileCheck = checkQoveryUser(isQoveryAdminUser) + const newConsoleUrl = getNewConsoleUrl() const matchLogInfraRoute = pathname.includes(INFRA_LOGS_URL(organizationId, clusterStatuses?.[0]?.cluster_id)) @@ -129,6 +143,29 @@ export function LayoutPage(props: PropsWithChildren) { return deployingClusters.length > 0 }, [deployingClusters]) + const shouldShowConsoleMigrationBanner = Boolean( + isNewNavigationActivationEnabled && newConsoleUrl && !useNewConsoleByDefault && !isConsoleMigrationBannerDismissed + ) + + useEffect(() => { + if (!useNewConsoleByDefault || shouldBypassLegacyConsoleRedirect() || !newConsoleUrl) { + return + } + + if (window.location.href !== newConsoleUrl) { + window.location.assign(newConsoleUrl) + } + }, [newConsoleUrl, useNewConsoleByDefault]) + + const handleConsoleMigration = () => { + if (!newConsoleUrl) { + return + } + + setUseNewConsoleByDefault(true) + window.location.assign(newConsoleUrl) + } + return ( <> {displayQoveryAdminBanner && ( @@ -193,6 +230,11 @@ export function LayoutPage(props: PropsWithChildren) { {showFloatingDeploymentCard && ( )} + setIsConsoleMigrationBannerDismissed(true)} + /> ) diff --git a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx index 5b766c79766..160edbda4ef 100644 --- a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx +++ b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx @@ -1,9 +1,16 @@ +import { useFeatureFlagEnabled } from 'posthog-js/react' import * as domainUserFeature from '@qovery/shared/iam/feature' import { renderWithProviders, screen } from '@qovery/shared/util-tests' import PageUserGeneralFeature from './page-user-general-feature' +jest.mock('posthog-js/react', () => ({ + useFeatureFlagEnabled: jest.fn(() => false), +})) + const useEditUserAccountMockSpy = jest.spyOn(domainUserFeature, 'useEditUserAccount') as jest.Mock const useUserAccountMockSky = jest.spyOn(domainUserFeature, 'useUserAccount') as jest.Mock +const useConsoleRedirectPreferenceMockSpy = jest.spyOn(domainUserFeature, 'useConsoleRedirectPreference') as jest.Mock +const useFeatureFlagEnabledMock = useFeatureFlagEnabled as jest.MockedFunction describe('PageUserGeneral', () => { beforeEach(() => { @@ -17,6 +24,11 @@ describe('PageUserGeneral', () => { communication_email: '', }, }) + useConsoleRedirectPreferenceMockSpy.mockReturnValue({ + useNewConsoleByDefault: false, + setUseNewConsoleByDefault: jest.fn(), + }) + useFeatureFlagEnabledMock.mockReturnValue(false) }) it('should render successfully', () => { @@ -42,4 +54,12 @@ describe('PageUserGeneral', () => { communication_email: 'test2@test.com', }) }) + + it('should render the console toggle only when the feature flag is enabled', () => { + useFeatureFlagEnabledMock.mockReturnValue(true) + + renderWithProviders() + + expect(screen.getByText('Use the new console by default')).toBeInTheDocument() + }) }) diff --git a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx index 431b61dc47b..5d6d73ac844 100644 --- a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx +++ b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx @@ -1,7 +1,8 @@ +import { useFeatureFlagEnabled } from 'posthog-js/react' import { FormProvider, useForm } from 'react-hook-form' import { useAuth } from '@qovery/shared/auth' import { type IconEnum } from '@qovery/shared/enums' -import { useEditUserAccount, useUserAccount } from '@qovery/shared/iam/feature' +import { useConsoleRedirectPreference, useEditUserAccount, useUserAccount } from '@qovery/shared/iam/feature' import { Icon } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' import PageUserGeneral from '../../ui/page-user-general/page-user-general' @@ -12,6 +13,8 @@ export function PageUserGeneralFeature() { const { user: userToken } = useAuth() const { data: user } = useUserAccount() const { mutateAsync, isLoading: loading } = useEditUserAccount() + const { useNewConsoleByDefault, setUseNewConsoleByDefault } = useConsoleRedirectPreference() + const isNewNavigationActivationEnabled = Boolean(useFeatureFlagEnabled('new-navigation-activation')) const methods = useForm({ mode: 'onChange', @@ -49,6 +52,9 @@ export function PageUserGeneralFeature() { loading={loading} picture={user?.profile_picture_url as string} accountOptions={accountOptions} + showNewConsoleToggle={isNewNavigationActivationEnabled} + useNewConsoleByDefault={useNewConsoleByDefault} + onUseNewConsoleByDefaultChange={setUseNewConsoleByDefault} /> ) diff --git a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx index 9d539593b2a..f7c5c89fc70 100644 --- a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx +++ b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx @@ -10,6 +10,9 @@ describe('PageUserGeneral', () => { loading: false, accountOptions: [], picture: '/', + showNewConsoleToggle: true, + useNewConsoleByDefault: false, + onUseNewConsoleByDefaultChange: jest.fn(), } const defaultValues = { @@ -38,6 +41,7 @@ describe('PageUserGeneral', () => { screen.getByLabelText('Last name') screen.getAllByLabelText('Account email') screen.getByLabelText('Communication email') + screen.getByText('Use the new console by default') }) it('should submit the form', async () => { @@ -57,4 +61,35 @@ describe('PageUserGeneral', () => { await userEvent.click(submitButton) expect(mockSubmit).toHaveBeenCalled() }) + + it('should toggle the console preference', async () => { + const onUseNewConsoleByDefaultChange = jest.fn() + + const { userEvent } = renderWithProviders( + wrapWithReactHookForm( + , + { + defaultValues: defaultValues, + } + ) + ) + + await userEvent.click(screen.getByText('Use the new console by default')) + + expect(onUseNewConsoleByDefaultChange).toHaveBeenCalledWith(true) + }) + + it('should not render the console preference toggle when disabled', () => { + renderWithProviders( + wrapWithReactHookForm(, { + defaultValues: defaultValues, + }) + ) + + expect(screen.queryByText('Use the new console by default')).not.toBeInTheDocument() + }) }) diff --git a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx index 35909a90386..b16b7ec8cf9 100644 --- a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx +++ b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx @@ -1,19 +1,30 @@ import { type FormEventHandler } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { type Value } from '@qovery/shared/interfaces' -import { BlockContent, Button, Heading, InputSelect, InputText, Section } from '@qovery/shared/ui' +import { BlockContent, Button, Heading, InputSelect, InputText, InputToggle, Section } from '@qovery/shared/ui' export interface PageUserGeneralProps { onSubmit: FormEventHandler loading: boolean picture: string accountOptions: Value[] + showNewConsoleToggle: boolean + useNewConsoleByDefault: boolean + onUseNewConsoleByDefaultChange: (value: boolean) => void } const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone const timezoneOffset = new Date().getTimezoneOffset() / -60 -export function PageUserGeneral({ onSubmit, loading, picture, accountOptions }: PageUserGeneralProps) { +export function PageUserGeneral({ + onSubmit, + loading, + picture, + accountOptions, + showNewConsoleToggle, + useNewConsoleByDefault, + onUseNewConsoleByDefaultChange, +}: PageUserGeneralProps) { const { control, formState, watch } = useFormContext() return ( @@ -112,6 +123,17 @@ export function PageUserGeneral({ onSubmit, loading, picture, accountOptions }:

Timezone used to display timestamp within the interface

+ {showNewConsoleToggle && ( + + )}
- +
From efbfad2704e8045a45106664fcc527058d25ea53 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Apr 2026 16:27:24 +0200 Subject: [PATCH 4/8] refactor(console-migration-prompt): change button variant from plain to outline for improved UI consistency --- .../ui/console-migration-prompt/console-migration-prompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx index dfe255a25a7..77a451353ca 100644 --- a/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx +++ b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx @@ -34,7 +34,7 @@ export function ConsoleMigrationPrompt({ open, onClose, onConfirm }: ConsoleMigr - From 170becc1e926c81a53e135a3ec88adb6400e77f0 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Mon, 20 Apr 2026 16:33:15 +0200 Subject: [PATCH 5/8] refactor(console-migration-prompt): adjust button spacing for improved layout consistency --- .../ui/console-migration-prompt/console-migration-prompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx index 77a451353ca..a2430579a87 100644 --- a/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx +++ b/libs/pages/layout/src/lib/ui/console-migration-prompt/console-migration-prompt.tsx @@ -30,7 +30,7 @@ export function ConsoleMigrationPrompt({ open, onClose, onConfirm }: ConsoleMigr

A new version of the Qovery console available! Join the early access now.

-
+
From be10e455639cd2cdce366654a9c3e591b3bb1cd9 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Tue, 21 Apr 2026 08:37:32 +0200 Subject: [PATCH 6/8] refactor(console-redirect): rename console preference hooks for clarity and consistency --- .../layout/src/lib/ui/layout-page/layout-page.tsx | 10 +++++----- .../page-user-general-feature.spec.tsx | 4 ++-- .../page-user-general-feature.tsx | 6 +++--- .../ui/page-user-general/page-user-general.spec.tsx | 12 ++++++------ .../lib/ui/page-user-general/page-user-general.tsx | 12 ++++++------ .../use-console-redirect-preference.ts | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx index 485a241cef4..a7e14e39f02 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx @@ -64,7 +64,7 @@ export function LayoutPage(props: PropsWithChildren) { const { data: clusterStatuses } = useClusterStatuses({ organizationId, enabled: !!organizationId }) const { data: organization } = useOrganization({ organizationId }) const { roles, isQoveryAdminUser } = useUserRole() - const { useNewConsoleByDefault, setUseNewConsoleByDefault } = useConsoleRedirectPreference() + const { isNewConsoleDefault, setIsNewConsoleDefault } = useConsoleRedirectPreference() const isNewNavigationActivationEnabled = Boolean(useFeatureFlagEnabled('new-navigation-activation')) const isAlertingFeatureFlagEnabled = useFeatureFlagVariantKey('alerting') const isFeatureFlag = useFeatureFlagVariantKey('devops-copilot') @@ -144,25 +144,25 @@ export function LayoutPage(props: PropsWithChildren) { }, [deployingClusters]) const shouldShowConsoleMigrationBanner = Boolean( - isNewNavigationActivationEnabled && newConsoleUrl && !useNewConsoleByDefault && !isConsoleMigrationBannerDismissed + isNewNavigationActivationEnabled && newConsoleUrl && !isNewConsoleDefault && !isConsoleMigrationBannerDismissed ) useEffect(() => { - if (!useNewConsoleByDefault || shouldBypassLegacyConsoleRedirect() || !newConsoleUrl) { + if (!isNewConsoleDefault || shouldBypassLegacyConsoleRedirect() || !newConsoleUrl) { return } if (window.location.href !== newConsoleUrl) { window.location.assign(newConsoleUrl) } - }, [newConsoleUrl, useNewConsoleByDefault]) + }, [newConsoleUrl, isNewConsoleDefault]) const handleConsoleMigration = () => { if (!newConsoleUrl) { return } - setUseNewConsoleByDefault(true) + setIsNewConsoleDefault(true) window.location.assign(newConsoleUrl) } diff --git a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx index 160edbda4ef..232eb122900 100644 --- a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx +++ b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.spec.tsx @@ -25,8 +25,8 @@ describe('PageUserGeneral', () => { }, }) useConsoleRedirectPreferenceMockSpy.mockReturnValue({ - useNewConsoleByDefault: false, - setUseNewConsoleByDefault: jest.fn(), + isNewConsoleDefault: false, + setIsNewConsoleDefault: jest.fn(), }) useFeatureFlagEnabledMock.mockReturnValue(false) }) diff --git a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx index 5d6d73ac844..9e48900b303 100644 --- a/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx +++ b/libs/pages/user/src/lib/feature/page-user-general-feature/page-user-general-feature.tsx @@ -13,7 +13,7 @@ export function PageUserGeneralFeature() { const { user: userToken } = useAuth() const { data: user } = useUserAccount() const { mutateAsync, isLoading: loading } = useEditUserAccount() - const { useNewConsoleByDefault, setUseNewConsoleByDefault } = useConsoleRedirectPreference() + const { isNewConsoleDefault, setIsNewConsoleDefault } = useConsoleRedirectPreference() const isNewNavigationActivationEnabled = Boolean(useFeatureFlagEnabled('new-navigation-activation')) const methods = useForm({ @@ -53,8 +53,8 @@ export function PageUserGeneralFeature() { picture={user?.profile_picture_url as string} accountOptions={accountOptions} showNewConsoleToggle={isNewNavigationActivationEnabled} - useNewConsoleByDefault={useNewConsoleByDefault} - onUseNewConsoleByDefaultChange={setUseNewConsoleByDefault} + isNewConsoleDefault={isNewConsoleDefault} + onNewConsoleDefaultChange={setIsNewConsoleDefault} /> ) diff --git a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx index f7c5c89fc70..52c347f8ffa 100644 --- a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx +++ b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.spec.tsx @@ -11,8 +11,8 @@ describe('PageUserGeneral', () => { accountOptions: [], picture: '/', showNewConsoleToggle: true, - useNewConsoleByDefault: false, - onUseNewConsoleByDefaultChange: jest.fn(), + isNewConsoleDefault: false, + onNewConsoleDefaultChange: jest.fn(), } const defaultValues = { @@ -63,14 +63,14 @@ describe('PageUserGeneral', () => { }) it('should toggle the console preference', async () => { - const onUseNewConsoleByDefaultChange = jest.fn() + const onNewConsoleDefaultChange = jest.fn() const { userEvent } = renderWithProviders( wrapWithReactHookForm( , { defaultValues: defaultValues, @@ -80,7 +80,7 @@ describe('PageUserGeneral', () => { await userEvent.click(screen.getByText('Use the new console by default')) - expect(onUseNewConsoleByDefaultChange).toHaveBeenCalledWith(true) + expect(onNewConsoleDefaultChange).toHaveBeenCalledWith(true) }) it('should not render the console preference toggle when disabled', () => { diff --git a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx index b16b7ec8cf9..33a7fcf9eca 100644 --- a/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx +++ b/libs/pages/user/src/lib/ui/page-user-general/page-user-general.tsx @@ -9,8 +9,8 @@ export interface PageUserGeneralProps { picture: string accountOptions: Value[] showNewConsoleToggle: boolean - useNewConsoleByDefault: boolean - onUseNewConsoleByDefaultChange: (value: boolean) => void + isNewConsoleDefault: boolean + onNewConsoleDefaultChange: (value: boolean) => void } const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone @@ -22,8 +22,8 @@ export function PageUserGeneral({ picture, accountOptions, showNewConsoleToggle, - useNewConsoleByDefault, - onUseNewConsoleByDefaultChange, + isNewConsoleDefault, + onNewConsoleDefaultChange, }: PageUserGeneralProps) { const { control, formState, watch } = useFormContext() @@ -126,8 +126,8 @@ export function PageUserGeneral({ {showNewConsoleToggle && ( { setPreferredConsole(value ? 'new' : 'legacy') }, @@ -97,8 +97,8 @@ export function useConsoleRedirectPreference() { return { preferredConsole, - useNewConsoleByDefault: preferredConsole === 'new', + isNewConsoleDefault: preferredConsole === 'new', setPreferredConsole, - setUseNewConsoleByDefault, + setIsNewConsoleDefault, } } From e060f23cc7337707ad0c8dd3fbb0fe92e50c5394 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Tue, 21 Apr 2026 08:47:06 +0200 Subject: [PATCH 7/8] feat(console-migration): implement analytics tracking for console migration prompt interactions in LayoutPage --- .../lib/ui/layout-page/layout-page.spec.tsx | 118 +++++++++++++++--- .../src/lib/ui/layout-page/layout-page.tsx | 16 ++- 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx index 2aa489dfdb2..bf61227b9d1 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx @@ -1,11 +1,28 @@ +import posthog from 'posthog-js' +import { useFeatureFlagEnabled } from 'posthog-js/react' import { CloudProviderEnum } from 'qovery-typescript-axios' -import { IntercomProvider } from 'react-use-intercom' +import { type ReactNode } from 'react' +import * as iamFeature from '@qovery/shared/iam/feature' import { renderWithProviders, screen } from '@qovery/shared/util-tests' import LayoutPage, { type LayoutPageProps } from './layout-page' +import { redirectToUrl } from './layout-page.utils' + +jest.mock('posthog-js', () => ({ + capture: jest.fn(), + getFeatureFlagPayload: jest.fn(), + isFeatureEnabled: jest.fn(() => false), + onFeatureFlags: jest.fn(), +})) + +jest.mock('posthog-js/react', () => ({ + useFeatureFlagEnabled: jest.fn(() => true), + useFeatureFlagVariantKey: jest.fn(() => false), +})) jest.mock('@qovery/domains/clusters/feature', () => { return { ...jest.requireActual('@qovery/domains/clusters/feature'), + ClusterDeploymentProgressCard: () => null, useClusterStatuses: () => ({ data: [ { @@ -17,11 +34,36 @@ jest.mock('@qovery/domains/clusters/feature', () => { } }) -const renderComponent = (props: LayoutPageProps) => ( - - - -) +jest.mock('@qovery/domains/organizations/feature', () => ({ + FreeTrialBanner: () => null, + InvoiceBanner: () => null, + useOrganization: () => ({ data: undefined }), +})) + +jest.mock('@qovery/shared/assistant/feature', () => ({ + AssistantTrigger: () => null, +})) + +jest.mock('@qovery/shared/devops-copilot/feature', () => ({ + DevopsCopilotButton: () => null, + DevopsCopilotTrigger: () => null, +})) + +jest.mock('@qovery/shared/posthog/feature', () => ({ + AnnouncementBanner: () => null, +})) + +jest.mock('./layout-page.utils', () => ({ + redirectToUrl: jest.fn(), +})) + +jest.mock('../navigation/navigation', () => () => null) +jest.mock('../top-bar/top-bar', () => ({ children }: { children: ReactNode }) => <>{children}) +jest.mock('../../feature/spotlight-trigger/spotlight-trigger', () => () => null) + +const useFeatureFlagEnabledMock = useFeatureFlagEnabled as jest.MockedFunction + +const renderComponent = (props: LayoutPageProps) => describe('LayoutPage', () => { const props: LayoutPageProps = { @@ -29,23 +71,65 @@ describe('LayoutPage', () => { topBar: false, } + beforeEach(() => { + localStorage.clear() + jest.clearAllMocks() + useFeatureFlagEnabledMock.mockReturnValue(true) + jest.spyOn(iamFeature, 'getNewConsoleUrl').mockReturnValue('https://new-console.qovery.com/organization/123') + jest.spyOn(iamFeature, 'useConsoleRedirectPreference').mockReturnValue({ + preferredConsole: 'legacy', + isNewConsoleDefault: false, + setPreferredConsole: jest.fn(), + setIsNewConsoleDefault: jest.fn(), + }) + jest.spyOn(iamFeature, 'useUserRole').mockReturnValue({ + roles: [], + isQoveryAdminUser: false, + loading: false, + } as ReturnType) + }) + it('should render successfully', () => { const { baseElement } = renderWithProviders(renderComponent({ ...props })) expect(baseElement).toBeTruthy() }) it('should have cluster deployment error banner', () => { - props.clusters = [ - { - id: '0000-0000-0000-0000', - name: 'cluster-name', - created_at: '', - region: '', - cloud_provider: CloudProviderEnum.AWS, - }, - ] - - renderWithProviders(renderComponent({ ...props })) + renderWithProviders( + renderComponent({ + ...props, + clusters: [ + { + id: '0000-0000-0000-0000', + name: 'cluster-name', + created_at: '', + region: '', + cloud_provider: CloudProviderEnum.AWS, + }, + ], + }) + ) screen.getByText('Check the credentials configuration') }) + + it('should capture an analytics event when dismissing the console migration prompt', async () => { + const { userEvent } = renderWithProviders(renderComponent({ ...props })) + + await userEvent.click(screen.getByRole('button', { name: 'Not interested' })) + + expect(posthog.capture).toHaveBeenCalledWith('legacy-console-migration-prompt-dismissed', { + target_url: 'https://new-console.qovery.com/organization/123', + }) + }) + + it('should capture an analytics event when confirming the console migration prompt', async () => { + const { userEvent } = renderWithProviders(renderComponent({ ...props })) + + await userEvent.click(screen.getByRole('button', { name: 'Try now' })) + + expect(posthog.capture).toHaveBeenCalledWith('legacy-console-migration-prompt-confirmed', { + target_url: 'https://new-console.qovery.com/organization/123', + }) + expect(redirectToUrl).toHaveBeenCalledWith('https://new-console.qovery.com/organization/123') + }) }) diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx index a7e14e39f02..0e01628ef0d 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx @@ -1,3 +1,4 @@ +import posthog from 'posthog-js' import { useFeatureFlagEnabled, useFeatureFlagVariantKey } from 'posthog-js/react' import { type Cluster, ClusterStateEnum, type Organization } from 'qovery-typescript-axios' import { type PropsWithChildren, useEffect, useMemo } from 'react' @@ -28,6 +29,9 @@ import ConsoleMigrationPrompt from '../console-migration-prompt/console-migratio import Navigation from '../navigation/navigation' import TopBar from '../top-bar/top-bar' +const CONSOLE_MIGRATION_PROMPT_CONFIRMED = 'console-migration-prompt-confirmed' +const CONSOLE_MIGRATION_PROMPT_DISMISSED = 'console-migration-prompt-dismissed' + export interface LayoutPageProps { defaultOrganizationId: string topBar?: boolean @@ -162,10 +166,20 @@ export function LayoutPage(props: PropsWithChildren) { return } + posthog.capture(CONSOLE_MIGRATION_PROMPT_CONFIRMED, { + target_url: newConsoleUrl, + }) setIsNewConsoleDefault(true) window.location.assign(newConsoleUrl) } + const handleConsoleMigrationDismiss = () => { + posthog.capture(CONSOLE_MIGRATION_PROMPT_DISMISSED, { + target_url: newConsoleUrl, + }) + setIsConsoleMigrationBannerDismissed(true) + } + return ( <> {displayQoveryAdminBanner && ( @@ -233,7 +247,7 @@ export function LayoutPage(props: PropsWithChildren) { setIsConsoleMigrationBannerDismissed(true)} + onClose={handleConsoleMigrationDismiss} /> From 64f793a391b99f7772c6c2f2d5a2f4184f4cb99c Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Tue, 21 Apr 2026 08:54:28 +0200 Subject: [PATCH 8/8] refactor(console-redirect): extract redirection logic into a utility function and update analytics tracking for console migration prompts --- .../pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx | 4 ++-- libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx | 5 +++-- .../pages/layout/src/lib/ui/layout-page/layout-page.utils.ts | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 libs/pages/layout/src/lib/ui/layout-page/layout-page.utils.ts diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx index bf61227b9d1..f2ed1cf100d 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.spec.tsx @@ -117,7 +117,7 @@ describe('LayoutPage', () => { await userEvent.click(screen.getByRole('button', { name: 'Not interested' })) - expect(posthog.capture).toHaveBeenCalledWith('legacy-console-migration-prompt-dismissed', { + expect(posthog.capture).toHaveBeenCalledWith('console-migration-prompt-dismissed', { target_url: 'https://new-console.qovery.com/organization/123', }) }) @@ -127,7 +127,7 @@ describe('LayoutPage', () => { await userEvent.click(screen.getByRole('button', { name: 'Try now' })) - expect(posthog.capture).toHaveBeenCalledWith('legacy-console-migration-prompt-confirmed', { + expect(posthog.capture).toHaveBeenCalledWith('console-migration-prompt-confirmed', { target_url: 'https://new-console.qovery.com/organization/123', }) expect(redirectToUrl).toHaveBeenCalledWith('https://new-console.qovery.com/organization/123') diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx index 0e01628ef0d..652b40d0897 100644 --- a/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.tsx @@ -28,6 +28,7 @@ import SpotlightTrigger from '../../feature/spotlight-trigger/spotlight-trigger' import ConsoleMigrationPrompt from '../console-migration-prompt/console-migration-prompt' import Navigation from '../navigation/navigation' import TopBar from '../top-bar/top-bar' +import { redirectToUrl } from './layout-page.utils' const CONSOLE_MIGRATION_PROMPT_CONFIRMED = 'console-migration-prompt-confirmed' const CONSOLE_MIGRATION_PROMPT_DISMISSED = 'console-migration-prompt-dismissed' @@ -157,7 +158,7 @@ export function LayoutPage(props: PropsWithChildren) { } if (window.location.href !== newConsoleUrl) { - window.location.assign(newConsoleUrl) + redirectToUrl(newConsoleUrl) } }, [newConsoleUrl, isNewConsoleDefault]) @@ -170,7 +171,7 @@ export function LayoutPage(props: PropsWithChildren) { target_url: newConsoleUrl, }) setIsNewConsoleDefault(true) - window.location.assign(newConsoleUrl) + redirectToUrl(newConsoleUrl) } const handleConsoleMigrationDismiss = () => { diff --git a/libs/pages/layout/src/lib/ui/layout-page/layout-page.utils.ts b/libs/pages/layout/src/lib/ui/layout-page/layout-page.utils.ts new file mode 100644 index 00000000000..4565eee9d10 --- /dev/null +++ b/libs/pages/layout/src/lib/ui/layout-page/layout-page.utils.ts @@ -0,0 +1,3 @@ +export function redirectToUrl(url: string) { + window.location.assign(url) +}