diff --git a/packages/manager/apps/pci-savings-plan/package.json b/packages/manager/apps/pci-savings-plan/package.json index 036b4dfa2e5b..d2b5a003bc56 100644 --- a/packages/manager/apps/pci-savings-plan/package.json +++ b/packages/manager/apps/pci-savings-plan/package.json @@ -23,16 +23,14 @@ "@ovh-ux/manager-config": "^8.0.2", "@ovh-ux/manager-core-api": "^0.9.0", "@ovh-ux/manager-core-utils": "*", - "@ovh-ux/manager-pci-common": "^0.14.2", - "@ovh-ux/manager-react-components": "^1.43.0", + "@ovh-ux/manager-pci-common": "^1.0.4", "@ovh-ux/manager-react-core-application": "^0.11.5", "@ovh-ux/manager-react-shell-client": "^0.8.5", + "@ovh-ux/manager-react-components": "^2.5.1", "@ovh-ux/manager-tailwind-config": "^0.2.1", "@ovh-ux/request-tagger": "^0.4.0", - "@ovhcloud/ods-common-core": "17.2.2", - "@ovhcloud/ods-common-theming": "17.2.2", - "@ovhcloud/ods-components": "17.2.2", - "@ovhcloud/ods-theme-blue-jeans": "17.2.1", + "@ovhcloud/ods-components": "^18.4.1", + "@ovhcloud/ods-themes": "^18.4.1", "@tanstack/react-query": "5.51.11", "@tanstack/react-query-devtools": "5.29.2", "@testing-library/react": "^16.0.0", diff --git a/packages/manager/apps/pci-savings-plan/src/App.tsx b/packages/manager/apps/pci-savings-plan/src/App.tsx index 641830d51bad..931cb50149fa 100644 --- a/packages/manager/apps/pci-savings-plan/src/App.tsx +++ b/packages/manager/apps/pci-savings-plan/src/App.tsx @@ -1,12 +1,9 @@ import React, { useEffect, useContext } from 'react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; -import { odsSetup } from '@ovhcloud/ods-common-core'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { RouterProvider, createHashRouter } from 'react-router-dom'; import { Routes } from './routes/routes'; -odsSetup(); - export const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/packages/manager/apps/pci-savings-plan/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/apps/pci-savings-plan/src/components/Breadcrumb/Breadcrumb.tsx index da40a1a4751a..5e59eadb1dda 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Breadcrumb/Breadcrumb.tsx @@ -1,7 +1,10 @@ import React, { useState } from 'react'; import { Params, useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { OsdsBreadcrumb } from '@ovhcloud/ods-components/react'; +import { + OdsBreadcrumb, + OdsBreadcrumbItem, +} from '@ovhcloud/ods-components/react'; import { useNavigation } from '@ovh-ux/manager-react-shell-client'; import { useProject } from '@ovh-ux/manager-pci-common'; @@ -16,6 +19,7 @@ const getPageName = (location: string, t: (key: string) => string) => { return [ { label: t('createSavingsPlan'), + href: location, }, ]; } @@ -45,20 +49,28 @@ const Breadcrumb: React.FC = () => { updateNav(); }, [navigation, projectId]); + const breadcrumbItems = [ + { + href: urlProject, + label: project?.description, + }, + { + href: `${urlProject}/savings-plan`, + label: t('title'), + }, + ...items, + ]; + return ( - + + {breadcrumbItems.map((item) => ( + + ))} + ); }; diff --git a/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.test.tsx b/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.test.tsx index 0c8c76629247..74ef0835fb6f 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.test.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.test.tsx @@ -7,6 +7,12 @@ import '@testing-library/jest-dom'; import { render } from '@/utils/testProvider'; +vi.mock('@ovh-ux/manager-react-components', () => ({ + useCatalogPrice: vi.fn().mockReturnValue({ + getTextPrice: vi.fn().mockReturnValue('€10.00'), + }), +})); + const defaultProps = { duration: 1, price: '1', diff --git a/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.tsx b/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.tsx index 83b75be5e39f..9cab2ccb4273 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Commitment/Commitment.tsx @@ -1,17 +1,13 @@ -import { - ODS_THEME_COLOR_INTENT, - ODS_THEME_TYPOGRAPHY_SIZE, -} from '@ovhcloud/ods-common-theming'; import { ButtonType, PageLocation, useOvhTracking, } from '@ovh-ux/manager-react-shell-client'; -import { ODS_TILE_SIZE, ODS_TILE_VARIANT } from '@ovhcloud/ods-components'; -import { OsdsText, OsdsTile } from '@ovhcloud/ods-components/react'; +import { OdsText, OdsCard } from '@ovhcloud/ods-components/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useCatalogPrice } from '@ovh-ux/manager-react-components'; +import clsx from 'clsx'; import { getDiffInPercent } from './Commitment.utils'; import { CENTS_PRICE, @@ -57,54 +53,40 @@ const Commitment = ({ }; return ( - - - - {t('commitment_month', { value: duration })} - - - {diffInPercent ? `- ${diffInPercent} %` : ''} - + + {t('commitment_month', { value: duration })} + {diffInPercent && ( + + + {`- ${diffInPercent} %`} + + + )} - +
{priceByMonthWithoutCommitment && ( - + {`~ ${getTextPrice(priceByMonthWithoutCommitment * CENTS_PRICE)}`} - + )} - - {getTextPrice(priceNumber * CENTS_PRICE)} - + + + {getTextPrice(priceNumber * CENTS_PRICE)} + +
- {t('commitment_price_month')} + {t('commitment_price_month')}
-
+ ); }; diff --git a/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlan.test.tsx b/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlan.test.tsx index ce2af2758cb8..96e5a8f45fcd 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlan.test.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlan.test.tsx @@ -11,6 +11,10 @@ import { import { render } from '@/utils/testProvider'; +vi.mock('@ovh-ux/manager-pci-common', () => ({ + usePciUrl: vi.fn(() => '/url'), +})); + vi.mock('react-i18next', () => ({ useTranslation: vi.fn().mockReturnValue({ t: (key: string) => key, @@ -38,10 +42,6 @@ vi.mock('react-router-dom', async (importOriginal) => { }; }); -vi.mock('@ovh-ux/manager-pci-common', () => ({ - usePciUrl: vi.fn(() => '/url'), -})); - const mockOnCreatePlan = vi.fn(); const mockSetTechnicalModel = vi.fn(); @@ -139,30 +139,21 @@ describe('CreatePlanForm', () => { // Select duration fireEvent.click(screen.getByText('commitment_month')); - // Select quantity - const plusButton = screen.getByTestId('plus-button'); - fireEvent.click(plusButton); // Select model fireEvent.click(screen.getByText('select_model_description_instance_b3')); // Accept legal checkbox fireEvent.click(screen.getByText('legal_checkbox')); // Click on create button - fireEvent.click(screen.getByText('cta_plan')); + fireEvent.click(screen.getByTestId('cta-plan-button')); expect(defaultProps.onCreatePlan).toHaveBeenCalled(); }); - it('When no instance selected, create button should be disabled ', async () => { - await setupSpecTest(); - - fireEvent.click(screen.getByText('cta_plan')); - expect(screen.getByText('cta_plan')).toHaveAttribute('disabled'); - }); - it('should not call onCreatePlan if form not valid', async () => { await setupSpecTest(); - fireEvent.click(screen.getByText('cta_plan')); + const ctaPlanButton = screen.getByTestId('cta-plan-button'); + fireEvent.click(ctaPlanButton); expect(defaultProps.onCreatePlan).not.toHaveBeenCalled(); }); diff --git a/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlanForm.tsx b/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlanForm.tsx index 25c72e190b88..6f8c6d7cbd33 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlanForm.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/CreatePlanForm/CreatePlanForm.tsx @@ -1,52 +1,47 @@ import { usePciUrl } from '@ovh-ux/manager-pci-common'; -import { Description, Subtitle } from '@ovh-ux/manager-react-components'; -import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; -import { - ODS_THEME_COLOR_INTENT, - ODS_THEME_TYPOGRAPHY_LEVEL, - ODS_THEME_TYPOGRAPHY_SIZE, -} from '@ovhcloud/ods-common-theming'; +import { Subtitle, useNotifications } from '@ovh-ux/manager-react-components'; import { ODS_BUTTON_VARIANT, - ODS_CHECKBOX_BUTTON_SIZE, ODS_ICON_NAME, - ODS_ICON_SIZE, ODS_INPUT_TYPE, - ODS_MESSAGE_TYPE, ODS_SPINNER_SIZE, - ODS_TILE_VARIANT, - OdsInputValueChangeEventDetail, - OsdsInputCustomEvent, + OdsInputChangeEvent, } from '@ovhcloud/ods-components'; + import { - OsdsButton, - OsdsCheckbox, - OsdsCheckboxButton, - OsdsInput, - OsdsLink, - OsdsMessage, - OsdsSpinner, - OsdsTabBar, - OsdsTabBarItem, - OsdsTabs, - OsdsText, - OsdsTile, - OsdsIcon, + OdsButton, + OdsCard, + OdsCheckbox, + OdsInput, + OdsLink, + OdsMessage, + OdsQuantity, + OdsSpinner, + OdsTab, + OdsTabs, + OdsText, } from '@ovhcloud/ods-components/react'; -import React, { FC, useEffect, useMemo, useState, Suspense } from 'react'; +import React, { + FC, + Suspense, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { MutationStatus, useMutationState } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { MutationStatus, useMutationState } from '@tanstack/react-query'; import { ButtonType, PageLocation, useOvhTracking, + ShellContext, } from '@ovh-ux/manager-react-shell-client'; -import QuantitySelector, { - MAX_QUANTITY, -} from '@/components/QuantitySelector/QuantitySelector'; +import clsx from 'clsx'; import useTechnicalInfo, { usePricingInfo } from '@/hooks/useCatalogCommercial'; import { getMutationKeyCreateSavingsPlan, @@ -55,31 +50,32 @@ import { } from '@/hooks/useSavingsPlan'; import rancherSrc from '../../assets/images/rancher.png'; import serviceSrc from '../../assets/images/service.png'; +import { + PricingByDurationType, + useDefaultOfferId, +} from '../../hooks/planCreation/useDefaultOffer'; import { InstanceInfo, InstanceTechnicalName, Resource, ResourceType, } from '../../types/CreatePlan.type'; -import { formatDate } from '../../utils/formatter/date'; +import { formatDate, toLocalDateUTC } from '../../utils/formatter/date'; import { isValidSavingsPlanName } from '../../utils/savingsPlan'; import Commitment from '../Commitment/Commitment'; -import LegalLinks from '../LegalLinks/LegalLinks'; import SimpleTile from '../SimpleTile/SimpleTile'; import { TileTechnicalInfo } from '../TileTechnicalInfo/TileTechnicalInfo'; -import { - PricingByDurationType, - useDefaultOfferId, -} from '../../hooks/planCreation/useDefaultOffer'; +import LegalLinks from '../LegalLinks/LegalLinks'; const COMMON_SPACING = 'my-4'; export const DescriptionWrapper: React.FC<{ children: string; -}> = ({ children }) => { + className?: string; +}> = ({ children, className }) => { return ( -
- {children} +
+ {children}
); }; @@ -195,6 +191,8 @@ const CreatePlanForm: FC = ({ ) : []; + const isValidPlanName = isValidSavingsPlanName(planName); + const isButtonActive = useMemo( () => quantity > 0 && @@ -203,7 +201,8 @@ const CreatePlanForm: FC = ({ selectedResource && isLegalChecked && planName && - !isDiscoveryProject, + !isDiscoveryProject && + isValidPlanName, [ quantity, offerIdSelected, @@ -212,6 +211,7 @@ const CreatePlanForm: FC = ({ isLegalChecked, planName, isDiscoveryProject, + isValidPlanName, ], ); @@ -246,6 +246,12 @@ const CreatePlanForm: FC = ({ const onCreateSavingsPlan = () => { if (offerIdSelected) { + trackClick({ + location: PageLocation.funnel, + buttonType: ButtonType.button, + actionType: 'action', + actions: [`add_savings_plan::cancell`], + }); onCreatePlan({ offerId: offerIdSelected, displayName: planName, @@ -254,21 +260,28 @@ const CreatePlanForm: FC = ({ } }; - const onChangeQuantity = (v: number) => setQuantity(v); + const handleQuantityChange = useCallback( + (event: OdsInputChangeEvent) => { + const newValue = Number(event.detail.value); + if (newValue >= 1 && newValue <= 1000) { + setQuantity(newValue); + } else { + setQuantity(1); + } + }, + [setQuantity], + ); return (
{hasCreationErrorMessage && ( - - + + Une erreur est survenue lors de la création :   {hasCreationErrorMessage} - - + + )} {t('choose_ressource')} @@ -277,12 +290,19 @@ const CreatePlanForm: FC = ({
{resources.map((resource) => ( onChangeResource(resource.value)} > - - {resource.label} + {resource.value} +
+ {resource.label} +
))}
@@ -290,27 +310,25 @@ const CreatePlanForm: FC = ({ {t('select_model')} {isInstance && ( -
- - - {tabsList.map((tab) => ( - setInstanceCategory(tab.technicalName)} - > - {tab.label} - - ))} - - +
+ + {tabsList.map((tab) => ( + setInstanceCategory(tab.technicalName)} + > + {tab.label} + + ))} +
)} {t(getDescriptionInstanceKey(instanceCategory))} {!isTechnicalInfoLoading ? ( -
+
{currentInstanceSelected.technical?.map(({ name, technical }) => ( = ({ ))}
) : ( - + )} {t('select_quantity')} - + {t('select_quantity_description')} - - - {t('quantity_label')} - - - setQuantity(quantity - 1)} - onPlusClick={() => { - if (quantity < MAX_QUANTITY) { - setQuantity(quantity + 1); - } - }} - onChangeQuantity={onChangeQuantity} - /> - - - - + + {t('quantity_label')} + + + + {t( isInstance ? 'quantity_banner_instance' : 'quantity_banner_rancher', )} {isInstance && ( - - {t('quantity_banner_instance_link')} - - + target="_blank" + icon={ODS_ICON_NAME.externalLink} + label={t('quantity_banner_instance_link')} + /> )} - - + + {t('select_commitment')} @@ -403,77 +399,54 @@ const CreatePlanForm: FC = ({ ); }) ) : ( - + )} {t('choose_name')} - , - ) => setPlanName(e.target.value as string)} + onOdsChange={(e) => setPlanName(e.target.value as string)} /> - {t('input_name_rules')} +
+ {t('input_name_rules')} +
- - setIsLegalChecked(!isLegalChecked)} - hasFocus={true} - > - - {t('legal_checkbox')} - - - - + setIsLegalChecked(!isLegalChecked)} + /> + + -
- + { - trackClick({ - location: PageLocation.funnel, - buttonType: ButtonType.button, - actionType: 'action', - actions: [`add_savings_plan::cancell`], - }); - navigate('..'); - }} - > - {t('cta_cancel')} - - navigate('..')} + /> + + - {t('cta_plan')} - + />
); @@ -486,7 +459,11 @@ export const CreatePlanFormContainer = ({ }: { isDiscoveryProject: boolean; }) => { - const { t } = useTranslation('create'); + const { environment } = useContext(ShellContext); + const locale = environment.getUserLocale(); + + const { t } = useTranslation(['create', 'listing']); + const { addSuccess } = useNotifications(); const [instanceCategory, setInstanceCategory] = useState< InstanceTechnicalName @@ -515,7 +492,20 @@ export const CreatePlanFormContainer = ({ } }, [technicalList]); - const { mutate: onCreatePlan } = useSavingsPlanCreate(); + const handleCreateSavingsPlanSuccess = useCallback( + (data: { startDate: string }) => { + addSuccess( + t('listing:banner_create_sp', { + startDate: toLocalDateUTC(data.startDate, locale), + }), + ); + }, + [addSuccess, t, locale], + ); + + const { mutate: onCreatePlan } = useSavingsPlanCreate( + handleCreateSavingsPlanSuccess, + ); const sortedPriceByDuration = [...pricingByDuration].sort( (a, b) => a.duration - b.duration, diff --git a/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.test.tsx b/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.test.tsx index 8f429a7ba9ad..b41d6bc55664 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.test.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.test.tsx @@ -1,9 +1,8 @@ import '@testing-library/jest-dom'; -import { screen, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; -import { vi } from 'vitest'; import { render } from '@/utils/testProvider'; import LegalLinks from './LegalLinks'; import { SavingsPlanContract } from '@/types'; @@ -24,15 +23,12 @@ const MOCK_CONTRACTS: SavingsPlanContract[] = [ ]; const server = setupServer( - http.get('/engine/apiv6/services?resourceName=undefined', ({ request }) => { + http.get('/engine/apiv6/services?resourceName=undefined', () => { return HttpResponse.json([123]); }), - http.get( - '/engine/apiv6/services/123/savingsPlans/contracts', - ({ request }) => { - return HttpResponse.json(MOCK_CONTRACTS); - }, - ), + http.get('/engine/apiv6/services/123/savingsPlans/contracts', () => { + return HttpResponse.json(MOCK_CONTRACTS); + }), ); beforeAll(() => server.listen()); @@ -41,11 +37,16 @@ afterAll(() => server.close()); describe('LegalLinks', async () => { it('renders the legal links correctly', async () => { - render(); + const { container } = render(); await waitFor(() => { - expect(screen.getByText(MOCK_CONTRACTS[0].name)).toBeInTheDocument(); - expect(screen.getByText(MOCK_CONTRACTS[1].name)).toBeInTheDocument(); + MOCK_CONTRACTS.forEach((contract) => { + expect( + container.querySelector( + `[label='${contract.name}'][href='${contract.url}']`, + ), + ).toBeInTheDocument(); + }); }); }); }); diff --git a/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.tsx b/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.tsx index 01846d36b38b..7ce160b503b7 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/LegalLinks/LegalLinks.tsx @@ -1,19 +1,25 @@ -import { LinkType, Links } from '@ovh-ux/manager-react-components'; +import { Links, LinkType } from '@ovh-ux/manager-react-components'; import React from 'react'; -import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; import { useSavingsPlanContract } from '@/hooks/useSavingsPlan'; -const LegalLinks = () => { +type TLegalLinksProps = { + className?: string; +}; + +const LegalLinks: React.FC = ({ className = '' }) => { const { data = [] } = useSavingsPlanContract(); return ( <> {data.map((link) => ( ))} diff --git a/packages/manager/apps/pci-savings-plan/src/components/Loading/Loading.tsx b/packages/manager/apps/pci-savings-plan/src/components/Loading/Loading.tsx index bb74a4fa8e15..863da5efde54 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Loading/Loading.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Loading/Loading.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { OsdsSpinner } from '@ovhcloud/ods-components/react'; +import { OdsSpinner } from '@ovhcloud/ods-components/react'; export default function Loading() { return (
- +
); } diff --git a/packages/manager/apps/pci-savings-plan/src/components/Modal/RenewModal.tsx b/packages/manager/apps/pci-savings-plan/src/components/Modal/RenewModal.tsx index 4dc4e3338c75..0cb4333dc292 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Modal/RenewModal.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Modal/RenewModal.tsx @@ -1,15 +1,6 @@ import React from 'react'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { - ODS_BUTTON_VARIANT, - ODS_TEXT_LEVEL, - ODS_TEXT_SIZE, -} from '@ovhcloud/ods-components'; -import { - OsdsButton, - OsdsModal, - OsdsText, -} from '@ovhcloud/ods-components/react'; +import { ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; +import { OdsButton, OdsModal, OdsText } from '@ovhcloud/ods-components/react'; import { useTranslation } from 'react-i18next'; import { SavingsPlanPlanedChangeStatus } from '@/types/api.type'; @@ -26,43 +17,30 @@ export default function RenewModal({ }: Readonly) { const { t } = useTranslation('renew'); + const isReactivate = + periodEndAction === SavingsPlanPlanedChangeStatus.REACTIVATE; return ( - - -
- - {t( - periodEndAction === SavingsPlanPlanedChangeStatus.REACTIVATE - ? 'message_deactivate' - : 'message_activate', - )} - -
-
- - {t('buttons_cancel')} - - - {t( - periodEndAction === SavingsPlanPlanedChangeStatus.REACTIVATE - ? 'buttons_deactivate' - : 'buttons_activate', - )} - -
+ + {t('title')} + +
+ + {t(isReactivate ? 'message_deactivate' : 'message_activate')} + +
+
+ + + +
+
); } diff --git a/packages/manager/apps/pci-savings-plan/src/components/PlannedChangeStatusChip/PlannedChangeStatusChip.tsx b/packages/manager/apps/pci-savings-plan/src/components/PlannedChangeStatusChip/PlannedChangeStatusChip.tsx index b96c8d943df9..d9fea74111af 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/PlannedChangeStatusChip/PlannedChangeStatusChip.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/PlannedChangeStatusChip/PlannedChangeStatusChip.tsx @@ -1,6 +1,5 @@ -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { ODS_SPINNER_SIZE } from '@ovhcloud/ods-components'; -import { OsdsChip, OsdsSpinner } from '@ovhcloud/ods-components/react'; +import { ODS_SPINNER_SIZE, ODS_BADGE_COLOR } from '@ovhcloud/ods-components'; +import { OdsBadge, OdsSpinner } from '@ovhcloud/ods-components/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { SavingsPlanPlanedChangeStatus } from '@/types/api.type'; @@ -8,21 +7,19 @@ import { SavingsPlanPlanedChangeStatus } from '@/types/api.type'; const PlannedChangeStatusChip = ({ label }: { label: string }) => { const { t } = useTranslation('listing'); const colorByProductStatus: { - [key in SavingsPlanPlanedChangeStatus]: ODS_THEME_COLOR_INTENT; + [key in SavingsPlanPlanedChangeStatus]: ODS_BADGE_COLOR; } = { - [SavingsPlanPlanedChangeStatus.REACTIVATE]: ODS_THEME_COLOR_INTENT.success, - [SavingsPlanPlanedChangeStatus.TERMINATE]: ODS_THEME_COLOR_INTENT.error, + [SavingsPlanPlanedChangeStatus.REACTIVATE]: ODS_BADGE_COLOR.success, + [SavingsPlanPlanedChangeStatus.TERMINATE]: ODS_BADGE_COLOR.critical, }; return label ? ( - - {t(label.toLowerCase() as SavingsPlanPlanedChangeStatus)} - + /> ) : ( - + ); }; diff --git a/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.test.tsx b/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.test.tsx deleted file mode 100644 index c5d766d5f81d..000000000000 --- a/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { vi, describe, it, expect } from 'vitest'; -import { screen, fireEvent } from '@testing-library/react'; -import QuantitySelector, { MAX_QUANTITY } from './QuantitySelector'; -import '@testing-library/jest-dom'; - -import { render } from '@/utils/testProvider'; - -const defaultProps = { - quantity: 1, - onMinusClick: vi.fn(), - onPlusClick: vi.fn(), - onChangeQuantity: vi.fn(), -}; - -const setupSpecTest = async (props = defaultProps) => - render(); - -describe('QuantitySelector', () => { - it('should render the quantity input with the correct value', async () => { - await setupSpecTest(); - const input = screen.getByTestId('input-quantity'); - expect(input).toHaveValue(defaultProps.quantity); - }); - - it('should call onMinusClick when minus button is clicked', async () => { - await setupSpecTest(); - const minusButton = screen.getByTestId('minus-button'); - fireEvent.click(minusButton); - expect(defaultProps.onMinusClick).toHaveBeenCalled(); - }); - - it('should call onPlusClick when plus button is clicked', async () => { - await setupSpecTest(); - const plusButton = screen.getByTestId('plus-button'); - fireEvent.click(plusButton); - expect(defaultProps.onPlusClick).toHaveBeenCalled(); - }); - - it('should call onChangeQuantity with the correct value when input changes', async () => { - await setupSpecTest(); - const input = screen.getByTestId('input-quantity'); - fireEvent.change(input, { target: { value: '5' } }); - expect(defaultProps.onChangeQuantity).toHaveBeenCalledWith(5); - }); - it('should not call onChangeQuantity with a value greater than MAX_QUANTITY', async () => { - await setupSpecTest(); - const input = screen.getByTestId('input-quantity'); - fireEvent.change(input, { target: { value: MAX_QUANTITY + 1 } }); - expect(defaultProps.onChangeQuantity).not.toHaveBeenCalledWith( - MAX_QUANTITY + 1, - ); - }); -}); diff --git a/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.tsx b/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.tsx deleted file mode 100644 index f5bee8c1093c..000000000000 --- a/packages/manager/apps/pci-savings-plan/src/components/QuantitySelector/QuantitySelector.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_TYPE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, - ODS_ICON_SIZE, - ODS_INPUT_SIZE, - ODS_INPUT_TYPE, -} from '@ovhcloud/ods-components'; -import { - OsdsButton, - OsdsIcon, - OsdsInput, - OsdsQuantity, -} from '@ovhcloud/ods-components/react'; -import React from 'react'; - -type QuantitySelectorProps = { - quantity: number; - onMinusClick: () => void; - onPlusClick: () => void; - onChangeQuantity: (v?: number) => void; -}; - -export const MAX_QUANTITY = 1000; -const QuantitySelector: React.FC = ({ - quantity, - onMinusClick, - onPlusClick, - onChangeQuantity, -}) => { - const isDisabledPlus = quantity >= MAX_QUANTITY; - - return ( - - - - - { - const value = Number(e.detail.value); - if (value > 0 && value <= MAX_QUANTITY) { - onChangeQuantity(value); - } - }} - size={ODS_INPUT_SIZE.md} - > - - - - - ); -}; - -export default QuantitySelector; diff --git a/packages/manager/apps/pci-savings-plan/src/components/SimpleTile/SimpleTile.tsx b/packages/manager/apps/pci-savings-plan/src/components/SimpleTile/SimpleTile.tsx index 2412cc602ef7..1eb796de13cf 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/SimpleTile/SimpleTile.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/SimpleTile/SimpleTile.tsx @@ -1,28 +1,27 @@ -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { ODS_TILE_VARIANT } from '@ovhcloud/ods-components'; -import { OsdsTile } from '@ovhcloud/ods-components/react'; +import { OdsCard } from '@ovhcloud/ods-components/react'; +import clsx from 'clsx'; import React from 'react'; const SimpleTile: React.FC void; isActive?: boolean; -}>> = ({ children, onClick, isActive }) => ( - > = ({ children, onClick, isActive, className }) => ( + {children} - + ); export default SimpleTile; diff --git a/packages/manager/apps/pci-savings-plan/src/components/StatusChip/StatusChip.tsx b/packages/manager/apps/pci-savings-plan/src/components/StatusChip/StatusChip.tsx index 40ce02e05c87..387a1ca1685a 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/StatusChip/StatusChip.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/StatusChip/StatusChip.tsx @@ -1,6 +1,5 @@ -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { ODS_SPINNER_SIZE } from '@ovhcloud/ods-components'; -import { OsdsChip, OsdsSpinner } from '@ovhcloud/ods-components/react'; +import { ODS_SPINNER_SIZE, ODS_BADGE_COLOR } from '@ovhcloud/ods-components'; +import { OdsBadge, OdsSpinner } from '@ovhcloud/ods-components/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { SavingsPlanStatus } from '@/types/api.type'; @@ -8,19 +7,20 @@ import { SavingsPlanStatus } from '@/types/api.type'; const StatusChip = ({ label }: { label: string }) => { const { t } = useTranslation('listing'); const colorByProductStatus: { - [key in SavingsPlanStatus]: ODS_THEME_COLOR_INTENT; + [key in SavingsPlanStatus]: ODS_BADGE_COLOR; } = { - [SavingsPlanStatus.ACTIVE]: ODS_THEME_COLOR_INTENT.success, - [SavingsPlanStatus.PENDING]: ODS_THEME_COLOR_INTENT.warning, - [SavingsPlanStatus.TERMINATED]: ODS_THEME_COLOR_INTENT.error, + [SavingsPlanStatus.ACTIVE]: ODS_BADGE_COLOR.success, + [SavingsPlanStatus.PENDING]: ODS_BADGE_COLOR.warning, + [SavingsPlanStatus.TERMINATED]: ODS_BADGE_COLOR.critical, }; return label ? ( - - {t(label.toLowerCase() as SavingsPlanStatus)} - + ) : ( - + ); }; diff --git a/packages/manager/apps/pci-savings-plan/src/components/Table/ActionsCell.tsx b/packages/manager/apps/pci-savings-plan/src/components/Table/ActionsCell.tsx index 0e4173bc39fe..8af7c5c80a75 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Table/ActionsCell.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Table/ActionsCell.tsx @@ -1,20 +1,13 @@ import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; import { ODS_BUTTON_SIZE, - ODS_BUTTON_TYPE, ODS_BUTTON_VARIANT, ODS_ICON_NAME, - ODS_ICON_SIZE, } from '@ovhcloud/ods-components'; -import { - OsdsButton, - OsdsIcon, - OsdsMenu, - OsdsMenuItem, -} from '@ovhcloud/ods-components/react'; +import { OdsButton, OdsPopover } from '@ovhcloud/ods-components/react'; import { usePciUrl } from '@ovh-ux/manager-pci-common'; import { ButtonType, @@ -36,6 +29,7 @@ interface SavingsPlanActionsCell { } const MenuItems = ({ + id, status, flavor, onClickEdit, @@ -43,6 +37,7 @@ const MenuItems = ({ periodEndAction, pciUrl, }: { + id: string; status: SavingsPlanStatus; flavor: string; onClickEdit: () => void; @@ -53,49 +48,37 @@ const MenuItems = ({ const { t } = useTranslation('listing'); const { trackClick } = useOvhTracking(); + const navigate = useNavigate(); // We don't have a better way to check that, api return only a specific code and not an id related to scope (instance, rancher), // So if we have number in the flavor (b3-8, c3-16) it's an instance else it's a Rancher const isInstance = useMemo(() => /\d/.test(flavor), [flavor]); return ( - <> - - +
+ - - {t('edit')} - - - - - {status !== SavingsPlanStatus.TERMINATED && ( - - + {status !== SavingsPlanStatus.TERMINATED && ( + - - - {periodEndAction === SavingsPlanPlanedChangeStatus.TERMINATE - ? t('enableAutoRenew') - : t('disableAutoRenew')} - - - - - )} + /> + )} - - - - {t(isInstance ? 'order_instance' : 'order_rancher')} - - - - + /> +
+ ); }; @@ -135,24 +116,18 @@ export default function ActionsCell({ const onClickEdit = useCallback(() => onClickEditName(id), [id]); return ( - - - +
+ - +
-
+ ); } diff --git a/packages/manager/apps/pci-savings-plan/src/components/Table/TableContainer.tsx b/packages/manager/apps/pci-savings-plan/src/components/Table/TableContainer.tsx index 0305f5a13eec..cb71d43db672 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Table/TableContainer.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Table/TableContainer.tsx @@ -1,11 +1,6 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - useLocation, - useNavigate, - useParams, - useSearchParams, -} from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { diff --git a/packages/manager/apps/pci-savings-plan/src/components/TileTechnicalInfo/TileTechnicalInfo.tsx b/packages/manager/apps/pci-savings-plan/src/components/TileTechnicalInfo/TileTechnicalInfo.tsx index a70fb32f9ff8..00e402827141 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/TileTechnicalInfo/TileTechnicalInfo.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/TileTechnicalInfo/TileTechnicalInfo.tsx @@ -1,10 +1,5 @@ import React from 'react'; -import { - ODS_THEME_COLOR_HUE, - ODS_THEME_COLOR_INTENT, - ODS_THEME_TYPOGRAPHY_SIZE, -} from '@ovhcloud/ods-common-theming'; -import { OsdsText } from '@ovhcloud/ods-components/react'; +import { OdsText } from '@ovhcloud/ods-components/react'; import { useTranslation } from 'react-i18next'; import SimpleTile from '../SimpleTile/SimpleTile'; import { formatTechnicalInfo } from '@/utils/formatter/formatter'; @@ -18,30 +13,27 @@ const DisplayTechnicalInfo = ({ const { memory, cpu, storage, bandwidth } = technical; return ( <> - + {t('resource_model_characteristics_gb', { value: memory.size, })} - - + + {t('resource_model_characteristics_cpu', { cores: cpu.cores, ghz: cpu.frequency, })} - - + + {t('resource_model_characteristics_disk', { value: storage.disks[0].capacity, })} - - + + {t('resource_model_characteristics_mbits', { value: bandwidth.level, })} - + ); }; @@ -61,14 +53,9 @@ export const TileTechnicalInfo: React.FC = ({ }) => (
- - {name} - + + {name} + {technical && technical.bandwidth && ( )} diff --git a/packages/manager/apps/pci-savings-plan/src/hooks/useSavingsPlan.tsx b/packages/manager/apps/pci-savings-plan/src/hooks/useSavingsPlan.tsx index 973476f5d843..14626d9f452b 100644 --- a/packages/manager/apps/pci-savings-plan/src/hooks/useSavingsPlan.tsx +++ b/packages/manager/apps/pci-savings-plan/src/hooks/useSavingsPlan.tsx @@ -8,6 +8,7 @@ import { } from '@ovh-ux/manager-react-shell-client'; import { useServices } from './useService'; import { + UseSavingsPlanParams, SavingsPlanContract, SavingsPlanPlanedChangeStatus, SavingsPlanService, @@ -105,12 +106,18 @@ export const getMutationKeySPChangePeriod = ( serviceId: number, ) => ['savings-plan', serviceId, 'change-period', savingsPlanId]; -export const useSavingsPlanChangePeriod = (savingsPlanId: string) => { +export const useSavingsPlanChangePeriod = ({ + savingsPlanId, + onSuccess, +}: UseSavingsPlanParams) => { const { refetch } = useSavingsPlan(); const serviceId = useServiceId(); return useMutation({ - onSuccess: () => refetch(), + onSuccess: async () => { + onSuccess?.(); + refetch(); + }, mutationKey: getMutationKeySPChangePeriod(savingsPlanId, serviceId), mutationFn: ({ periodEndAction, @@ -136,19 +143,27 @@ export const getMutationKeyCreateSavingsPlan = (serviceId: number) => [ 'create', ]; -export const useSavingsPlanEditName = (savingsPlanId: string) => { +export const useSavingsPlanEditName = ({ + savingsPlanId, + onSuccess, +}: UseSavingsPlanParams) => { const { refetch } = useSavingsPlan(); const serviceId = useServiceId(); return useMutation({ - onSuccess: () => refetch(), mutationKey: getMutationKeySPEditName(savingsPlanId, serviceId), mutationFn: ({ displayName }: { displayName: string }) => putSubscribedSavingsPlanEditName(serviceId, savingsPlanId, displayName), + onSuccess: async () => { + onSuccess?.(); + refetch(); + }, }); }; -export const useSavingsPlanCreate = () => { +export const useSavingsPlanCreate = ( + onSuccess?: (data: SavingsPlanService) => void, +) => { const { refetch } = useSavingsPlan(); const { trackClick } = useOvhTracking(); const serviceId = useServiceId(); @@ -157,7 +172,6 @@ export const useSavingsPlanCreate = () => { return useMutation({ onSuccess: async (res) => { - const { data } = await refetch(); trackClick({ location: PageLocation.funnel, buttonType: ButtonType.button, @@ -166,8 +180,9 @@ export const useSavingsPlanCreate = () => { `add_savings_plan::confirm::savings_plan_created_${res.period}_${res.flavor}_${res.model}_${res.size}`, ], }); - - if (data?.length) { + const { data: refetchData } = await refetch(); + if (refetchData?.length) { + onSuccess?.(refetchData[0]); navigate(getSavingsPlansUrl(projectId)); } }, diff --git a/packages/manager/apps/pci-savings-plan/src/index.scss b/packages/manager/apps/pci-savings-plan/src/index.scss index 65dd5f63a7df..565a6e57a720 100644 --- a/packages/manager/apps/pci-savings-plan/src/index.scss +++ b/packages/manager/apps/pci-savings-plan/src/index.scss @@ -1 +1,47 @@ @tailwind utilities; +@import '@ovhcloud/ods-themes/default'; + +html { + font-family: var(--ods-font-family-default); +} + +.input-at::part(input) { + text-align: center; +} + +ods-text::part(text) { + width: 100%; +} + +ods-button.action-menu-item::part(button) { + width: 100%; + justify-content: left; +} + +ods-modal::part(dialog) { + max-height: 100vh; +} + +// tag doesn't seems centered if line is bigger +ods-tag.org-tag::part(tag) { + transform: translateY(-10%); +} + +.dns-field { + width: 100%; + + &::part(text) { + display: flex; + align-items: center; + gap: 8px; + } + + ods-clipboard { + flex: 1; + width: 100%; + + &::part(input) { + width: 100%; + } + } +} diff --git a/packages/manager/apps/pci-savings-plan/src/index.tsx b/packages/manager/apps/pci-savings-plan/src/index.tsx index e77e222d08b2..dba52b09bb05 100644 --- a/packages/manager/apps/pci-savings-plan/src/index.tsx +++ b/packages/manager/apps/pci-savings-plan/src/index.tsx @@ -3,7 +3,6 @@ import { initI18n, initShellContext, } from '@ovh-ux/manager-react-shell-client'; -import '@ovhcloud/ods-theme-blue-jeans/dist/index.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/packages/manager/apps/pci-savings-plan/src/pages/listing/edit-name/index.tsx b/packages/manager/apps/pci-savings-plan/src/pages/listing/edit-name/index.tsx index 508a387eb7c0..5b7f2eea2d9c 100644 --- a/packages/manager/apps/pci-savings-plan/src/pages/listing/edit-name/index.tsx +++ b/packages/manager/apps/pci-savings-plan/src/pages/listing/edit-name/index.tsx @@ -1,46 +1,64 @@ -import React, { Suspense } from 'react'; +import React, { startTransition, Suspense } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { UpdateNameModal } from '@ovh-ux/manager-react-components'; +import { + UpdateNameModal, + useNotifications, +} from '@ovh-ux/manager-react-components'; import { useSavingsPlan, useSavingsPlanEditName } from '@/hooks/useSavingsPlan'; import { REGEX } from '@/utils/savingsPlan'; -const EditNamePage = () => { - const { t } = useTranslation('edit-name'); - const { t: tCreate } = useTranslation('create'); +const EditNameChildren = () => { + const { t } = useTranslation(['edit-name', 'listing', 'create']); const navigate = useNavigate(); const { savingsPlanId } = useParams(); + const { addSuccess } = useNotifications(); const { data: savingsPlan } = useSavingsPlan(); - const { mutate: editName } = useSavingsPlanEditName(savingsPlanId); + const { mutate: editName } = useSavingsPlanEditName({ + savingsPlanId, + onSuccess: () => { + addSuccess(t('listing:banner_edit_name')); + }, + }); const [searchParams] = useSearchParams(); - const currentPlan = savingsPlan.find((plan) => plan.id === savingsPlanId); + const currentPlan = savingsPlan?.find((plan) => plan.id === savingsPlanId); return ( - + <> {currentPlan ? ( navigate({ pathname: '..', search: searchParams.toString() }) } defaultValue={currentPlan?.displayName || ''} updateDisplayName={(displayName) => { - editName({ - displayName, + startTransition(() => { + editName({ + displayName, + }); + navigate({ pathname: '..', search: searchParams.toString() }); }); - navigate({ pathname: '..', search: searchParams.toString() }); }} - headline={t('title')} + headline={t('edit-name:title')} pattern={REGEX} - description={t('description')} - patternMessage={tCreate('input_name_rules')} + description={t('edit-name:description')} + patternMessage={t('create:input_name_rules')} /> ) : ( <> )} + + ); +}; +const EditNamePage = () => { + return ( + + ); }; diff --git a/packages/manager/apps/pci-savings-plan/src/pages/listing/index.tsx b/packages/manager/apps/pci-savings-plan/src/pages/listing/index.tsx index e51adaeccf5f..95b213965870 100644 --- a/packages/manager/apps/pci-savings-plan/src/pages/listing/index.tsx +++ b/packages/manager/apps/pci-savings-plan/src/pages/listing/index.tsx @@ -1,38 +1,32 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useHref, useNavigate, useParams } from 'react-router-dom'; -import { MutationStatus, useMutationState } from '@tanstack/react-query'; +import { Outlet, useNavigate, useParams } from 'react-router-dom'; + +import { ODS_BUTTON_SIZE, ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; +import { OdsButton, OdsText } from '@ovhcloud/ods-components/react'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { - ODS_BUTTON_SIZE, - ODS_BUTTON_VARIANT, - ODS_ICON_NAME, - ODS_ICON_SIZE, - ODS_MESSAGE_TYPE, -} from '@ovhcloud/ods-components'; import { - OsdsButton, - OsdsIcon, - OsdsMessage, - OsdsText, -} from '@ovhcloud/ods-components/react'; -import { Title } from '@ovh-ux/manager-react-components'; + Notifications, + Title, + useNotifications, +} from '@ovh-ux/manager-react-components'; import { ButtonType, PageLocation, useOvhTracking, - ShellContext, } from '@ovh-ux/manager-react-shell-client'; import TableContainer from '@/components/Table/TableContainer'; -import { - getMutationKeyCreateSavingsPlan, - useSavingsPlan, - useServiceId, -} from '@/hooks/useSavingsPlan'; +import { useSavingsPlan } from '@/hooks/useSavingsPlan'; import { SavingsPlanService } from '@/types'; -import { toLocalDateUTC } from '@/utils/formatter/date'; + +interface ListingTablePageProps { + data: SavingsPlanService[]; + refetchSavingsPlans: () => void; +} +interface ListingProps { + refetchSavingsPlans: () => void; +} export const formatDateString = (dateString: string, locale?: string) => { const date = new Date(dateString); @@ -47,151 +41,48 @@ export const formatDateString = (dateString: string, locale?: string) => { : '-'; }; -const Banner = ({ message }: { message: string }) => { - const [showBanner, setShowBanner] = useState(true); - - useEffect(() => { - if (message) { - setShowBanner(true); - } - }, [message]); - return ( - showBanner && ( - setShowBanner(false)} - > - - {message} - - - ) - ); -}; - -export interface ListingProps { - data: SavingsPlanService[]; - refetchSavingsPlans: () => void; -} - -type MutationInfo = { - submittedAt: number; - error?: { - code: string; - }; - status: MutationStatus; - data?: Data; -}; - -const getMutationFilters = (filters: (string | number)[]) => ({ +export const getMutationFilters = (filters: (string | number)[]) => ({ filters: { mutationKey: filters }, }); -const ListingTablePage: React.FC = ({ +const ListingTablePage: React.FC = ({ data, refetchSavingsPlans, }) => { const { t } = useTranslation('listing'); - const { environment } = useContext(ShellContext); - const locale = environment.getUserLocale(); const { trackClick } = useOvhTracking(); - const hrefDashboard = useHref(''); - const serviceId = useServiceId(); - const mutationSPChangePeriod = useMutationState< - MutationInfo & { - variables: { - periodEndAction: 'REACTIVATE' | 'ACTIVATE'; - }; - } - >(getMutationFilters(['savings-plan', serviceId, 'change-period'])); - - const mutationSPEditName = useMutationState( - getMutationFilters(['savings-plan', serviceId, 'edit-name']), - ); - - const mutationSpCreate = useMutationState>( - getMutationFilters(getMutationKeyCreateSavingsPlan(serviceId)), - ); - - const lastMutationEditName = - mutationSPEditName[mutationSPEditName.length - 1]; - const lastMutationChangePeriod = - mutationSPChangePeriod[mutationSPChangePeriod.length - 1]; - const lastMutationSpCreate = mutationSpCreate[mutationSpCreate.length - 1]; - - const renewBannerMessage = t( - lastMutationChangePeriod?.variables.periodEndAction === 'REACTIVATE' - ? 'banner_renew_activate' - : 'banner_renew_deactivate', - { - planName: lastMutationChangePeriod?.data?.displayName, - endDate: lastMutationChangePeriod?.data?.endDate, - }, - ); + const navigate = useNavigate(); + const { projectId } = useParams(); + const { clearNotifications } = useNotifications(); + + const handleClick = () => { + trackClick({ + location: PageLocation.page, + buttonType: ButtonType.button, + actionType: 'navigation', + actions: ['add_savings_plan'], + }); + clearNotifications(); + navigate(`/pci/projects/${projectId}/savings-plan/new`); + }; return ( <> {t('title')}
- { - trackClick({ - location: PageLocation.page, - buttonType: ButtonType.button, - actionType: 'navigation', - actions: ['add_savings_plan'], - }); - }} - > - - - {t('createSavingsPlan')} - - + variant={ODS_BUTTON_VARIANT.outline} + onClick={handleClick} + label={t('createSavingsPlan')} + />
- + {t('informationMessage')} - - {mutationSPChangePeriod.length > 0 && ( - - )} - {mutationSpCreate.length > 0 && !lastMutationSpCreate.error?.code && ( - - )} - - {mutationSPEditName.length > 0 && ( - - )} + + ); diff --git a/packages/manager/apps/pci-savings-plan/src/pages/listing/renew-modal/index.tsx b/packages/manager/apps/pci-savings-plan/src/pages/listing/renew-modal/index.tsx index b44d327fbae5..f0794eb761cd 100644 --- a/packages/manager/apps/pci-savings-plan/src/pages/listing/renew-modal/index.tsx +++ b/packages/manager/apps/pci-savings-plan/src/pages/listing/renew-modal/index.tsx @@ -1,5 +1,7 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { useTranslation } from 'react-i18next'; import Errors from '@/components/Error/Error'; import RenewModal from '@/components/Modal/RenewModal'; import { @@ -11,18 +13,37 @@ import { SavingsPlanPlanedChangeStatus } from '@/types/api.type'; const RenewModalPage = () => { const navigate = useNavigate(); const { savingsPlanId } = useParams(); - const { data: savingsPlan, error, isError } = useSavingsPlan(); - const { mutate: changePeriod } = useSavingsPlanChangePeriod(savingsPlanId); + const { t } = useTranslation('listing'); + const { addSuccess } = useNotifications(); + + const currentPlan = useMemo( + () => savingsPlan?.find((p) => p.id === savingsPlanId), + [savingsPlan, savingsPlanId], + ); + + const periodEndAction = currentPlan?.periodEndAction === 'REACTIVATE'; + + const onSuccess = () => { + addSuccess( + t(periodEndAction ? 'banner_renew_deactivate' : 'banner_renew_activate', { + planName: currentPlan.displayName, + endDate: currentPlan.endDate, + }), + ); + }; + + const { mutate: changePeriod } = useSavingsPlanChangePeriod({ + savingsPlanId, + onSuccess, + }); + const [searchParams] = useSearchParams(); if (isError || error) { return ; } - const currentPlan = savingsPlan?.find((plan) => plan.id === savingsPlanId); - const periodEndAction = currentPlan?.periodEndAction === 'REACTIVATE'; - const goToPrevious = () => navigate({ pathname: '..', search: searchParams.toString() }); diff --git a/packages/manager/apps/pci-savings-plan/src/types/api.type.ts b/packages/manager/apps/pci-savings-plan/src/types/api.type.ts index 153fbad73b51..187d42229532 100644 --- a/packages/manager/apps/pci-savings-plan/src/types/api.type.ts +++ b/packages/manager/apps/pci-savings-plan/src/types/api.type.ts @@ -1,3 +1,5 @@ +import { MutationStatus } from '@tanstack/react-query'; + export interface PciProject { project_id: string; projectName: string; @@ -65,3 +67,17 @@ export interface SavingsPlanService { startDate: string; status: SavingsPlanStatus; } + +export type MutationInfo = { + submittedAt: number; + error?: { + code: string; + }; + status: MutationStatus; + data?: T; +}; + +export type UseSavingsPlanParams = { + savingsPlanId: string; + onSuccess?: () => void; +}; diff --git a/packages/manager/apps/pci-savings-plan/vitest.config.js b/packages/manager/apps/pci-savings-plan/vitest.config.js index c8a1ca9993bb..0b1799e6e234 100644 --- a/packages/manager/apps/pci-savings-plan/vitest.config.js +++ b/packages/manager/apps/pci-savings-plan/vitest.config.js @@ -6,6 +6,11 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { + server: { + deps: { + inline: ['clsx'], + }, + }, globals: true, environment: 'jsdom', coverage: { diff --git a/yarn.lock b/yarn.lock index 017b6bf43ab0..a1b7e6b64732 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5792,6 +5792,17 @@ date-fns "^3.6.0" lodash.isequal "^4.5.0" +"@ovh-ux/manager-pci-common@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@ovh-ux/manager-pci-common/-/manager-pci-common-1.0.4.tgz#9fb79f247208a350d9de6f70178677ee00432ad3" + integrity sha512-QKItmyT2Uu4Gpq6RbGm+vsTcN7eSZamvLTswdfkNFH3YHHTx47a1PofCjgsUXlBZ+YD7H0bZl3CVBexGY7T8iw== + dependencies: + "@ovh-ux/manager-core-utils" "^0.3.0" + "@ovh-ux/manager-tailwind-config" "^0.2.1" + clsx "2.1.1" + date-fns "^3.6.0" + lodash.isequal "^4.5.0" + "@ovh-ux/manager-react-components@^1.41.1", "@ovh-ux/manager-react-components@^1.41.2": version "1.41.2" resolved "https://registry.yarnpkg.com/@ovh-ux/manager-react-components/-/manager-react-components-1.41.2.tgz#087cacbbff37c594201851f5f27d2a0c524545b5"