diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_de_DE.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_de_DE.json index bb89af4ddfe7..df05d268da9b 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_de_DE.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_de_DE.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Eine ähnliche Integration ist bereits vorhanden.", "addIntegrationErrorStringParamRequired": "Erforderlich", "addIntegrationErrorIntegerParamRequired": "Geben Sie eine positive Zahl ein.", - "addIntegrationButtonAdd": "Hinzufügen" + "addIntegrationButtonAdd": "Hinzufügen", + "addIntegrationButtonCancel": "Abbrechen" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_en_GB.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_en_GB.json index 831c246e46a9..5baca67aa7c3 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_en_GB.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_en_GB.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "A similar integration already exists", "addIntegrationErrorStringParamRequired": "Required", "addIntegrationErrorIntegerParamRequired": "Please enter a positive number", - "addIntegrationButtonAdd": "Add" + "addIntegrationButtonAdd": "Add", + "addIntegrationButtonCancel": "Cancel" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_es_ES.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_es_ES.json index cf5619035b22..cfb5be99fe39 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_es_ES.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_es_ES.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Ya existe una integración similar.", "addIntegrationErrorStringParamRequired": "Requerido", "addIntegrationErrorIntegerParamRequired": "Introduzca un número positivo", - "addIntegrationButtonAdd": "Añadir" + "addIntegrationButtonAdd": "Añadir", + "addIntegrationButtonCancel": "Cancelar" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_CA.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_CA.json index 4a2b2129bbc3..c9962dde2a53 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_CA.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_CA.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Une intégration similaire existe déjà", "addIntegrationErrorStringParamRequired": "Requis", "addIntegrationErrorIntegerParamRequired": "Veuillez entrer un nombre positif", - "addIntegrationButtonAdd": "Ajouter" + "addIntegrationButtonAdd": "Ajouter", + "addIntegrationButtonCancel": "Annuler" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_FR.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_FR.json index 4a2b2129bbc3..c9962dde2a53 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_FR.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_fr_FR.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Une intégration similaire existe déjà", "addIntegrationErrorStringParamRequired": "Requis", "addIntegrationErrorIntegerParamRequired": "Veuillez entrer un nombre positif", - "addIntegrationButtonAdd": "Ajouter" + "addIntegrationButtonAdd": "Ajouter", + "addIntegrationButtonCancel": "Annuler" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_it_IT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_it_IT.json index 2c6ac4b6a870..cefb1548401b 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_it_IT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_it_IT.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Esiste già un'integrazione analoga.", "addIntegrationErrorStringParamRequired": "Obbligatorio", "addIntegrationErrorIntegerParamRequired": "Inserisci un numero positivo", - "addIntegrationButtonAdd": "Aggiungi" + "addIntegrationButtonAdd": "Aggiungi", + "addIntegrationButtonCancel": "Annulla" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pl_PL.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pl_PL.json index 40594f9fa4d2..6307bcf2cfa3 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pl_PL.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pl_PL.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Podobna integracja już istnieje.", "addIntegrationErrorStringParamRequired": "Wymagane", "addIntegrationErrorIntegerParamRequired": "Wprowadź liczbę dodatnią", - "addIntegrationButtonAdd": "Dodaj" + "addIntegrationButtonAdd": "Dodaj", + "addIntegrationButtonCancel": "Anuluj" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pt_PT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pt_PT.json index ac5bb94135ed..be5c771e02a5 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pt_PT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/integrations/Messages_pt_PT.json @@ -43,5 +43,6 @@ "addIntegrationErrorNoDuplicate": "Já existe uma integração semelhante", "addIntegrationErrorStringParamRequired": "Obrigatório", "addIntegrationErrorIntegerParamRequired": "Introduza um número positivo", - "addIntegrationButtonAdd": "Adicionar" + "addIntegrationButtonAdd": "Adicionar", + "addIntegrationButtonCancel": "Anular" } diff --git a/packages/manager/apps/pci-databases-analytics/src/components/order/flavor/FlavorSelect.component.tsx b/packages/manager/apps/pci-databases-analytics/src/components/order/flavor/FlavorSelect.component.tsx index 78f936a9ac8f..0ab5d8a0200b 100644 --- a/packages/manager/apps/pci-databases-analytics/src/components/order/flavor/FlavorSelect.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/components/order/flavor/FlavorSelect.component.tsx @@ -63,7 +63,7 @@ const FlavorsSelect = React.forwardRef( diff --git a/packages/manager/apps/pci-databases-analytics/src/components/order/price/OrderPrice.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/components/order/price/OrderPrice.spec.tsx index e843feb8ade5..728de126607f 100644 --- a/packages/manager/apps/pci-databases-analytics/src/components/order/price/OrderPrice.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/components/order/price/OrderPrice.spec.tsx @@ -3,7 +3,25 @@ import { describe, it, vi } from 'vitest'; import OrderPrice from '@/components/order/price/OrderPrice.component'; describe('OrderPrice component', () => { + const mockedPrices = { + hourly: { + price: 1000500, + tax: 1005000, + }, + monthly: { + price: 1000050000, + tax: 1000500000, + }, + }; beforeEach(() => { + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options: Record): string => { + return `${key} ${options?.price}`; + }, + }), + Trans: ({ children }: { children: React.ReactNode }) => children, + })); vi.mock('@/hooks/api/catalog/useGetCatalog.hook', () => { return { useGetCatalog: vi.fn(() => ({ @@ -27,17 +45,6 @@ describe('OrderPrice component', () => { }); it('should display Price component', async () => { - const mockedPrices = { - hourly: { - price: 10, - tax: 1, - }, - monthly: { - price: 10, - tax: 1, - }, - }; - render(); await waitFor(() => { expect(screen.getByTestId('order-price-container')).toBeInTheDocument(); @@ -45,4 +52,13 @@ describe('OrderPrice component', () => { expect(screen.getByTestId('pricing-ttc')).toBeInTheDocument(); }); }); + it('should display Price component with montly values', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('order-price-container')).toBeInTheDocument(); + expect(screen.getByTestId('pricing-ht')).toBeInTheDocument(); + expect(screen.getByText('pricing_ht 10,00 €')).toBeInTheDocument(); + expect(screen.getByText('(pricing_ttc 20,01 €)')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/components/price/Price.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/components/price/Price.spec.tsx index 318aa8151240..d3eb24f65f5b 100644 --- a/packages/manager/apps/pci-databases-analytics/src/components/price/Price.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/components/price/Price.spec.tsx @@ -1,6 +1,9 @@ import { render, screen } from '@testing-library/react'; import { vi } from 'vitest'; +import { UseQueryResult } from '@tanstack/react-query'; import Price from '@/components/price/Price.component'; +import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; +import { order } from '@/types/catalog'; vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -82,4 +85,22 @@ describe('Price component value', () => { '(pricing_ttc 0,00 €)', ); }); + + it('should display skeleton if catalog is fetching', () => { + vi.mocked(useGetCatalog).mockImplementation(() => { + return ({ + isSuccess: false, + isLoading: true, // Simulate loading state + isError: false, + data: undefined, + error: null, + refetch: vi.fn(), + status: 'loading', // Provide status explicitly + } as unknown) as UseQueryResult; + }); + render( + , + ); + expect(screen.getByTestId('pricing-skeleton')).toBeInTheDocument(); + }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/components/route-modal/RouteModal.tsx b/packages/manager/apps/pci-databases-analytics/src/components/route-modal/RouteModal.tsx new file mode 100644 index 000000000000..7f7d3f93c5db --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/components/route-modal/RouteModal.tsx @@ -0,0 +1,53 @@ +import { useNavigate } from 'react-router-dom'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, +} from '../ui/dialog'; +import { Skeleton } from '../ui/skeleton'; + +interface RouteModalProps { + backUrl?: string; + isLoading?: boolean; + children: React.ReactNode | React.ReactNode[]; +} +const RouteModal = ({ + backUrl = '../', + isLoading = false, + children, +}: RouteModalProps) => { + const navigate = useNavigate(); + const onOpenChange = (open: boolean) => { + if (!open) navigate(backUrl); + }; + + return ( + + {isLoading ? ( + + + + + + + + + + + + + + + + + + ) : ( + children + )} + + ); +}; + +export default RouteModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/components/ui/skeleton.tsx b/packages/manager/apps/pci-databases-analytics/src/components/ui/skeleton.tsx index 01b8b6d4f716..7fec2f335e3e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/components/ui/skeleton.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/components/ui/skeleton.tsx @@ -5,7 +5,7 @@ function Skeleton({ ...props }: React.HTMLAttributes) { return ( -
diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.hook.tsx index 48f1416ec44a..018f5367feba 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AddConnectionPool, @@ -13,14 +13,26 @@ export interface UseAddConnectionPool { } export function useAddConnectionPool({ onError, - onSuccess, + onSuccess: customOnSuccess, }: UseAddConnectionPool) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (cpInfo: AddConnectionPool) => { return addConnectionPool(cpInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'connectionPool', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.spec.tsx index 723b9bfe5f8a..b4ff36c375c5 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useAddConnectionPool.spec.tsx @@ -42,11 +42,7 @@ describe('useAddConnectionPool', () => { expect(databaseAPI.addConnectionPool).toHaveBeenCalledWith( addConnectionPoolProps, ); - expect(onSuccess).toHaveBeenCalledWith( - mockedConnectionPool, - addConnectionPoolProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedConnectionPool); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.hook.tsx index d3670245a8a3..e83dd979b669 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DeleteConnectionPool, deleteConnectionPool, @@ -12,14 +12,26 @@ interface UseDeleteConnectionPool { export function useDeleteConnectionPool({ onError, - onSuccess, + onSuccess: customOnSuccess, }: UseDeleteConnectionPool) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (connectionPoolInfo: DeleteConnectionPool) => { return deleteConnectionPool(connectionPoolInfo); }, onError, - onSuccess, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'connectionPool', + ], + }); + customOnSuccess(); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.spec.tsx index 94d91d551181..288dffbaa6eb 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useDeleteConnectionPool.spec.tsx @@ -37,11 +37,7 @@ describe('useDeleteConnectionPool', () => { expect(databaseAPI.deleteConnectionPool).toHaveBeenCalledWith( deleteConnectionPoolProps, ); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteConnectionPoolProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.hook.tsx index 95f75349e453..aa0992c8abf8 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EditConnectionPool, @@ -13,14 +13,26 @@ export interface UseEditConnectionPool { } export function useEditConnectionPool({ onError, - onSuccess, + onSuccess: customOnSuccess, }: UseEditConnectionPool) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (cpInfo: EditConnectionPool) => { return editConnectionPool(cpInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'connectionPool', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.spec.tsx index 1abcf51fb30d..2197b2cdd734 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/connectionPool/useEditConnectionPool.spec.tsx @@ -42,11 +42,7 @@ describe('useEditConnectionPool', () => { expect(databaseAPI.editConnectionPool).toHaveBeenCalledWith( editConnectionPoolProps, ); - expect(onSuccess).toHaveBeenCalledWith( - mockedConnectionPool, - editConnectionPoolProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedConnectionPool); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.hook.tsx index 143d3baf8db2..0a4dc662f3d0 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as database from '@/types/cloud/project/database'; import { AddDatabase, addDatabase } from '@/data/api/database/database.api'; import { CdbError } from '@/data/api/database'; @@ -7,13 +7,28 @@ interface UseAddDatabase { onError: (cause: CdbError) => void; onSuccess: (database: database.service.Database) => void; } -export function useAddDatabase({ onError, onSuccess }: UseAddDatabase) { +export function useAddDatabase({ + onError, + onSuccess: customOnSuccess, +}: UseAddDatabase) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (databaseInfo: AddDatabase) => { return addDatabase(databaseInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'database', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.spec.tsx index f8c4e4f8c771..24603b1b042d 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useAddDatabase.spec.tsx @@ -36,11 +36,7 @@ describe('useAddDatabase', () => { await waitFor(() => { expect(databaseAPI.addDatabase).toHaveBeenCalledWith(addDatabaseProps); - expect(onSuccess).toHaveBeenCalledWith( - mockedDatabase, - addDatabaseProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedDatabase); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.hook.tsx index 694c5966a4c4..5c3ec6b1c46f 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DeleteDatabase, deleteDatabase, @@ -9,13 +9,28 @@ interface UseDeleteDatabase { onError: (cause: CdbError) => void; onSuccess: () => void; } -export function useDeleteDatabase({ onError, onSuccess }: UseDeleteDatabase) { +export function useDeleteDatabase({ + onError, + onSuccess: customOnSuccess, +}: UseDeleteDatabase) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (databaseInfo: DeleteDatabase) => { return deleteDatabase(databaseInfo); }, onError, - onSuccess, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'database', + ], + }); + customOnSuccess(); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.spec.tsx index 5e6f1baefa06..415b320b19a1 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/database/useDeleteDatabase.spec.tsx @@ -38,11 +38,7 @@ describe('useDeleteDatabase', () => { expect(databaseAPI.deleteDatabase).toHaveBeenCalledWith( deleteDatabaseProps, ); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteDatabaseProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.hook.tsx index d395daca8bc6..82f1da491f76 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as database from '@/types/cloud/project/database'; import { AddIntegration, @@ -10,13 +10,28 @@ interface UseAddIntegration { onError: (cause: CdbError) => void; onSuccess: (database: database.service.Integration) => void; } -export function useAddIntegration({ onError, onSuccess }: UseAddIntegration) { +export function useAddIntegration({ + onError, + onSuccess: customOnSuccess, +}: UseAddIntegration) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (integrationInfo: AddIntegration) => { return addIntegration(integrationInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'integration', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.spec.tsx index bb3f0ddd5e9d..b1aba2b8d70c 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useAddIntegration.spec.tsx @@ -43,11 +43,7 @@ describe('useAddIntegration', () => { expect(databaseAPI.addIntegration).toHaveBeenCalledWith( addIntegrationProps, ); - expect(onSuccess).toHaveBeenCalledWith( - mockedIntegrations, - addIntegrationProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedIntegrations); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.hook.tsx index fc0a9402890b..9f0ed4a4bd5a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DeleteIntegration, deleteIntegration, @@ -11,14 +11,26 @@ interface UseDeleteIntegration { } export function useDeleteIntegration({ onError, - onSuccess, + onSuccess: customOnSuccess, }: UseDeleteIntegration) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (integrationInfo: DeleteIntegration) => { return deleteIntegration(integrationInfo); }, onError, - onSuccess, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'integration', + ], + }); + customOnSuccess(); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.spec.tsx index 327ae19febd8..c5fdf7d55f53 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/integration/useDeleteIntegration.spec.tsx @@ -40,11 +40,7 @@ describe('useDeleteIntegration', () => { expect(databaseAPI.deleteIntegration).toHaveBeenCalledWith( deleteIntegrationProps, ); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteIntegrationProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.hook.tsx index f400292b4b59..d279efd446bd 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { CdbError } from '@/data/api/database'; import * as database from '@/types/cloud/project/database'; import { AddNamespace, addNamespace } from '@/data/api/database/namespace.api'; @@ -7,13 +7,28 @@ export interface UseAddNamespace { onError: (cause: CdbError) => void; onSuccess: (namespace: database.m3db.Namespace) => void; } -export function useAddNamespace({ onError, onSuccess }: UseAddNamespace) { +export function useAddNamespace({ + onError, + onSuccess: customOnSuccess, +}: UseAddNamespace) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (npInfo: AddNamespace) => { return addNamespace(npInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'namespace', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.spec.tsx index 233676b72586..434f9891a2a2 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useAddNamespace.spec.tsx @@ -41,11 +41,7 @@ describe('useAddNamespace', () => { await waitFor(() => { expect(databaseAPI.addNamespace).toHaveBeenCalledWith(addNamespaceProps); - expect(onSuccess).toHaveBeenCalledWith( - mockedNamespaces, - addNamespaceProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedNamespaces); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.hook.tsx index 8d5a4a9ed153..96dc3f40c4af 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DeleteNamespace, deleteNamespace, @@ -9,13 +9,28 @@ export interface UseDeleteNamespace { onError: (cause: CdbError) => void; onSuccess: () => void; } -export function useDeleteNamespace({ onError, onSuccess }: UseDeleteNamespace) { +export function useDeleteNamespace({ + onError, + onSuccess: customOnSuccess, +}: UseDeleteNamespace) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (npInfo: DeleteNamespace) => { return deleteNamespace(npInfo); }, onError, - onSuccess, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'namespace', + ], + }); + customOnSuccess(); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.spec.tsx index 626048d03a87..b7b72537df1a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useDeleteNamespace.spec.tsx @@ -40,11 +40,7 @@ describe('useDeleteNamespace', () => { expect(databaseAPI.deleteNamespace).toHaveBeenCalledWith( deleteNamespaceProps, ); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteNamespaceProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamepsace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamepsace.spec.tsx index 584112f3420a..b1c31054e76d 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamepsace.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamepsace.spec.tsx @@ -43,11 +43,7 @@ describe('useEditNamepsace', () => { expect(databaseAPI.editNamespace).toHaveBeenCalledWith( editNamespaceProps, ); - expect(onSuccess).toHaveBeenCalledWith( - mockedNamespaces, - editNamespaceProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedNamespaces); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamespace.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamespace.hook.tsx index dda5fabbef2b..43eea34bd036 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamespace.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/namespace/useEditNamespace.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EditNamespace, editNamespace, @@ -10,13 +10,28 @@ export interface UsEditNamespace { onError: (cause: CdbError) => void; onSuccess: (namespace: database.m3db.Namespace) => void; } -export function useEditNamespace({ onError, onSuccess }: UsEditNamespace) { +export function useEditNamespace({ + onError, + onSuccess: customOnSuccess, +}: UsEditNamespace) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (npInfo: EditNamespace) => { return editNamespace(npInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'namespace', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.hook.tsx index 7f5a8f91ff0b..9078fef55fcb 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { AddUser, GenericUser, addUser } from '@/data/api/database/user.api'; import { CdbError } from '@/data/api/database'; @@ -6,13 +6,28 @@ export interface UseAddUser { onError: (cause: CdbError) => void; onSuccess: (user: GenericUser) => void; } -export function useAddUser({ onError, onSuccess }: UseAddUser) { +export function useAddUser({ + onError, + onSuccess: customOnSuccess, +}: UseAddUser) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (userInfo: AddUser) => { return addUser(userInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'user', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.spec.tsx index d433f85d1b98..9d40efdac9ce 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useAddUser.spec.tsx @@ -42,11 +42,7 @@ describe('useAddUser', () => { await waitFor(() => { expect(databaseAPI.addUser).toHaveBeenCalledWith(addUserProps); - expect(onSuccess).toHaveBeenCalledWith( - mockedDatabaseUser, - addUserProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedDatabaseUser); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.hook.tsx index 1856c62f9544..4d68fd84a701 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DeleteUser, deleteUser } from '@/data/api/database/user.api'; import { CdbError } from '@/data/api/database'; @@ -6,13 +6,28 @@ interface UseDeleteUser { onError: (cause: CdbError) => void; onSuccess: () => void; } -export function useDeleteUser({ onError, onSuccess }: UseDeleteUser) { +export function useDeleteUser({ + onError, + onSuccess: customOnSuccess, +}: UseDeleteUser) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (userInfo: DeleteUser) => { return deleteUser(userInfo); }, onError, - onSuccess, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'user', + ], + }); + customOnSuccess(); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.spec.tsx index e0440a847e83..382b85f2e123 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useDeleteUser.spec.tsx @@ -39,11 +39,7 @@ describe('useDeleteUser', () => { await waitFor(() => { expect(databaseAPI.deleteUser).toHaveBeenCalledWith(deleteUserProps); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteUserProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.hook.tsx index b215283a231f..b5c4beecb4b9 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.hook.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.hook.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { EditUser, GenericUser, editUser } from '@/data/api/database/user.api'; import { CdbError } from '@/data/api/database'; @@ -6,13 +6,28 @@ export interface UseEditUser { onError: (cause: CdbError) => void; onSuccess: (user: GenericUser) => void; } -export function useEditUser({ onError, onSuccess }: UseEditUser) { +export function useEditUser({ + onError, + onSuccess: customOnSuccess, +}: UseEditUser) { + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (userInfo: EditUser) => { return editUser(userInfo); }, onError, - onSuccess, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: [ + variables.projectId, + 'database', + variables.engine, + variables.serviceId, + 'user', + ], + }); + customOnSuccess(data); + }, }); return { diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.spec.tsx index 3e4da59ab670..2bcf7f1af619 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/api/database/user/useEditUser.spec.tsx @@ -4,13 +4,10 @@ import { vi } from 'vitest'; import * as databaseAPI from '@/data/api/database/user.api'; import * as database from '@/types/cloud/project/database'; import { QueryClientWrapper } from '@/__tests__/helpers/wrappers/QueryClientWrapper'; -import { useResetUserPassword } from './useResetUserPassword.hook'; -import { useDeleteUser } from './useDeleteUser.hook'; import { useEditUser } from './useEditUser.hook'; import { mockedDatabaseUser, mockedDatabaseUserEdition, - mockedDatabaseUserWithPassword, } from '@/__tests__/helpers/mocks/databaseUser'; vi.mock('@/data/api/database/user.api', () => ({ @@ -45,81 +42,7 @@ describe('useEditUser', () => { await waitFor(() => { expect(databaseAPI.editUser).toHaveBeenCalledWith(editUserProps); - expect(onSuccess).toHaveBeenCalledWith( - mockedDatabaseUser, - editUserProps, - undefined, - ); - }); - }); -}); - -describe('useResetUserPassword', () => { - it('should call useResetUserPassword on mutation with data', async () => { - const projectId = 'projectId'; - const engine = database.EngineEnum.mysql; - const serviceId = 'serviceId'; - const onSuccess = vi.fn(); - const onError = vi.fn(); - - vi.mocked(databaseAPI.resetUserPassword).mockResolvedValue( - mockedDatabaseUserWithPassword, - ); - const { result } = renderHook( - () => useResetUserPassword({ onError, onSuccess }), - { wrapper: QueryClientWrapper }, - ); - - const resetPasswordUserProps = { - projectId, - engine, - serviceId, - userId: 'userId', - }; - result.current.resetUserPassword(resetPasswordUserProps); - - await waitFor(() => { - expect(databaseAPI.resetUserPassword).toHaveBeenCalledWith( - resetPasswordUserProps, - ); - expect(onSuccess).toHaveBeenCalledWith( - mockedDatabaseUserWithPassword, - resetPasswordUserProps, - undefined, - ); - }); - }); -}); - -describe('useDeleteUser', () => { - it('should call useDeleteUser on mutation with data', async () => { - const projectId = 'projectId'; - const engine = database.EngineEnum.mysql; - const serviceId = 'serviceId'; - const onSuccess = vi.fn(); - const onError = vi.fn(); - - vi.mocked(databaseAPI.deleteUser).mockResolvedValue(undefined); - - const { result } = renderHook(() => useDeleteUser({ onError, onSuccess }), { - wrapper: QueryClientWrapper, - }); - - const deleteUserProps = { - projectId, - engine, - serviceId, - userId: 'userId', - }; - result.current.deleteUser(deleteUserProps); - - await waitFor(() => { - expect(databaseAPI.deleteUser).toHaveBeenCalledWith(deleteUserProps); - expect(onSuccess).toHaveBeenCalledWith( - undefined, - deleteUserProps, - undefined, - ); + expect(onSuccess).toHaveBeenCalledWith(mockedDatabaseUser); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.spec.tsx deleted file mode 100644 index 344f3bf3a05c..000000000000 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.spec.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, it } from 'vitest'; -import { act, renderHook, screen, waitFor } from '@testing-library/react'; -import { DEFAULT_OPEN_VALUE, useModale } from '@/hooks/useModale'; -import { RouterWithLocationWrapper } from '../__tests__/helpers/wrappers/RouterWithLocationWrapper'; - -describe('useModale', () => { - it('should init properly', async () => { - const { result } = renderHook(() => useModale('modalKey'), { - wrapper: RouterWithLocationWrapper, - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(false); - expect(result.current.value).toBe(undefined); - expect(screen.getByText('/test')).toBeInTheDocument(); - }); - }); - - it('should open properly with DEFAULT_OPEN_VALUE', async () => { - const modalkey = 'modalKey'; - const page = '/modal'; - const { result } = renderHook(() => useModale(modalkey), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - act(() => { - result.current.open(); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(true); - expect(result.current.value).toBe(DEFAULT_OPEN_VALUE); - expect( - screen.getByText(`${page}?${modalkey}=${DEFAULT_OPEN_VALUE}`), - ).toBeInTheDocument(); - }); - }); - - it('should open properly with custom value', async () => { - const modalkey = 'modalKey'; - const page = '/modal'; - const customValue = '123456'; - const { result } = renderHook(() => useModale(modalkey), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - act(() => { - result.current.open(customValue); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(true); - expect(result.current.value).toBe(customValue); - expect( - screen.getByText(`${page}?${modalkey}=${customValue}`), - ).toBeInTheDocument(); - }); - }); - - it('should close properly', async () => { - const modalkey = 'modalKey'; - const page = '/modal'; - const customValue = '123456'; - const { result } = renderHook(() => useModale(modalkey), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - act(() => { - result.current.open(customValue); - result.current.close(); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(false); - expect(result.current.value).toBe(undefined); - expect(screen.getByText(`${page}`)).toBeInTheDocument(); - }); - }); - - it('should toggle properly', async () => { - const modalkey = 'modalKey'; - const page = '/modal'; - const { result } = renderHook(() => useModale(modalkey), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(false); - }); - act(() => { - result.current.open(); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(true); - }); - act(() => { - result.current.toggle(); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(false); - }); - act(() => { - result.current.toggle(); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(true); - }); - }); - - it('should expose a controller', async () => { - const modalkey = 'modalKey'; - const page = '/modal'; - const { result } = renderHook(() => useModale(modalkey), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - await waitFor(() => { - expect(result.current.controller.open).toBe(false); - }); - act(() => { - result.current.open(); - }); - await waitFor(() => { - expect(result.current.controller.open).toBe(true); - }); - act(() => { - result.current.controller.onOpenChange(true); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(true); - }); - act(() => { - result.current.controller.onOpenChange(false); - }); - await waitFor(() => { - expect(result.current.isOpen).toBe(false); - }); - }); -}); diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.tsx b/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.tsx deleted file mode 100644 index 5e99ac8ca43c..000000000000 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/useModale.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useQueryState } from './useQueryState'; - -export const DEFAULT_OPEN_VALUE = 'true'; - -export interface ModalController { - open: boolean; - onOpenChange: (newValue: boolean) => void; -} -export function useModale(queryParamKey: string) { - const [modalState, setModalState] = useQueryState(queryParamKey); - - const setOpen = (value = DEFAULT_OPEN_VALUE) => setModalState(value); - const setClose = () => setModalState(undefined); - const toggle = () => (modalState ? setClose() : setOpen()); - const isOpen = !!modalState; - const controller: ModalController = { - open: isOpen, - onOpenChange: (newStatus: boolean) => - newStatus === false ? setClose() : null, - }; - return { - isOpen, - value: modalState, - open: setOpen, - close: setClose, - toggle, - controller, - }; -} diff --git a/packages/manager/apps/pci-databases-analytics/src/hooks/useTracking.ts b/packages/manager/apps/pci-databases-analytics/src/hooks/useTracking.ts index f6a06548e3da..f1d2ef7c1d66 100644 --- a/packages/manager/apps/pci-databases-analytics/src/hooks/useTracking.ts +++ b/packages/manager/apps/pci-databases-analytics/src/hooks/useTracking.ts @@ -1,8 +1,10 @@ -import { useContext, useEffect } from 'react'; +import { useLocation, useMatches, useParams } from 'react-router-dom'; +import { useContext, useEffect, useRef } from 'react'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import usePciProject from './api/project/usePciProject.hook'; import { PCI_LEVEL2 } from '@/configuration/tracking.constants'; import { PlanCode } from '@/types/cloud/Project'; +import { useGetService } from './api/database/service/useGetService.hook'; // Set the project mode, needed to track discovery actions function useProjectModeTracking() { @@ -47,3 +49,70 @@ export function useTrackPage(pageTracking: string) { }); }, []); } + +export function useTrackPageAuto() { + useProjectModeTracking(); + const { shell } = useContext(ShellContext); + const { trackPage } = shell.tracking; + const matches = useMatches(); + const location = useLocation(); + const params = useParams(); + // Last match is the current route, we need it + // to get the tracking key associated with the route + const match = matches[matches.length - 1]; + const serviceQuery = useGetService( + match.params.projectId, + match.params.serviceId, + { + enabled: !!match.params.serviceId, + }, + ); + const service = serviceQuery.data; + const hasTrackedRef = useRef(false); + + useEffect(() => { + if (hasTrackedRef.current) return; + if (params.serviceId && !service) return; + const prefix = 'PublicCloud::databases_analytics::{category}'; + const { id } = match; + const routerTrackingKey = (match?.handle as { tracking: string })?.tracking; + const suffix = + routerTrackingKey || id || location.pathname.split('/').pop(); + let injectedTrackingKey = `${prefix}::${suffix}`; + + // inject params in key. For exemple replace {category} by params.category if it exists + injectedTrackingKey = injectedTrackingKey.replace(/{(\w+)}/g, (_, key) => { + return params[key] || ''; + }); + + // Inject service data into the key if available + if (service) { + injectedTrackingKey = injectedTrackingKey.replace( + /{service\.(\w+(\.\w+)*)}/g, + (_, path) => { + // Split the path by "." and traverse the `service` object + const value = path.split('.').reduce((acc: unknown, key: string) => { + if (typeof acc === 'object' && acc !== null && key in acc) { + return (acc as Record)[key]; + } + return undefined; + }, service); + + return typeof value === 'string' ? value : ''; // Ensure only strings are returned + }, + ); + } + + // replace . by :: + injectedTrackingKey = injectedTrackingKey.replaceAll('.', '::'); + trackPage({ + name: injectedTrackingKey, + level2: PCI_LEVEL2, + }); + hasTrackedRef.current = true; + }, [location.pathname, params.serviceId, service, serviceQuery.isLoading]); + + useEffect(() => { + hasTrackedRef.current = false; + }, [location.pathname]); +} diff --git a/packages/manager/apps/pci-databases-analytics/src/lib/availabilitiesHelper.ts b/packages/manager/apps/pci-databases-analytics/src/lib/availabilitiesHelper.ts index 05f944eea29b..c292de49ba44 100644 --- a/packages/manager/apps/pci-databases-analytics/src/lib/availabilitiesHelper.ts +++ b/packages/manager/apps/pci-databases-analytics/src/lib/availabilitiesHelper.ts @@ -286,7 +286,7 @@ export function createTree( suggestions: database.availability.Suggestion[], catalog: order.publicOrder.Catalog, ) { - return availabilities.reduce((acc, curr) => { + const tree = availabilities.reduce((acc, curr) => { const engineSuggestion = suggestions.find((s) => s.engine === curr.engine); // Map engine const treeEngine = mapEngine(acc, curr, capabilities, engineSuggestion); @@ -317,4 +317,12 @@ export function createTree( setPrices(curr, catalog, treePlan, treeFlavor); return acc; }, [] as Engine[]); + // sanitize: if default version returned from suggestions by api does not exist, + // set the last one as default + tree.forEach((engine) => { + if (!engine.versions.find((v) => v.name === engine.defaultVersion)) { + engine.defaultVersion = engine.versions[engine.versions.length - 1].name; + } + }); + return tree; } diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/Root.layout.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/Root.layout.tsx index cca9aa4674fd..44312eb03656 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/Root.layout.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/Root.layout.tsx @@ -14,6 +14,7 @@ import { UserActivityProvider } from '@/contexts/UserActivityContext'; import { getProject } from '@/data/api/project/project.api'; import { useLoadingIndicatorContext } from '@/contexts/LoadingIndicator.context'; import { USER_INACTIVITY_TIMEOUT } from '@/configuration/polling.constants'; +import { useTrackPageAuto } from '@/hooks/useTracking'; export function breadcrumb({ params }: BreadcrumbHandleParams) { return ( @@ -80,6 +81,8 @@ function RoutingSynchronisation() { defineCurrentPage(`app.pci-databases-analytics.${match[0].id}`); }, [location]); + useTrackPageAuto(); + return <>; } diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/Root.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/Root.page.tsx index 132c1fd3ad5f..8703056bd344 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/Root.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/Root.page.tsx @@ -1,4 +1,33 @@ +import { redirect } from 'react-router-dom'; +import queryClient from '@/query.client'; import Services from './services/Services.page'; +import { getServices } from '@/data/api/database/service.api'; + +interface ServicesProps { + params: { + projectId: string; + category: string; + }; + request: Request; +} +export const Loader = ({ params }: ServicesProps) => { + // check if we have a correct category + const { category, projectId } = params; + // check if we have a correct projectId + return queryClient + .fetchQuery({ + queryKey: [projectId, 'database/service'], + queryFn: () => getServices({ projectId }), + }) + .then((services) => { + if (services.length === 0) { + return redirect( + `/pci/projects/${projectId}/databases-analytics/${category}/services/onboarding`, + ); + } + return null; + }); +}; export default function Root() { return ; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/Root.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/Root.spec.tsx index e19d3fa75080..79099fe189c3 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/Root.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/Root.spec.tsx @@ -3,6 +3,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import Root from '@/pages/Root.page'; import { Locale } from '@/hooks/useLocale'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import * as serviceApi from '@/data/api/database/service.api'; +import * as database from '@/types/cloud/project/database'; describe('Home page', () => { beforeEach(() => { @@ -14,6 +17,18 @@ describe('Home page', () => { }), })); + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + redirect: vi.fn(), + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + }), + }; + }); + vi.mock('@/data/api/database/service.api', () => ({ getServices: vi.fn(() => []), })); @@ -35,10 +50,13 @@ describe('Home page', () => { }); }); - it('should display onboarding pages', async () => { + it('should display service page', async () => { + vi.mocked(serviceApi.getServices).mockResolvedValue([mockedService]); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { - expect(screen.getByTestId('onbaording-container')).toBeInTheDocument(); + expect( + screen.getByTestId('services-guides-container'), + ).toBeInTheDocument(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.page.tsx index 4d0d04c728bc..b36c025d9c81 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.page.tsx @@ -1,24 +1,22 @@ import { useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { Outlet, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Plus } from 'lucide-react'; import { useGetServices } from '@/hooks/api/database/service/useGetServices.hook'; import ServicesList from './_components/ServiceListTable.component'; -import Onboarding from './_components/Onboarding.component'; import LegalMentions from '../_components/LegalMentions.component'; import { POLLING } from '@/configuration/polling.constants'; import Link from '@/components/links/Link.component'; import { Button } from '@/components/ui/button'; import Guides from '@/components/guides/Guides.component'; import { GuideSections } from '@/types/guide'; -import { useTrackPage, useTrackAction } from '@/hooks/useTracking'; +import { useTrackAction } from '@/hooks/useTracking'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { TRACKING } from '@/configuration/tracking.constants'; import * as database from '@/types/cloud/project/database'; const Services = () => { const { t } = useTranslation('pci-databases-analytics/services'); - useTrackPage(TRACKING.servicesList.page()); const track = useTrackAction(); const { projectId, category } = useParams(); const { isUserActive } = useUserActivityContext(); @@ -35,9 +33,6 @@ const Services = () => { }, [servicesQuery.data, category]); if (servicesQuery.isLoading) return ; - if (servicesQuery.isSuccess && filteredServices.length === 0) { - return ; - } return ( <>
{ {t('createNewService')} - + + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.spec.tsx index 5483f208748d..a7923ce4cd0c 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/Services.spec.tsx @@ -13,8 +13,8 @@ import * as serviceApi from '@/data/api/database/service.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { mockedUser } from '@/__tests__/helpers/mocks/user'; import { mockedService } from '@/__tests__/helpers/mocks/services'; -import { TERMINATE_CONFIRMATION } from '@/configuration/polling.constants'; +const mockedUsedNavigate = vi.fn(); describe('Services List page', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -44,7 +44,7 @@ describe('Services List page', () => { useTranslation: () => ({ t: (key: string) => key, }), - Trans: ({ children }: any) => children, + Trans: ({ children }: { children: React.ReactNode }) => children, })); vi.mock('@/data/api/database/service.api', () => ({ @@ -57,6 +57,7 @@ describe('Services List page', () => { const mod = await vi.importActual('react-router-dom'); return { ...mod, + useNavigate: () => mockedUsedNavigate, useParams: () => ({ projectId: 'projectId', category: database.engine.CategoryEnum.all, @@ -81,7 +82,9 @@ describe('Services List page', () => { vi.mocked(serviceApi.getServices).mockImplementationOnce(() => null); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { - expect(screen.getByTestId('onbaording-container')).toBeInTheDocument(); + expect( + screen.getByTestId('services-guides-container'), + ).toBeInTheDocument(); }); }); @@ -126,84 +129,17 @@ describe('Open modals', () => { vi.clearAllMocks(); }); - it('shows and close rename-service modal', async () => { + it('open rename service modal', async () => { await openButtonInMenu('service-action-rename-button'); await waitFor(() => { - expect(screen.getByTestId('rename-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.keyDown(screen.getByTestId('rename-service-modal'), { - key: 'Escape', - code: 'Escape', - }); - }); - await waitFor(() => { - expect( - screen.queryByTestId('rename-service-modal'), - ).not.toBeInTheDocument(); - }); - }); - - it('call update service on rename success', async () => { - await openButtonInMenu('service-action-rename-button'); - await waitFor(() => { - expect(screen.getByTestId('rename-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('rename-service-input'), { - target: { - value: 'newName', - }, - }); - fireEvent.click(screen.getByTestId('rename-service-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('rename-service-modal'), - ).not.toBeInTheDocument(); - expect(serviceApi.editService).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./rename/serviceId'); }); }); - it('open and close delete service Modal', async () => { + it('open delete service Modal', async () => { await openButtonInMenu('service-action-delete-button'); await waitFor(() => { - expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-service-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-service-modal'), - ).not.toBeInTheDocument(); - }); - }); - - it('call delete service on success', async () => { - await openButtonInMenu('service-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); - expect( - screen.getByTestId('delete-service-confirmation-input'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.change( - screen.getByTestId('delete-service-confirmation-input'), - { - target: { - value: TERMINATE_CONFIRMATION, - }, - }, - ); - fireEvent.click(screen.getByTestId('delete-service-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-service-modal'), - ).not.toBeInTheDocument(); - expect(serviceApi.deleteService).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete/serviceId'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/Service.context.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/Service.context.tsx index 5724f60bb57c..86e172b1b4f1 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/Service.context.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/Service.context.tsx @@ -1,6 +1,7 @@ import { UseQueryResult } from '@tanstack/react-query'; -import { useOutletContext, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import * as database from '@/types/cloud/project/database'; +import { useGetService } from '@/hooks/api/database/service/useGetService.hook'; // Share data with the child routes export type ServiceLayoutContext = { @@ -8,7 +9,7 @@ export type ServiceLayoutContext = { serviceQuery: UseQueryResult; }; export const useServiceData = () => { - const { projectId, category } = useParams(); - const { service, serviceQuery } = useOutletContext() as ServiceLayoutContext; - return { projectId, category, service, serviceQuery }; + const { projectId, category, serviceId } = useParams(); + const serviceQuery = useGetService(projectId, serviceId); + return { projectId, category, service: serviceQuery.data, serviceQuery }; }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.component.tsx index 7b3dee627cf7..45b3dc2163e1 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.component.tsx @@ -4,7 +4,6 @@ import { useMemo, useState } from 'react'; import { AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -13,62 +12,60 @@ import { } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; import { Input } from '@/components/ui/input'; - -import { ModalController } from '@/hooks/useModale'; - import * as database from '@/types/cloud/project/database'; import { useDeleteService } from '@/hooks/api/database/service/useDeleteService.hook'; import { useGetServices } from '@/hooks/api/database/service/useGetServices.hook'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { useTrackAction, useTrackPage } from '@/hooks/useTracking'; +import { useTrackAction } from '@/hooks/useTracking'; import { TRACKING } from '@/configuration/tracking.constants'; import { useGetIntegrations } from '@/hooks/api/database/integration/useGetIntegrations.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; import { TERMINATE_CONFIRMATION } from '@/configuration/polling.constants'; +import { Label } from '@/components/ui/label'; +import RouteModal from '@/components/route-modal/RouteModal'; interface DeleteServiceModalProps { service: database.Service; - controller: ModalController; onSuccess?: (service: database.Service) => void; onError?: (service: Error) => void; } const DeleteService = ({ service, - controller, onError, onSuccess, }: DeleteServiceModalProps) => { const { projectId } = useParams(); - useTrackPage( - TRACKING.deleteService.page(service.engine, service.nodes[0].region), - ); const track = useTrackAction(); const { t } = useTranslation('pci-databases-analytics/services/service'); const toast = useToast(); const [confirmationInput, setConfirmationInput] = useState(''); const integrationsQuery = useGetIntegrations( projectId, - service.engine, - service.id, + service?.engine, + service?.id, { enabled: - service.capabilities.integrations?.read === - database.service.capability.StateEnum.enabled, + !!service?.id && + service?.capabilities.integrations?.read === + database.service.capability.StateEnum.enabled, }, ); - const serivcesQuery = useGetServices(projectId, { - enabled: integrationsQuery.isSuccess && integrationsQuery.data?.length > 0, + const servicesQuery = useGetServices(projectId, { + enabled: !!integrationsQuery.data, }); const integratedServices = useMemo(() => { + // return empty arrray if there are no integrations for + // the selected engine (eg mongoDB) if ( - !integrationsQuery.isSuccess || - !serivcesQuery.isSuccess || - integrationsQuery.data.length === 0 - ) + service?.capabilities.integrations?.read !== + database.service.capability.StateEnum.enabled + ) { return []; - return serivcesQuery.data.filter((serv) => + } + if (!integrationsQuery.data || !servicesQuery.data) return null; + return servicesQuery.data.filter((serv) => integrationsQuery.data.find( (integration) => (integration.destinationServiceId === serv.id && @@ -77,7 +74,7 @@ const DeleteService = ({ integration.sourceServiceId !== service.id), ), ); - }, [integrationsQuery.data, serivcesQuery.data]); + }, [integrationsQuery, servicesQuery]); const { deleteService, isPending } = useDeleteService({ onError: (err) => { @@ -119,53 +116,60 @@ const DeleteService = ({ engine: service.engine, }); }; + return ( - - - + + + {t('deleteServiceTitle')} - {integratedServices.length > 0 && ( - - -
- -
- {integratedServices.length === 1 ? ( -

{t('deleteServiceIntegrationDescription')}

- ) : ( -

{t('deleteServiceIntegrationsDescription')}

- )} -
    - {integratedServices.map((integratedService) => ( -
  • - {integratedService.description} -
  • - ))} -
+
+ {integratedServices?.length > 0 && ( + + +
+ +
+ {integratedServices.length === 1 ? ( +

{t('deleteServiceIntegrationDescription')}

+ ) : ( +

{t('deleteServiceIntegrationsDescription')}

+ )} +
    + {integratedServices.map((integratedService) => ( +
  • + {integratedService.description} +
  • + ))} +
+
-
- - - )} -

- {t('deleteServiceDescription', { - name: service.description, - })} -

-
-

{t('deleteServiceConfirmation')}

- { - setConfirmationInput(event.target.value); - }} - /> + + + )} +

+ {t('deleteServiceDescription', { + name: service?.description, + })} +

+
+ + { + setConfirmationInput(event.target.value); + }} + /> +
- +
+ ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.spec.tsx new file mode 100644 index 000000000000..d68e14effd5c --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/DeleteService.spec.tsx @@ -0,0 +1,162 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import * as serviceApi from '@/data/api/database/service.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import DeleteService from './DeleteService.component'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import { TERMINATE_CONFIRMATION } from '@/configuration/polling.constants'; +import { mockedIntegrations } from '@/__tests__/helpers/mocks/integrations'; +import { StateEnum } from '@/types/cloud/project/database/service/capability'; + +describe('Delete service modal', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@/data/api/database/service.api', () => ({ + getServices: vi.fn(() => [ + mockedService, + { + ...mockedService, + id: mockedIntegrations.destinationServiceId, + description: 'destinationService', + }, + ]), + deleteService: vi.fn(), + })); + vi.mock('@/data/api/database/integration.api', () => ({ + getServiceIntegrations: vi.fn(() => [mockedIntegrations]), + })); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('delete-service-modal')).toBeInTheDocument(); + }); + }); + + it('should delete the service on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change( + screen.getByTestId('delete-service-confirmation-input'), + { + target: { value: TERMINATE_CONFIRMATION }, + }, + ); + }); + act(() => { + fireEvent.click(screen.getByTestId('delete-service-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.deleteService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteServiceToastSuccessTitle', + description: 'deleteServiceToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(serviceApi.deleteService).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change( + screen.getByTestId('delete-service-confirmation-input'), + { + target: { value: TERMINATE_CONFIRMATION }, + }, + ); + }); + act(() => { + fireEvent.click(screen.getByTestId('delete-service-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.deleteService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteServiceToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); + + it('should disable the submit button if confirmation input is incorrect', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change( + screen.getByTestId('delete-service-confirmation-input'), + { + target: { value: 'WRONG_CONFIRMATION' }, + }, + ); + }); + expect(screen.getByTestId('delete-service-submit-button')).toBeDisabled(); + }); + + it('should display integrations if they exist', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + await waitFor(() => { + expect( + screen.getByText('deleteServiceIntegrationDescription'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx index 5e534c1ea7e3..204df2e4f929 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.component.tsx @@ -16,7 +16,6 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import * as database from '@/types/cloud/project/database'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -24,29 +23,22 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; -import { useTrackAction, useTrackPage } from '@/hooks/useTracking'; +import { useTrackAction } from '@/hooks/useTracking'; import { TRACKING } from '@/configuration/tracking.constants'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import RouteModal from '@/components/route-modal/RouteModal'; interface RenameServiceProps { service: database.Service; - controller: ModalController; onSuccess?: (service: database.Service) => void; onError?: (error: Error) => void; } -const RenameService = ({ - service, - controller, - onError, - onSuccess, -}: RenameServiceProps) => { +const RenameService = ({ service, onError, onSuccess }: RenameServiceProps) => { // import translations const { projectId } = useParams(); - useTrackPage(TRACKING.renameService.page(service.engine)); const track = useTrackAction(); const { t } = useTranslation('pci-databases-analytics/services/service'); const toast = useToast(); @@ -110,7 +102,7 @@ const RenameService = ({ }); return ( - + @@ -162,7 +154,7 @@ const RenameService = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.spec.tsx new file mode 100644 index 000000000000..d294f7bd1584 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/RenameService.spec.tsx @@ -0,0 +1,112 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import * as serviceApi from '@/data/api/database/service.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import RenameService from './RenameService.component'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; + +describe('Rename service modal', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@/data/api/database/service.api', () => ({ + editService: vi.fn((s) => s), + })); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + vi.mock('@/hooks/useTracking', () => ({ + useTrackAction: vi.fn(() => vi.fn()), + })); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('rename-service-modal')).toBeInTheDocument(); + }); + }); + + it('should rename the service on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change(screen.getByTestId('rename-service-input'), { + target: { value: 'New Service Name' }, + }); + }); + act(() => { + fireEvent.click(screen.getByTestId('rename-service-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalledWith({ + serviceId: mockedService.id, + projectId: 'projectId', + engine: mockedService.engine, + data: { + description: 'New Service Name', + }, + }); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'renameServiceToastSuccessTitle', + description: 'renameServiceToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(serviceApi.editService).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change(screen.getByTestId('rename-service-input'), { + target: { value: 'New Service Name' }, + }); + }); + act(() => { + fireEvent.click(screen.getByTestId('rename-service-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'renameServiceToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.page.tsx index 2f6220caf168..3db338816475 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.page.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; import { add } from 'date-fns'; -import { useNavigate } from 'react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; import { Pen } from 'lucide-react'; import Link from '@/components/links/Link.component'; import * as database from '@/types/cloud/project/database'; @@ -9,8 +9,6 @@ import { getColumns } from './_components/BackupsTableColumns.component'; import { DataTable } from '@/components/ui/data-table'; import { POLLING } from '@/configuration/polling.constants'; import { Button } from '@/components/ui/button'; -import { useModale } from '@/hooks/useModale'; -import RestoreServiceModal from './_components/Restore.component'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import Guides from '@/components/guides/Guides.component'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; @@ -24,26 +22,21 @@ const Backups = () => { const { t } = useTranslation( 'pci-databases-analytics/services/service/backups', ); - const restoreModal = useModale('restore'); const navigate = useNavigate(); - const { projectId, service, serviceQuery } = useServiceData(); + const { projectId, service } = useServiceData(); const { isUserActive } = useUserActivityContext(); const backupsQuery = useGetBackups(projectId, service.engine, service.id, { refetchInterval: isUserActive && POLLING.BACKUPS, }); const columns = getColumns({ onRestoreClick: (backup) => { - restoreModal.open(backup.id); + navigate(`./restore/${backup.id}`); }, onForkClick: (backup) => { navigate(`fork?backup=${backup.id}`); }, }); - const selectedBackup = backupsQuery.data?.find( - (b) => b.id === restoreModal.value, - ); - return ( <>
@@ -109,7 +102,7 @@ const Backups = () => { variant="outline" size="sm" className="text-base" - onClick={() => restoreModal.open()} + onClick={() => navigate('./restore')} > {t('actionRestore')} @@ -132,18 +125,7 @@ const Backups = () => {
)} - - {backupsQuery.data && ( - { - restoreModal.close(); - serviceQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.spec.tsx index 2d9c3354ddcb..0d9ffc4ae727 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/Backups.spec.tsx @@ -16,6 +16,7 @@ import { Locale } from '@/hooks/useLocale'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; import { mockedBackup } from '@/__tests__/helpers/mocks/backup'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -32,10 +33,18 @@ const mockedService = { }, }; +const mockedUsedNavigate = vi.fn(); describe('Backups page', () => { beforeEach(() => { vi.restoreAllMocks(); // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -107,7 +116,7 @@ describe('Backups page', () => { capabilities: {}, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('fork-button')).toBeNull(); @@ -129,7 +138,7 @@ describe('Backups page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const restoreButton = screen.queryByTestId('restore-backup-button'); @@ -172,7 +181,7 @@ describe('Open restore modals', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); const ResizeObserverMock = vi.fn(() => ({ observe: vi.fn(), @@ -189,68 +198,20 @@ describe('Open restore modals', () => { vi.clearAllMocks(); }); - it('open and close restore modal from restore button', async () => { + it('open restore modal from restore button', async () => { act(() => { fireEvent.click(screen.getByTestId('restore-backup-button')); }); await waitFor(() => { - expect(screen.getByTestId('restore-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.keyDown(screen.getByTestId('restore-modal'), { - key: 'Escape', - code: 'Escape', - }); - }); - await waitFor(() => { - expect(screen.queryByTestId('restore-modal')).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./restore'); }); }); - it('restore from backup on restore success', async () => { + it('open restore modal from backup menu', async () => { await openButtonInMenu('backups-action-restore-button'); await waitFor(() => { - expect(screen.getByTestId('restore-modal')).toBeInTheDocument(); - expect(screen.getByTestId('restore-submit-button')).toBeInTheDocument(); - expect(screen.getByTestId('restore-submit-button')).not.toBeDisabled(); - }); - act(() => { - fireEvent.click(screen.getByTestId('restore-submit-button')); - }); - await waitFor(() => { - expect(backupsApi.restoreBackup).toHaveBeenCalled(); - }); - }); - - it('restore from now on restore success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('restore-backup-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('restore-modal')).toBeInTheDocument(); - }); - - act(() => { - fireEvent.click(screen.getByTestId('restore-modal-radio-now')); - fireEvent.click(screen.getByTestId('restore-submit-button')); - }); - await waitFor(() => { - expect(backupsApi.restoreBackup).toHaveBeenCalled(); - }); - }); - it('restore from pitr on restore success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('restore-backup-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('restore-modal')).toBeInTheDocument(); - }); - - act(() => { - fireEvent.click(screen.getByTestId('restore-modal-radio-pitr')); - fireEvent.click(screen.getByTestId('restore-submit-button')); - }); - await waitFor(() => { - expect(backupsApi.restoreBackup).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith( + './restore/testBackup123', + ); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/_components/Restore.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.modal.tsx similarity index 94% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/_components/Restore.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.modal.tsx index 65e848a9d0b2..4197ce4951e3 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/_components/Restore.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.modal.tsx @@ -1,3 +1,4 @@ +import { useNavigate, useParams } from 'react-router-dom'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useTranslation } from 'react-i18next'; @@ -14,16 +15,13 @@ import { FormMessage, } from '@/components/ui/form'; import { Button } from '@/components/ui/button'; -import * as database from '@/types/cloud/project/database'; import { - Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; import { ForkSourceType } from '@/types/orderFunnel'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; @@ -47,32 +45,27 @@ import { cn } from '@/lib/utils'; import { Calendar } from '@/components/ui/calendar'; import { TimePicker } from '@/components/ui/time-picker'; import { useDateFnsLocale } from '@/hooks/useDateFnsLocale.hook'; -import { CdbError } from '@/data/api/database'; import { useRestoreBackup } from '@/hooks/api/database/backup/useRestoreBackup.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetBackups } from '@/hooks/api/database/backup/useGetBackups.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface RestoreServiceModalProps { - controller: ModalController; - backups: database.Backup[]; - backup?: database.Backup; - onSuccess?: () => void; - onError?: (error: CdbError) => void; -} - -const RestoreServiceModal = ({ - controller, - backups, - backup, - onSuccess, - onError, -}: RestoreServiceModalProps) => { +const RestoreServiceModal = () => { // import translations + const navigate = useNavigate(); + const { backupId } = useParams(); const dateLocale = useDateFnsLocale(); const { projectId, service } = useServiceData(); const { t } = useTranslation( 'pci-databases-analytics/services/service/backups', ); const toast = useToast(); + const backupsQuery = useGetBackups(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + const backups = backupsQuery.data; + const backup = backups?.find((b) => b.id === backupId); + const { restoreBackup, isPending } = useRestoreBackup({ onError: (err) => { toast.toast({ @@ -80,18 +73,13 @@ const RestoreServiceModal = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('restoreBackupToastSuccessTitle'), description: t('restoreBackupToastSuccessDescription'), }); - if (onSuccess) { - onSuccess(); - } + navigate('../'); }, }); @@ -161,16 +149,13 @@ const RestoreServiceModal = ({ }); }); - useEffect(() => { - form.reset(); - }, [controller.open]); useEffect(() => { form.setValue('backupId', backup?.id); }, [backup]); const selectedType = form.watch('type'); return ( - + @@ -344,7 +329,7 @@ const RestoreServiceModal = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.spec.tsx new file mode 100644 index 000000000000..8ee820c59697 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/backups/restore/Restore.spec.tsx @@ -0,0 +1,140 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import RestoreServiceModal from './Restore.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as backupApi from '@/data/api/database/backup.api'; +import { mockedBackup as mockedBackupOrig } from '@/__tests__/helpers/mocks/backup'; +import { Locale } from '@/hooks/useLocale'; + +const mockedBackup = { + ...mockedBackupOrig, + id: '4e201af3-cb92-4b9d-a788-ab3359205e28', +}; +const mockedService = { + ...mockedServiceOrig, + id: '98054125-0189-4250-96d7-5e06cd8fd645', +}; +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + backupId: mockedBackup.id, + }), + }; +}); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); +vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; +}); +vi.mock('@/data/api/database/backup.api', () => ({ + getServiceBackups: vi.fn(() => [mockedBackup]), + restoreBackup: vi.fn(), +})); + +describe('Restore Service Modal', () => { + beforeEach(() => { + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + + const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('restore-modal')).toBeInTheDocument(); + }); + }); + + it('should restore the service on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('restore-submit-button')); + }); + await waitFor(() => { + expect(backupApi.restoreBackup).toHaveBeenCalledWith( + expect.objectContaining({ + projectId: 'projectId', + engine: mockedService.engine, + serviceId: mockedService.id, + backupId: mockedBackup.id, + }), + ); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'restoreBackupToastSuccessTitle', + description: 'restoreBackupToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(backupApi.restoreBackup).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('restore-submit-button')); + }); + await waitFor(() => { + expect(backupApi.restoreBackup).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'restoreBackupToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Database.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Database.page.tsx index 9ce5aca29a23..f6e1ae6da4b9 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Database.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Database.page.tsx @@ -1,15 +1,13 @@ import { useTranslation } from 'react-i18next'; import { ColumnDef } from '@tanstack/react-table'; import { Plus } from 'lucide-react'; +import { Outlet, useNavigate } from 'react-router-dom'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; import * as database from '@/types/cloud/project/database'; import { useServiceData } from '../Service.context'; import { DataTable } from '@/components/ui/data-table'; import { getColumns } from './_components/DatabasesTableColumns.component'; import { Button } from '@/components/ui/button'; -import { useModale } from '@/hooks/useModale'; -import AddDatabase from './_components/AddDatabase.component'; -import DeleteDatabase from './_components/DeleteDatabase.component'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { POLLING } from '@/configuration/polling.constants'; import { useGetDatabases } from '@/hooks/api/database/database/useGetDatabases.hook'; @@ -27,9 +25,8 @@ const Databases = () => { const { t } = useTranslation( 'pci-databases-analytics/services/service/databases', ); - const addModale = useModale('add'); - const deleteModale = useModale('delete'); - const { projectId, service, serviceQuery } = useServiceData(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); const { isUserActive } = useUserActivityContext(); const databasesQuery = useGetDatabases( projectId, @@ -39,11 +36,9 @@ const Databases = () => { refetchInterval: isUserActive && POLLING.DATABASES, }, ); - const deletingDatabase = databasesQuery.data?.find( - (d) => d.id === deleteModale.value, - ); const columns: ColumnDef[] = getColumns({ - onDeleteClick: (db: database.service.Database) => deleteModale.open(db.id), + onDeleteClick: (db: database.service.Database) => + navigate(`./delete/${db.id}`), }); return ( <> @@ -58,7 +53,7 @@ const Databases = () => { service.capabilities.databases?.create === database.service.capability.StateEnum.disabled } - onClick={() => addModale.open()} + onClick={() => navigate('./add')} > {t('addButtonLabel')} @@ -73,27 +68,7 @@ const Databases = () => {
)} - { - addModale.close(); - databasesQuery.refetch(); - serviceQuery.refetch(); - }} - /> - - {deletingDatabase && ( - { - deleteModale.close(); - databasesQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Databases.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Databases.spec.tsx index 703626a4ab9e..d3bfb47f6c32 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Databases.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/Databases.spec.tsx @@ -18,6 +18,7 @@ import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/Route import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -31,10 +32,18 @@ const mockedService = { }, }, }; +const mockedUsedNavigate = vi.fn(); describe('Databases page', () => { beforeEach(() => { // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -50,7 +59,7 @@ describe('Databases page', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, })), })); vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { @@ -104,7 +113,7 @@ describe('Databases page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('add-button')).toBeInTheDocument(); @@ -117,7 +126,7 @@ describe('Databases page', () => { capabilities: {}, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('add-button')).toBeNull(); @@ -134,7 +143,7 @@ describe('Databases page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const addButton = screen.queryByTestId('add-button'); @@ -179,81 +188,14 @@ describe('Open modals', () => { fireEvent.click(screen.getByTestId('add-button')); }); await waitFor(() => { - expect(screen.getByTestId('add-database-modal')).toBeInTheDocument(); - }); - }); - it('closes add user modal', async () => { - act(() => { - fireEvent.click(screen.getByTestId('add-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-database-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-database-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-database-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on add user success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('add-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-database-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('add-database-name-input'), { - target: { - value: 'newdb', - }, - }); - fireEvent.click(screen.getByTestId('add-database-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-database-modal'), - ).not.toBeInTheDocument(); - expect(databasesApi.getServiceDatabases).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add'); }); }); it('shows delete databases modal', async () => { await openButtonInMenu('databases-action-delete-button'); await waitFor(() => { - expect(screen.getByTestId('delete-database-modal')).toBeInTheDocument(); - }); - }); - it('closes delete databases modal', async () => { - await openButtonInMenu('databases-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-database-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-database-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-database-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on delete user success', async () => { - await openButtonInMenu('databases-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-database-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-database-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-database-modal'), - ).not.toBeInTheDocument(); - expect(databasesApi.getServiceDatabases).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete/databaseId'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/AddDatabase.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.modal.tsx similarity index 85% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/AddDatabase.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.modal.tsx index 2543e0811c9f..e2f65ee59271 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/AddDatabase.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.modal.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useTranslation } from 'react-i18next'; @@ -13,9 +13,7 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import * as database from '@/types/cloud/project/database'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -23,26 +21,17 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; import { useAddDatabase } from '@/hooks/api/database/database/useAddDatabase.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface AddDatabaseModalProps { - service: database.Service; - controller: ModalController; - onSuccess?: (database: database.service.Database) => void; - onError?: (error: Error) => void; -} - -const AddDatabase = ({ - service, - controller, - onError, - onSuccess, -}: AddDatabaseModalProps) => { +const AddDatabase = () => { // import translations const { projectId } = useParams(); + const { service } = useServiceData(); + const navigate = useNavigate(); const { t } = useTranslation( 'pci-databases-analytics/services/service/databases', ); @@ -54,9 +43,6 @@ const AddDatabase = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: (addedDb) => { toast.toast({ @@ -65,9 +51,7 @@ const AddDatabase = ({ name: addedDb.name, }), }); - if (onSuccess) { - onSuccess(addedDb); - } + navigate('../'); }, }); // define the schema for the form @@ -99,7 +83,7 @@ const AddDatabase = ({ }); return ( - + @@ -144,7 +128,7 @@ const AddDatabase = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.spec.tsx new file mode 100644 index 000000000000..3a40bd30eb54 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/add/AddDatabase.spec.tsx @@ -0,0 +1,173 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import AddDatabase from './AddDatabase.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import * as databaseApi from '@/data/api/database/database.api'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; + +const mockedUsedNavigate = vi.fn(); +vi.mock('@/data/api/database/database.api', () => ({ + addDatabase: vi.fn(), +})); + +vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + service: mockedService, + })), +})); + +vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; +}); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe('AddDatabase Component', () => { + const mockToast = useToast(); + + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + useParams: () => ({ + projectId: 'projectId', + }), + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the modal', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + + expect(screen.getByTestId('add-database-modal')).toBeInTheDocument(); + expect(screen.getByTestId('add-database-name-input')).toBeInTheDocument(); + expect( + screen.getByTestId('add-database-submit-button'), + ).toBeInTheDocument(); + }); + + it('should show validation error on empty submission', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + + const submitButton = screen.getByTestId('add-database-submit-button'); + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(screen.getByText('addDatabaseErrorMinLength')).toBeInTheDocument(); + }); + }); + + it('should call addDatabase API on valid form submission', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + + const nameInput = screen.getByTestId('add-database-name-input'); + const submitButton = screen.getByTestId('add-database-submit-button'); + + act(() => { + fireEvent.change(nameInput, { target: { value: 'MyDatabase' } }); + }); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(databaseApi.addDatabase).toHaveBeenCalledWith({ + serviceId: mockedService.id, + projectId: 'projectId', + engine: mockedService.engine, + name: 'MyDatabase', + }); + }); + }); + + it('should show success toast on successful database addition', async () => { + vi.mocked(databaseApi.addDatabase).mockResolvedValue({ + name: 'MyDatabase', + }); + + render(, { wrapper: RouterWithQueryClientWrapper }); + + const nameInput = screen.getByTestId('add-database-name-input'); + const submitButton = screen.getByTestId('add-database-submit-button'); + + act(() => { + fireEvent.change(nameInput, { target: { value: 'MyDatabase' } }); + }); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(mockToast.toast).toHaveBeenCalledWith({ + title: 'addDatabaseToastSuccessTitle', + description: 'addDatabaseToastSuccessDescription', + }); + }); + }); + + it('should show error toast on failed database addition', async () => { + vi.mocked(databaseApi.addDatabase).mockRejectedValue(apiErrorMock); + + render(, { wrapper: RouterWithQueryClientWrapper }); + + const nameInput = screen.getByTestId('add-database-name-input'); + const submitButton = screen.getByTestId('add-database-submit-button'); + + act(() => { + fireEvent.change(nameInput, { target: { value: 'MyDatabase' } }); + }); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(databaseApi.addDatabase).toHaveBeenCalled(); + expect(mockToast.toast).toHaveBeenCalledWith({ + title: 'addDatabaseToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); + + it('should navigate back when the modal is closed', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + + const cancelButton = screen.getByTestId('add-database-cancel-button'); + act(() => { + fireEvent.click(cancelButton); + }); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalledWith('../'); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/DeleteDatabase.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.modal.tsx similarity index 66% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/DeleteDatabase.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.modal.tsx index 1bd3c1153d13..7da7ca8a5044 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/_components/DeleteDatabase.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.modal.tsx @@ -1,10 +1,8 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import * as dbTypes from '@/types/cloud/project/database'; - import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -12,28 +10,29 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; import { useDeleteDatabase } from '@/hooks/api/database/database/useDeleteDatabase.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetDatabases } from '@/hooks/api/database/database/useGetDatabases.hook'; +import { useServiceData } from '../../Service.context'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteDatabaseModalProps { - service: dbTypes.Service; - controller: ModalController; - database: dbTypes.service.Database; - onSuccess?: (database: dbTypes.service.Database) => void; - onError?: (error: Error) => void; -} - -const DeleteDatabase = ({ - service, - database, - controller, - onError, - onSuccess, -}: DeleteDatabaseModalProps) => { +const DeleteDatabase = () => { // import translations - const { projectId } = useParams(); + const { projectId, databaseId } = useParams(); + const navigate = useNavigate(); + const { service } = useServiceData(); + const databasesQuery = useGetDatabases( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const databases = databasesQuery.data; + const deletedDatabase = databases?.find((d) => d.id === databaseId); + const { t } = useTranslation( 'pci-databases-analytics/services/service/databases', ); @@ -45,41 +44,40 @@ const DeleteDatabase = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteDatabaseToastSuccessTitle'), description: t('deleteDatabaseToastSuccessDescription', { - name: database.name, + name: deletedDatabase.name, }), }); - if (onSuccess) { - onSuccess(database); - } + navigate('../'); }, }); + useEffect(() => { + if (databases && !deletedDatabase) navigate('../'); + }, [databases, deletedDatabase]); + const handleDelete = () => { deleteDatabase({ serviceId: service.id, projectId, engine: service.engine, - databaseId: database.id, + databaseId: deletedDatabase.id, }); }; return ( - + {t('deleteDatabaseTitle')} - {t('deleteDatabaseDescription', { name: database.name })} + {t('deleteDatabaseDescription', { name: deletedDatabase?.name })} @@ -102,7 +100,7 @@ const DeleteDatabase = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.spec.tsx new file mode 100644 index 000000000000..6dfd0f6719cc --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/databases/delete/DeleteDatabase.spec.tsx @@ -0,0 +1,119 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import * as database from '@/types/cloud/project/database'; +import { Locale } from '@/hooks/useLocale'; +import * as databaseApi from '@/data/api/database/database.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import DeleteDatabase from './DeleteDatabase.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; + +describe('Delete database modal', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + databaseId: mockedDatabase.id, + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@/data/api/database/database.api', () => ({ + getServiceDatabases: vi.fn(() => [mockedDatabase]), + addDatabase: vi.fn(), + deleteDatabase: vi.fn(), + })); + + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + category: 'operational', + serviceQuery: {} as UseQueryResult, + })), + })); + + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('should open the modal', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.queryByTestId('delete-database-modal')).toBeInTheDocument(); + expect( + screen.getByTestId('delete-database-submit-button'), + ).toBeInTheDocument(); + }); + }); + it('should delete a database on submit', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-database-submit-button')); + }); + await waitFor(() => { + expect(databaseApi.deleteDatabase).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteDatabaseToastSuccessTitle', + description: 'deleteDatabaseToastSuccessDescription', + }); + }); + }); + it('should call onError when api failed', async () => { + vi.mocked(databaseApi.deleteDatabase).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-database-submit-button')); + }); + await waitFor(() => { + expect(databaseApi.deleteDatabase).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteDatabaseToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.page.tsx index c1dbcbb61915..1634a494b6df 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.page.tsx @@ -1,17 +1,15 @@ import { useTranslation } from 'react-i18next'; import { ColumnDef } from '@tanstack/react-table'; import { Plus } from 'lucide-react'; +import { Outlet, useNavigate } from 'react-router-dom'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; import * as database from '@/types/cloud/project/database'; import { useServiceData } from '../Service.context'; import { DataTable } from '@/components/ui/data-table'; import { Button } from '@/components/ui/button'; -import { useModale } from '@/hooks/useModale'; import { POLLING } from '@/configuration/polling.constants'; import { getColumns } from './_components/IntegrationListColumns.component'; import { useGetServices } from '@/hooks/api/database/service/useGetServices.hook'; -import DeleteIntegration from './_components/DeleteIntegration.component'; -import AddIntegration from './_components/AddIntegration.component'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { GuideSections } from '@/types/guide'; import Guides from '@/components/guides/Guides.component'; @@ -34,9 +32,8 @@ const Integrations = () => { const { t } = useTranslation( 'pci-databases-analytics/services/service/integrations', ); - const addModale = useModale('add'); - const deleteModale = useModale('delete'); - const { projectId, service, serviceQuery } = useServiceData(); + const { projectId, service } = useServiceData(); + const navigate = useNavigate(); const { isUserActive } = useUserActivityContext(); const servicesQuery = useGetServices(projectId, { refetchInterval: isUserActive && POLLING.INTEGRATIONS, @@ -62,12 +59,9 @@ const Integrations = () => { ), })); - const deletingIntegration = integrations.find( - (i) => i.id === deleteModale.value, - ); const columns: ColumnDef[] = getColumns({ - onDeleteClick: (db: database.service.Integration) => - deleteModale.open(db.id), + onDeleteClick: (integration: database.service.Integration) => + navigate(`./delete/${integration.id}`), }); return ( @@ -76,7 +70,6 @@ const Integrations = () => {

{t('title')}

- {service.capabilities.integrations?.create && ( )} - {!isLoading ? ( ) : ( @@ -101,29 +93,7 @@ const Integrations = () => { )} - - { - addModale.close(); - integrationsQuery.refetch(); - serviceQuery.refetch(); - }} - /> - - {deletingIntegration && ( - { - deleteModale.close(); - integrationsQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.spec.tsx index 55563e5d69da..845fc3ba29d7 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/Integrations.spec.tsx @@ -22,6 +22,7 @@ import { import { mockedIntegrations } from '@/__tests__/helpers/mocks/integrations'; import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; import { useToast } from '@/components/ui/use-toast'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedNewService = { @@ -35,10 +36,18 @@ const mockedNewService = { }, }, }; +const mockedUsedNavigate = vi.fn(); describe('Integrations page', () => { beforeEach(() => { // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -124,7 +133,7 @@ describe('Integrations page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('integrations-add-button')).toBeInTheDocument(); @@ -139,7 +148,7 @@ describe('Integrations page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('integrations-add-button')).toBeNull(); @@ -156,7 +165,7 @@ describe('Integrations page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const addButton = screen.queryByTestId('integrations-add-button'); @@ -196,91 +205,19 @@ describe('Open modals', () => { vi.clearAllMocks(); }); - it('open and close add integrations modal', async () => { + it('shows add modal', async () => { act(() => { fireEvent.click(screen.getByTestId('integrations-add-button')); }); await waitFor(() => { - expect(screen.getByTestId('add-integrations-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.keyDown(screen.getByTestId('add-integrations-modal'), { - key: 'Escape', - code: 'Escape', - }); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-integrations-modal'), - ).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add'); }); }); - it('shows delete integrations modal', async () => { - await openButtonInMenu('integrations-action-delete-button'); - await waitFor(() => { - expect( - screen.getByTestId('delete-integrations-modal'), - ).toBeInTheDocument(); - }); - }); - it('closes delete integrations modal', async () => { + it('shows delete databases modal', async () => { await openButtonInMenu('integrations-action-delete-button'); await waitFor(() => { - expect( - screen.getByTestId('delete-integrations-modal'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-integrations-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-integrations-modal'), - ).not.toBeInTheDocument(); - }); - }); - - it('delete integrations on error trigger toast', async () => { - const errorMsg = { - description: 'api error message', - title: 'deleteIntegrationToastErrorTitle', - variant: 'destructive', - }; - vi.mocked(integrationApi.deleteIntegration).mockImplementationOnce(() => { - throw apiErrorMock; - }); - await openButtonInMenu('integrations-action-delete-button'); - await waitFor(() => { - expect( - screen.getByTestId('delete-integrations-modal'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-integrations-submit-button')); - }); - await waitFor(() => { - expect(integrationApi.deleteIntegration).toHaveBeenCalled(); - expect(useToast().toast).toHaveBeenCalledWith(errorMsg); - }); - }); - - it('refetch data on delete integrations success', async () => { - await openButtonInMenu('integrations-action-delete-button'); - await waitFor(() => { - expect( - screen.getByTestId('delete-integrations-modal'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-integrations-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-integrations-modal'), - ).not.toBeInTheDocument(); - expect(integrationApi.deleteIntegration).toHaveBeenCalled(); - expect(integrationApi.getServiceIntegrations).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete/integrationId'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegration.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegration.modal.tsx similarity index 92% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegration.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegration.modal.tsx index f67957e8a271..3be3de9ca7ab 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegration.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegration.modal.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { Form, FormControl, @@ -11,14 +12,13 @@ import { import { Button } from '@/components/ui/button'; import * as database from '@/types/cloud/project/database'; import { - Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; import { Select, @@ -28,32 +28,21 @@ import { SelectValue, } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; -import { useServiceData } from '../../Service.context'; import { useAddIntegrationForm } from './useAddIntegrationForm.hook'; -import { CdbError } from '@/data/api/database'; import { Alert, AlertTitle } from '@/components/ui/alert'; import { useAddIntegration } from '@/hooks/api/database/integration/useAddIntegration.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface AddIntegrationModalProps { - service: database.Service; - integrations: database.service.Integration[]; - controller: ModalController; - onSuccess?: (integration: database.service.Integration) => void; - onError?: (error: CdbError) => void; -} - -const AddIntegration = ({ - controller, - onError, - onSuccess, -}: AddIntegrationModalProps) => { - const { service, projectId } = useServiceData(); - const model = useAddIntegrationForm(); - +const AddIntegration = () => { + const { projectId } = useParams(); + const { service } = useServiceData(); + const navigate = useNavigate(); const { t } = useTranslation( 'pci-databases-analytics/services/service/integrations', ); + const model = useAddIntegrationForm(); const toast = useToast(); const { addIntegration, isPending } = useAddIntegration({ onError: (err) => { @@ -62,9 +51,6 @@ const AddIntegration = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: (addedIntegration) => { toast.toast({ @@ -73,9 +59,7 @@ const AddIntegration = ({ name: addedIntegration.type, }), }); - if (onSuccess) { - onSuccess(addedIntegration); - } + navigate('../'); }, }); @@ -96,10 +80,6 @@ const AddIntegration = ({ }); }); - useEffect(() => { - if (!controller.open) model.form.reset(); - }, [controller.open]); - const errors = useMemo(() => { const messages: string[] = []; const formErrors = model.form.formState.errors; @@ -109,8 +89,9 @@ const AddIntegration = ({ } return messages; }, [model.form.formState.errors]); + return ( - + @@ -303,6 +284,15 @@ const AddIntegration = ({ ))} + + + + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegrations.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegrations.spec.tsx similarity index 92% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegrations.spec.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegrations.spec.tsx index 4a30dd97ddff..60e3b2addd21 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/AddIntegrations.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/AddIntegrations.spec.tsx @@ -7,7 +7,6 @@ import { waitFor, } from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; -import Integrations from '@/pages/services/[serviceId]/integrations/Integrations.page'; import * as database from '@/types/cloud/project/database'; import { Locale } from '@/hooks/useLocale'; import * as integrationApi from '@/data/api/database/integration.api'; @@ -25,6 +24,7 @@ import { mockedServiceInteMySQL, } from '@/__tests__/helpers/mocks/services'; import { handleSelectOption } from '@/__tests__/helpers/selectHelper'; +import AddIntegration from './AddIntegration.modal'; describe('Integrations page', () => { beforeEach(() => { @@ -95,12 +95,8 @@ describe('Integrations page', () => { }); it('open and close add integrations modal', async () => { - render(, { wrapper: RouterWithQueryClientWrapper }); - act(() => { - fireEvent.click(screen.getByTestId('integrations-add-button')); - }); + render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { - expect(screen.getByTestId('add-integrations-modal')).toBeInTheDocument(); expect(screen.getByTestId('add-integrations-modal')).toBeVisible(); expect( screen.getByTestId('select-integration-trigger'), diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/useAddIntegrationForm.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/useAddIntegrationForm.hook.tsx similarity index 100% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/useAddIntegrationForm.hook.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/add/useAddIntegrationForm.hook.tsx diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/DeleteIntegration.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.modal.tsx similarity index 64% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/DeleteIntegration.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.modal.tsx index ce64b25aa777..6f943cc1495f 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/_components/DeleteIntegration.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.modal.tsx @@ -1,9 +1,8 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; -import * as dbTypes from '@/types/cloud/project/database'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -11,28 +10,27 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; -import { IntegrationWithServices } from '../Integrations.page'; import { useDeleteIntegration } from '@/hooks/api/database/integration/useDeleteIntegration.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import { useGetIntegrations } from '@/hooks/api/database/integration/useGetIntegrations.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteIntegrationProps { - service: dbTypes.Service; - controller: ModalController; - integration: IntegrationWithServices; - onSuccess?: (integration: IntegrationWithServices) => void; - onError?: (error: Error) => void; -} - -const DeleteIntegration = ({ - service, - integration, - controller, - onError, - onSuccess, -}: DeleteIntegrationProps) => { - const { projectId } = useParams(); +const DeleteIntegration = () => { + const { projectId, integrationId } = useParams(); + const navigate = useNavigate(); + const { service } = useServiceData(); + const integrationsQuery = useGetIntegrations( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const integrations = integrationsQuery.data; + const deletedIntegration = integrations?.find((i) => i.id === integrationId); const { t } = useTranslation( 'pci-databases-analytics/services/service/integrations', ); @@ -44,41 +42,42 @@ const DeleteIntegration = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteIntegrationToastSuccessTitle'), description: t('deleteIntegrationToastSuccessDescription', { - type: integration.type, + type: deletedIntegration.type, }), }); - if (onSuccess) { - onSuccess(integration); - } + navigate('../'); }, }); + useEffect(() => { + if (integrations && !deletedIntegration) navigate('../'); + }, [integrations, deletedIntegration]); + const handleDelete = () => { deleteIntegration({ serviceId: service.id, projectId, engine: service.engine, - integrationId: integration.id, + integrationId: deletedIntegration.id, }); }; return ( - + {t('deleteIntegrationTitle')} - {t('deleteIntegrationDescription', { type: integration.type })} + {t('deleteIntegrationDescription', { + type: deletedIntegration?.type, + })} @@ -101,7 +100,7 @@ const DeleteIntegration = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.spec.tsx new file mode 100644 index 000000000000..d03d0ac94bc8 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/integrations/delete/DeleteIntegration.spec.tsx @@ -0,0 +1,127 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import * as database from '@/types/cloud/project/database'; +import { Locale } from '@/hooks/useLocale'; +import * as integrationsApi from '@/data/api/database/integration.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import DeleteIntegration from './DeleteIntegration.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import { + mockedCapabilitiesIntegrations, + mockedIntegrations, +} from '@/__tests__/helpers/mocks/integrations'; + +describe('Delete integration modal', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + integrationId: mockedIntegrations.id, + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@/data/api/database/integration.api', () => ({ + getServiceIntegrations: vi.fn(() => [mockedIntegrations]), + getServiceCapabilitiesIntegrations: vi.fn(() => [ + mockedCapabilitiesIntegrations, + ]), + addIntegration: vi.fn(), + deleteIntegration: vi.fn(), + })); + + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + category: 'operational', + serviceQuery: {} as UseQueryResult, + })), + })); + + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('should open the modal', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect( + screen.queryByTestId('delete-integrations-modal'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('delete-integrations-submit-button'), + ).toBeInTheDocument(); + }); + }); + it('should delete an integration on submit', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-integrations-submit-button')); + }); + await waitFor(() => { + expect(integrationsApi.deleteIntegration).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteIntegrationToastSuccessTitle', + description: 'deleteIntegrationToastSuccessDescription', + }); + }); + }); + it('should call onError when api failed', async () => { + vi.mocked(integrationsApi.deleteIntegration).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-integrations-submit-button')); + }); + await waitFor(() => { + expect(integrationsApi.deleteIntegration).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteIntegrationToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespace.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespace.page.tsx index f9952f0c5cb4..670484429f2e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespace.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespace.page.tsx @@ -2,17 +2,15 @@ import { useTranslation } from 'react-i18next'; import { ColumnDef } from '@tanstack/react-table'; import { Plus } from 'lucide-react'; +import { Outlet, useNavigate } from 'react-router-dom'; import * as database from '@/types/cloud/project/database'; import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/ui/data-table'; -import { useModale } from '@/hooks/useModale'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { useServiceData } from '../Service.context'; import { POLLING } from '@/configuration/polling.constants'; import { getColumns } from './_components/NamespacesTableColumns.component'; -import DeleteNamespaceModal from './_components/DeleteNamespace.component'; -import AddEditNamespace from './_components/AddEditNamespace.component'; import { NAMESPACES_CONFIG } from './_components/formNamespace/namespace.constants'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; import { useGetNamespaces } from '@/hooks/api/database/namespace/useGetNamespaces.hook'; @@ -27,14 +25,12 @@ export function breadcrumb() { } const Namespaces = () => { + const navigate = useNavigate(); const { t } = useTranslation( 'pci-databases-analytics/services/service/namespaces', ); const { projectId, service } = useServiceData(); const { isUserActive } = useUserActivityContext(); - const addModale = useModale('add'); - const deleteModale = useModale('delete'); - const editModale = useModale('edit'); const namespacesQuery = useGetNamespaces( projectId, service.engine, @@ -46,19 +42,11 @@ const Namespaces = () => { const columns: ColumnDef[] = getColumns({ onEditClick: (namespace: database.m3db.Namespace) => - editModale.open(namespace.id), + navigate(`./edit/${namespace.id}`), onDeleteClick: (namespace: database.m3db.Namespace) => - deleteModale.open(namespace.id), + navigate(`./delete/${namespace.id}`), }); - const namespaceToDelete: database.m3db.Namespace = namespacesQuery.data?.find( - (np) => np.id === deleteModale.value, - ); - - const namespaceToEdit: database.m3db.Namespace = namespacesQuery.data?.find( - (np) => np.id === editModale.value, - ); - return ( <>

{t('title')}

@@ -80,7 +68,7 @@ const Namespaces = () => { variant={'outline'} size="sm" className="text-base" - onClick={() => addModale.open()} + onClick={() => navigate('./add')} > {t('addButtonLabel')} @@ -97,44 +85,7 @@ const Namespaces = () => { )} - {namespacesQuery.isSuccess && ( - { - addModale.close(); - namespacesQuery.refetch(); - }} - /> - )} - - {namespaceToDelete && ( - { - deleteModale.close(); - namespacesQuery.refetch(); - }} - /> - )} - - {namespacesQuery.isSuccess && namespaceToEdit && ( - { - editModale.close(); - namespacesQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespaces.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespaces.spec.tsx index 262b34da87ce..e0d93fca22e9 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespaces.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/Namespaces.spec.tsx @@ -17,6 +17,7 @@ import * as namespaceApi from '@/data/api/database/namespace.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; import { mockedNamespaces } from '@/__tests__/helpers/mocks/namespaces'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -36,11 +37,19 @@ const ResizeObserverMock = vi.fn(() => ({ unobserve: vi.fn(), disconnect: vi.fn(), })); +const mockedUsedNavigate = vi.fn(); describe('Namespaces page', () => { beforeEach(() => { vi.restoreAllMocks(); // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -113,7 +122,7 @@ describe('Namespaces page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('namespaces-add-button')).toBeInTheDocument(); @@ -128,7 +137,7 @@ describe('Namespaces page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('namespaces-add-button')).toBeNull(); @@ -145,7 +154,7 @@ describe('Namespaces page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const addButton = screen.queryByTestId('namespaces-add-button'); @@ -197,140 +206,26 @@ describe('Open modals', () => { vi.clearAllMocks(); }); - it('open and close add namespaces modal', async () => { + it('open add namespaces modal', async () => { act(() => { fireEvent.click(screen.getByTestId('namespaces-add-button')); }); - screen.debug(); await waitFor(() => { - expect( - screen.getByTestId('add-edit-namespaces-modal'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-edit-namespaces-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-namespaces-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on add namespaces success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('namespaces-add-button')); - }); - await waitFor(() => { - expect( - screen.getByTestId('add-edit-namespaces-modal'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { - target: { - value: 'newNamespaces', - }, - }); - fireEvent.change( - screen.getByTestId('add-edit-namespaces-resolution-input'), - { - target: { - value: '1D', - }, - }, - ); - fireEvent.change( - screen.getByTestId('add-edit-namespaces-retention-input'), - { - target: { - value: '48H', - }, - }, - ); - fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-namespaces-modal'), - ).not.toBeInTheDocument(); - expect(namespaceApi.addNamespace).toHaveBeenCalled(); - expect(namespaceApi.getNamespaces).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add'); }); }); it('shows edit namespaces modal', async () => { await openButtonInMenu('namespaces-action-edit-button'); await waitFor(() => { - expect( - screen.getByTestId('add-edit-namespaces-modal'), - ).toBeInTheDocument(); - }); - }); - - it('refetch data on edit namespaces success', async () => { - await openButtonInMenu('namespaces-action-edit-button'); - await waitFor(() => { - expect( - screen.getByTestId('add-edit-namespaces-modal'), - ).toBeInTheDocument(); - }); - - act(() => { - fireEvent.change( - screen.getByTestId( - 'add-edit-namespaces-blockDataExpirationDuration-input', - ), - { - target: { - value: '5m', - }, - }, - ); - fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-namespaces-modal'), - ).not.toBeInTheDocument(); - expect(namespaceApi.editNamespace).toHaveBeenCalled(); - expect(namespaceApi.getNamespaces).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./edit/namespaceId'); }); }); it('shows delete namespace modal', async () => { await openButtonInMenu('namespaces-action-delete-button'); await waitFor(() => { - expect(screen.getByTestId('delete-namespaces-modal')).toBeInTheDocument(); - }); - }); - it('closes delete namespaces modal', async () => { - await openButtonInMenu('namespaces-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-namespaces-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-namespaces-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-namespaces-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on delete namespaces success', async () => { - await openButtonInMenu('namespaces-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-namespaces-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-namespaces-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-namespaces-modal'), - ).not.toBeInTheDocument(); - expect(namespaceApi.getNamespaces).toHaveBeenCalled(); - expect(namespaceApi.deleteNamespace).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete/namespaceId'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.component.tsx index 062e838a3a10..beac1b5a5a5e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.component.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { ChevronDown, ChevronRight, HelpCircle } from 'lucide-react'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -31,7 +31,6 @@ import { import { Switch } from '@/components/ui/switch'; import { useToast } from '@/components/ui/use-toast'; -import { ModalController } from '@/hooks/useModale'; import { useNamespaceForm } from './formNamespace/useNamespaceForm.hook'; import * as database from '@/types/cloud/project/database'; @@ -44,25 +43,19 @@ import { import { useEditNamespace } from '@/hooks/api/database/namespace/useEditNamespace.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; import { ScrollArea } from '@/components/ui/scroll-area'; +import RouteModal from '@/components/route-modal/RouteModal'; interface AddEditNamespaceModalProps { - isEdition: boolean; editedNamespace?: database.m3db.Namespace; namespaces: database.m3db.Namespace[]; service: database.Service; - controller: ModalController; - onSuccess?: (namespace?: database.m3db.Namespace) => void; - onError?: (error: Error) => void; } const AddEditNamespace = ({ - isEdition, editedNamespace, namespaces, service, - controller, - onSuccess, - onError, }: AddEditNamespaceModalProps) => { + const isEdition = !!editedNamespace?.id; const [advancedConfiguration, setAdvancedConfiguration] = useState( isEdition && (editedNamespace.snapshotEnabled || @@ -72,17 +65,13 @@ const AddEditNamespace = ({ editedNamespace.retention.bufferFutureDuration || editedNamespace.retention.bufferPastDuration), ); - + const navigate = useNavigate(); const { projectId } = useParams(); const { form } = useNamespaceForm({ editedNamespace, existingNamespaces: namespaces, }); - useEffect(() => { - if (!controller.open) form.reset(); - }, [controller.open]); - const { t } = useTranslation( 'pci-databases-analytics/services/service/namespaces', ); @@ -97,9 +86,6 @@ const AddEditNamespace = ({ description: getCdbApiErrorMessage(err), duration: TOAST.ERROR_DURATION, }); - if (onError) { - onError(err); - } }, onSuccess(ns) { form.reset(); @@ -110,9 +96,7 @@ const AddEditNamespace = ({ name: ns.name, }), }); - if (onSuccess) { - onSuccess(ns); - } + navigate('../'); }, }; @@ -144,7 +128,6 @@ const AddEditNamespace = ({ if (isEdition) { if (Object.entries(form.formState.dirtyFields).length === 0) { - onSuccess(); return; } @@ -185,7 +168,7 @@ const AddEditNamespace = ({ }); return ( - + @@ -608,7 +591,7 @@ const AddEditNamespace = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.spec.tsx new file mode 100644 index 000000000000..6fe99aace655 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/AddEditNamespace.spec.tsx @@ -0,0 +1,271 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedNamespaces } from '@/__tests__/helpers/mocks/namespaces'; +import { useToast } from '@/components/ui/use-toast'; +import AddEditNamespace from './AddEditNamespace.component'; +import * as namespacesApi from '@/data/api/database/namespace.api'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; + +const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); +describe('AddEditNamespace', () => { + beforeEach(() => { + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + + vi.mock('@/data/api/database/namespace.api', () => ({ + getNamespaces: vi.fn(() => [mockedNamespaces]), + addNamespace: vi.fn((namespace) => namespace), + editNamespace: vi.fn((namespace) => namespace), + deleteNamespace: vi.fn(), + })); + + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the form', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect( + screen.getByTestId('add-edit-namespaces-modal'), + ).toBeInTheDocument(); + }); + }); + + it('should show validation errors for empty required fields', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); + }); + + await waitFor(() => { + expect( + screen.getAllByText('formNamespaceErrorMandatoryField').length, + ).toBeGreaterThan(0); + }); + }); + it('should add a namespace on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { + target: { + value: 'newNamespace', + }, + }); + fireEvent.change( + screen.getByTestId('add-edit-namespaces-retention-input'), + { + target: { + value: '3D', + }, + }, + ); + fireEvent.change( + screen.getByTestId('add-edit-namespaces-resolution-input'), + { + target: { + value: '3D', + }, + }, + ); + fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); + }); + await waitFor(() => { + expect(namespacesApi.addNamespace).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'formNamespaceToastSuccessTitle', + description: 'addNamespaceToastSuccessDescription', + }); + }); + }); + + it('should not submit if namespace name is already used', async () => { + render( + , + { wrapper: RouterWithQueryClientWrapper }, + ); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { + target: { value: mockedNamespaces.name }, + }); + fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); + }); + + await waitFor(() => { + expect( + screen.getByText('formNamespaceNameErrorDuplicate'), + ).toBeInTheDocument(); + }); + }); + + it('should toggle advanced configuration', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + const toggleButton = screen.getByText( + 'formNamespaceButtonAdvancedConfiguration', + ); + + act(() => { + fireEvent.click(toggleButton); + }); + + await waitFor(() => { + expect( + screen.getByText('retention.blockDataExpirationDuration'), + ).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(toggleButton); + }); + + await waitFor(() => { + expect( + screen.queryByText('retention.blockDataExpirationDuration'), + ).not.toBeInTheDocument(); + }); + }); + + it('should edit a namespace successfully', async () => { + render( + , + { wrapper: RouterWithQueryClientWrapper }, + ); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { + target: { value: 'updatedNamespace' }, + }); + fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); + }); + + await waitFor(() => { + expect(namespacesApi.editNamespace).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'formNamespaceToastSuccessTitle', + }), + ); + }); + }); + + it('should handle API error when adding a namespace', async () => { + vi.mocked(namespacesApi.addNamespace).mockRejectedValue(apiErrorMock); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { + target: { value: 'newNamespace' }, + }); + fireEvent.change( + screen.getByTestId('add-edit-namespaces-retention-input'), + { + target: { + value: '3D', + }, + }, + ); + fireEvent.change( + screen.getByTestId('add-edit-namespaces-resolution-input'), + { + target: { + value: '3D', + }, + }, + ); + fireEvent.click(screen.getByTestId('add-edit-namespaces-submit-button')); + }); + + await waitFor(() => { + expect(namespacesApi.addNamespace).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'addNamespaceToastErrorTitle', + }), + ); + }); + }); + + it('should close the dialog when canceled', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.click(screen.getByTestId('add-edit-namespaces-cancel-button')); + }); + + await waitFor(() => { + expect( + screen.queryByTestId('add-edit-namespaces-modal'), + ).not.toBeInTheDocument(); + }); + }); + + it('should render resolution field only for aggregated namespaces', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-namespaces-name-input'), { + target: { value: 'newNamespace' }, + }); + fireEvent.change( + screen.getByTestId('add-edit-namespaces-retention-input'), + { target: { value: '3D' } }, + ); + }); + + expect( + screen.queryByTestId('add-edit-namespaces-resolution-input'), + ).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.modal.tsx new file mode 100644 index 000000000000..797fb544f230 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.modal.tsx @@ -0,0 +1,22 @@ +import { useServiceData } from '../../Service.context'; +import AddEditNamespace from '../_components/AddEditNamespace.component'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useGetNamespaces } from '@/hooks/api/database/namespace/useGetNamespaces.hook'; + +const AddNamespaceModal = () => { + const { projectId, service } = useServiceData(); + const namespacesQuery = useGetNamespaces( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const namespaces = namespacesQuery.data; + + if (!namespacesQuery.data) return ; + return ; +}; + +export default AddNamespaceModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.spec.tsx new file mode 100644 index 000000000000..1fbb1b4fbc7c --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/add/AddNamespace.spec.tsx @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import AddNamespace from './AddNamespace.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as namespacesApi from '@/data/api/database/namespace.api'; +import { mockedNamespaces } from '@/__tests__/helpers/mocks/namespaces'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditNamespace.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('@/data/api/database/namespace.api', () => ({ + getNamespaces: vi.fn(() => [mockedNamespaces]), + addNamespace: vi.fn((namespace) => namespace), + deleteNamespace: vi.fn(), + editNamespace: vi.fn((namespace) => namespace), +})); + +describe('Add Namespace modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render a skeleton loader while users are being fetched', () => { + // Simulate loading state in the useGetUsers hook + vi.mocked(namespacesApi.getNamespaces).mockImplementationOnce(() => { + throw apiErrorMock; + }); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('should render AddEditUserModal with users when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(namespacesApi.getNamespaces).mockResolvedValue([ + mockedNamespaces, + ]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect( + screen.getByTestId('add-edit-namespace-modal'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/DeleteNamespace.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.modal.tsx similarity index 67% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/DeleteNamespace.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.modal.tsx index 40b370d3591f..ea18f6fca440 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/_components/DeleteNamespace.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.modal.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -11,27 +11,27 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; -import { ModalController } from '@/hooks/useModale'; -import * as database from '@/types/cloud/project/database'; import { useDeleteNamespace } from '@/hooks/api/database/namespace/useDeleteNamespace.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import { useGetNamespaces } from '@/hooks/api/database/namespace/useGetNamespaces.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteNamespaceModalProps { - service: database.Service; - controller: ModalController; - namespace: database.m3db.Namespace; - onSuccess?: (namespace: database.m3db.Namespace) => void; - onError?: (error: Error) => void; -} +const DeleteNamespaceModal = () => { + const { projectId, namespaceId } = useParams(); + const navigate = useNavigate(); + const { service } = useServiceData(); + const namespacesQuery = useGetNamespaces( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const namespaces = namespacesQuery.data; + const deletedNamespace = namespaces?.find((n) => n.id === namespaceId); -const DeleteNamespaceModal = ({ - service, - namespace, - controller, - onError, - onSuccess, -}: DeleteNamespaceModalProps) => { - const { projectId } = useParams(); const { t } = useTranslation( 'pci-databases-analytics/services/service/namespaces', ); @@ -43,33 +43,33 @@ const DeleteNamespaceModal = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteNamespaceToastSuccessTitle'), description: t('deleteNamespaceToastSuccessDescription', { - name: namespace.name, + name: deletedNamespace.name, }), }); - if (onSuccess) { - onSuccess(namespace); - } + navigate('../'); }, }); + useEffect(() => { + if (namespaces && !deletedNamespace) navigate('../'); + }, [namespaces, deletedNamespace]); + const handleDelete = () => { deleteNamespace({ serviceId: service.id, projectId, engine: service.engine, - namespaceId: namespace.id, + namespaceId: deletedNamespace.id, }); }; + return ( - + @@ -77,7 +77,7 @@ const DeleteNamespaceModal = ({ {t('deleteNamespaceDescription', { - name: namespace.name, + name: deletedNamespace?.name, })} @@ -101,7 +101,7 @@ const DeleteNamespaceModal = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.spec.tsx new file mode 100644 index 000000000000..8a16253e096a --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/delete/DeleteNamespace.spec.tsx @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import * as database from '@/types/cloud/project/database'; +import { Locale } from '@/hooks/useLocale'; +import * as namespacesApi from '@/data/api/database/namespace.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import DeleteNamespaceModal from './DeleteNamespace.modal'; +import { mockedNamespaces } from '@/__tests__/helpers/mocks/namespaces'; + +describe('Delete namespace modal', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + namespaceId: mockedNamespaces.id, + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + vi.mock('@/data/api/database/namespace.api', () => ({ + getNamespaces: vi.fn(() => [mockedNamespaces]), + addNamespace: vi.fn((namespace) => namespace), + deleteNamespace: vi.fn(), + editNamespace: vi.fn((namespace) => namespace), + })); + + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + category: 'operational', + serviceQuery: {} as UseQueryResult, + })), + })); + + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect( + screen.queryByTestId('delete-namespaces-modal'), + ).toBeInTheDocument(); + }); + }); + it('should delete a namespace on submit', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-namespaces-submit-button')); + }); + await waitFor(() => { + expect(namespacesApi.deleteNamespace).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteNamespaceToastSuccessTitle', + description: 'deleteNamespaceToastSuccessDescription', + }); + }); + }); + it('should call onError when api failed', async () => { + vi.mocked(namespacesApi.deleteNamespace).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-namespaces-submit-button')); + }); + await waitFor(() => { + expect(namespacesApi.deleteNamespace).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteNamespaceToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.modal.tsx new file mode 100644 index 000000000000..de4870ec9421 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.modal.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useServiceData } from '../../Service.context'; +import AddEditNamespace from '../_components/AddEditNamespace.component'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useGetNamespaces } from '@/hooks/api/database/namespace/useGetNamespaces.hook'; + +const EditNamespaceModal = () => { + const { namespaceId } = useParams(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); + const namespacesQuery = useGetNamespaces( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const namespaces = namespacesQuery.data; + const editedNamespace = namespaces?.find((n) => n.id === namespaceId); + + useEffect(() => { + if (namespaces && !editedNamespace) navigate('../'); + }, [namespaces, editedNamespace]); + + if (!namespacesQuery.data) return ; + return ( + + ); +}; + +export default EditNamespaceModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.spec.tsx new file mode 100644 index 000000000000..28ebde7eea03 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/namespaces/edit/EditNamespace.spec.tsx @@ -0,0 +1,77 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import EditNamespace from './EditNamespace.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as namespacesApi from '@/data/api/database/namespace.api'; +import * as database from '@/types/cloud/project/database'; +import { mockedNamespaces } from '@/__tests__/helpers/mocks/namespaces'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditNamespace.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + namespaceId: mockedNamespaces.id, + }), + }; +}); +vi.mock('@/data/api/database/namespace.api', () => ({ + getNamespaces: vi.fn(() => [mockedNamespaces]), + addNamespace: vi.fn((namespace) => namespace), + deleteNamespace: vi.fn(), + editNamespace: vi.fn((namespace) => namespace), +})); + +describe('Edit Namespace modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render a skeleton loader while namespaces are being fetched', () => { + // Simulate loading state in the useGetUsers hook + vi.mocked(namespacesApi.getNamespaces).mockImplementationOnce(() => { + throw apiErrorMock; + }); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('should render the modal when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(namespacesApi.getNamespaces).mockResolvedValue([ + mockedNamespaces, + ]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect( + screen.getByTestId('add-edit-namespace-modal'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.page.tsx index bf00669dbeca..99f27cbacedf 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.page.tsx @@ -1,22 +1,16 @@ import { useTranslation } from 'react-i18next'; import { ColumnDef } from '@tanstack/react-table'; import { useEffect, useState } from 'react'; - +import { Outlet, useNavigate } from 'react-router-dom'; import { Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/ui/data-table'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; - -import { useModale } from '@/hooks/useModale'; - import { useServiceData } from '../Service.context'; import { POLLING } from '@/configuration/polling.constants'; import { GenericUser } from '@/data/api/database/user.api'; import * as database from '@/types/cloud/project/database'; import { getColumns } from './_components/PoolsTableColumns.component'; -import InfoConnectionPool from './_components/InfoConnectionPool.component'; -import AddEditConnectionPool from './_components/AddEditconnectionPool.component'; -import DeleteConnectionPool from './_components/DeleteConnectionPool.component'; import Guides from '@/components/guides/Guides.component'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { GuideSections } from '@/types/guide'; @@ -40,6 +34,7 @@ export interface ConnectionPoolWithData } const Pools = () => { + const navigate = useNavigate(); const { t } = useTranslation( 'pci-databases-analytics/services/service/pools', ); @@ -48,10 +43,6 @@ const Pools = () => { ConnectionPoolWithData[] >([]); const { isUserActive } = useUserActivityContext(); - const addModale = useModale('add'); - const getInfoModale = useModale('information'); - const deleteModale = useModale('delete'); - const editModale = useModale('edit'); const connectionPoolsQuery = useGetConnectionPools( projectId, service.engine, @@ -94,25 +85,14 @@ const Pools = () => { }, [connectionPoolsQuery.data, usersQuery.data, databasesQuery.data]); const columns: ColumnDef[] = getColumns({ - onGetInformationClick: (pools: ConnectionPoolWithData) => - getInfoModale.open(pools.id), - onEditClick: (pools: ConnectionPoolWithData) => editModale.open(pools.id), - onDeleteClick: (pools: ConnectionPoolWithData) => - deleteModale.open(pools.id), + onGetInformationClick: (pool: ConnectionPoolWithData) => + navigate(`./informations/${pool.id}`), + onEditClick: (pool: ConnectionPoolWithData) => + navigate(`./edit/${pool.id}`), + onDeleteClick: (pool: ConnectionPoolWithData) => + navigate(`./delete/${pool.id}`), }); - const connectionPoolToDelete = connectionPoolListWithData?.find( - (cp) => cp.id === deleteModale.value, - ); - - const connectionPoolToDisplayInfo = connectionPoolListWithData?.find( - (cp) => cp.id === getInfoModale.value, - ); - - const connectionPoolToEdit = connectionPoolListWithData?.find( - (cp) => cp.id === editModale.value, - ); - return ( <>
@@ -126,7 +106,7 @@ const Pools = () => { variant={'outline'} size="sm" className="text-base" - onClick={() => addModale.open()} + onClick={() => navigate('./add')} disabled={ service.capabilities.connectionPools.create === database.service.capability.StateEnum.disabled @@ -147,68 +127,7 @@ const Pools = () => {
)} - {connectionPoolsQuery.isSuccess && - usersQuery.isSuccess && - databasesQuery.isSuccess && ( - { - addModale.close(); - databasesQuery.refetch(); - connectionPoolsQuery.refetch(); - }} - /> - )} - - {connectionPoolToDisplayInfo && - connectionPoolsQuery.isSuccess && - databasesQuery.isSuccess && ( - - )} - - {connectionPoolToEdit && - connectionPoolsQuery.isSuccess && - usersQuery.isSuccess && - databasesQuery.isSuccess && ( - { - editModale.close(); - databasesQuery.refetch(); - connectionPoolsQuery.refetch(); - }} - /> - )} - - {connectionPoolToDelete && ( - { - deleteModale.close(); - connectionPoolsQuery.refetch(); - usersQuery.refetch(); - databasesQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.spec.tsx index c4adc8ac42b4..9c989b3ca53f 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/Pools.spec.tsx @@ -13,13 +13,12 @@ import Pools, { } from '@/pages/services/[serviceId]/pools/Pools.page'; import * as database from '@/types/cloud/project/database'; import { Locale } from '@/hooks/useLocale'; -import * as connectionPoolApi from '@/data/api/database/connectionPool.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; import { mockedUser } from '@/__tests__/helpers/mocks/user'; -import { useToast } from '@/components/ui/use-toast'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -35,11 +34,19 @@ const mockedService = { }, }; const mockCertificate = { ca: 'certificateCA' }; +const mockedUsedNavigate = vi.fn(); describe('Connection pool page', () => { beforeEach(() => { vi.restoreAllMocks(); // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -126,7 +133,7 @@ describe('Connection pool page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('pools-add-button')).toBeInTheDocument(); @@ -139,7 +146,7 @@ describe('Connection pool page', () => { capabilities: {}, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('pools-add-button')).toBeNull(); @@ -157,7 +164,7 @@ describe('Connection pool page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const addButton = screen.queryByTestId('pools-add-button'); @@ -200,7 +207,7 @@ describe('Open modals', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { @@ -211,114 +218,30 @@ describe('Open modals', () => { vi.clearAllMocks(); }); - it('open and close add pool modal', async () => { + it('open add pool modal', async () => { act(() => { fireEvent.click(screen.getByTestId('pools-add-button')); }); await waitFor(() => { - expect(screen.getByTestId('add-edit-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-edit-pools-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-pools-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on add pool success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('pools-add-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-edit-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-pools-name-input'), { - target: { - value: 'newConnectionPools', - }, - }); - fireEvent.change(screen.getByTestId('add-edit-pools-size-input'), { - target: { - value: 3, - }, - }); - fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-pools-modal'), - ).not.toBeInTheDocument(); - expect(connectionPoolApi.addConnectionPool).toHaveBeenCalled(); - expect(connectionPoolApi.getConnectionPools).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add'); }); }); it('shows edit pool modal', async () => { await openButtonInMenu('pools-action-edit-button'); await waitFor(() => { - expect(screen.getByTestId('add-edit-pools-modal')).toBeInTheDocument(); - }); - }); - - it('refetch data on edit pool success', async () => { - await openButtonInMenu('pools-action-edit-button'); - await waitFor(() => { - expect(screen.getByTestId('add-edit-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-pools-size-input'), { - target: { - value: 2, - }, - }); - fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-pools-modal'), - ).not.toBeInTheDocument(); - expect(connectionPoolApi.editConnectionPool).toHaveBeenCalled(); - expect(connectionPoolApi.getConnectionPools).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith( + `./edit/${mockedConnectionPool.id}`, + ); }); }); it('shows delete pool modal', async () => { await openButtonInMenu('pools-action-delete-button'); await waitFor(() => { - expect(screen.getByTestId('delete-pools-modal')).toBeInTheDocument(); - }); - }); - it('closes delete pool modal', async () => { - await openButtonInMenu('pools-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-pools-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-pools-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on delete pool success', async () => { - await openButtonInMenu('pools-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-pools-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-pools-modal'), - ).not.toBeInTheDocument(); - expect(connectionPoolApi.getConnectionPools).toHaveBeenCalled(); - expect(connectionPoolApi.deleteConnectionPool).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith( + `./delete/${mockedConnectionPool.id}`, + ); }); }); @@ -330,40 +253,9 @@ describe('Open modals', () => { }); await openButtonInMenu('pools-action-info-button'); await waitFor(() => { - expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); - expect(screen.getByTestId('info-pools-table')).toBeInTheDocument(); - }); - - act(() => { - fireEvent.click(screen.getByTestId('info-pools-copy-certificate-action')); - }); - await waitFor(() => { - expect(window.navigator.clipboard.writeText).toHaveBeenCalled(); - expect(useToast().toast).toHaveBeenCalled(); - }); - - act(() => { - fireEvent.click(screen.getByTestId('info-pools-copy-uri-action')); - }); - await waitFor(() => { - expect(window.navigator.clipboard.writeText).toHaveBeenCalled(); - expect(useToast().toast).toHaveBeenCalled(); - }); - }); - - it('closes info pool modal', async () => { - await openButtonInMenu('pools-action-info-button'); - await waitFor(() => { - expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.keyDown(screen.getByTestId('info-pools-modal'), { - key: 'Escape', - code: 'Escape', - }); - }); - await waitFor(() => { - expect(screen.queryByTestId('info-pools-modal')).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith( + `./informations/${mockedConnectionPool.id}`, + ); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditconnectionPool.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.component.tsx similarity index 94% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditconnectionPool.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.component.tsx index 79cdb0ec4e34..d61b3390656d 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditconnectionPool.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.component.tsx @@ -1,10 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; -import { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -29,10 +27,7 @@ import { SelectValue, } from '@/components/ui/select'; import { useToast } from '@/components/ui/use-toast'; - import { useConnectionPoolForm } from './formPools/useConnectionPoolForm.hook'; -import { ModalController } from '@/hooks/useModale'; - import { GenericUser } from '@/data/api/database/user.api'; import * as database from '@/types/cloud/project/database'; import { ConnectionPoolEdition } from '@/data/api/database/connectionPool.api'; @@ -42,40 +37,31 @@ import { } from '@/hooks/api/database/connectionPool/useAddConnectionPool.hook'; import { useEditConnectionPool } from '@/hooks/api/database/connectionPool/useEditConnectionPool.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import RouteModal from '@/components/route-modal/RouteModal'; interface AddEditConnectionPoolModalProps { - isEdition: boolean; editedConnectionPool?: database.postgresql.ConnectionPool; connectionPools: database.postgresql.ConnectionPool[]; users: GenericUser[]; databases: database.service.Database[]; service: database.Service; - controller: ModalController; - onSuccess?: (connectionPool?: database.postgresql.ConnectionPool) => void; - onError?: (error: Error) => void; } const AddEditConnectionPool = ({ - isEdition, editedConnectionPool, connectionPools, users, databases, service, - controller, - onSuccess, - onError, }: AddEditConnectionPoolModalProps) => { + const isEdition = !!editedConnectionPool?.id; const { projectId } = useParams(); + const navigate = useNavigate(); const { form } = useConnectionPoolForm({ editedConnectionPool, existingConnectionPools: connectionPools, databases, }); - useEffect(() => { - if (!controller.open) form.reset(); - }, [controller.open]); - const { t } = useTranslation( 'pci-databases-analytics/services/service/pools', ); @@ -89,9 +75,6 @@ const AddEditConnectionPool = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess(cp) { form.reset(); @@ -101,9 +84,7 @@ const AddEditConnectionPool = ({ name: cp.name, }), }); - if (onSuccess) { - onSuccess(cp); - } + navigate('../'); }, }; @@ -127,7 +108,6 @@ const AddEditConnectionPool = ({ userId: formValues.userId || null, }; if (Object.entries(form.formState.dirtyFields).length === 0) { - onSuccess(); return; } editConnectionPool({ @@ -154,7 +134,7 @@ const AddEditConnectionPool = ({ }); return ( - + @@ -317,7 +297,7 @@ const AddEditConnectionPool = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.spec.tsx new file mode 100644 index 000000000000..34166856381d --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/AddEditPool.spec.tsx @@ -0,0 +1,246 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; +import { useToast } from '@/components/ui/use-toast'; +import AddEditPool from './AddEditPool.component'; +import * as connectionPoolsApi from '@/data/api/database/connectionPool.api'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { mockedUser } from '@/__tests__/helpers/mocks/user'; +import { GenericUser } from '@/data/api/database/user.api'; + +const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); +describe('AddEditPool', () => { + beforeEach(() => { + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + + vi.mock('@/data/api/database/connectionPool.api', () => ({ + getConnectionPools: vi.fn(() => [mockedConnectionPool]), + addConnectionPool: vi.fn((pool) => pool), + editConnectionPool: vi.fn((pool) => pool), + deleteConnectionPool: vi.fn(), + })); + + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the form', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + await waitFor(() => { + expect(screen.getByTestId('add-edit-pools-modal')).toBeInTheDocument(); + }); + }); + + it('should show validation errors for empty required fields', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + act(() => { + fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); + }); + + await waitFor(() => { + expect( + screen.getAllByText('formConnectionPoolErrorMinLength').length, + ).toBeGreaterThan(0); + }); + }); + it('should add a pool on submit', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + act(() => { + fireEvent.change(screen.getByTestId('add-edit-pools-name-input'), { + target: { + value: 'newPool', + }, + }); + }); + act(() => { + fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); + }); + await waitFor(() => { + expect(connectionPoolsApi.addConnectionPool).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'formConnectionPoolToastSuccessTitle', + description: 'addConnectionPoolToastSuccessDescription', + }); + }); + }); + + it('should not submit if pool name is already used', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-pools-name-input'), { + target: { + value: mockedConnectionPool.name, + }, + }); + }); + act(() => { + fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); + }); + + await waitFor(() => { + expect( + screen.getByText('formConnectionPoolNameErrorDuplicate'), + ).toBeInTheDocument(); + }); + }); + + it('should edit a pool successfully', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-pools-name-input'), { + target: { value: 'updatedPool' }, + }); + fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); + }); + + await waitFor(() => { + expect(connectionPoolsApi.editConnectionPool).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'formConnectionPoolToastSuccessTitle', + }), + ); + }); + }); + + it('should handle API error when adding a pool', async () => { + vi.mocked(connectionPoolsApi.addConnectionPool).mockRejectedValue( + apiErrorMock, + ); + + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + act(() => { + fireEvent.change(screen.getByTestId('add-edit-pools-name-input'), { + target: { value: 'newPool' }, + }); + fireEvent.click(screen.getByTestId('add-edit-pools-submit-button')); + }); + + await waitFor(() => { + expect(connectionPoolsApi.addConnectionPool).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'addConnectionPoolToastErrorTitle', + }), + ); + }); + }); + + it('should close the dialog when canceled', async () => { + render( + , + { + wrapper: RouterWithQueryClientWrapper, + }, + ); + + act(() => { + fireEvent.click(screen.getByTestId('add-edit-pools-cancel-button')); + }); + + await waitFor(() => { + expect( + screen.queryByTestId('add-edit-pools-modal'), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.modal.tsx new file mode 100644 index 000000000000..2e24de6e76d3 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.modal.tsx @@ -0,0 +1,45 @@ +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; +import { useServiceData } from '../../Service.context'; +import AddEditPool from '../_components/AddEditPool.component'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useGetConnectionPools } from '@/hooks/api/database/connectionPool/useGetConnectionPools.hook'; +import { useGetDatabases } from '@/hooks/api/database/database/useGetDatabases.hook'; + +const AddPoolModal = () => { + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + const databasesQuery = useGetDatabases( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const connectionPoolsQuery = useGetConnectionPools( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const users = usersQuery.data; + const databases = databasesQuery.data; + const connectionPools = connectionPoolsQuery.data; + + if (!users || !databases || !connectionPools) + return ; + return ( + + ); +}; + +export default AddPoolModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.spec.tsx new file mode 100644 index 000000000000..dc0e93597b33 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/add/AddPool.spec.tsx @@ -0,0 +1,73 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import AddPool from './AddPool.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as connectionPoolsApi from '@/data/api/database/connectionPool.api'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; +import { mockedUser } from '@/__tests__/helpers/mocks/user'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditPool.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('@/data/api/database/connectionPool.api', async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@/data/api/database/connectionPool.api') + >()), + getConnectionPools: vi.fn(() => [mockedConnectionPool]), +})); +vi.mock('@/data/api/database/database.api', () => ({ + getServiceDatabases: vi.fn(() => [mockedDatabase]), +})); +vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedUser]), +})); + +describe('Add Pool modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render a skeleton loader while datas are being fetched', () => { + // Simulate loading state in the useGetUsers hook + vi.mocked(connectionPoolsApi.getConnectionPools).mockImplementationOnce( + () => { + throw apiErrorMock; + }, + ); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('should render AddEditUserModal with users when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(connectionPoolsApi.getConnectionPools).mockResolvedValue([ + mockedConnectionPool, + ]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('add-edit-pool-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/DeleteConnectionPool.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.modal.tsx similarity index 68% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/DeleteConnectionPool.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.modal.tsx index 1af7a43ba507..09e4a5bab9cc 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/DeleteConnectionPool.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.modal.tsx @@ -1,9 +1,8 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; - +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -12,29 +11,27 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { useToast } from '@/components/ui/use-toast'; - -import { ModalController } from '@/hooks/useModale'; - -import * as database from '@/types/cloud/project/database'; import { useDeleteConnectionPool } from '@/hooks/api/database/connectionPool/useDeleteConnectionPool.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import { useGetConnectionPools } from '@/hooks/api/database/connectionPool/useGetConnectionPools.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteConnectionPoolModalProps { - service: database.Service; - controller: ModalController; - connectionPool: database.postgresql.ConnectionPool; - onSuccess?: (connectionPool: database.postgresql.ConnectionPool) => void; - onError?: (connectionPool: Error) => void; -} +const DeletePool = () => { + const { projectId, poolId } = useParams(); + const navigate = useNavigate(); + const { service } = useServiceData(); + const poolsQuery = useGetConnectionPools( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const pools = poolsQuery.data; + const deletedPool = pools?.find((p) => p.id === poolId); -const DeleteConnectionPool = ({ - service, - connectionPool, - controller, - onError, - onSuccess, -}: DeleteConnectionPoolModalProps) => { - const { projectId } = useParams(); const { t } = useTranslation( 'pci-databases-analytics/services/service/pools', ); @@ -46,33 +43,33 @@ const DeleteConnectionPool = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteConnectionPoolToastSuccessTitle'), description: t('deleteConnectionPoolToastSuccessDescription', { - name: connectionPool.name, + name: deletedPool.name, }), }); - if (onSuccess) { - onSuccess(connectionPool); - } + navigate('../'); }, }); + useEffect(() => { + if (pools && !deletedPool) navigate('../'); + }, [pools, deletedPool]); + const handleDelete = () => { deleteConnectionPool({ serviceId: service.id, projectId, engine: service.engine, - connectionPoolId: connectionPool.id, + connectionPoolId: deletedPool.id, }); }; + return ( - + @@ -80,7 +77,7 @@ const DeleteConnectionPool = ({ {t('deleteConnectionPoolDescription', { - name: connectionPool.name, + name: deletedPool?.name, })} @@ -104,8 +101,8 @@ const DeleteConnectionPool = ({ - + ); }; -export default DeleteConnectionPool; +export default DeletePool; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.spec.tsx new file mode 100644 index 000000000000..5e5e295a6b1b --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/delete/DeletePool.spec.tsx @@ -0,0 +1,133 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { UseQueryResult } from '@tanstack/react-query'; +import * as database from '@/types/cloud/project/database'; +import { Locale } from '@/hooks/useLocale'; +import * as connectionPoolsApi from '@/data/api/database/connectionPool.api'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import DeletePool from './DeletePool.modal'; +import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { mockedUser } from '@/__tests__/helpers/mocks/user'; + +describe('Delete pool modal', () => { + beforeEach(() => { + vi.mock( + '@/data/api/database/connectionPool.api', + async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@/data/api/database/connectionPool.api') + >()), + getConnectionPools: vi.fn(() => [mockedConnectionPool]), + deleteConnectionPool: vi.fn(), + }), + ); + vi.mock('@/data/api/database/database.api', () => ({ + getServiceDatabases: vi.fn(() => [mockedDatabase]), + })); + vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedUser]), + })); + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + poolId: mockedConnectionPool.id, + }), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + category: 'operational', + serviceQuery: {} as UseQueryResult, + })), + })); + + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + })), + }; + }); + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('delete-pools-modal')).toBeInTheDocument(); + }); + }); + it('should delete a pool on submit', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-pools-submit-button')); + }); + await waitFor(() => { + expect(connectionPoolsApi.deleteConnectionPool).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteConnectionPoolToastSuccessTitle', + description: 'deleteConnectionPoolToastSuccessDescription', + }); + }); + }); + it('should call onError when api failed', async () => { + vi.mocked(connectionPoolsApi.deleteConnectionPool).mockImplementation( + () => { + throw apiErrorMock; + }, + ); + render(, { wrapper: RouterWithQueryClientWrapper }); + act(() => { + fireEvent.click(screen.getByTestId('delete-pools-submit-button')); + }); + await waitFor(() => { + expect(connectionPoolsApi.deleteConnectionPool).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteConnectionPoolToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.modal.tsx new file mode 100644 index 000000000000..1c9a6a2c9601 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.modal.tsx @@ -0,0 +1,55 @@ +import { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useServiceData } from '../../Service.context'; +import AddEditPool from '../_components/AddEditPool.component'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useGetConnectionPools } from '@/hooks/api/database/connectionPool/useGetConnectionPools.hook'; +import { useGetDatabases } from '@/hooks/api/database/database/useGetDatabases.hook'; +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; + +const EditPoolModal = () => { + const { poolId } = useParams(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + const databasesQuery = useGetDatabases( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const connectionPoolsQuery = useGetConnectionPools( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const users = usersQuery.data; + const databases = databasesQuery.data; + const connectionPools = connectionPoolsQuery.data; + const editedConnectionPools = connectionPools?.find((c) => c.id === poolId); + + useEffect(() => { + if (connectionPools && !editedConnectionPools) navigate('../'); + }, [connectionPools, editedConnectionPools]); + + if (!users || !databases || !connectionPools) + return ; + return ( + + ); +}; + +export default EditPoolModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.spec.tsx new file mode 100644 index 000000000000..2010685021ff --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/edit/EditPool.spec.tsx @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import EditPool from './EditPool.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as connectionPoolsApi from '@/data/api/database/connectionPool.api'; +import * as database from '@/types/cloud/project/database'; +import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import { mockedUser } from '@/__tests__/helpers/mocks/user'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditPool.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('@/data/api/database/connectionPool.api', async (importOriginal) => ({ + ...(await importOriginal< + typeof import('@/data/api/database/connectionPool.api') + >()), + getConnectionPools: vi.fn(() => [mockedConnectionPool]), +})); +vi.mock('@/data/api/database/database.api', () => ({ + getServiceDatabases: vi.fn(() => [mockedDatabase]), +})); +vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedUser]), +})); +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + poolId: mockedConnectionPool.id, + }), + }; +}); + +describe('Edit Pool modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render a skeleton loader while data are being fetched', () => { + // Simulate loading state in the useGetUsers hook + vi.mocked(connectionPoolsApi.getConnectionPools).mockImplementationOnce( + () => { + throw apiErrorMock; + }, + ); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('should render the modal when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(connectionPoolsApi.getConnectionPools).mockResolvedValue([ + mockedConnectionPool, + ]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('add-edit-pool-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/InfoConnectionPool.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.modal.tsx similarity index 72% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/InfoConnectionPool.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.modal.tsx index 56a21936cb4e..86ce085fac8d 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/_components/InfoConnectionPool.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.modal.tsx @@ -1,9 +1,9 @@ import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Download, Files } from 'lucide-react'; +import { useEffect } from 'react'; import { - Dialog, DialogContent, DialogHeader, DialogTitle, @@ -11,45 +11,65 @@ import { import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; - -import { ModalController } from '@/hooks/useModale'; import useDownload from '@/hooks/useDownload'; import * as database from '@/types/cloud/project/database'; import { useGetCertificate } from '@/hooks/api/database/certificate/useGetCertificate.hook'; +import { useServiceData } from '../../Service.context'; +import { useGetConnectionPools } from '@/hooks/api/database/connectionPool/useGetConnectionPools.hook'; +import { useGetDatabases } from '@/hooks/api/database/database/useGetDatabases.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface InfoConnectionPoolModalProps { - service: database.Service; - connectionPool: database.postgresql.ConnectionPool; - databases: database.service.Database[]; - controller: ModalController; -} -const InfoConnectionPool = ({ - service, - connectionPool, - databases, - controller, -}: InfoConnectionPoolModalProps) => { - const { projectId } = useParams(); +const InfoConnectionPool = () => { + const { projectId, poolId } = useParams(); + const navigate = useNavigate(); + const { service } = useServiceData(); + const connectionPoolsQuery = useGetConnectionPools( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const connectionPools = connectionPoolsQuery.data; + const connectionPool = connectionPools?.find((c) => c.id === poolId); const { t } = useTranslation( 'pci-databases-analytics/services/service/pools', ); const toast = useToast(); + const { download } = useDownload(); const certificateQuery = useGetCertificate( projectId, service.engine, service.id, + { + enabled: !!service.id, + }, ); + const databasesQuery = useGetDatabases( + projectId, + service.engine, + service.id, + { + enabled: !!service.id, + }, + ); + const databases = databasesQuery.data; - const { download } = useDownload(); + useEffect(() => { + if (connectionPools && !connectionPool) navigate('../'); + }, [connectionPools, connectionPool]); - const poolDb: database.service.Database = databases.find( - (db) => db.id === connectionPool.databaseId, + const poolDb: database.service.Database = databases?.find( + (db) => db.id === connectionPool?.databaseId, ); return ( - + @@ -64,19 +84,19 @@ const InfoConnectionPool = ({ {t('infoConnectionPoolDatabaseLabel')} - {poolDb.name} + {poolDb?.name} {t('infoConnectionPoolPortLabel')} - {connectionPool.port} + {connectionPool?.port} {t('infoConnectionPoolSslLabel')} - {connectionPool.sslMode} + {connectionPool?.sslMode} @@ -85,9 +105,9 @@ const InfoConnectionPool = ({

- {certificateQuery.data.ca} + {certificateQuery.data?.ca}

@@ -99,7 +119,7 @@ const InfoConnectionPool = ({ variant="table" onClick={() => { navigator.clipboard.writeText( - certificateQuery.data.ca, + certificateQuery.data?.ca, ); toast.toast({ title: t('infoConnectionPoolCertificateCopyToast'), @@ -114,7 +134,7 @@ const InfoConnectionPool = ({ size="table" variant="table" onClick={() => { - download(certificateQuery.data.ca, 'ca.pem'); + download(certificateQuery.data?.ca, 'ca.pem'); toast.toast({ title: t( 'infoConnectionPoolCertificateDownloadToast', @@ -134,9 +154,9 @@ const InfoConnectionPool = ({

- {connectionPool.uri} + {connectionPool?.uri}

@@ -147,7 +167,7 @@ const InfoConnectionPool = ({ size="table" variant="table" onClick={() => { - navigator.clipboard.writeText(connectionPool.uri); + navigator.clipboard.writeText(connectionPool?.uri); toast.toast({ title: t('infoConnectionPoolUriToast'), }); @@ -162,13 +182,13 @@ const InfoConnectionPool = ({ {t('infoConnectionPoolModeLabel')} - {connectionPool.mode} + {connectionPool?.mode}
{t('infoConnectionPoolSizeLabel')} - {connectionPool.size} + {connectionPool?.size}
@@ -177,7 +197,7 @@ const InfoConnectionPool = ({

{t('infoConnectionPoolLoading')}

)} - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.spec.tsx new file mode 100644 index 000000000000..9a0e6184cb50 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/pools/informations/InfoConnectionPool.spec.tsx @@ -0,0 +1,187 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +// import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedConnectionPool } from '@/__tests__/helpers/mocks/connectionPool'; +import { mockedDatabase } from '@/__tests__/helpers/mocks/databases'; +import InfoConnectionPool from './InfoConnectionPool.modal'; +import * as connectionPoolApi from '@/data/api/database/connectionPool.api'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedUser } from '@/__tests__/helpers/mocks/user'; +import * as database from '@/types/cloud/project/database'; + +const mockedCertificate = { ca: 'certificateCA' }; +const mockedUsedNavigate = vi.fn(); +const downloadMock = vi.fn(); +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); + +describe('InfoConnectionPool', () => { + beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + poolId: mockedConnectionPool.id, + }), + }; + }); + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + vi.mock('@/data/api/database/connectionPool.api', () => ({ + getConnectionPools: vi.fn(() => [mockedConnectionPool]), + addConnectionPool: vi.fn((pool) => pool), + editConnectionPool: vi.fn((pool) => pool), + deleteConnectionPool: vi.fn(), + })); + vi.mock('@/data/api/database/certificate.api', () => ({ + getCertificate: vi.fn(() => mockedCertificate), + })); + vi.mock('@/data/api/database/database.api', () => ({ + getServiceDatabases: vi.fn(() => [mockedDatabase]), + })); + vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedUser]), + })); + + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the dialog with the correct information', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + + await waitFor(() => { + expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); + expect(screen.getByTestId('info-pools-table')).toBeInTheDocument(); + }); + + expect(screen.getByText(mockedDatabase.name)).toBeInTheDocument(); + expect(screen.getByText(mockedConnectionPool.port)).toBeInTheDocument(); + expect(screen.getByText(mockedConnectionPool.sslMode)).toBeInTheDocument(); + expect(screen.getByText(mockedConnectionPool.uri)).toBeInTheDocument(); + expect(screen.getByText(mockedConnectionPool.mode)).toBeInTheDocument(); + expect(screen.getByText(mockedConnectionPool.size)).toBeInTheDocument(); + }); + + it('should copy the certificate to the clipboard', async () => { + const writeTextMock = vi.fn(); + Object.assign(window.navigator, { + clipboard: { + writeText: writeTextMock, + }, + }); + + render(, { wrapper: RouterWithQueryClientWrapper }); + + await waitFor(() => { + expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(screen.getByTestId('info-pools-copy-certificate-action')); + }); + + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith(mockedCertificate.ca); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'infoConnectionPoolCertificateCopyToast', + }); + }); + }); + + it('should download the certificate', async () => { + vi.mock('@/hooks/useDownload', () => ({ + default: vi.fn(() => ({ download: downloadMock })), + })); + + render(, { wrapper: RouterWithQueryClientWrapper }); + + await waitFor(() => { + expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(screen.getByTestId('info-pools-download-ca-action')); + }); + + await waitFor(() => { + expect(downloadMock).toHaveBeenCalledWith(mockedCertificate.ca, 'ca.pem'); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'infoConnectionPoolCertificateDownloadToast', + }); + }); + }); + + it('should copy the URI to the clipboard', async () => { + const writeTextMock = vi.fn(); + Object.assign(window.navigator, { + clipboard: { + writeText: writeTextMock, + }, + }); + + render(, { wrapper: RouterWithQueryClientWrapper }); + + await waitFor(() => { + expect(screen.getByTestId('info-pools-modal')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(screen.getByTestId('info-pools-copy-uri-action')); + }); + + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith(mockedConnectionPool.uri); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'infoConnectionPoolUriToast', + }); + }); + }); + + it('should navigate back if no connection pool is found', async () => { + vi.mocked(connectionPoolApi.getConnectionPools).mockResolvedValueOnce([]); + render(, { wrapper: RouterWithQueryClientWrapper }); + + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalledWith('../'); + }); + }); + + it('should show a skeleton if data is loading', () => { + vi.mocked(connectionPoolApi.getConnectionPools).mockResolvedValueOnce([]); + render(, { wrapper: RouterWithQueryClientWrapper }); + expect(screen.getAllByTestId('skeleton').length).toBeGreaterThan(0); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/queries/Queries.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/queries/Queries.spec.tsx index 64367d3b259f..84a0efa0c1f9 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/queries/Queries.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/queries/Queries.spec.tsx @@ -20,6 +20,7 @@ import { mockedQueries, mockedQueryStatisticsPG, } from '@/__tests__/helpers/mocks/queries'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -126,7 +127,7 @@ describe('Queries page', () => { capabilities: {}, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect( @@ -152,7 +153,7 @@ describe('Queries page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.getByTestId('current-queries-container')).toBeInTheDocument(); @@ -180,7 +181,7 @@ describe('Queries page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.getByTestId('current-queries-container')).toBeInTheDocument(); @@ -219,7 +220,7 @@ describe('Action of queries and statistics', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.page.tsx index 8fb50f09f610..a183926839fe 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.page.tsx @@ -1,3 +1,4 @@ +import { Outlet } from 'react-router-dom'; import { AccordionItem } from '@radix-ui/react-accordion'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader } from '@/components/ui/card'; @@ -82,6 +83,7 @@ const Settings = () => { )}
+ ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.spec.tsx index a8d225a40848..aff50550b2ce 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/Settings.spec.tsx @@ -22,11 +22,15 @@ import { import { mockedMaintenance } from '@/__tests__/helpers/mocks/maintenances'; import { mockedUser } from '@/__tests__/helpers/mocks/user'; import { Locale } from '@/hooks/useLocale'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { ...mockedServiceOrig, capabilities: { + service: { + update: database.service.capability.StateEnum.enabled, + }, advancedConfiguration: { create: database.service.capability.StateEnum.enabled, update: database.service.capability.StateEnum.enabled, @@ -179,7 +183,7 @@ describe('Settings page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect( diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/AdvancedConfigurationUpdate.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/AdvancedConfigurationUpdate.spec.tsx index 49981c6945c0..eac225625f43 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/AdvancedConfigurationUpdate.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/AdvancedConfigurationUpdate.spec.tsx @@ -28,6 +28,9 @@ import { Locale } from '@/hooks/useLocale'; const mockedService = { ...mockedServiceOrig, capabilities: { + service: { + update: database.service.capability.StateEnum.enabled, + }, advancedConfiguration: { create: database.service.capability.StateEnum.enabled, update: database.service.capability.StateEnum.enabled, diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/Maintenances.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/Maintenances.spec.tsx index 9980234ea14a..afc2d9d10144 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/Maintenances.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/Maintenances.spec.tsx @@ -24,11 +24,15 @@ import { import { mockedMaintenanceTer } from '@/__tests__/helpers/mocks/maintenances'; import { mockedUser } from '@/__tests__/helpers/mocks/user'; import { Locale } from '@/hooks/useLocale'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { ...mockedServiceOrig, capabilities: { + service: { + update: database.service.capability.StateEnum.enabled, + }, maintenanceApply: { create: database.service.capability.StateEnum.enabled, }, @@ -146,13 +150,16 @@ describe('Maintenance in settings page', () => { service: { ...mockedService, capabilities: { + service: { + update: database.service.capability.StateEnum.enabled, + }, maintenanceApply: { create: database.service.capability.StateEnum.disabled, }, }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx index 8b9eced4fe51..dcf7cef1573a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx @@ -4,13 +4,10 @@ import { useTranslation } from 'react-i18next'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { useServiceData } from '../../Service.context'; import { Button } from '@/components/ui/button'; -import { useModale } from '@/hooks/useModale'; -import RenameService from '../../_components/RenameService.component'; import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; import { useGetServices } from '@/hooks/api/database/service/useGetServices.hook'; import { useToast } from '@/components/ui/use-toast'; import TimeUpdate from './serviceConfiguration/TimeUpdate.component'; -import DeleteService from '../../_components/DeleteService.component'; import * as database from '@/types/cloud/project/database'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; @@ -19,12 +16,7 @@ const ServiceConfiguration = () => { 'pci-databases-analytics/services/service/settings', ); const navigate = useNavigate(); - const renameModale = useModale('rename'); - const deleteModale = useModale('delete'); const { service, projectId, serviceQuery } = useServiceData(); - const getServicesQuery = useGetServices(projectId, { - enabled: false, - }); const toast = useToast(); const { editService } = useEditService({ onError: (err) => { @@ -101,7 +93,7 @@ const ServiceConfiguration = () => { variant="ghost" size="table" className="py-0 h-auto" - onClick={() => renameModale.open()} + onClick={() => navigate('./rename')} > @@ -157,29 +149,11 @@ const ServiceConfiguration = () => { } variant="destructive" className="w-full bg-background border-2 hover:bg-destructive/10 font-semibold border-destructive text-destructive" - onClick={() => deleteModale.open()} + onClick={() => navigate('./delete')} > {t('serviceConfigurationDeleteService')} )} - { - renameModale.close(); - serviceQuery.refetch(); - }} - /> - { - deleteModale.close(); - serviceQuery.refetch(); - getServicesQuery.refetch(); - navigate(`../../`); - }} - /> ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.spec.tsx index f27a6cdef418..e03d7246078a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.spec.tsx @@ -9,7 +9,6 @@ import { } from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; import * as ServiceContext from '@/pages/services/[serviceId]/Service.context'; -import * as serviceApi from '@/data/api/database/service.api'; import Settings from '@/pages/services/[serviceId]/settings/Settings.page'; import * as database from '@/types/cloud/project/database'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; @@ -29,7 +28,7 @@ import { mockedMaintenance } from '@/__tests__/helpers/mocks/maintenances'; import { mockedUser } from '@/__tests__/helpers/mocks/user'; import { Locale } from '@/hooks/useLocale'; import { mockedIntegrations } from '@/__tests__/helpers/mocks/integrations'; -import { TERMINATE_CONFIRMATION } from '@/configuration/polling.constants'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -59,10 +58,18 @@ const mockedService = { }, }; +const mockedUsedNavigate = vi.fn(); describe('Service configuration page', () => { beforeEach(() => { vi.restoreAllMocks(); // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -176,7 +183,7 @@ describe('Service configuration page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { @@ -217,7 +224,7 @@ describe('Service configuration page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { @@ -245,7 +252,7 @@ describe('Open modals', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { @@ -256,92 +263,21 @@ describe('Open modals', () => { vi.clearAllMocks(); }); - it('open and close rename service Modal', async () => { - act(() => { - fireEvent.click(screen.getByTestId('service-confi-rename-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('rename-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.keyDown(screen.getByTestId('rename-service-modal'), { - key: 'Escape', - code: 'Escape', - }); - }); - await waitFor(() => { - expect( - screen.queryByTestId('rename-service-modal'), - ).not.toBeInTheDocument(); - }); - }); - - it('call update service on rename success', async () => { + it('open rename service Modal', async () => { act(() => { fireEvent.click(screen.getByTestId('service-confi-rename-button')); }); await waitFor(() => { - expect(screen.getByTestId('rename-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('rename-service-input'), { - target: { - value: 'newName', - }, - }); - fireEvent.click(screen.getByTestId('rename-service-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('rename-service-modal'), - ).not.toBeInTheDocument(); - expect(serviceApi.editService).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./rename'); }); }); - it('open and close delete service Modal', async () => { + it('open delete service Modal', async () => { act(() => { fireEvent.click(screen.getByTestId('service-confi-delete-button')); }); await waitFor(() => { - expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-service-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-service-modal'), - ).not.toBeInTheDocument(); - }); - }); - - it('call delete service on success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('service-confi-delete-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); - expect( - screen.getByTestId('delete-service-confirmation-input'), - ).toBeInTheDocument(); - }); - act(() => { - fireEvent.change( - screen.getByTestId('delete-service-confirmation-input'), - { - target: { - value: TERMINATE_CONFIRMATION, - }, - }, - ); - fireEvent.click(screen.getByTestId('delete-service-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('delete-service-modal'), - ).not.toBeInTheDocument(); - expect(serviceApi.deleteService).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.component.tsx index 71264cdef366..32f6f9eb082d 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.component.tsx @@ -1,33 +1,22 @@ import { MinusCircle, PlusCircle } from 'lucide-react'; -import { useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'; import { humanizeEngine } from '@/lib/engineNameHelper'; import * as database from '@/types/cloud/project/database'; import { useServiceData } from '../../Service.context'; import { Button } from '@/components/ui/button'; -import { formatStorage } from '@/lib/bytesHelper'; -import { useModale } from '@/hooks/useModale'; -import UpdateVersion from '../update/_components/modals/UpdateVersion.component'; -import UpdatePlan from '../update/_components/modals/UpdatePlan.component'; -import UpdateFlavor from '../update/_components/modals/UpdateFlavor.component'; -import AddNode from '../update/_components/modals/AddNode.component'; -import DeleteNode from '../update/_components/modals/RemoveNode.component'; -import { updateTags } from '@/lib/tagsHelper'; -import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; -import { - FullCapabilities, - useGetFullCapabilities, -} from '@/hooks/api/database/capabilities/useGetFullCapabilities.hook'; +import { compareStorage, formatStorage } from '@/lib/bytesHelper'; import { useGetAvailabilities } from '@/hooks/api/database/availability/useGetAvailabilities.hook'; const UpdateTable = () => { const { t } = useTranslation( 'pci-databases-analytics/services/service/settings/update', ); - const { service, projectId, serviceQuery } = useServiceData(); - const catalogQuery = useGetCatalog(); - const capabilitiesQuery = useGetFullCapabilities(projectId); + const { service, projectId } = useServiceData(); + const navigate = useNavigate(); + // fetch available updates const availabilitiesVersionQuery = useGetAvailabilities( projectId, service.id, @@ -46,214 +35,110 @@ const UpdateTable = () => { database.availability.ActionEnum.update, database.availability.TargetEnum.flavor, ); - - const updateVersionModal = useModale('update-version'); - const updatePlanModal = useModale('update-plan'); - const updateFlavorModal = useModale('update-flavor'); - const addNode = useModale('add-node'); - const deleteNode = useModale('delete-node'); - - const capabilities: FullCapabilities = useMemo(() => { - if (!capabilitiesQuery.data) - return { - flavors: [], - disks: [], - engines: [], - options: [], - plans: [], - regions: [], - }; - const { - flavors, - plans, - regions, - engines, - ...rest - } = capabilitiesQuery.data; - - return { - ...rest, - engines: engines.map((e) => ({ - ...e, - versions: updateTags(e.versions, service.version), - })), - flavors: updateTags(flavors, service.flavor), - plans: updateTags(plans, service.plan), - regions: updateTags(regions, service.nodes[0].region), - } as FullCapabilities; - }, [capabilitiesQuery.data, service]); - - const suggestions: database.availability.Suggestion[] = [ - { - default: true, - engine: service.engine, - flavor: service.flavor, - plan: service.plan, - region: service.region, - version: service.version, - }, - ]; // refetch availabilities when service status changes useEffect(() => { availabilitiesVersionQuery.refetch(); availabilitiesFlavorQuery.refetch(); availabilitiesPlanQuery.refetch(); }, [service.status]); - const rows = [ { title: t('tableVersion'), cell: `${humanizeEngine(service.engine)} ${service.version}`, - onClick: () => updateVersionModal.open(), + onClick: () => navigate('./update-version'), updateButtonDisplayed: availabilitiesVersionQuery.data?.length > 1, }, { title: t('tablePlan'), cell: service.plan, - onClick: () => updatePlanModal.open(), + onClick: () => navigate('./update-plan'), updateButtonDisplayed: availabilitiesPlanQuery.data?.length > 1, }, { title: t('tableFlavor'), cell: service.flavor, - onClick: () => updateFlavorModal.open(), + onClick: () => navigate('./update-flavor'), updateButtonDisplayed: availabilitiesFlavorQuery.data?.length > 1, }, service.storage?.size.value && { title: t('tableStorage'), cell: `${formatStorage(service.storage.size)} ${service.storage.type}`, - onClick: () => updateFlavorModal.open(), - updateButtonDisplayed: availabilitiesFlavorQuery.data?.length > 1, + onClick: () => navigate('./update-flavor'), + updateButtonDisplayed: + availabilitiesFlavorQuery.data?.length > 0 && + availabilitiesFlavorQuery.data[0].specifications.storage && + compareStorage( + availabilitiesFlavorQuery.data[0].specifications.storage.minimum, + availabilitiesFlavorQuery.data[0].specifications.storage.maximum, + ) !== 0, }, ].filter((row) => Boolean(row)); return ( - <> - - - {rows.map((row) => ( - - {row.title} - {row.cell} - {row.updateButtonDisplayed && ( - - - - )} - - ))} - - {t('tableNodes')} - {service.nodes.length} - {availabilitiesFlavorQuery.data?.length > 1 && ( - - {service.capabilities.nodes?.delete && ( - - )} - {service.capabilities.nodes?.create && ( - - )} +
+ + {rows.map((row) => ( + + {row.title} + {row.cell} + {row.updateButtonDisplayed && ( + + )} - -
- {/* Modals */} - {catalogQuery.isSuccess && capabilitiesQuery.isSuccess && ( - <> - {availabilitiesVersionQuery.isSuccess && ( - { - updateVersionModal.close(); - serviceQuery.refetch(); - }} - /> - )} - {availabilitiesPlanQuery.isSuccess && ( - { - updatePlanModal.close(); - serviceQuery.refetch(); - }} - /> - )} - {availabilitiesFlavorQuery.isSuccess && ( - { - updateFlavorModal.close(); - serviceQuery.refetch(); - }} - /> + ))} + + {t('tableNodes')} + {service.nodes.length} + {availabilitiesFlavorQuery.data?.length > 1 && ( + + {service.capabilities.nodes?.delete && ( + + )} + {service.capabilities.nodes?.create && ( + + )} + )} - { - addNode.close(); - serviceQuery.refetch(); - }} - /> - { - deleteNode.close(); - serviceQuery.refetch(); - }} - /> - - )} - + + + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.spec.tsx index 0b4163da3be2..52cd5993ffc3 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/UpdateTable.spec.tsx @@ -9,36 +9,20 @@ import { } from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; import * as ServiceContext from '@/pages/services/[serviceId]/Service.context'; -import Settings from '@/pages/services/[serviceId]/settings/Settings.page'; import * as database from '@/types/cloud/project/database'; -import * as nodesApi from '@/data/api/database/node.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; -import { mockedCatalog } from '@/__tests__/helpers/mocks/catalog'; import { mockedAvailabilities, mockedAvailabilitiesUpdate, - mockedCapabilities, - mockedEngineCapabilities, - mockedRegionCapabilities, } from '@/__tests__/helpers/mocks/availabilities'; -import { mockedMaintenance } from '@/__tests__/helpers/mocks/maintenances'; -import { mockedUser } from '@/__tests__/helpers/mocks/user'; -import { Locale } from '@/hooks/useLocale'; +import { CdbError } from '@/data/api/database'; +import UpdateTable from './UpdateTable.component'; // Override mock to add capabilities const mockedService = { ...mockedServiceOrig, capabilities: { - advancedConfiguration: { - create: database.service.capability.StateEnum.enabled, - update: database.service.capability.StateEnum.enabled, - delete: database.service.capability.StateEnum.enabled, - read: database.service.capability.StateEnum.enabled, - }, - maintenanceApply: { - create: database.service.capability.StateEnum.enabled, - }, service: { update: database.service.capability.StateEnum.enabled, }, @@ -49,63 +33,11 @@ const mockedService = { }, }; -const mockAdvancedConfiguration = { capability: 'capabilityMocked' }; - -const mockCapabilities: database.capabilities.advancedConfiguration.Property[] = [ - { - name: 'capability', - type: database.capabilities.advancedConfiguration.property.TypeEnum.string, - description: 'capabilityMocked', - }, -]; +const mockedUsedNavigate = vi.fn(); describe('Update table in settings page', () => { beforeEach(() => { vi.restoreAllMocks(); - // Mock necessary hooks and dependencies - vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), - })); - - vi.mock('@/data/api/catalog/catalog.api', () => ({ - catalogApi: { - getCatalog: vi.fn(() => mockedCatalog), - }, - })); - - vi.mock('@/data/api/database/availability.api', () => ({ - getAvailabilities: vi.fn(() => [ - mockedAvailabilities, - mockedAvailabilitiesUpdate, - ]), - })); - vi.mock('@/data/api/database/capabilities.api', () => ({ - getCapabilities: vi.fn(() => mockedCapabilities), - getEnginesCapabilities: vi.fn(() => [mockedEngineCapabilities]), - getRegionsCapabilities: vi.fn(() => [mockedRegionCapabilities]), - })); - - vi.mock('@/data/api/database/maintenance.api', () => ({ - getMaintenances: vi.fn(() => [mockedMaintenance]), - applyMaintenance: vi.fn((maintenance) => maintenance), - })); - - vi.mock('@/data/api/database/advancedConfiguration.api', () => ({ - getAdvancedConfiguration: vi.fn(() => mockAdvancedConfiguration), - getAdvancedConfigurationCapabilities: vi.fn(() => mockCapabilities), - })); - - vi.mock('@/data/api/database/service.api', () => ({ - editService: vi.fn((service) => service), - })); - - vi.mock('@/data/api/database/node.api', () => ({ - addNode: vi.fn((node) => node), - deleteNode: vi.fn(), - })); - vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ useServiceData: vi.fn(() => ({ projectId: 'projectId', @@ -114,28 +46,22 @@ describe('Update table in settings page', () => { serviceQuery: {} as UseQueryResult, })), })); - - vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { - const mod = await importOriginal< - typeof import('@ovh-ux/manager-react-shell-client') - >(); + // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); return { ...mod, - useShell: vi.fn(() => ({ - i18n: { - getLocale: vi.fn(() => Locale.fr_FR), - onLocaleChange: vi.fn(), - setLocale: vi.fn(), - }, - environment: { - getEnvironment: vi.fn(() => ({ - getUser: vi.fn(() => mockedUser), - })), - }, - })), + useNavigate: () => mockedUsedNavigate, }; }); + vi.mock('@/data/api/database/availability.api', () => ({ + getAvailabilities: vi.fn(() => [ + mockedAvailabilities, + mockedAvailabilitiesUpdate, + ]), + })); + vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -144,31 +70,31 @@ describe('Update table in settings page', () => { })); }); - it('renders and shows skeletons while loading', async () => { - render(, { wrapper: RouterWithQueryClientWrapper }); - await waitFor(() => { - expect( - screen.getByTestId('advanced-config-accordion-trigger'), - ).toBeInTheDocument(); - expect( - screen.getByTestId('maintenance-settings-skeleton'), - ).toBeInTheDocument(); - }); + afterEach(() => { + vi.clearAllMocks(); }); it('renders and shows update table with button', async () => { - render(, { wrapper: RouterWithQueryClientWrapper }); - const updateVersionButton = screen.getByTestId( - 'update-button-tableVersion', - ); - const updatePlanButton = screen.getByTestId('update-button-tablePlan'); - const updateFlavorButton = screen.getByTestId('update-button-tableFlavor'); - const updateStorageButton = screen.getByTestId( - 'update-button-tableStorage', - ); - const createNodeButton = screen.getByTestId('create-node-button'); - const deleteNodeButton = screen.getByTestId('delete-node-button'); - await waitFor(() => { + vi.mocked(ServiceContext.useServiceData).mockReturnValue({ + projectId: 'projectId', + service: mockedService, + category: 'operational', + serviceQuery: {} as UseQueryResult, + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + const updateVersionButton = screen.getByTestId( + 'update-button-tableVersion', + ); + const updatePlanButton = screen.getByTestId('update-button-tablePlan'); + const updateFlavorButton = screen.getByTestId( + 'update-button-tableFlavor', + ); + const updateStorageButton = screen.getByTestId( + 'update-button-tableStorage', + ); + const createNodeButton = screen.getByTestId('create-node-button'); + const deleteNodeButton = screen.getByTestId('delete-node-button'); expect(screen.getByText(mockedServiceOrig.plan)).toBeInTheDocument(); expect(updateVersionButton).toBeInTheDocument(); expect(updatePlanButton).toBeInTheDocument(); @@ -180,8 +106,7 @@ describe('Update table in settings page', () => { }); it('renders and shows update table with button disabled', async () => { - render(, { wrapper: RouterWithQueryClientWrapper }); - vi.mocked(ServiceContext.useServiceData).mockReturnValue({ + vi.mocked(ServiceContext.useServiceData).mockReturnValueOnce({ projectId: 'projectId', service: { ...mockedService, @@ -196,8 +121,9 @@ describe('Update table in settings page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); + render(, { wrapper: RouterWithQueryClientWrapper }); const updateVersionButton = screen.getByTestId( 'update-button-tableVersion', ); @@ -233,9 +159,9 @@ describe('Open modals', () => { projectId: 'projectId', service: mockedService, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); - render(, { wrapper: RouterWithQueryClientWrapper }); + render(, { wrapper: RouterWithQueryClientWrapper }); await waitFor(() => { expect(screen.getByText(mockedServiceOrig.plan)).toBeInTheDocument(); }); @@ -243,39 +169,25 @@ describe('Open modals', () => { afterEach(() => { vi.clearAllMocks(); }); - it('open and close add updateVersionModal', async () => { + it('open update version modal', async () => { act(() => { fireEvent.click(screen.getByTestId('update-button-tableVersion')); }); await waitFor(() => { - expect(screen.getByTestId('update-version-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('update-version-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('update-version-modal'), - ).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./update-version'); }); }); - it('open and close add updatePlanModal', async () => { + it('open update plan modal', async () => { act(() => { fireEvent.click(screen.getByTestId('update-button-tablePlan')); }); await waitFor(() => { - expect(screen.getByTestId('update-plan-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('update-plan-cancel-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('update-plan-modal')).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./update-plan'); }); }); - it('open and close add updateFlavorModal', async () => { + it('open update flavor modal', async () => { const ResizeObserverMock = vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), @@ -288,77 +200,25 @@ describe('Open modals', () => { fireEvent.click(screen.getByTestId('update-button-tableFlavor')); }); await waitFor(() => { - expect(screen.getByTestId('update-flavor-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('update-flavor-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('update-flavor-modal'), - ).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./update-flavor'); }); }); - it('open and close add node modal', async () => { + it('open add node modal', async () => { act(() => { fireEvent.click(screen.getByTestId('create-node-button')); }); await waitFor(() => { - expect(screen.getByTestId('add-node-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-node-cancel-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('add-node-modal')).not.toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add-node'); }); }); - it('call add node on add node success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('create-node-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-node-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-node-submit-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('add-node-modal')).not.toBeInTheDocument(); - expect(nodesApi.addNode).toHaveBeenCalled(); - }); - }); - - it('open and close delete node modal', async () => { - act(() => { - fireEvent.click(screen.getByTestId('delete-node-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('delete-node-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-node-cancel-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('delete-node-modal')).not.toBeInTheDocument(); - }); - }); - - it('call deleteNode on delete node success', async () => { + it('open delete node modal', async () => { act(() => { fireEvent.click(screen.getByTestId('delete-node-button')); }); await waitFor(() => { - expect(screen.getByTestId('delete-node-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-node-submit-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('delete-node-modal')).not.toBeInTheDocument(); - expect(nodesApi.deleteNode).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete-node'); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/serviceConfiguration/TimeUpdate.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/serviceConfiguration/TimeUpdate.spec.tsx index 366d6ae972f5..0bbf77d7aae2 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/serviceConfiguration/TimeUpdate.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/serviceConfiguration/TimeUpdate.spec.tsx @@ -1,7 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; - -import { act } from 'react-dom/test-utils'; +import { + fireEvent, + render, + screen, + waitFor, + act, +} from '@testing-library/react'; import TimeUpdate from '@/pages/services/[serviceId]/settings/_components/serviceConfiguration/TimeUpdate.component'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.modal.tsx new file mode 100644 index 000000000000..be8eb31dc4f2 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetService } from '@/hooks/api/database/service/useGetService.hook'; +import DeleteService from '../../_components/DeleteService.component'; + +const DeleteServiceModal = () => { + const { projectId, serviceId } = useParams(); + const navigate = useNavigate(); + const serviceQuery = useGetService(projectId, serviceId); + return ( + navigate('../../../')} + service={serviceQuery.data} + /> + ); +}; + +export default DeleteServiceModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.spec.tsx new file mode 100644 index 000000000000..6b341ee9053e --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/delete/Delete.spec.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import DeleteModal from './Delete.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; + +vi.mock('../../_components/DeleteService.component', () => ({ + default: vi.fn(() =>
), +})); +describe('Settings delete modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render delete modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.modal.tsx new file mode 100644 index 000000000000..5dc01831417c --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetService } from '@/hooks/api/database/service/useGetService.hook'; +import RenameService from '../../_components/RenameService.component'; + +const RenameServiceModal = () => { + const { projectId, serviceId } = useParams(); + const navigate = useNavigate(); + const serviceQuery = useGetService(projectId, serviceId); + return ( + navigate('../')} + service={serviceQuery.data} + /> + ); +}; + +export default RenameServiceModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.spec.tsx new file mode 100644 index 000000000000..289da2a204a2 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/rename/Rename.spec.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import RenameModal from './Rename.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; + +vi.mock('../../_components/RenameService.component', () => ({ + default: vi.fn(() =>
), +})); +describe('Settings rename modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render rename modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('rename-service-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateFlavor.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateFlavor.component.tsx deleted file mode 100644 index 97a6d30a20ea..000000000000 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateFlavor.component.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { z } from 'zod'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useTranslation } from 'react-i18next'; -import { ArrowRight } from 'lucide-react'; -import FlavorsSelect from '@/components/order/flavor/FlavorSelect.component'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; -import { FullCapabilities } from '@/hooks/api/database/capabilities/useGetFullCapabilities.hook'; -import { ModalController } from '@/hooks/useModale'; -import { createTree } from '@/lib/availabilitiesHelper'; -import { order } from '@/types/catalog'; -import * as database from '@/types/cloud/project/database'; -import { Engine, Version, Plan, Region } from '@/types/orderFunnel'; -import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { useToast } from '@/components/ui/use-toast'; -import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; -import Price from '@/components/price/Price.component'; -import { computeServicePrice } from '@/lib/pricingHelper'; -import StorageConfig from '@/components/order/cluster-configuration/StorageConfig.component'; -import { formatStorage } from '@/lib/bytesHelper'; -import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; -import { Label } from '@/components/ui/label'; -import PricingDetails from '../PricingDetails.component'; -import { getCdbApiErrorMessage } from '@/lib/apiHelper'; - -interface UpdateFlavorProps { - controller: ModalController; - suggestions: database.availability.Suggestion[]; - availabilities: database.Availability[]; - capabilities: FullCapabilities; - catalog: order.publicOrder.Catalog; - onSuccess?: (service: database.Service) => void; - onError?: (error: Error) => void; -} - -const UpdateFlavorContent = ({ - controller, - suggestions, - availabilities, - capabilities, - catalog, - onSuccess, - onError, -}: UpdateFlavorProps) => { - const [showMonthly, setShowMonthly] = useState(false); - const { service, projectId } = useServiceData(); - const toast = useToast(); - const { t } = useTranslation( - 'pci-databases-analytics/services/service/settings/update', - ); - const hasStorage = - service.storage?.size.value > 0 && service.storage.size.unit === 'GB'; - const { editService, isPending } = useEditService({ - onError: (err) => { - toast.toast({ - title: t('updateFlavorToastErrorTitle'), - variant: 'destructive', - description: getCdbApiErrorMessage(err), - }); - if (onError) { - onError(err); - } - }, - onEditSuccess: (updatedService) => { - toast.toast({ - title: t('updateFlavorToastSuccessTitle'), - description: hasStorage - ? t('updateFlavorAndStorageToastSuccessDescription', { - newFlavor: updatedService.flavor, - storage: formatStorage(updatedService.storage.size), - }) - : t('updateFlavorToastSuccessDescription', { - newFlavor: updatedService.flavor, - }), - }); - if (onSuccess) { - onSuccess(updatedService); - } - }, - }); - const listEngines = useMemo( - () => createTree(availabilities, capabilities, suggestions, catalog), - [availabilities, capabilities], - ); - const listFlavors = useMemo( - () => - listEngines - ?.find((e: Engine) => e.name === service.engine) - ?.versions.find((v: Version) => v.name === service.version) - ?.plans.find((p: Plan) => p.name === service.plan) - ?.regions.find((r: Region) => r.name === service.nodes[0].region) - ?.flavors.sort((a, b) => a.order - b.order) || [], - [listEngines, service], - ); - - // initialFlavor can be undefined - const initialFlavorObject = useMemo( - () => listFlavors.find((f) => f.name === service.flavor), - [service.flavor, listFlavors], - ); - const initialAddedStorage = - hasStorage && initialFlavorObject - ? service.storage.size.value - initialFlavorObject.storage?.minimum.value - : 0; - - const schema = z.object({ - flavor: z.string().min(1), - storage: z.coerce.number().nonnegative(), - }); - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - flavor: service.flavor, - storage: initialAddedStorage, - }, - }); - - const selectedFlavor = form.watch('flavor'); - const selectedStorage = form.watch('storage'); - const flavorObject = useMemo( - () => listFlavors.find((f) => f.name === selectedFlavor), - [selectedFlavor], - ); - const availability = useMemo( - () => - availabilities.find( - (a) => - a.engine === service.engine && - a.specifications.flavor === selectedFlavor && - a.plan === service.plan && - a.region === service.nodes[0].region, - ), - [availabilities, service, selectedFlavor], - ); - - useEffect(() => { - form.setValue( - 'storage', - selectedFlavor === service.flavor ? initialAddedStorage : 0, - ); - }, [selectedFlavor]); - - const oldPrice = useMemo(() => { - const initialFlavor = listFlavors.find((f) => f.name === service.flavor); - return computeServicePrice({ - offerPricing: initialFlavor.pricing, - nbNodes: service.nodes.length, - storagePricing: initialFlavor.storage?.pricing, - additionalStorage: initialAddedStorage, - storageMode: listEngines.find((e) => e.name === service.engine) - .storageMode, - }); - }, [service.flavor]); - const newPrice = useMemo(() => { - return computeServicePrice({ - offerPricing: flavorObject.pricing, - nbNodes: service.nodes.length, - storagePricing: flavorObject.storage?.pricing, - additionalStorage: selectedStorage, - storageMode: listEngines.find((e) => e.name === service.engine) - .storageMode, - }); - }, [flavorObject, selectedStorage]); - - const onSubmit = form.handleSubmit((formValues) => { - editService({ - serviceId: service.id, - projectId, - engine: service.engine, - data: { - flavor: formValues.flavor, - ...(hasStorage && { - disk: { - size: - availability.specifications.storage.minimum.value + - formValues.storage, - }, - }), - }, - }); - }); - return ( - - -
- - - - {t('updateFlavorTitle')} - - - - - - ( - - {t('updateFlavorInputLabel')} - - - - - - )} - /> - {hasStorage && ( - ( - - {t('updateStorageInputLabel')} - - - - -
- {t('updateStorageTotal')} - - {formatStorage({ - unit: 'GB', - value: - availability.specifications.storage.minimum - .value + field.value, - })} - -
-
- )} - /> - )} - - -
- -
- - - - - -
-
- - - - -
-
- - -
-
- ); -}; - -const UpdateFlavor = ({ controller, ...otherProps }: UpdateFlavorProps) => { - if (!controller.open) return <>; - return ; -}; - -export default UpdateFlavor; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdatePlan.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdatePlan.component.tsx deleted file mode 100644 index 79ca50e32c27..000000000000 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdatePlan.component.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { z } from 'zod'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useTranslation } from 'react-i18next'; -import { ArrowRight } from 'lucide-react'; -import PlansSelect from '@/components/order/plan/PlanSelect.component'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { FullCapabilities } from '@/hooks/api/database/capabilities/useGetFullCapabilities.hook'; -import { ModalController } from '@/hooks/useModale'; -import { createTree } from '@/lib/availabilitiesHelper'; -import { order } from '@/types/catalog'; -import * as database from '@/types/cloud/project/database'; -import { Engine, Version } from '@/types/orderFunnel'; -import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { useToast } from '@/components/ui/use-toast'; -import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; -import { computeServicePrice } from '@/lib/pricingHelper'; -import Price from '@/components/price/Price.component'; -import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; -import { Label } from '@/components/ui/label'; -import PricingDetails from '../PricingDetails.component'; -import { getCdbApiErrorMessage } from '@/lib/apiHelper'; - -interface UpdatePlanProps { - controller: ModalController; - suggestions: database.availability.Suggestion[]; - availabilities: database.Availability[]; - capabilities: FullCapabilities; - catalog: order.publicOrder.Catalog; - onSuccess?: (service: database.Service) => void; - onError?: (error: Error) => void; -} - -const UpdatePlanContent = ({ - controller, - suggestions, - availabilities, - capabilities, - catalog, - onSuccess, - onError, -}: UpdatePlanProps) => { - const [showMonthly, setShowMonthly] = useState(false); - const { service, projectId } = useServiceData(); - const errorMessageRef = useRef(null); - const toast = useToast(); - const { t } = useTranslation( - 'pci-databases-analytics/services/service/settings/update', - ); - const { editService, isPending } = useEditService({ - onError: (err) => { - toast.toast({ - title: t('updatePlanToastErrorTitle'), - variant: 'destructive', - description: getCdbApiErrorMessage(err), - }); - if (onError) { - onError(err); - } - }, - onEditSuccess: (updatedService) => { - toast.toast({ - title: t('updatePlanToastSuccessTitle'), - description: t('updatePlanToastSuccessDescription', { - newPlan: updatedService.plan, - }), - }); - if (onSuccess) { - onSuccess(updatedService); - } - }, - }); - const listEngines = useMemo( - () => - createTree(availabilities, capabilities, suggestions, catalog).map( - (e) => { - // order the versions in the engines - e.versions.sort((a, b) => a.order - b.order); - return e; - }, - ), - [availabilities, capabilities], - ); - const listPlans = useMemo( - () => - listEngines - ?.find((e: Engine) => e.name === service.engine) - ?.versions.find((v: Version) => v.name === service.version) - ?.plans.sort((a, b) => a.order - b.order) || [], - [listEngines, service], - ); - - const schema = z.object({ - plan: z - .string() - .min(1) - .refine((newPlan) => newPlan !== service.plan, { - message: t('updatePlanErrorSimilar'), - }), - }); - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - plan: service.plan, - }, - }); - useEffect(() => { - errorMessageRef.current?.lastElementChild?.scrollIntoView({ - behavior: 'smooth', - }); - }, [form.formState.errors.plan]); - const onSubmit = form.handleSubmit((formValues) => { - // Get the data to submit. We want to check the flavor for some edge cases - // such as mongodb discovery, where the flavor must be updated with the plan - const { flavors } = listPlans - .find((p) => p.name === formValues.plan) - .regions.find((r) => r.name === service.nodes[0].region); - const flavor = flavors.find((f) => f.name === service.flavor) || flavors[0]; - const data = { - plan: formValues.plan, - ...(flavor.name !== service.flavor && { - flavor: flavor.name, - }), - }; - editService({ - serviceId: service.id, - projectId, - engine: service.engine, - data, - }); - }); - - const initialFlavorObject = listPlans - .find((p) => p.name === service.plan) - .regions.find((r) => r.name === service.nodes[0].region) - .flavors.find((f) => f.name === service.flavor); - const hasStorage = - service.storage?.size.value > 0 && service.storage.size.unit === 'GB'; - const initialAddedStorage = - hasStorage && initialFlavorObject - ? service.storage.size.value - initialFlavorObject.storage?.minimum.value - : 0; - - const selectedPlan = form.watch('plan'); - - const oldPrice = useMemo(() => { - return computeServicePrice({ - offerPricing: initialFlavorObject.pricing, - nbNodes: service.nodes.length, - storagePricing: initialFlavorObject.storage?.pricing, - additionalStorage: initialAddedStorage, - storageMode: listEngines.find((e) => e.name === service.engine) - .storageMode, - }); - }, [service.flavor]); - const newPrice = useMemo(() => { - const plan = listPlans.find((p) => p.name === selectedPlan); - const region = plan.regions.find((r) => r.name === service.nodes[0].region); - const flavor = - region.flavors.find((f) => f.name === service.flavor) || - region.flavors[0]; - const { storageMode } = listEngines.find((e) => e.name === service.engine); - return computeServicePrice({ - offerPricing: flavor.pricing, - nbNodes: Math.max(service.nodes.length, plan.nodes.minimum), - storagePricing: flavor.storage?.pricing, - additionalStorage: initialAddedStorage, - storageMode, - }); - }, [selectedPlan]); - - return ( - - - -
- - - - {t('updatePlanTitle')} - - - - - ( - -
- {t('updatePlanInputLabel')} - - - - -
-
- )} - /> - - -
- -
- - - - - -
-
- - - - -
-
-
-
- ); -}; - -const UpdatePlan = ({ controller, ...otherProps }: UpdatePlanProps) => { - if (!controller.open) return <>; - return ; -}; - -export default UpdatePlan; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useCapabilitiesWithCurrentTags.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useCapabilitiesWithCurrentTags.hook.tsx new file mode 100644 index 000000000000..36db3a4d68c4 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useCapabilitiesWithCurrentTags.hook.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { + FullCapabilities, + useGetFullCapabilities, +} from '@/hooks/api/database/capabilities/useGetFullCapabilities.hook'; +import * as database from '@/types/cloud/project/database'; +import { updateTags } from '@/lib/tagsHelper'; + +export function useCapabilitiesWithCurrentTags(service: database.Service) { + const { projectId } = useParams(); + const capabilitiesQuery = useGetFullCapabilities(projectId); + const capabilities: FullCapabilities = useMemo(() => { + if (!capabilitiesQuery.data) return null; + const { + flavors, + plans, + regions, + engines, + ...rest + } = capabilitiesQuery.data; + return { + ...rest, + engines: engines.map((e) => ({ + ...e, + versions: updateTags(e.versions, service.version), + })), + flavors: updateTags(flavors, service.flavor), + plans: updateTags(plans, service.plan), + regions: updateTags(regions, service.nodes[0].region), + } as FullCapabilities; + }, [capabilitiesQuery.data, service]); + return capabilities; +} diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useUpdateTree.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useUpdateTree.tsx new file mode 100644 index 000000000000..2385d7e69376 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/useUpdateTree.tsx @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import * as database from '@/types/cloud/project/database'; +import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; +import { useCapabilitiesWithCurrentTags } from './useCapabilitiesWithCurrentTags.hook'; +import { useServiceData } from '../../../Service.context'; +import { createTree } from '@/lib/availabilitiesHelper'; + +export function useUpdateTree(availabilities: database.Availability[]) { + const { service } = useServiceData(); + const capabilities = useCapabilitiesWithCurrentTags(service); + const catalogQuery = useGetCatalog(); + const tree = useMemo(() => { + if (!availabilities || !catalogQuery.data || !capabilities) return null; + const suggestions: database.availability.Suggestion[] = [ + { + default: true, + engine: service.engine, + flavor: service.flavor, + plan: service.plan, + region: service.region, + version: service.version, + }, + ]; + return createTree( + availabilities, + capabilities, + suggestions, + catalogQuery.data, + ).map((e) => { + // order the versions in the engines + e.versions.sort((a, b) => a.order - b.order); + return e; + }); + }, [availabilities, catalogQuery, capabilities, service]); + return tree; +} diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/AddNode.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.modal.tsx similarity index 68% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/AddNode.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.modal.tsx index e48b6831808e..b83c4d18ff3e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/AddNode.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.modal.tsx @@ -1,10 +1,10 @@ import { useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; import Price from '@/components/price/Price.component'; import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -14,23 +14,17 @@ import { import { Label } from '@/components/ui/label'; import { useToast } from '@/components/ui/use-toast'; import { useAddNode } from '@/hooks/api/database/node/useAddNode.hook'; -import { ModalController } from '@/hooks/useModale'; import { Pricing } from '@/lib/pricingHelper'; -import { order } from '@/types/catalog'; -import * as database from '@/types/cloud/project/database'; import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface AddNodeProps { - controller: ModalController; - catalog: order.publicOrder.Catalog; - onSuccess?: (node: database.service.Node) => void; - onError?: (error: Error) => void; -} - -const AddNode = ({ controller, catalog, onSuccess, onError }: AddNodeProps) => { +const AddNode = () => { const { service, projectId } = useServiceData(); const [showMonthly, setShowMonthly] = useState(false); + const catalogQuery = useGetCatalog(); + const navigate = useNavigate(); const toast = useToast(); const { t } = useTranslation( @@ -43,34 +37,30 @@ const AddNode = ({ controller, catalog, onSuccess, onError }: AddNodeProps) => { variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, - onSuccess: (node) => { + onSuccess: () => { toast.toast({ title: t('addNodeToastSuccessTitle'), description: t('addNodeToastSuccessDescription'), }); - if (onSuccess) { - onSuccess(node); - } + navigate('../'); }, }); const price: Pricing = useMemo(() => { + if (!catalogQuery.data) return null; const prefix = `databases.${service.engine.toLowerCase()}-${service.plan}-${ service.flavor }`; return { - hourly: catalog.addons.find( + hourly: catalogQuery.data?.addons.find( (a) => a.planCode === `${prefix}.hour.consumption`, ).pricings[0], - monthly: catalog.addons.find( + monthly: catalogQuery.data?.addons.find( (a) => a.planCode === `${prefix}.month.consumption`, ).pricings[0], }; - }, []); + }, [catalogQuery.data]); const handleSumbit = () => { addNode({ @@ -85,7 +75,7 @@ const AddNode = ({ controller, catalog, onSuccess, onError }: AddNodeProps) => { }; return ( - + @@ -95,27 +85,29 @@ const AddNode = ({ controller, catalog, onSuccess, onError }: AddNodeProps) => {

- - ), - }} - > + {price && ( + + ), + }} + > + )}

@@ -137,7 +129,7 @@ const AddNode = ({ controller, catalog, onSuccess, onError }: AddNodeProps) => {
-
+ ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.spec.tsx new file mode 100644 index 000000000000..bc4c2db854ac --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/addNode/AddNode.spec.tsx @@ -0,0 +1,126 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import AddNode from './AddNode.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as nodeApi from '@/data/api/database/node.api'; + +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + }; +}); +vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), +})); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ children }: { children: React.ReactNode }) => children, +})); +vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; +}); +vi.mock('@/data/api/database/node.api', () => ({ + addNode: vi.fn((data) => data.node), + deleteNode: vi.fn(), +})); +vi.mock('@/hooks/api/catalog/useGetCatalog.hook', () => ({ + useGetCatalog: vi.fn(() => ({ + data: { + addons: [ + { + planCode: 'databases.mongodb-plan-flavor.hour.consumption', + pricings: [{ price: 1000, tax: 200 }], + }, + { + planCode: 'databases.mongodb-plan-flavor.month.consumption', + pricings: [{ price: 20000, tax: 4000 }], + }, + ], + }, + isLoading: false, + })), +})); + +describe('Add Node Modal', () => { + beforeEach(() => {}); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('add-node-modal')).toBeInTheDocument(); + }); + }); + + it('should add a node on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('add-node-submit-button')); + }); + await waitFor(() => { + expect(nodeApi.addNode).toHaveBeenCalledWith({ + projectId: 'projectId', + engine: mockedService.engine, + serviceId: mockedService.id, + node: { + flavor: mockedService.flavor, + region: mockedService.nodes[0].region, + }, + }); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'addNodeToastSuccessTitle', + description: 'addNodeToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(nodeApi.addNode).mockImplementation(() => { + throw apiErrorMock; + }); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('add-node-submit-button')); + }); + await waitFor(() => { + expect(nodeApi.addNode).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'addNodeToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/RemoveNode.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.modal.tsx similarity index 69% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/RemoveNode.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.modal.tsx index d447191ec7d0..6359e440ccbe 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/RemoveNode.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.modal.tsx @@ -1,10 +1,10 @@ import { useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; import Price from '@/components/price/Price.component'; import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, @@ -13,28 +13,18 @@ import { } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { useToast } from '@/components/ui/use-toast'; -import { ModalController } from '@/hooks/useModale'; import { Pricing } from '@/lib/pricingHelper'; -import { order } from '@/types/catalog'; import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; import { useDeleteNode } from '@/hooks/api/database/node/useDeleteNode.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetCatalog } from '@/hooks/api/catalog/useGetCatalog.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteNodeProps { - controller: ModalController; - catalog: order.publicOrder.Catalog; - onSuccess?: () => void; - onError?: (error: Error) => void; -} - -const DeleteNode = ({ - controller, - catalog, - onSuccess, - onError, -}: DeleteNodeProps) => { +const DeleteNode = () => { const { service, projectId } = useServiceData(); const [showMonthly, setShowMonthly] = useState(false); + const catalogQuery = useGetCatalog(); + const navigate = useNavigate(); const toast = useToast(); const { t } = useTranslation( @@ -47,34 +37,30 @@ const DeleteNode = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteNodeToastSuccessTitle'), description: t('deleteNodeToastSuccessDescription'), }); - if (onSuccess) { - onSuccess(); - } + navigate('../'); }, }); const price: Pricing = useMemo(() => { + if (!catalogQuery.data) return null; const prefix = `databases.${service.engine.toLowerCase()}-${service.plan}-${ service.flavor }`; return { - hourly: catalog.addons.find( + hourly: catalogQuery.data?.addons.find( (a) => a.planCode === `${prefix}.hour.consumption`, ).pricings[0], - monthly: catalog.addons.find( + monthly: catalogQuery.data?.addons.find( (a) => a.planCode === `${prefix}.month.consumption`, ).pricings[0], }; - }, []); + }, [catalogQuery.data]); const handleSumbit = () => { deleteNode({ @@ -86,7 +72,7 @@ const DeleteNode = ({ }; return ( - + @@ -96,27 +82,29 @@ const DeleteNode = ({

- - ), - }} - > + {price && ( + + ), + }} + > + )}

@@ -138,7 +126,7 @@ const DeleteNode = ({
-
+ ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.spec.tsx new file mode 100644 index 000000000000..4fa0d6a291e2 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/deleteNode/DeleteNode.spec.tsx @@ -0,0 +1,124 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import DeleteNode from './DeleteNode.modal'; +import { useToast } from '@/components/ui/use-toast'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import * as nodeApi from '@/data/api/database/node.api'; +import { mockedNode } from '@/__tests__/helpers/mocks/nodes'; + +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + }; +}); +vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), +})); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ children }: { children: React.ReactNode }) => children, +})); +vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; +}); +vi.mock('@/data/api/database/node.api', () => ({ + addNode: vi.fn((data) => data.node), + deleteNode: vi.fn(), +})); +vi.mock('@/hooks/api/catalog/useGetCatalog.hook', () => ({ + useGetCatalog: vi.fn(() => ({ + data: { + addons: [ + { + planCode: 'databases.mongodb-plan-flavor.hour.consumption', + pricings: [{ price: 1000, tax: 200 }], + }, + { + planCode: 'databases.mongodb-plan-flavor.month.consumption', + pricings: [{ price: 20000, tax: 4000 }], + }, + ], + }, + isLoading: false, + })), +})); + +describe('Delete Node Modal', () => { + beforeEach(() => {}); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should open the modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + await waitFor(() => { + expect(screen.queryByTestId('delete-node-modal')).toBeInTheDocument(); + }); + }); + + it('should delete a node on submit', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('delete-node-submit-button')); + }); + await waitFor(() => { + expect(nodeApi.deleteNode).toHaveBeenCalledWith({ + projectId: 'projectId', + engine: mockedService.engine, + serviceId: mockedService.id, + nodeId: mockedNode.id, + }); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteNodeToastSuccessTitle', + description: 'deleteNodeToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(nodeApi.deleteNode).mockImplementation(() => { + throw apiErrorMock; + }); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('delete-node-submit-button')); + }); + await waitFor(() => { + expect(nodeApi.deleteNode).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'deleteNodeToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/UpdateFlavor.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/UpdateFlavor.modal.tsx new file mode 100644 index 000000000000..b687c299d7ef --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/UpdateFlavor.modal.tsx @@ -0,0 +1,233 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ArrowRight } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import FlavorsSelect from '@/components/order/flavor/FlavorSelect.component'; +import { Button } from '@/components/ui/button'; +import { + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; +import * as database from '@/types/cloud/project/database'; +import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useToast } from '@/components/ui/use-toast'; +import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; +import Price from '@/components/price/Price.component'; +import StorageConfig from '@/components/order/cluster-configuration/StorageConfig.component'; +import { formatStorage } from '@/lib/bytesHelper'; +import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; +import { Label } from '@/components/ui/label'; +import PricingDetails from '../_components/PricingDetails.component'; +import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetAvailabilities } from '@/hooks/api/database/availability/useGetAvailabilities.hook'; +import { useUpdateFlavor } from './useUpdateFlavor.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; + +const UpdateFlavor = () => { + const [showMonthly, setShowMonthly] = useState(false); + const navigate = useNavigate(); + const { service, projectId } = useServiceData(); + const toast = useToast(); + const { t } = useTranslation( + 'pci-databases-analytics/services/service/settings/update', + ); + const availabilitiesQuery = useGetAvailabilities( + projectId, + service.id, + database.availability.ActionEnum.update, + database.availability.TargetEnum.flavor, + ); + const { + form, + listFlavors, + availability, + hasStorage, + initialFlavorObject, + oldPrice, + newPrice, + } = useUpdateFlavor({ availabilities: availabilitiesQuery.data, service }); + const { editService, isPending } = useEditService({ + onError: (err) => { + toast.toast({ + title: t('updateFlavorToastErrorTitle'), + variant: 'destructive', + description: getCdbApiErrorMessage(err), + }); + }, + onEditSuccess: (updatedService) => { + toast.toast({ + title: t('updateFlavorToastSuccessTitle'), + description: hasStorage + ? t('updateFlavorAndStorageToastSuccessDescription', { + newFlavor: updatedService.flavor, + storage: formatStorage(updatedService.storage.size), + }) + : t('updateFlavorToastSuccessDescription', { + newFlavor: updatedService.flavor, + }), + }); + navigate('../'); + }, + }); + + const onSubmit = form.handleSubmit((formValues) => { + editService({ + serviceId: service.id, + projectId, + engine: service.engine, + data: { + flavor: formValues.flavor, + ...(hasStorage && { + disk: { + size: + availability.specifications.storage.minimum.value + + formValues.storage, + }, + }), + }, + }); + }); + + return ( + + +
+ + + + {t('updateFlavorTitle')} + + + + + + ( + + {t('updateFlavorInputLabel')} + + + + + + )} + /> + {hasStorage && ( + ( + + {t('updateStorageInputLabel')} + + + + +
+ {t('updateStorageTotal')} + + {formatStorage({ + unit: 'GB', + value: + availability.specifications.storage.minimum + .value + field.value, + })} + +
+
+ )} + /> + )} + + +
+ +
+
+ + + + + +
+
+ + + + +
+
+
+ + +
+
+ ); +}; + +export default UpdateFlavor; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/useUpdateFlavor.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/useUpdateFlavor.hook.tsx new file mode 100644 index 000000000000..9cfd2d7e944a --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateFlavor/useUpdateFlavor.hook.tsx @@ -0,0 +1,128 @@ +import { useEffect, useMemo } from 'react'; +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Engine, Flavor, Plan, Region, Version } from '@/types/orderFunnel'; +import * as databases from '@/types/cloud/project/database'; +import { computeServicePrice } from '@/lib/pricingHelper'; +import { useUpdateTree } from '../_components/useUpdateTree'; + +interface UseUpdateFlavorProps { + availabilities: databases.Availability[]; + service: databases.Service; +} + +export function useUpdateFlavor({ + availabilities, + service, +}: UseUpdateFlavorProps) { + // Validation schema + const schema = z.object({ + flavor: z.string().min(1), + storage: z.coerce.number().nonnegative(), + }); + + // Compute available flavors for this service + const listEngines = useUpdateTree(availabilities); + const listFlavors = useMemo(() => { + const engine = listEngines?.find((e: Engine) => e.name === service.engine); + const version = engine?.versions.find( + (v: Version) => v.name === service.version, + ); + const plan = version?.plans.find((p: Plan) => p.name === service.plan); + const region = plan?.regions.find( + (r: Region) => r.name === service.nodes[0].region, + ); + return region?.flavors.sort((a, b) => a.order - b.order) || []; + }, [listEngines, service]); + + // Initial values + const hasStorage = + service.storage?.size.value > 0 && service.storage.size.unit === 'GB'; + const initialFlavorObject = useMemo( + () => listFlavors.find((f: Flavor) => f.name === service.flavor), + [service.flavor, listFlavors], + ); + + const initialAddedStorage = useMemo(() => { + if (hasStorage && initialFlavorObject) { + return Math.max( + 0, + service.storage.size.value - + (initialFlavorObject.storage?.minimum.value || 0), + ); + } + return 0; + }, [service.storage, initialFlavorObject]); + + // Form setup + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + flavor: service.flavor, + storage: initialAddedStorage, + }, + }); + + const selectedFlavor = form.watch('flavor'); + const selectedStorage = form.watch('storage'); + + const flavorObject = useMemo( + () => listFlavors.find((f: Flavor) => f.name === selectedFlavor), + [selectedFlavor, listFlavors], + ); + + const availability = useMemo(() => { + return availabilities?.find( + (a) => + a.engine === service.engine && + a.specifications.flavor === selectedFlavor && + a.plan === service.plan && + a.region === service.nodes[0].region, + ); + }, [availabilities, service, selectedFlavor]); + + useEffect(() => { + form.setValue( + 'storage', + selectedFlavor === service.flavor ? initialAddedStorage : 0, + ); + }, [selectedFlavor, service.flavor, initialAddedStorage, form]); + + // Pricing calculations + const computePrice = ( + flavor: Flavor | undefined, + additionalStorage: number, + ) => { + if (!flavor || !listEngines) return null; + const { storageMode } = + listEngines.find((e) => e.name === service.engine) || {}; + return computeServicePrice({ + offerPricing: flavor.pricing || {}, + nbNodes: service.nodes.length, + storagePricing: flavor.storage?.pricing || {}, + additionalStorage, + storageMode, + }); + }; + + const oldPrice = useMemo(() => { + return computePrice(initialFlavorObject, initialAddedStorage); + }, [initialFlavorObject, initialAddedStorage, service.nodes.length]); + + const newPrice = useMemo(() => { + return computePrice(flavorObject, selectedStorage); + }, [flavorObject, selectedStorage, service.nodes.length]); + + return { + form, + listEngines, + listFlavors, + availability, + hasStorage, + initialFlavorObject, + initialAddedStorage, + oldPrice, + newPrice, + }; +} diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/UpdatePlan.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/UpdatePlan.modal.tsx new file mode 100644 index 000000000000..d623a6643e13 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/UpdatePlan.modal.tsx @@ -0,0 +1,205 @@ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ArrowRight } from 'lucide-react'; +import PlansSelect from '@/components/order/plan/PlanSelect.component'; +import { Button } from '@/components/ui/button'; +import { + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import * as database from '@/types/cloud/project/database'; +import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useToast } from '@/components/ui/use-toast'; +import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; +import Price from '@/components/price/Price.component'; +import PriceUnitSwitch from '@/components/price-unit-switch/PriceUnitSwitch.component'; +import { Label } from '@/components/ui/label'; +import PricingDetails from '../_components/PricingDetails.component'; +import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetAvailabilities } from '@/hooks/api/database/availability/useGetAvailabilities.hook'; +import { useUpdatePlan } from './useUpdatePlan.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; + +const UpdatePlan = () => { + const [showMonthly, setShowMonthly] = useState(false); + const navigate = useNavigate(); + const { service, projectId } = useServiceData(); + const errorMessageRef = useRef(null); + const toast = useToast(); + const { t } = useTranslation( + 'pci-databases-analytics/services/service/settings/update', + ); + const availabilitiesQuery = useGetAvailabilities( + projectId, + service.id, + database.availability.ActionEnum.update, + database.availability.TargetEnum.plan, + ); + + const { + form, + listPlans, + initialFlavorObject, + oldPrice, + newPrice, + } = useUpdatePlan({ availabilities: availabilitiesQuery.data, service }); + + const { editService, isPending } = useEditService({ + onError: (err) => { + toast.toast({ + title: t('updatePlanToastErrorTitle'), + variant: 'destructive', + description: getCdbApiErrorMessage(err), + }); + }, + onEditSuccess: (updatedService) => { + toast.toast({ + title: t('updatePlanToastSuccessTitle'), + description: t('updatePlanToastSuccessDescription', { + newPlan: updatedService.plan, + }), + }); + navigate('../'); + }, + }); + + useEffect(() => { + errorMessageRef.current?.lastElementChild?.scrollIntoView({ + behavior: 'smooth', + }); + }, [form.formState.errors.plan]); + + const onSubmit = form.handleSubmit((formValues) => { + // Get the data to submit. We want to check the flavor for some edge cases + // such as mongodb discovery, where the flavor must be updated with the plan + const { flavors } = listPlans + .find((p) => p.name === formValues.plan) + .regions.find((r) => r.name === service.nodes[0].region); + const flavor = flavors.find((f) => f.name === service.flavor) || flavors[0]; + const data = { + plan: formValues.plan, + ...(flavor.name !== service.flavor && { + flavor: flavor.name, + }), + }; + editService({ + serviceId: service.id, + projectId, + engine: service.engine, + data, + }); + }); + + return ( + + + +
+ + + + {t('updatePlanTitle')} + + + + + ( + +
+ {t('updatePlanInputLabel')} + + + + +
+
+ )} + /> + + +
+ +
+
+ + + + + +
+
+ + + + +
+
+
+
+
+ ); +}; + +export default UpdatePlan; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/useUpdatePlan.hook.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/useUpdatePlan.hook.tsx new file mode 100644 index 000000000000..595eb016c617 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updatePlan/useUpdatePlan.hook.tsx @@ -0,0 +1,114 @@ +import { useMemo } from 'react'; +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useTranslation } from 'react-i18next'; +import { Engine, Flavor, Plan, Version } from '@/types/orderFunnel'; +import * as databases from '@/types/cloud/project/database'; +import { computeServicePrice } from '@/lib/pricingHelper'; +import { useUpdateTree } from '../_components/useUpdateTree'; + +interface UseUpdatePlanProps { + availabilities: databases.Availability[]; + service: databases.Service; +} +export function useUpdatePlan({ availabilities, service }: UseUpdatePlanProps) { + const { t } = useTranslation( + 'pci-databases-analytics/services/service/settings/update', + ); + + // Validation schema + const schema = z.object({ + plan: z + .string() + .min(1) + .refine((newPlan) => newPlan !== service.plan, { + message: t('updatePlanErrorSimilar'), + }), + }); + + // Form setup + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + plan: service.plan, + }, + }); + const selectedPlan = form.watch('plan'); + + // Compute available plans for this service + const listEngines = useUpdateTree(availabilities); + const listPlans = + listEngines + ?.find((e: Engine) => e.name === service.engine) + ?.versions.find((v: Version) => v.name === service.version) + ?.plans.sort((a, b) => a.order - b.order) || []; + + // Initial values + const initialPlan = listPlans?.find((p) => p.name === service.plan); + const initialFlavorObject = useMemo(() => { + return initialPlan?.regions + .find((r) => r.name === service.nodes[0].region) + ?.flavors?.find((f) => f.name === service.flavor); + }, [initialPlan, service.nodes, service.flavor]); + + const initialAddedStorage = useMemo(() => { + if ( + service.storage?.size.value > 0 && + service.storage.size.unit === 'GB' && + initialFlavorObject + ) { + return ( + service.storage.size.value - initialFlavorObject.storage?.minimum.value + ); + } + return 0; + }, [service.storage, initialFlavorObject]); + + // Pricing calculations + const computePrice = ( + flavor: Flavor | undefined, + plan: Plan | undefined, + isNewPrice = false, + ) => { + if (!flavor) return null; + const { storageMode } = + listEngines?.find((e) => e.name === service.engine) || {}; + return computeServicePrice({ + offerPricing: flavor.pricing, + nbNodes: isNewPrice + ? Math.max(service.nodes.length, plan?.nodes.minimum || 0) + : service.nodes.length, + storagePricing: flavor.storage?.pricing, + additionalStorage: initialAddedStorage, + storageMode, + }); + }; + + const oldPrice = useMemo( + () => computePrice(initialFlavorObject, initialPlan), + [initialFlavorObject, initialAddedStorage], + ); + + const newPrice = useMemo(() => { + const selectedPlanObj = listPlans.find((p) => p.name === selectedPlan); + const region = selectedPlanObj?.regions.find( + (r) => r.name === service.nodes[0].region, + ); + const flavor = + region?.flavors.find((f) => f.name === service.flavor) || + region?.flavors[0]; + return computePrice(flavor, selectedPlanObj, true); + }, [selectedPlan, listPlans, initialAddedStorage]); + + return { + form, + listEngines, + listPlans, + initialPlan, + initialFlavorObject, + initialAddedStorage, + oldPrice, + newPrice, + }; +} diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateVersion.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateVersion/UpdateVersion.modal.tsx similarity index 71% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateVersion.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateVersion/UpdateVersion.modal.tsx index b4aafab4b930..444ffee3fedf 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/_components/modals/UpdateVersion.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/update/updateVersion/UpdateVersion.modal.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -6,16 +6,12 @@ import { useTranslation } from 'react-i18next'; import VersionSelector from '@/components/order/engine/EngineTileVersion.component'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; -import { createTree } from '@/lib/availabilitiesHelper'; -import { order } from '@/types/catalog'; import * as database from '@/types/cloud/project/database'; import { Engine } from '@/types/orderFunnel'; import { useServiceData } from '@/pages/services/[serviceId]/Service.context'; @@ -29,33 +25,24 @@ import { } from '@/components/ui/form'; import { useEditService } from '@/hooks/api/database/service/useEditService.hook'; import { useToast } from '@/components/ui/use-toast'; -import { FullCapabilities } from '@/hooks/api/database/capabilities/useGetFullCapabilities.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useGetAvailabilities } from '@/hooks/api/database/availability/useGetAvailabilities.hook'; +import { useUpdateTree } from '../_components/useUpdateTree'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface UpdateVersionProps { - controller: ModalController; - suggestions: database.availability.Suggestion[]; - availabilities: database.Availability[]; - capabilities: FullCapabilities; - catalog: order.publicOrder.Catalog; - onSuccess?: (service: database.Service) => void; - onError?: (error: Error) => void; -} - -const UpdateVersionContent = ({ - controller, - suggestions, - availabilities, - capabilities, - catalog, - onSuccess, - onError, -}: UpdateVersionProps) => { +const UpdateVersion = () => { const { service, projectId } = useServiceData(); + const navigate = useNavigate(); const toast = useToast(); const { t } = useTranslation( 'pci-databases-analytics/services/service/settings/update', ); + const availabilitiesQuery = useGetAvailabilities( + projectId, + service.id, + database.availability.ActionEnum.update, + database.availability.TargetEnum.version, + ); const { editService, isPending } = useEditService({ onError: (err) => { toast.toast({ @@ -63,9 +50,6 @@ const UpdateVersionContent = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onEditSuccess: (updatedService) => { toast.toast({ @@ -74,22 +58,15 @@ const UpdateVersionContent = ({ newVersion: updatedService.version, }), }); - if (onSuccess) { - onSuccess(updatedService); - } + navigate('../'); }, }); - const listVersions = useMemo( - () => - createTree(availabilities, capabilities, suggestions, catalog) - .map((e) => { - // order the versions in the engines - e.versions.sort((a, b) => a.order - b.order); - return e; - }) - ?.find((e: Engine) => e.name === service.engine)?.versions || [], - [availabilities, capabilities, service], - ); + + const listVersions = + useUpdateTree(availabilitiesQuery.data)?.find( + (e: Engine) => e.name === service.engine, + )?.versions || []; + const schema = z.object({ version: z .string() @@ -98,12 +75,14 @@ const UpdateVersionContent = ({ message: t('updateVersionErrorSimilar'), }), }); + const form = useForm({ resolver: zodResolver(schema), defaultValues: { version: service.version, }, }); + const onSubmit = form.handleSubmit((formValues) => { editService({ serviceId: service.id, @@ -114,8 +93,9 @@ const UpdateVersionContent = ({ }, }); }); + return ( - +
@@ -162,13 +142,8 @@ const UpdateVersionContent = ({
-
+ ); }; -const UpdateVersion = ({ controller, ...otherProps }: UpdateVersionProps) => { - if (!controller.open) return <>; - return ; -}; - export default UpdateVersion; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.page.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.page.tsx index 9b2de61e58e6..49ac4efa778a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.page.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.page.tsx @@ -1,6 +1,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; import { Plus } from 'lucide-react'; +import { Outlet, useNavigate } from 'react-router-dom'; import BreadcrumbItem from '@/components/breadcrumb/BreadcrumbItem.component'; import { useServiceData } from '../Service.context'; import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; @@ -9,12 +10,8 @@ import * as database from '@/types/cloud/project/database'; import { getColumns } from './_components/UsersTableColumns.component'; import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/ui/data-table'; -import { useModale } from '@/hooks/useModale'; -import DeleteUser from './_components/DeleteUser.component'; -import ResetUserPassword from './_components/ResetUserPassword.component'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { POLLING } from '@/configuration/polling.constants'; -import AddEditUserModal from './_components/AddEditUser.component'; export function breadcrumb() { return ( @@ -29,11 +26,9 @@ const Users = () => { const { t } = useTranslation( 'pci-databases-analytics/services/service/users', ); - const { projectId, service, serviceQuery } = useServiceData(); - const addEditModale = useModale('add-edit'); - const deleteModale = useModale('delete'); + const { projectId, service } = useServiceData(); + const navigate = useNavigate(); const { isUserActive } = useUserActivityContext(); - const resetPasswordModale = useModale('reset-password'); const usersQuery = useGetUsers(projectId, service.engine, service.id, { refetchInterval: isUserActive && POLLING.USERS, }); @@ -48,25 +43,16 @@ const Users = () => { displayCommandsCol: service.engine === database.EngineEnum.redis, displayChannelsCol: service.engine === database.EngineEnum.redis, onDeleteClicked: (user: GenericUser) => { - deleteModale.open(user.id); + navigate(`./delete/${user.id}`); }, onResetPasswordClicked: (user: GenericUser) => { - resetPasswordModale.open(user.id); + navigate(`./reset-password/${user.id}`); }, onEditClicked: (user: GenericUser) => { - addEditModale.open(user.id); + navigate(`./edit/${user.id}`); }, }); - const userToDelete = usersQuery.data?.find( - (u) => u.id === deleteModale.value, - ); - - const userToEdit = usersQuery.data?.find((u) => u.id === addEditModale.value); - - const userToResetPassword = usersQuery.data?.find( - (u) => u.id === resetPasswordModale.value, - ); return ( <>

{t('title')}

@@ -80,7 +66,7 @@ const Users = () => { service.capabilities.users?.create === database.service.capability.StateEnum.disabled } - onClick={() => addEditModale.open()} + onClick={() => navigate('./add')} > {t('addButtonLabel')} @@ -95,43 +81,7 @@ const Users = () => {
)} - { - addEditModale.close(); - usersQuery.refetch(); - serviceQuery.refetch(); - }} - /> - - {userToDelete && ( - { - deleteModale.close(); - usersQuery.refetch(); - serviceQuery.refetch(); - }} - /> - )} - {userToResetPassword && ( - { - resetPasswordModale.close(); - serviceQuery.refetch(); - }} - /> - )} + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.spec.tsx index 6089bcd3a7e9..d781e57f513e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/Users.spec.tsx @@ -18,6 +18,7 @@ import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/Route import { mockedService as mockedServiceOrig } from '@/__tests__/helpers/mocks/services'; import { mockedDatabaseUser } from '@/__tests__/helpers/mocks/databaseUser'; import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; +import { CdbError } from '@/data/api/database'; // Override mock to add capabilities const mockedService = { @@ -34,10 +35,17 @@ const mockedService = { }, }, }; - +const mockedUsedNavigate = vi.fn(); describe('Users page', () => { beforeEach(() => { // Mock necessary hooks and dependencies + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useNavigate: () => mockedUsedNavigate, + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -118,7 +126,7 @@ describe('Users page', () => { engine: database.EngineEnum.redis, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); vi.mocked(usersApi.getUsers).mockResolvedValue([ { @@ -145,7 +153,7 @@ describe('Users page', () => { engine: database.EngineEnum.m3db, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); vi.mocked(usersApi.getUsers).mockResolvedValue([ { @@ -170,7 +178,7 @@ describe('Users page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('users-add-button')).toBeInTheDocument(); @@ -183,7 +191,7 @@ describe('Users page', () => { capabilities: {}, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); expect(screen.queryByTestId('users-add-button')).toBeNull(); @@ -200,7 +208,7 @@ describe('Users page', () => { }, }, category: 'operational', - serviceQuery: {} as UseQueryResult, + serviceQuery: {} as UseQueryResult, }); render(, { wrapper: RouterWithQueryClientWrapper }); const addButton = screen.queryByTestId('users-add-button'); @@ -241,149 +249,29 @@ describe('Open modals', () => { }); it('shows add user modal', async () => { - await openButtonInMenu('user-action-delete-button'); + await openButtonInMenu('users-add-button'); await waitFor(() => { - expect(screen.getByTestId('delete-user-modal')).toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./add'); }); }); - it('closes add user modal', async () => { - act(() => { - fireEvent.click(screen.getByTestId('users-add-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-edit-user-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on add user success', async () => { - act(() => { - fireEvent.click(screen.getByTestId('users-add-button')); - }); - await waitFor(() => { - expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-username-input'), { - target: { - value: 'newUser', - }, - }); - fireEvent.click(screen.getByTestId('add-edit-user-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); - expect(usersApi.getUsers).toHaveBeenCalled(); - }); - }); - - it('shows delete user modal', async () => { - await openButtonInMenu('user-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-user-modal')).toBeInTheDocument(); - }); - }); - it('closes delete user modal', async () => { - await openButtonInMenu('user-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-user-cancel-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('delete-user-modal')).not.toBeInTheDocument(); - }); - }); - it('refetch data on delete user success', async () => { - const mockedServiceData = vi - .mocked(ServiceContext.useServiceData) - .getMockImplementation(); - mockedServiceData().serviceQuery.refetch = vi.fn(); - await openButtonInMenu('user-action-delete-button'); - await waitFor(() => { - expect(screen.getByTestId('delete-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('delete-user-submit-button')); - }); - await waitFor(() => { - expect(screen.queryByTestId('delete-user-modal')).not.toBeInTheDocument(); - expect(mockedServiceData().serviceQuery.refetch).toHaveBeenCalled(); - }); - }); - it('shows edit user modal', async () => { await openButtonInMenu('user-action-edit-button'); await waitFor(() => { - expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - }); - it('closes edit user modal', async () => { - await openButtonInMenu('user-action-edit-button'); - await waitFor(() => { - expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-edit-user-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); - }); - }); - it('refetch data on edit user success', async () => { - const mockedServiceData = vi - .mocked(ServiceContext.useServiceData) - .getMockImplementation(); - mockedServiceData().serviceQuery.refetch = vi.fn(); - await openButtonInMenu('user-action-edit-button'); - await waitFor(() => { - expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('add-edit-user-submit-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); - expect(mockedServiceData().serviceQuery.refetch).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./edit/userId'); }); }); - - it('shows reset password user modal', async () => { - await openButtonInMenu('user-action-reset-password-button'); + it('shows delete user modal', async () => { + await openButtonInMenu('user-action-delete-button'); await waitFor(() => { - expect(screen.getByTestId('reset-password-modal')).toBeInTheDocument(); + expect(mockedUsedNavigate).toHaveBeenCalledWith('./delete/userId'); }); }); - it('closes reset password user modal', async () => { - const mockedServiceData = vi - .mocked(ServiceContext.useServiceData) - .getMockImplementation(); - mockedServiceData().serviceQuery.refetch = vi.fn(); + it('shows reset user password modal', async () => { await openButtonInMenu('user-action-reset-password-button'); await waitFor(() => { - expect(screen.getByTestId('reset-password-modal')).toBeInTheDocument(); - }); - act(() => { - fireEvent.click(screen.getByTestId('reset-password-cancel-button')); - }); - await waitFor(() => { - expect( - screen.queryByTestId('reset-password-modal'), - ).not.toBeInTheDocument(); - expect(mockedServiceData().serviceQuery.refetch).toHaveBeenCalled(); + expect(mockedUsedNavigate).toHaveBeenCalledWith( + './reset-password/userId', + ); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.component.tsx index 70e23f03bc3e..ce2850bc0925 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.component.tsx @@ -1,9 +1,8 @@ import { z } from 'zod'; import { useTranslation } from 'react-i18next'; -import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -28,7 +27,6 @@ import { } from '@/components/ui/form'; import TagsInput from '@/components/tags-input/TagsInput.component'; -import { ModalController } from '@/hooks/useModale'; import { UseAddUser, useAddUser, @@ -39,36 +37,30 @@ import { useUserForm } from './formUser/useUserForm.hook'; import RolesSelect from './formUser/RolesSelect.component'; import { useServiceData } from '../../Service.context'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import RouteModal from '@/components/route-modal/RouteModal'; interface AddEditUserModalProps { - isEdition: boolean; editedUser?: GenericUser; - users: GenericUser[]; + existingUsers: GenericUser[]; service: database.Service; - controller: ModalController; onSuccess?: (user?: GenericUser) => void; onError?: (error: Error) => void; } const AddEditUserModal = ({ - isEdition, editedUser, - users, + existingUsers, service, - controller, - onSuccess, - onError, }: AddEditUserModalProps) => { + const navigate = useNavigate(); const { projectId } = useServiceData(); const { form, schema } = useUserForm({ - existingUsers: users, + existingUsers, service, editedUser, }); - useEffect(() => { - if (!controller.open) form.reset(); - }, [controller.open]); + const isEdition = !!editedUser?.id; const { t } = useTranslation( 'pci-databases-analytics/services/service/users', @@ -83,9 +75,6 @@ const AddEditUserModal = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: (user) => { toast.toast({ @@ -94,9 +83,7 @@ const AddEditUserModal = ({ name: user.username, }), }); - if (onSuccess) { - onSuccess(user); - } + navigate('../'); }, }; @@ -133,7 +120,7 @@ const AddEditUserModal = ({ }); return ( - + @@ -341,7 +328,7 @@ const AddEditUserModal = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.spec.tsx similarity index 69% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddUser.spec.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.spec.tsx index a2345297939e..dc5196df9b3a 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddUser.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/AddEditUser.spec.tsx @@ -5,9 +5,9 @@ import { waitFor, fireEvent, getByText, + act, } from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; -import { act } from 'react-dom/test-utils'; import * as database from '@/types/cloud/project/database'; import { Locale } from '@/hooks/useLocale'; import * as usersApi from '@/data/api/database/user.api'; @@ -21,7 +21,7 @@ import { } from '@/__tests__/helpers/mocks/databaseUser'; import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; -describe('Add user modal', () => { +describe('AddEdit user form', () => { beforeEach(async () => { vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -71,52 +71,22 @@ describe('Add user modal', () => { afterEach(() => { vi.clearAllMocks(); }); - it('should open the modal', async () => { - const controller = { - open: false, - onOpenChange: vi.fn(), - }; - const { rerender } = render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); + it('should render the form', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, }); - controller.open = true; - rerender( - , - ); await waitFor(() => { expect(screen.queryByTestId('add-edit-user-modal')).toBeInTheDocument(); }); }); it('should have redis inputs when provided a Redis engine', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; render( , { wrapper: RouterWithQueryClientWrapper }, ); @@ -135,19 +105,13 @@ describe('Add user modal', () => { }); }); it('should have group input when provided a m3db engine', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; render( , { wrapper: RouterWithQueryClientWrapper }, ); @@ -157,21 +121,9 @@ describe('Add user modal', () => { }); }); it('should add a user on submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); act(() => { fireEvent.change(screen.getByTestId('add-edit-username-input'), { target: { @@ -186,25 +138,16 @@ describe('Add user modal', () => { title: 'formUserToastSuccessTitle', description: 'addUserToastSuccessDescription', }); - expect(onSuccess).toHaveBeenCalled(); }); }); it('should add a user with redis values on submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); render( , { wrapper: RouterWithQueryClientWrapper }, ); @@ -235,38 +178,14 @@ describe('Add user modal', () => { title: 'formUserToastSuccessTitle', description: 'addUserToastSuccessDescription', }); - expect(onSuccess).toHaveBeenCalledWith({ - engine: 'redis', - projectId: 'projectId', - serviceId: 'serviceId', - user: { - categories: [], - channels: [], - commands: [], - keys: ['newKey'], - name: 'newUser', - }, - }); }); }); it('should add a user with a role', async () => { const mockScrollIntoView = vi.fn(); window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); await waitFor(() => { expect( screen.getByText('formUserRoleInputPlaceholder'), @@ -305,30 +224,13 @@ describe('Add user modal', () => { title: 'formUserToastSuccessTitle', description: 'addUserToastSuccessDescription', }); - expect(onSuccess).toHaveBeenCalledWith({ - engine: 'mongodb', - projectId: 'projectId', - serviceId: 'serviceId', - user: { - name: 'newUser', - roles: ['backup@admin'], - }, - }); }); }); it('should display an error if user already exists', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); render( { }); await waitFor(() => { expect(usersApi.addUser).not.toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); expect( screen.getByText('formUserNameErrorDuplicate'), ).toBeInTheDocument(); @@ -363,28 +264,15 @@ describe('Add user modal', () => { }); await waitFor(() => { expect(usersApi.addUser).not.toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); expect( screen.getByText('formUserNameErrorDuplicate'), ).toBeInTheDocument(); }); }); it('should display an error if userName is too short', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); act(() => { fireEvent.change(screen.getByTestId('add-edit-username-input'), { target: { @@ -395,26 +283,13 @@ describe('Add user modal', () => { }); await waitFor(() => { expect(usersApi.addUser).not.toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); expect(screen.getByText('formUserErrorMinLength')).toBeInTheDocument(); }); }); it('should display an error if userName is too long', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); act(() => { fireEvent.change(screen.getByTestId('add-edit-username-input'), { target: { @@ -425,26 +300,13 @@ describe('Add user modal', () => { }); await waitFor(() => { expect(usersApi.addUser).not.toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); expect(screen.getByText('formUserErrorMaxLength')).toBeInTheDocument(); }); }); it('should display an error if userName does not match patter', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); act(() => { fireEvent.change(screen.getByTestId('add-edit-username-input'), { target: { @@ -455,7 +317,6 @@ describe('Add user modal', () => { }); await waitFor(() => { expect(usersApi.addUser).not.toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); expect(screen.getByText('formUserNameErrorPattern')).toBeInTheDocument(); }); }); @@ -463,21 +324,9 @@ describe('Add user modal', () => { vi.mocked(usersApi.addUser).mockImplementation(() => { throw apiErrorMock; }); - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onError = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); act(() => { fireEvent.change(screen.getByTestId('add-edit-username-input'), { target: { @@ -493,7 +342,6 @@ describe('Add user modal', () => { description: apiErrorMock.response.data.message, variant: 'destructive', }); - expect(onError).toHaveBeenCalled(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/UpdateUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/UpdateUser.spec.tsx deleted file mode 100644 index 7288a17eae6e..000000000000 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/UpdateUser.spec.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; -import { UseQueryResult } from '@tanstack/react-query'; -import { act } from 'react-dom/test-utils'; -import * as database from '@/types/cloud/project/database'; -import { Locale } from '@/hooks/useLocale'; -import * as usersApi from '@/data/api/database/user.api'; -import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; -import { useToast } from '@/components/ui/use-toast'; -import { mockedService } from '@/__tests__/helpers/mocks/services'; -import { mockedDatabaseUser } from '@/__tests__/helpers/mocks/databaseUser'; -import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; -import AddEditUserModal from '@/pages/services/[serviceId]/users/_components/AddEditUser.component'; - -describe('Edit user modal', () => { - beforeEach(() => { - // Mock necessary hooks and dependencies - vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), - })); - vi.mock('@/data/api/database/user.api', () => ({ - getUsers: vi.fn(() => [mockedDatabaseUser]), - addUser: vi.fn(), - deleteUser: vi.fn(), - resetUserPassword: vi.fn(), - getRoles: vi.fn(() => []), - editUser: vi.fn((user) => user), - })); - - vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ - useServiceData: vi.fn(() => ({ - projectId: 'projectId', - service: mockedService, - category: 'operational', - serviceQuery: {} as UseQueryResult, - })), - })); - - vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { - const mod = await importOriginal< - typeof import('@ovh-ux/manager-react-shell-client') - >(); - return { - ...mod, - useShell: vi.fn(() => ({ - i18n: { - getLocale: vi.fn(() => Locale.fr_FR), - onLocaleChange: vi.fn(), - setLocale: vi.fn(), - }, - })), - }; - }); - vi.mock('@/components/ui/use-toast', () => { - const toastMock = vi.fn(); - return { - useToast: vi.fn(() => ({ - toast: toastMock, - })), - }; - }); - }); - afterEach(() => { - vi.clearAllMocks(); - }); - it('should open the modal', async () => { - const controller = { - open: false, - onOpenChange: vi.fn(), - }; - const { rerender } = render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); - await waitFor(() => { - expect( - screen.queryByTestId('add-edit-user-modal'), - ).not.toBeInTheDocument(); - }); - controller.open = true; - rerender( - , - ); - await waitFor(() => { - expect(screen.queryByTestId('add-edit-user-modal')).toBeInTheDocument(); - }); - }); - it('should edit a user on submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-group-input'), { - target: { - value: 'newGroup', - }, - }); - fireEvent.click(screen.getByTestId('add-edit-user-submit-button')); - }); - await waitFor(() => { - expect(usersApi.editUser).toHaveBeenCalledWith({ - engine: database.EngineEnum.m3db, - projectId: 'projectId', - serviceId: mockedService.id, - user: { - id: mockedDatabaseUser.id, - group: 'newGroup', - }, - }); - expect(useToast().toast).toHaveBeenCalledWith({ - title: 'formUserToastSuccessTitle', - description: 'editUserToastSuccessDescription', - }); - expect(onSuccess).toHaveBeenCalled(); - }); - }); - it('should call onError when api failed', async () => { - vi.mocked(usersApi.editUser).mockImplementation(() => { - throw apiErrorMock; - }); - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onError = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); - act(() => { - fireEvent.change(screen.getByTestId('add-edit-group-input'), { - target: { - value: 'newGroup', - }, - }); - fireEvent.click(screen.getByTestId('add-edit-user-submit-button')); - }); - await waitFor(() => { - expect(usersApi.editUser).toHaveBeenCalled(); - expect(useToast().toast).toHaveBeenCalledWith({ - title: 'editUserToastErrorTitle', - description: apiErrorMock.response.data.message, - variant: 'destructive', - }); - expect(onError).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.modal.tsx new file mode 100644 index 000000000000..40e857214368 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.modal.tsx @@ -0,0 +1,13 @@ +import { useServiceData } from '../../Service.context'; +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; +import AddEditUserModal from '../_components/AddEditUser.component'; + +const AddUserModal = () => { + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + return ; +}; + +export default AddUserModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.spec.tsx new file mode 100644 index 000000000000..68914e24baf5 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/add/AddUser.spec.tsx @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import AddUserModal from './AddUser.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedDatabaseUser } from '@/__tests__/helpers/mocks/databaseUser'; +import * as usersApi from '@/data/api/database/user.api'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditUser.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedDatabaseUser]), + addUser: vi.fn((user) => user), + deleteUser: vi.fn(), + resetUserPassword: vi.fn(), + getRoles: vi.fn(() => []), + editUser: vi.fn((user) => user), +})); + +describe('AddUserModal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render AddEditUserModal with users when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(usersApi.getUsers).mockResolvedValue([mockedDatabaseUser]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.modal.tsx similarity index 67% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.modal.tsx index 978e1b6c60fc..5c37b2c6858e 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.modal.tsx @@ -1,8 +1,8 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -10,29 +10,23 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; -import * as database from '@/types/cloud/project/database'; -import { GenericUser } from '@/data/api/database/user.api'; import { useDeleteUser } from '@/hooks/api/database/user/useDeleteUser.hook'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface DeleteUserModalProps { - service: database.Service; - controller: ModalController; - user: GenericUser; - onSuccess?: (user: GenericUser) => void; - onError?: (error: Error) => void; -} +const DeleteUser = () => { + const { userId } = useParams(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service?.engine, service.id, { + enabled: !!service.id, + }); + const users = usersQuery.data; + const deletedUser = users?.find((u) => u.id === userId); -const DeleteUser = ({ - service, - user, - controller, - onError, - onSuccess, -}: DeleteUserModalProps) => { - const { projectId } = useParams(); const { t } = useTranslation( 'pci-databases-analytics/services/service/users', ); @@ -44,41 +38,40 @@ const DeleteUser = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: () => { toast.toast({ title: t('deleteUserToastSuccessTitle'), description: t('deleteUserToastSuccessDescription', { - name: user.username, + name: deletedUser.username, }), }); - if (onSuccess) { - onSuccess(user); - } + navigate('../'); }, }); + useEffect(() => { + if (users && !deletedUser) navigate('../'); + }, [users, deletedUser]); + const handleDelete = () => { deleteUser({ serviceId: service.id, projectId, engine: service.engine, - userId: user.id, + userId: deletedUser.id, }); }; return ( - + {t('deleteUserTitle')} - {t('deleteUserDescription', { name: user.username })} + {t('deleteUserDescription', { name: deletedUser?.username })} @@ -101,7 +94,7 @@ const DeleteUser = ({ - + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.spec.tsx similarity index 71% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.spec.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.spec.tsx index 49a2e108f5a3..70fbb5807439 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/DeleteUser.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/delete/DeleteUser.spec.tsx @@ -1,12 +1,17 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; -import { act } from 'react-dom/test-utils'; import * as database from '@/types/cloud/project/database'; import { Locale } from '@/hooks/useLocale'; import * as usersApi from '@/data/api/database/user.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; -import DeleteUser from '@/pages/services/[serviceId]/users/_components/DeleteUser.component'; +import DeleteUser from '@/pages/services/[serviceId]/users/delete/DeleteUser.modal'; import { useToast } from '@/components/ui/use-toast'; import { mockedService } from '@/__tests__/helpers/mocks/services'; import { mockedDatabaseUser } from '@/__tests__/helpers/mocks/databaseUser'; @@ -14,6 +19,17 @@ import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; describe('Delete user modal', () => { beforeEach(() => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + userId: mockedDatabaseUser.id, + }), + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -69,44 +85,20 @@ describe('Delete user modal', () => { open: false, onOpenChange: vi.fn(), }; - const { rerender } = render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + const { rerender } = render(, { + wrapper: RouterWithQueryClientWrapper, + }); await waitFor(() => { expect(screen.queryByTestId('delete-user-modal')).not.toBeInTheDocument(); }); controller.open = true; - rerender( - , - ); + rerender(); await waitFor(() => { expect(screen.queryByTestId('delete-user-modal')).toBeInTheDocument(); }); }); it('should delete a user on submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('delete-user-submit-button')); }); @@ -116,27 +108,13 @@ describe('Delete user modal', () => { title: 'deleteUserToastSuccessTitle', description: 'deleteUserToastSuccessDescription', }); - expect(onSuccess).toHaveBeenCalled(); }); }); it('should call onError when api failed', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onError = vi.fn(); vi.mocked(usersApi.deleteUser).mockImplementation(() => { throw apiErrorMock; }); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('delete-user-submit-button')); }); @@ -147,7 +125,6 @@ describe('Delete user modal', () => { description: apiErrorMock.response.data.message, variant: 'destructive', }); - expect(onError).toHaveBeenCalled(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.modal.tsx new file mode 100644 index 000000000000..bb5d735a1f8a --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.modal.tsx @@ -0,0 +1,30 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useEffect } from 'react'; +import { useServiceData } from '../../Service.context'; +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; +import AddEditUserModal from '../_components/AddEditUser.component'; + +const AddUserModal = () => { + const { userId } = useParams(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + const users = usersQuery.data; + const editedUser = users?.find((u) => u.id === userId); + + useEffect(() => { + if (users && !editedUser) navigate('../'); + }, [users, editedUser]); + + return ( + + ); +}; + +export default AddUserModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.spec.tsx new file mode 100644 index 000000000000..db12011c875f --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/edit/EditUser.spec.tsx @@ -0,0 +1,61 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import EditUser from './EditUser.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import { mockedDatabaseUser } from '@/__tests__/helpers/mocks/databaseUser'; +import * as usersApi from '@/data/api/database/user.api'; +import * as database from '@/types/cloud/project/database'; + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: vi.fn(() =>
), +})); +vi.mock('../_components/AddEditUser.component', () => ({ + default: vi.fn(() =>
), +})); +vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + userId: mockedDatabaseUser.id, + }), + }; +}); +vi.mock('@/data/api/database/user.api', () => ({ + getUsers: vi.fn(() => [mockedDatabaseUser]), + addUser: vi.fn((user) => user), + deleteUser: vi.fn(), + resetUserPassword: vi.fn(), + getRoles: vi.fn(() => []), + editUser: vi.fn((user) => user), +})); + +describe('EditUser modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render the modal when data is fetched successfully', async () => { + // Simulate successful data fetching in the useGetUsers hook + vi.mocked(usersApi.getUsers).mockResolvedValue([mockedDatabaseUser]); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('add-edit-user-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetUserPassword.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.modal.tsx similarity index 77% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetUserPassword.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.modal.tsx index ed69ab9439fd..623fbf3a6e2f 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetUserPassword.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.modal.tsx @@ -1,10 +1,9 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Copy } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { - Dialog, DialogClose, DialogContent, DialogDescription, @@ -12,34 +11,25 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; -import { ModalController } from '@/hooks/useModale'; import { useToast } from '@/components/ui/use-toast'; -import * as database from '@/types/cloud/project/database'; -import { GenericUser } from '@/data/api/database/user.api'; import { useResetUserPassword } from '@/hooks/api/database/user/useResetUserPassword.hook'; import { Alert } from '@/components/ui/alert'; import { getCdbApiErrorMessage } from '@/lib/apiHelper'; +import { useServiceData } from '../../Service.context'; +import { useGetUsers } from '@/hooks/api/database/user/useGetUsers.hook'; +import RouteModal from '@/components/route-modal/RouteModal'; -interface ResetUserPasswordModalProps { - service: database.Service; - controller: ModalController; - user: GenericUser; - onSuccess?: (user: GenericUser) => void; - onError?: (error: Error) => void; - onClose?: () => void; -} +const ResetUserPassword = () => { + const { userId } = useParams(); + const navigate = useNavigate(); + const { projectId, service } = useServiceData(); + const usersQuery = useGetUsers(projectId, service.engine, service.id, { + enabled: !!service.id, + }); + const users = usersQuery.data; + const user = users?.find((u) => u.id === userId); -const ResetUserPassword = ({ - service, - user, - controller, - onError, - onSuccess, - onClose, -}: ResetUserPasswordModalProps) => { - // import translations const [newPass, setNewPass] = useState(); - const { projectId } = useParams(); const { t } = useTranslation( 'pci-databases-analytics/services/service/users', ); @@ -51,9 +41,6 @@ const ResetUserPassword = ({ variant: 'destructive', description: getCdbApiErrorMessage(err), }); - if (onError) { - onError(err); - } }, onSuccess: (userWithPassword) => { toast.toast({ @@ -63,17 +50,12 @@ const ResetUserPassword = ({ }), }); setNewPass(userWithPassword.password); - if (onSuccess) { - onSuccess(user); - } }, }); - const handleClose = () => { - if (onClose) { - onClose(); - } - }; + useEffect(() => { + if (users && !user) navigate('../'); + }, [users, user]); const handleResetPassword = () => { resetUserPassword({ @@ -91,7 +73,7 @@ const ResetUserPassword = ({ }; return ( - + @@ -116,13 +98,13 @@ const ResetUserPassword = ({ ) : ( - {t('resetUserPasswordDescription', { name: user.username })} + {t('resetUserPasswordDescription', { name: user?.username })} )} {newPass ? ( - onClose()}> + + ); }; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetPassword.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.spec.tsx similarity index 69% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetPassword.spec.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.spec.tsx index 585c8fa91f6a..8ea53e582f19 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/_components/ResetPassword.spec.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/users/resetPassword/ResetPassword.spec.tsx @@ -1,13 +1,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { + render, + screen, + waitFor, + fireEvent, + act, +} from '@testing-library/react'; import { UseQueryResult } from '@tanstack/react-query'; -import { act } from 'react-dom/test-utils'; import * as database from '@/types/cloud/project/database'; import { Locale } from '@/hooks/useLocale'; import * as usersApi from '@/data/api/database/user.api'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import { useToast } from '@/components/ui/use-toast'; -import ResetUserPassword from '@/pages/services/[serviceId]/users/_components/ResetUserPassword.component'; +import ResetUserPassword from '@/pages/services/[serviceId]/users/resetPassword/ResetPassword.modal'; import { mockedService } from '@/__tests__/helpers/mocks/services'; import { mockedDatabaseUser, @@ -17,6 +22,17 @@ import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; describe('Reset user password modal', () => { beforeEach(async () => { + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + category: database.engine.CategoryEnum.all, + userId: mockedDatabaseUser.id, + }), + }; + }); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -68,56 +84,21 @@ describe('Reset user password modal', () => { vi.clearAllMocks(); }); it('should open the modal', async () => { - const controller = { - open: false, - onOpenChange: vi.fn(), - }; - const { rerender } = render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + const { rerender } = render(, { + wrapper: RouterWithQueryClientWrapper, + }); await waitFor(() => { expect( screen.queryByTestId('reset-password-modal'), ).not.toBeInTheDocument(); }); - controller.open = true; - rerender( - , - ); + rerender(); await waitFor(() => { expect(screen.queryByTestId('reset-password-modal')).toBeInTheDocument(); }); }); it('should reset a user password on submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const user = { - id: '0', - username: 'avadmin', - status: database.StatusEnum.READY, - createdAt: '2024-03-19T11:34:47.088723+01:00', - }; - const onSuccess = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('reset-password-submit-button')); }); @@ -127,27 +108,13 @@ describe('Reset user password modal', () => { title: 'resetUserPasswordToastSuccessTitle', description: 'resetUserPasswordToastSuccessDescription', }); - expect(onSuccess).toHaveBeenCalled(); }); }); it('should call onError when api failed', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onError = vi.fn(); vi.mocked(usersApi.resetUserPassword).mockImplementationOnce(() => { throw apiErrorMock; }); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('reset-password-submit-button')); }); @@ -158,24 +125,12 @@ describe('Reset user password modal', () => { description: apiErrorMock.response.data.message, variant: 'destructive', }); - expect(onError).toHaveBeenCalled(); }); }); it('should copy password to clipboard', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; const writeTextMock = vi.fn(); vi.stubGlobal('navigator', { clipboard: { writeText: writeTextMock } }); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('reset-password-submit-button')); }); @@ -197,20 +152,7 @@ describe('Reset user password modal', () => { }); }); it('should close modal on close button after submit', async () => { - const controller = { - open: true, - onOpenChange: vi.fn(), - }; - const onClose = vi.fn(); - render( - , - { wrapper: RouterWithQueryClientWrapper }, - ); + render(, { wrapper: RouterWithQueryClientWrapper }); act(() => { fireEvent.click(screen.getByTestId('reset-password-submit-button')); }); @@ -228,7 +170,6 @@ describe('Reset user password modal', () => { title: 'resetUserPasswordToastSuccessTitle', description: 'resetUserPasswordToastSuccessDescription', }); - expect(onClose).toHaveBeenCalled(); }); }); }); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/ServiceListTable.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/ServiceListTable.component.tsx index 4d7ab6e02690..0840de2ea2e4 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/ServiceListTable.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/ServiceListTable.component.tsx @@ -1,40 +1,24 @@ -import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { ColumnDef } from '@tanstack/react-table'; import { DataTable } from '@/components/ui/data-table'; import * as database from '@/types/cloud/project/database'; import { Skeleton } from '@/components/ui/skeleton'; import { getColumns } from './ServiceListColumns.component'; -import { useModale } from '@/hooks/useModale'; -import RenameService from '../[serviceId]/_components/RenameService.component'; -import DeleteService from '../[serviceId]/_components/DeleteService.component'; import { useTrackAction } from '@/hooks/useTracking'; import { TRACKING } from '@/configuration/tracking.constants'; interface ServicesListProps { services: database.Service[]; - refetchFn: () => void; } -export default function ServicesList({ - services, - refetchFn, -}: ServicesListProps) { +export default function ServicesList({ services }: ServicesListProps) { const track = useTrackAction(); - const renameModale = useModale('rename'); - const deleteModale = useModale('delete'); - const editingService = useMemo( - () => services.find((s) => s.id === renameModale.value), - [renameModale.value, services], - ); - const deletingService = useMemo( - () => services.find((s) => s.id === deleteModale.value), - [deleteModale.value, services], - ); + const navigate = useNavigate(); const columns: ColumnDef[] = getColumns({ onRenameClicked: (service: database.Service) => { track(TRACKING.servicesList.renameClick(service.engine)); - renameModale.open(service.id); + navigate(`./rename/${service.id}`); }, onDeleteClicked: (service: database.Service) => { track( @@ -43,7 +27,7 @@ export default function ServicesList({ service.nodes[0].region, ), ); - deleteModale.open(service.id); + navigate(`./delete/${service.id}`); }, }); @@ -55,26 +39,6 @@ export default function ServicesList({ pageSize={25} itemNumber={services.length} /> - {editingService && ( - { - renameModale.close(); - refetchFn(); - }} - /> - )} - {deletingService && ( - { - deleteModale.close(); - refetchFn(); - }} - /> - )} ); } diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.modal.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.modal.tsx new file mode 100644 index 000000000000..3785310a4dcc --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.modal.tsx @@ -0,0 +1,17 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import { useGetService } from '@/hooks/api/database/service/useGetService.hook'; +import DeleteService from '../[serviceId]/_components/DeleteService.component'; + +const DeleteServiceModal = () => { + const { projectId, serviceId } = useParams(); + const navigate = useNavigate(); + const serviceQuery = useGetService(projectId, serviceId); + return ( + navigate('../')} + service={serviceQuery.data} + /> + ); +}; + +export default DeleteServiceModal; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.spec.tsx new file mode 100644 index 000000000000..532dcba746cb --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/delete/Delete.spec.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import DeleteModal from './Delete.modal'; // Adjust the path as needed +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; + +vi.mock('../[serviceId]/_components/DeleteService.component', () => ({ + default: vi.fn(() =>
), +})); +describe('Services list delete modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + service: mockedService, + })), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render delete modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect(screen.getByTestId('delete-service-modal')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/Onboarding.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/onboarding/Onboarding.page.tsx similarity index 95% rename from packages/manager/apps/pci-databases-analytics/src/pages/services/_components/Onboarding.component.tsx rename to packages/manager/apps/pci-databases-analytics/src/pages/services/onboarding/Onboarding.page.tsx index 3c1d15402b8f..8226c877bcbd 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/_components/Onboarding.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/onboarding/Onboarding.page.tsx @@ -4,13 +4,12 @@ import Guides from '@/components/guides/Guides.component'; import { GuideSections } from '@/types/guide'; import { Button } from '@/components/ui/button'; import Link from '@/components/links/Link.component'; -import { useTrackPage, useTrackAction } from '@/hooks/useTracking'; +import { useTrackAction } from '@/hooks/useTracking'; import { TRACKING } from '@/configuration/tracking.constants'; import OnboardingTile from './OnboardingTile.component'; const Onboarding = () => { const { t } = useTranslation('pci-databases-analytics/services/onboarding'); - useTrackPage(TRACKING.onboarding.page()); const track = useTrackAction(); return ( @@ -39,7 +38,7 @@ const Onboarding = () => {