From a2b32c14d228075215c46c422549f53dde20ef23 Mon Sep 17 00:00:00 2001 From: Thibault Barske Date: Mon, 7 Oct 2024 10:48:41 +0200 Subject: [PATCH] feat(hycu): add listing page ref: MANAGER-14497 Signed-off-by: Thibault Barske --- .../src/hooks/services/services.type.ts | 2 +- packages/manager/apps/hycu/package.json | 1 + .../translations/error/Messages_fr_FR.json | 3 + .../translations/listing/Messages_fr_FR.json | 16 +- .../src/hooks/services/usePackTypeLabel.ts | 11 + .../apps/hycu/src/hooks/useFormattedDate.ts | 24 ++ .../src/mocks/licenseHycu/licenseHycu.data.ts | 32 ++- .../hycu/src/mocks/licenseHycu/licenseHycu.ts | 12 +- .../hycu/src/pages/listing/Listing.page.tsx | 265 ++++++++++++------ .../hycu/src/pages/listing/Listing.spec.tsx | 38 ++- .../listing/menu/HycuActionMenu.component.tsx | 31 ++ .../apps/hycu/src/routes/routes.constant.ts | 9 +- .../hycu/src/type/hycu.details.interface.ts | 42 +++ .../apps/hycu/src/utils/statusColor.ts | 18 ++ 14 files changed, 394 insertions(+), 110 deletions(-) create mode 100644 packages/manager/apps/hycu/public/translations/error/Messages_fr_FR.json create mode 100644 packages/manager/apps/hycu/src/hooks/services/usePackTypeLabel.ts create mode 100644 packages/manager/apps/hycu/src/hooks/useFormattedDate.ts create mode 100644 packages/manager/apps/hycu/src/pages/listing/menu/HycuActionMenu.component.tsx create mode 100644 packages/manager/apps/hycu/src/type/hycu.details.interface.ts create mode 100644 packages/manager/apps/hycu/src/utils/statusColor.ts diff --git a/packages/manager-react-components/src/hooks/services/services.type.ts b/packages/manager-react-components/src/hooks/services/services.type.ts index 4e35a725fe2a..9b3f00f70a03 100644 --- a/packages/manager-react-components/src/hooks/services/services.type.ts +++ b/packages/manager-react-components/src/hooks/services/services.type.ts @@ -80,7 +80,7 @@ export type ServiceDetails = { actions: LifecycleAction[]; }; current: { - createDate: string; + creationDate: string; pendingActions: LifecycleAction[]; state: LifecycleState; terminationDate: string; diff --git a/packages/manager/apps/hycu/package.json b/packages/manager/apps/hycu/package.json index 59010de921e7..6c027e737c9e 100644 --- a/packages/manager/apps/hycu/package.json +++ b/packages/manager/apps/hycu/package.json @@ -48,6 +48,7 @@ "@ovh-ux/manager-vite-config": "*", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.13", "@vitejs/plugin-react": "^4.3.2", "element-internals-polyfill": "^1.3.12", diff --git a/packages/manager/apps/hycu/public/translations/error/Messages_fr_FR.json b/packages/manager/apps/hycu/public/translations/error/Messages_fr_FR.json new file mode 100644 index 000000000000..4e563cb30d3d --- /dev/null +++ b/packages/manager/apps/hycu/public/translations/error/Messages_fr_FR.json @@ -0,0 +1,3 @@ +{ + "manager_error_page_default": "Une erreur est survenue lors du chargement de la page." +} diff --git a/packages/manager/apps/hycu/public/translations/listing/Messages_fr_FR.json b/packages/manager/apps/hycu/public/translations/listing/Messages_fr_FR.json index c882bc92cfec..6f9cd7ea16b2 100644 --- a/packages/manager/apps/hycu/public/translations/listing/Messages_fr_FR.json +++ b/packages/manager/apps/hycu/public/translations/listing/Messages_fr_FR.json @@ -1,4 +1,16 @@ { - "title": "Listing page", - "listing_resultats": "résultats" + "title": "HYCU", + "listing_resultats": "résultats", + "hycu_status_activated": "Active", + "hycu_status_toActivate": "A activer", + "hycu_status_pending": "En cours d'activation", + "hycu_status_error": "Error d'activation", + "hycu-cloud-vm-pack-unknown": "Pack inconnu", + "hycu_name": "Nom", + "hycu_controller_id": "Controller ID", + "hycu_status": "Status", + "hycu_commercial_name": "Type de pack", + "hycu_subscribed_date": "Date de souscription", + "hycu_order": "Commander", + "hycu_service_listing_terminate": "Résilié" } diff --git a/packages/manager/apps/hycu/src/hooks/services/usePackTypeLabel.ts b/packages/manager/apps/hycu/src/hooks/services/usePackTypeLabel.ts new file mode 100644 index 000000000000..c700c0b85a8f --- /dev/null +++ b/packages/manager/apps/hycu/src/hooks/services/usePackTypeLabel.ts @@ -0,0 +1,11 @@ +import { useTranslation } from 'react-i18next'; +import { packTypeLabel } from '@/constants'; + +export const usePackTypeLabel = (packType: string) => { + const { t } = useTranslation('listing'); + + return ( + packTypeLabel[packType as keyof typeof packTypeLabel] ?? + t('hycu-cloud-vm-pack-unknown') + ); +}; diff --git a/packages/manager/apps/hycu/src/hooks/useFormattedDate.ts b/packages/manager/apps/hycu/src/hooks/useFormattedDate.ts new file mode 100644 index 000000000000..ea5a7878b4ba --- /dev/null +++ b/packages/manager/apps/hycu/src/hooks/useFormattedDate.ts @@ -0,0 +1,24 @@ +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { useContext, useMemo } from 'react'; + +type UseDateProps = { + date: Date; + options: Intl.DateTimeFormatOptions; +}; + +function isValidDate(date: unknown) { + // eslint-disable-next-line no-restricted-globals + return date instanceof Date && !isNaN(date.getMilliseconds()); +} + +export const useFormattedDate = ({ date, options }: UseDateProps): string => { + const { + environment: { userLocale }, + } = useContext(ShellContext); + + return useMemo(() => { + return isValidDate(date) + ? Intl.DateTimeFormat(userLocale.replace('_', '-'), options).format(date) + : '-'; + }, [userLocale, date]); +}; diff --git a/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.data.ts b/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.data.ts index 416942d71b09..260accd6ab47 100644 --- a/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.data.ts +++ b/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.data.ts @@ -1,18 +1,28 @@ -export const licenseHycu: unknown[] = [ +import { IHycuDetails, LicenseStatus } from '@/type/hycu.details.interface'; + +export const licensesHycu: IHycuDetails[] = [ { - displayName: '425802fa-fb70-4b2a-9d5b-ec4de86bb40c', - extraParams: null, - parentName: null, + iam: { + id: '4a26ef55-d46b-4b71-88c8-76ad71b154b4', + urn: + 'urn:v1:eu:resource:licenseHycu:425802fa-fb70-4b2a-9d5b-ec4de86bb40c', + }, + comment: '', serviceName: '425802fa-fb70-4b2a-9d5b-ec4de86bb40c', - stateParams: ['425802fa-fb70-4b2a-9d5b-ec4de86bb40c'], - url: '#/425802fa-fb70-4b2a-9d5b-ec4de86bb40c', + controllerId: '', + licenseStatus: LicenseStatus.TO_ACTIVATE, + expirationDate: '0001-01-01T00:00:00Z', }, { - displayName: 'c1b7cb4f-6b63-45da-9a8a-f731f1a67b2c', - extraParams: null, - parentName: null, + iam: { + id: '06a89efa-cf14-431b-ab84-af5b3913e2ef', + urn: + 'urn:v1:eu:resource:licenseHycu:c1b7cb4f-6b63-45da-9a8a-f731f1a67b2c', + }, + comment: '', serviceName: 'c1b7cb4f-6b63-45da-9a8a-f731f1a67b2c', - stateParams: ['c1b7cb4f-6b63-45da-9a8a-f731f1a67b2c'], - url: '#/c1b7cb4f-6b63-45da-9a8a-f731f1a67b2c', + controllerId: '', + licenseStatus: LicenseStatus.ACTIVATED, + expirationDate: '0001-01-01T00:00:00Z', }, ]; diff --git a/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.ts b/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.ts index ad88cc2db856..664d5fb4e95f 100644 --- a/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.ts +++ b/packages/manager/apps/hycu/src/mocks/licenseHycu/licenseHycu.ts @@ -1,24 +1,24 @@ import { Handler } from '../../../../../../../playwright-helpers'; -import { licenseHycu } from './licenseHycu.data'; +import { licensesHycu } from './licenseHycu.data'; export type GetLicenseHycuMocksParams = { - isBackupKo?: boolean; + isGetLicenseHycuKo?: boolean; nbLicenseHycu?: number; }; export const getLicenseHycuMocks = ({ - isBackupKo, + isGetLicenseHycuKo, nbLicenseHycu = Number.POSITIVE_INFINITY, }: GetLicenseHycuMocksParams): Handler[] => { return [ { url: 'license/hycu', - response: isBackupKo + response: isGetLicenseHycuKo ? { message: 'Backup error', } - : licenseHycu.slice(0, nbLicenseHycu), - status: isBackupKo ? 500 : 200, + : licensesHycu.slice(0, nbLicenseHycu), + status: isGetLicenseHycuKo ? 500 : 200, api: 'v6', }, ]; diff --git a/packages/manager/apps/hycu/src/pages/listing/Listing.page.tsx b/packages/manager/apps/hycu/src/pages/listing/Listing.page.tsx index 56188e2b2c00..16b9d8f4a1ff 100644 --- a/packages/manager/apps/hycu/src/pages/listing/Listing.page.tsx +++ b/packages/manager/apps/hycu/src/pages/listing/Listing.page.tsx @@ -1,35 +1,126 @@ -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; -import { OsdsLink } from '@ovhcloud/ods-components/react'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + OsdsButton, + OsdsChip, + OsdsMessage, +} from '@ovhcloud/ods-components/react'; import { Datagrid, DataGridTextCell, useResourcesIcebergV6, - dataType, + RedirectionGuard, BaseLayout, + Links, + useServiceDetails, } from '@ovh-ux/manager-react-components'; -import Loading from '@/components/Loading/Loading.component'; -import ErrorBanner from '@/components/Error/Error'; -import Breadcrumb from '@/components/Breadcrumb/Breadcrumb.component'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + ODS_BUTTON_VARIANT, + ODS_CHIP_SIZE, + ODS_MESSAGE_TYPE, + ODS_BUTTON_SIZE, +} from '@ovhcloud/ods-components'; +import { IHycuDetails } from '@/type/hycu.details.interface'; -import appConfig from '@/hycu.config'; -import { urls } from '@/routes/routes.constant'; +import { urls, subRoutes } from '@/routes/routes.constant'; +import { useFormattedDate } from '@/hooks/useFormattedDate'; +import { usePackTypeLabel } from '@/hooks/services/usePackTypeLabel'; +import HycuActionMenu from './menu/HycuActionMenu.component'; +import { getStatusColor } from '@/utils/statusColor'; -export default function Listing() { - const myConfig = appConfig; - const serviceKey = myConfig.listing?.datagrid?.serviceKey; - const [columns, setColumns] = useState([]); +const dateFormat: Intl.DateTimeFormatOptions = { + day: '2-digit', + month: '2-digit', + year: 'numeric', +}; + +/* ========= datagrid cells ========= */ +const DatagridIdCell = (hycuDetail: IHycuDetails) => { const navigate = useNavigate(); - const location = useLocation(); + + return ( + + + navigate( + urls.dashboard.replace( + subRoutes.serviceName, + hycuDetail.serviceName, + ), + ) + } + label={hycuDetail.serviceName} + > + + ); +}; + +const DatagridControllerIdCell = (hycuDetail: IHycuDetails) => { + return {hycuDetail.controllerId}; +}; + +const DatagridStatusCell = (hycuDetail: IHycuDetails) => { const { t } = useTranslation('listing'); + + return ( + + + {t([`hycu_status_${hycuDetail.licenseStatus}`, 'hycu_status_error'])} + + + ); +}; + +const DatagridCommercialNameCell = (hycuDetail: IHycuDetails) => { + const { data: serviceDetails, isLoading } = useServiceDetails({ + resourceName: hycuDetail.serviceName, + }); + + const productName = usePackTypeLabel( + serviceDetails?.data.resource.product.name, + ); + + return ( + {isLoading ? 'Loading' : productName} + ); +}; + +const DatagridCreatedDateCell = (hycuDetail: IHycuDetails) => { + const { data: serviceDetails, isLoading } = useServiceDetails({ + resourceName: hycuDetail.serviceName, + }); + const date = new Date( + serviceDetails?.data.billing.lifecycle.current.creationDate, + ); + const formattedDate = useFormattedDate({ date, options: dateFormat }); + + return ( + {isLoading ? 'Loading' : formattedDate} + ); +}; + +const DatagridActionCell = (hycuDetail: IHycuDetails) => { + return ( + + + + ); +}; + +export default function Listing() { + const { t } = useTranslation('listing'); + const { t: tError } = useTranslation('error'); const { flattenData, isError, - error, totalCount, hasNextPage, fetchNextPage, @@ -37,85 +128,89 @@ export default function Listing() { status, sorting, setSorting, - pageIndex, } = useResourcesIcebergV6({ route: '/license/hycu', queryKey: ['hycu', '/license/hycu'], }); - const navigateToDashboard = (label: string) => { - const path = - location.pathname.indexOf('pci') > -1 ? `${location.pathname}/` : '/'; - navigate(`${path}${label}`); - }; - - useEffect(() => { - const isSuccess = status === 'success'; - if (columns && isSuccess && flattenData?.length > 0) { - const newColumns = Object.keys(flattenData[0]) - .filter((element) => element !== 'iam') - .map((element) => ({ - id: element, - label: element, - type: dataType( - (flattenData[0] as { [key: string]: string })[element], - ), - cell: (props: { [key: string]: string }) => { - const label = props[element]; - if (typeof label === 'string' || typeof label === 'number') { - if (serviceKey === element) - return ( - - navigateToDashboard(label)} - > - {label} - - - ); - return {label}; - } - return
-
; - }, - })); - setColumns(newColumns); - } else if (isSuccess && !flattenData?.length) { - navigate(urls.onboarding); - } - }, [flattenData]); - - if (isError) { - return ; - } - - if (isLoading && pageIndex === 1) { - return ( -
- -
- ); - } + const columns = useMemo(() => { + return [ + { + id: 'name', + label: t('hycu_name'), + cell: DatagridIdCell, + }, + { + id: 'controller_id', + label: t('hycu_controller_id'), + cell: DatagridControllerIdCell, + }, + { + id: 'status', + label: t('hycu_status'), + cell: DatagridStatusCell, + }, + { + id: 'commercial_name', + label: t('hycu_commercial_name'), + cell: DatagridCommercialNameCell, + }, + { + id: 'subscribed_date', + label: t('hycu_subscribed_date'), + cell: DatagridCreatedDateCell, + }, + { + id: 'action', + label: '', + cell: DatagridActionCell, + }, + ]; + }, []); const header = { title: t('title'), }; return ( - } header={header}> - - {columns && flattenData && ( - - )} - - + + {tError('manager_error_page_default')} + + } + > + + +
+
+ + {t('hycu_order')} + +
+ {columns && flattenData && ( + + )} +
+
+
+
); } diff --git a/packages/manager/apps/hycu/src/pages/listing/Listing.spec.tsx b/packages/manager/apps/hycu/src/pages/listing/Listing.spec.tsx index 39dd54d254a4..33ccbc9c9a82 100644 --- a/packages/manager/apps/hycu/src/pages/listing/Listing.spec.tsx +++ b/packages/manager/apps/hycu/src/pages/listing/Listing.spec.tsx @@ -1,10 +1,42 @@ -import { screen } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderTestApp } from '@/utils/tests/renderTestApp'; import '@testing-library/jest-dom'; import { labels } from '@/utils/tests/init.i18n'; +import { licensesHycu } from '@/mocks/licenseHycu/licenseHycu.data'; -describe('KMS listing test suite', () => { - it('should redirect to the onboarding page when the kms list is empty', async () => { +describe('License Hycu listing test suite', () => { + it('should redirect to the onboarding page when the license hycu list is empty', async () => { await renderTestApp({ nbLicenseHycu: 0 }); + + expect(screen.getByText(labels.onboarding.title)).toBeVisible(); + + expect(screen.queryByText(labels.listing.title)).not.toBeInTheDocument(); + }); + + it('should display the hycu listing page', async () => { + await renderTestApp(); + + expect(screen.getByText(labels.listing.title)).toBeVisible(); + + expect( + screen.queryByText(labels.onboarding.description), + ).not.toBeInTheDocument(); + }); + + it('should navigate to a hycu dashboard on click on license hycu name', async () => { + await renderTestApp(); + + await act(() => + userEvent.click(screen.getByText(licensesHycu[0].serviceName)), + ); + + await waitFor( + () => + expect( + screen.getByText(labels.dashboard.general_informations), + ).toBeVisible(), + { timeout: 30_000 }, + ); }); }); diff --git a/packages/manager/apps/hycu/src/pages/listing/menu/HycuActionMenu.component.tsx b/packages/manager/apps/hycu/src/pages/listing/menu/HycuActionMenu.component.tsx new file mode 100644 index 000000000000..050cc9f57c85 --- /dev/null +++ b/packages/manager/apps/hycu/src/pages/listing/menu/HycuActionMenu.component.tsx @@ -0,0 +1,31 @@ +import { ActionMenu, ActionMenuItem } from '@ovh-ux/manager-react-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import React from 'react'; +import { ODS_ICON_NAME } from '@ovhcloud/ods-components'; +import { useTranslation } from 'react-i18next'; +import { IHycuDetails } from '@/type/hycu.details.interface'; + +const HycuActionMenu = ({ + serviceName: _serviceName, +}: Pick) => { + const { t } = useTranslation('listing'); + + const items: ActionMenuItem[] = [ + { + id: 1, + label: t('hycu_service_listing_terminate'), + color: ODS_THEME_COLOR_INTENT.error, + onClick: () => {}, + }, + ]; + + return ( + + ); +}; + +export default HycuActionMenu; diff --git a/packages/manager/apps/hycu/src/routes/routes.constant.ts b/packages/manager/apps/hycu/src/routes/routes.constant.ts index 2ca8cbec5060..61e0fa44744f 100644 --- a/packages/manager/apps/hycu/src/routes/routes.constant.ts +++ b/packages/manager/apps/hycu/src/routes/routes.constant.ts @@ -1,7 +1,12 @@ +export const subRoutes = { + onboarding: 'onboarding', + serviceName: ':serviceName', +}; + export const urls = { root: '/', - onboarding: '/onboarding', + onboarding: `/${subRoutes.onboarding}`, listing: '/', - dashboard: '/:serviceName', + dashboard: `/${subRoutes.serviceName}`, tab2: 'Tab2', } as const; diff --git a/packages/manager/apps/hycu/src/type/hycu.details.interface.ts b/packages/manager/apps/hycu/src/type/hycu.details.interface.ts new file mode 100644 index 000000000000..64b1af7066ff --- /dev/null +++ b/packages/manager/apps/hycu/src/type/hycu.details.interface.ts @@ -0,0 +1,42 @@ +export enum LicenseStatus { + ACTIVATED = 'activated', + TO_ACTIVATE = 'toActivate', + PENDING = 'pending', + ERROR = 'error', +} + +export interface IamDetails { + id: string; + urn: string; + serviceName?: string; +} + +export interface IHycuDetails { + controllerId: string; + comment: string; + expirationDate: string; + iam: IamDetails; + licenseStatus: LicenseStatus; + serviceName: string; +} +export interface IHycuServiceInfo { + canDeleteAtExpiration: false; + contactAdmin: string; + contactBilling: string; + contactTech: string; + creation: string; + domain: string; + engagedUpTo: string; + expiration: string; + possibleRenewPeriod: number[]; + renew: { + automatic: boolean; + deleteAtExpiration: boolean; + forced: boolean; + manualPayment: boolean; + period: number; + }; + renewalType: 'automaticForcedProduct'; + serviceId: number; + status: 'expired'; +} diff --git a/packages/manager/apps/hycu/src/utils/statusColor.ts b/packages/manager/apps/hycu/src/utils/statusColor.ts new file mode 100644 index 000000000000..d460cce052a1 --- /dev/null +++ b/packages/manager/apps/hycu/src/utils/statusColor.ts @@ -0,0 +1,18 @@ +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { LicenseStatus } from '@/type/hycu.details.interface'; + +export const getStatusColor = ( + licenseStatus: LicenseStatus, +): ODS_THEME_COLOR_INTENT | string => { + switch (licenseStatus) { + case LicenseStatus.ACTIVATED: + return ODS_THEME_COLOR_INTENT.success; + case LicenseStatus.TO_ACTIVATE: + return ODS_THEME_COLOR_INTENT.warning; + case LicenseStatus.PENDING: + return ODS_THEME_COLOR_INTENT.primary; + case LicenseStatus.ERROR: + default: + return ODS_THEME_COLOR_INTENT.error; + } +};