Skip to content

Commit

Permalink
Merge pull request #13560 from ovh/feat/MANAGER-14504
Browse files Browse the repository at this point in the history
feat(hycu): add hycu order
  • Loading branch information
ThibaudCrespin authored Oct 23, 2024
2 parents abee140 + b6ac9d2 commit 88143a7
Show file tree
Hide file tree
Showing 23 changed files with 714 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/manager/apps/hycu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@ovh-ux/manager-config": "*",
"@ovh-ux/manager-core-api": "*",
"@ovh-ux/manager-core-utils": "*",
"@ovh-ux/manager-module-order": "*",
"@ovh-ux/manager-react-components": "*",
"@ovh-ux/manager-react-core-application": "*",
"@ovh-ux/manager-react-shell-client": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"hycu_order_title": "Commander une licence HYCU Hybrid Cloud",
"hycu_order_description": "HYCU Hybrid Cloud est un logiciel de sauvegarde et de restauration spécialement conçu pour Nutanix. Nous vous proposons différents packs de licences selon le nombre de machines virtuelles (VM) utilisées par vos charges de travail Nutanix.",
"hycu_order_subtitle": "Choisissez le type de pack",
"hycu_order_subtitle_description": "Sélectionnez le nombre de machines virtuelles (VM) à couvrir par la licence HYCU Hybrid Cloud. Veuillez noter qu'il vous sera impossible de sauvegarder plus de VM que le nombre prévu dans le pack. Si vous souhaitez protéger plus de 500 VM, merci de vous rapprocher de notre <contact>service commercial</contact>.",
"hycu_order_cta_cancel": "Annuler",
"hycu_order_cta_order": "Commander",
"hycu_order_initiated_title": "Commande de votre pack",
"hycu_order_initiated_description": "Si vous n'avez pas encore finalisé votre commande, vous pouvez la compléter en cliquant sur le lien suivant :",
"hycu_order_initiated_info": "Nous vous informerons de la disponibilité de votre licence par e-mail.",
"hycu_order_initiated_cta_done": "Terminer"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { OsdsSpinner } from '@ovhcloud/ods-components/react';
export default function Loading() {
return (
<div className="flex justify-center">
<OsdsSpinner />
<div>
<OsdsSpinner />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import {
OsdsText,
OsdsButton,
OsdsLink,
OsdsIcon,
OsdsTile,
} from '@ovhcloud/ods-components/react';
import {
ODS_BUTTON_SIZE,
ODS_BUTTON_VARIANT,
ODS_ICON_SIZE,
ODS_ICON_NAME,
ODS_LINK_REFERRER_POLICY,
} from '@ovhcloud/ods-components';
import {
ODS_THEME_COLOR_INTENT,
ODS_THEME_TYPOGRAPHY_SIZE,
ODS_THEME_TYPOGRAPHY_LEVEL,
} from '@ovhcloud/ods-common-theming';
import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { urls } from '@/routes/routes.constant';

type OrderConfirmationProps = {
orderLink: string;
};

const OrderConfirmation = ({ orderLink }: OrderConfirmationProps) => {
const { t } = useTranslation('hycu/order');
const navigate = useNavigate();

return (
<>
<OsdsTile className="mb-8">
<span slot="start">
<div className="flex flex-col gap-6 mb-6">
<OsdsText
level={ODS_THEME_TYPOGRAPHY_LEVEL.heading}
size={ODS_THEME_TYPOGRAPHY_SIZE._600}
color={ODS_THEME_COLOR_INTENT.text}
>
{t('hycu_order_initiated_title')}
</OsdsText>
<OsdsText
level={ODS_THEME_TYPOGRAPHY_LEVEL.subheading}
size={ODS_THEME_TYPOGRAPHY_SIZE._800}
color={ODS_THEME_COLOR_INTENT.text}
>
{t('hycu_order_initiated_description')}
</OsdsText>
<OsdsLink
color={ODS_THEME_COLOR_INTENT.primary}
target={OdsHTMLAnchorElementTarget._blank}
referrerpolicy={
ODS_LINK_REFERRER_POLICY.strictOriginWhenCrossOrigin
}
href={orderLink}
>
{orderLink}
<span slot="end">
<OsdsIcon
className="ml-4 cursor-pointer"
name={ODS_ICON_NAME.EXTERNAL_LINK}
size={ODS_ICON_SIZE.xs}
hoverable
></OsdsIcon>
</span>
</OsdsLink>
<OsdsText
level={ODS_THEME_TYPOGRAPHY_LEVEL.subheading}
size={ODS_THEME_TYPOGRAPHY_SIZE._800}
color={ODS_THEME_COLOR_INTENT.text}
>
{t('hycu_order_initiated_info')}
</OsdsText>
</div>
</span>
</OsdsTile>
<OsdsButton
inline
size={ODS_BUTTON_SIZE.md}
variant={ODS_BUTTON_VARIANT.flat}
color={ODS_THEME_COLOR_INTENT.primary}
onClick={() => {
navigate(urls.listing);
}}
>
{t('hycu_order_initiated_cta_done')}
</OsdsButton>
</>
);
};

export default OrderConfirmation;
140 changes: 140 additions & 0 deletions packages/manager/apps/hycu/src/components/Order/PackSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, { Dispatch, SetStateAction } from 'react';
import { useNavigate } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';

import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core';
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 {
OsdsText,
OsdsButton,
OsdsTile,
OsdsLink,
} from '@ovhcloud/ods-components/react';
import {
Description,
OvhSubsidiary,
Price,
Subtitle,
} from '@ovh-ux/manager-react-components';

import Error from '@/components/Error/Error';
import Loading from '@/components/Loading/Loading.component';
import { useOrderCatalogHYCU } from '@/hooks/order/useOrderCatalogHYCU';
import { urls } from '@/routes/routes.constant';
import { sortPacksByPrice } from '@/utils/sortPacks';
import { getRenewPrice } from '@/utils/getRenewPrice';
import { CONTACT_URL_BY_SUBSIDIARY } from '@/utils/contactList';

type PackSelectionProps = {
selectPack: Dispatch<SetStateAction<string>>;
setOrderInitiated: () => void;
orderLink: string;
selectedPack: string;
subsidiary: OvhSubsidiary;
};

const PackSelection = ({
selectPack,
setOrderInitiated,
orderLink,
selectedPack,
subsidiary,
}: PackSelectionProps) => {
const { t } = useTranslation('hycu/order');
const navigate = useNavigate();

const { data: orderCatalogHYCU, isLoading, error } = useOrderCatalogHYCU(
subsidiary,
{
refetchOnWindowFocus: false,
keepPreviousData: true,
},
);

if (!isLoading && error) return <Error error={error} />;

return (
<>
<Subtitle className="block mb-6">{t('hycu_order_subtitle')}</Subtitle>
<Description className="mb-8">
<Trans
t={t}
i18nKey="hycu_order_subtitle_description"
components={{
contact: (
<OsdsLink
color={ODS_THEME_COLOR_INTENT.primary}
href={CONTACT_URL_BY_SUBSIDIARY[subsidiary]}
target={OdsHTMLAnchorElementTarget._blank}
></OsdsLink>
),
}}
></Trans>
</Description>
{isLoading && <Loading />}
{orderCatalogHYCU && !isLoading && (
<div className="grid gap-8 xs:grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 mb-8">
{sortPacksByPrice(orderCatalogHYCU.plans).map((product) => (
<OsdsTile
checked={product.planCode === selectedPack || undefined}
className="flex flex-col w-full h-fit text-center"
hoverable
key={product.planCode}
onClick={() => {
selectPack(product.planCode);
}}
rounded
>
<div className="flex flex-col gap-6 pb-4">
<OsdsText
color={ODS_THEME_COLOR_INTENT.text}
level={ODS_TEXT_LEVEL.heading}
size={ODS_TEXT_SIZE._400}
>
{product.invoiceName}
</OsdsText>
<Price
locale={subsidiary}
ovhSubsidiary={subsidiary}
intervalUnit={getRenewPrice(product.pricings).intervalUnit}
tax={getRenewPrice(product.pricings).tax}
value={getRenewPrice(product.pricings).price}
></Price>
</div>
</OsdsTile>
))}
</div>
)}
<div className="flex flex-row">
<OsdsButton
className="mr-4"
color={ODS_THEME_COLOR_INTENT.primary}
onClick={() => {
navigate(urls.listing);
}}
slot="actions"
variant={ODS_BUTTON_VARIANT.ghost}
>
{t('hycu_order_cta_cancel')}
</OsdsButton>
<OsdsButton
color={ODS_THEME_COLOR_INTENT.primary}
disabled={(!selectedPack && !orderLink) || undefined}
onClick={() => {
setOrderInitiated();
}}
slot="actions"
>
{t('hycu_order_cta_order')}
</OsdsButton>
</div>
</>
);
};

export default PackSelection;
15 changes: 15 additions & 0 deletions packages/manager/apps/hycu/src/data/api/orderCatalogHYCU.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import apiClient from '@ovh-ux/manager-core-api';
import { OvhSubsidiary } from '@ovh-ux/manager-react-components';
import { HYCUCatalog } from '@/types/orderCatalogHYCU.type';

/**
* HYCU Catalog : Get HYCU Order Catalog
*/
export const getOrderCatalogHYCU = async (
ovhSubsidiary: OvhSubsidiary,
): Promise<HYCUCatalog> => {
const { data } = await apiClient.v6.get<HYCUCatalog>(
`/order/catalog/public/licenseHycu?ovhSubsidiary=${ovhSubsidiary}`,
);
return data;
};
22 changes: 22 additions & 0 deletions packages/manager/apps/hycu/src/hooks/order/useOrderCatalogHYCU.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DefinedInitialDataOptions, useQuery } from '@tanstack/react-query';
import { OvhSubsidiary } from '@ovh-ux/manager-react-components';
import { ApiError } from '@ovh-ux/manager-core-api';
import { getOrderCatalogHYCU } from '../../data/api/orderCatalogHYCU';
import { HYCUCatalog } from '@/types/orderCatalogHYCU.type';

export type OrderCatalogProps = {
ovhSubsidiary: OvhSubsidiary;
};

export const useOrderCatalogHYCU = (
ovhSubsidiary: OvhSubsidiary,
options: Partial<DefinedInitialDataOptions<HYCUCatalog, ApiError>> & {
keepPreviousData?: boolean;
} = {},
) => {
return useQuery<HYCUCatalog, ApiError>({
queryKey: ['order/catalog/public/licenseHycu', ovhSubsidiary],
queryFn: () => getOrderCatalogHYCU(ovhSubsidiary),
...options,
});
};
48 changes: 48 additions & 0 deletions packages/manager/apps/hycu/src/hooks/order/useOrderHYCU.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it, test, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { OvhSubsidiary } from '@ovh-ux/manager-react-components';
import useOrderHYCU from './useOrderHYCU';

vi.mock('@ovh-ux/manager-module-order', () => ({
useOrderURL: () => 'https://test',
getHYCUProductSettings: () => 'test-settings',
}));

describe('get hycu express order linl ', () => {
const useCases: {
planCode: string;
region: OvhSubsidiary;
}[] = [
{
planCode: 'hycu-cloud-vm-pack-25',
region: OvhSubsidiary.FR,
},
{
planCode: 'hycu-cloud-vm-pack-250',
region: OvhSubsidiary.US,
},
];
test.each(useCases)(
'should return the right translation key for $type',
({ planCode, region }) => {
// given type and translationKey
// when
const { result } = renderHook(() => useOrderHYCU({ planCode, region }));
// then
expect(result.current.orderLink).toContain(
'https://test?products=~(test-settings)',
);
},
);
it('should return null if planCode is not defined', () => {
// given
const region = OvhSubsidiary.FR;
// when
const { result } = renderHook(() =>
useOrderHYCU({ planCode: null, region }),
);

// then
expect(result.current.orderLink).toBeNull();
});
});
31 changes: 31 additions & 0 deletions packages/manager/apps/hycu/src/hooks/order/useOrderHYCU.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
useOrderURL,
getHYCUProductSettings,
} from '@ovh-ux/manager-module-order';
import { OvhSubsidiary } from '@ovh-ux/manager-react-components';
import { useMemo } from 'react';

interface HYCUOrder {
planCode: string;
region: OvhSubsidiary;
}

const useOrderHYCU = ({ planCode, region }: HYCUOrder) => {
const orderBaseUrl = useOrderURL('express_review_base');
const orderLink = useMemo(() => {
const HYCUProductSettings = getHYCUProductSettings({
planCode,
region,
});
if (planCode) return `${orderBaseUrl}?products=~(${HYCUProductSettings})`;
return null;
}, [planCode, region]);

const redirectToOrder = () => {
window.open(orderLink, '_blank', 'noopener,noreferrer');
};

return { orderLink, redirectToOrder };
};

export default useOrderHYCU;
Loading

0 comments on commit 88143a7

Please sign in to comment.