Skip to content

Commit

Permalink
feat(hub): created notifications carousel component (#13419)
Browse files Browse the repository at this point in the history
ref: MANAGER-15114

Signed-off-by: Jacques Larique <[email protected]>
  • Loading branch information
JacquesLarique authored Oct 10, 2024
1 parent 61a4e59 commit c2c7c82
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 3 deletions.
16 changes: 16 additions & 0 deletions packages/manager/apps/hub/src/data/api/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { aapi } from '@ovh-ux/manager-core-api';
import { ApiEnvelope } from '@/types/apiEnvelope.type';
import { Notification, NotificationsList } from '@/types/notifications.type';

const hubNotificationStatuses = ['warning', 'error'];

export const getNotifications: () => Promise<Notification[]> = async () => {
const { data } = await aapi.get<ApiEnvelope<NotificationsList>>(
`/hub/notifications`,
);
return (
data.data?.notifications.data || []
).filter((notification: Notification) =>
hubNotificationStatuses.includes(notification.level),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PropsWithChildren } from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, vi } from 'vitest';
import { aapi as Api } from '@ovh-ux/manager-core-api';
import { useFetchHubNotifications } from '@/data/hooks/notifications/useNotifications';
import { ApiEnvelope } from '@/types/apiEnvelope.type';
import { NotificationsList } from '@/types/notifications.type';

const queryClient = new QueryClient();

const wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useFetchHubNotifications', () => {
it('should return notifications after extracting them from api envelope', async () => {
const notifications: ApiEnvelope<NotificationsList> = {
data: {
notifications: {
data: [],
status: 'OK',
},
},
status: 'OK',
};
const getNotifications = vi
.spyOn(Api, 'get')
.mockReturnValue(Promise.resolve(notifications));

const { result } = renderHook(() => useFetchHubNotifications(), {
wrapper,
});

await waitFor(() => {
expect(getNotifications).toHaveBeenCalled();
expect(result.current.data).toEqual(
notifications.data.notifications.data,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Notification } from '@/types/notifications.type';
import { getNotifications } from '@/data/api/notifications';

export const useFetchHubNotifications = () =>
useQuery<Notification[], AxiosError>({
queryKey: ['getHubNotifications'],
queryFn: getNotifications,
retry: 0,
refetchOnWindowFocus: false,
});
60 changes: 60 additions & 0 deletions packages/manager/apps/hub/src/pages/layout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import * as reactShellClientModule from '@ovh-ux/manager-react-shell-client';
import {
ShellContext,
ShellContextType,
} from '@ovh-ux/manager-react-shell-client';
import Layout from '@/pages/layout';

const shellContext = {
environment: {
getUser: vi.fn(),
},
shell: {
ux: {
hidePreloader: vi.fn(),
},
},
};

const renderComponent = () =>
render(
<ShellContext.Provider
value={(shellContext as unknown) as ShellContextType}
>
<Layout />
</ShellContext.Provider>,
);

const mockPath = '/foo';

vi.mock('react-router-dom', () => ({
useLocation: () => ({
pathname: mockPath,
}),
}));

vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => {
const original: typeof reactShellClientModule = await importOriginal();
return {
...original,
useOvhTracking: vi.fn(() => ({
trackPage: vi.fn(),
trackClick: vi.fn(),
trackCurrentPage: vi.fn(),
usePageTracking: vi.fn(),
})),
useRouteSynchro: vi.fn(() => {}),
};
});

describe('Form.page', () => {
it('should render select LegalForms correctly when the sub is FR and legalForms is other', async () => {
const { getByText } = renderComponent();

expect(getByText('Layout')).not.toBeNull();
});
});
29 changes: 29 additions & 0 deletions packages/manager/apps/hub/src/pages/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useEffect, useContext } from 'react';
import { defineCurrentPage } from '@ovh-ux/request-tagger';
import { useLocation } from 'react-router-dom';
import {
useOvhTracking,
useRouteSynchro,
ShellContext,
} from '@ovh-ux/manager-react-shell-client';

export default function Layout() {
const location = useLocation();
const { shell } = useContext(ShellContext);
const { trackCurrentPage } = useOvhTracking();
useRouteSynchro();

useEffect(() => {
defineCurrentPage(`app.dashboard`);
}, []);

useEffect(() => {
trackCurrentPage();
}, [location]);

useEffect(() => {
shell.ux.hidePreloader();
}, []);

return <div>Layout</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useState } from 'react';
import {
OsdsIcon,
OsdsMessage,
OsdsText,
} from '@ovhcloud/ods-components/react';
import {
ODS_ICON_NAME,
ODS_ICON_SIZE,
ODS_MESSAGE_TYPE,
ODS_TEXT_COLOR_INTENT,
ODS_TEXT_LEVEL,
ODS_TEXT_SIZE,
} from '@ovhcloud/ods-components';
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import { useOvhTracking } from '@ovh-ux/manager-react-shell-client';
import { useFetchHubNotifications } from '@/data/hooks/notifications/useNotifications';
import { Notification, NotificationType } from '@/types/notifications.type';

const getMessageColor = (type: NotificationType) => {
switch (type) {
case NotificationType.Success:
return ODS_TEXT_COLOR_INTENT.success;
case NotificationType.Error:
return ODS_TEXT_COLOR_INTENT.error;
case NotificationType.Warning:
return ODS_TEXT_COLOR_INTENT.warning;
case NotificationType.Info:
return ODS_TEXT_COLOR_INTENT.info;
default:
return ODS_TEXT_COLOR_INTENT.info;
}
};

const getMessageType = (type: NotificationType) => {
switch (type) {
case NotificationType.Success:
return ODS_MESSAGE_TYPE.success;
case NotificationType.Error:
return ODS_MESSAGE_TYPE.error;
case NotificationType.Warning:
return ODS_MESSAGE_TYPE.warning;
case NotificationType.Info:
return ODS_MESSAGE_TYPE.info;
default:
return ODS_MESSAGE_TYPE.info;
}
};

const getTextColor = (type: NotificationType) => {
switch (type) {
case NotificationType.Success:
return ODS_THEME_COLOR_INTENT.success;
case NotificationType.Error:
return ODS_THEME_COLOR_INTENT.error;
case NotificationType.Warning:
return ODS_THEME_COLOR_INTENT.warning;
case NotificationType.Info:
return ODS_THEME_COLOR_INTENT.info;
default:
return ODS_THEME_COLOR_INTENT.info;
}
};

export default function NotificationsCarousel() {
const { trackClick } = useOvhTracking();
const { data: notifications } = useFetchHubNotifications();
const [currentIndex, setCurrentIndex] = useState(0);

const showNextNotification = () => {
setCurrentIndex(
(previousIndex) => (previousIndex + 1) % notifications.length,
);
trackClick({
actionType: 'action',
actions: ['hub', 'dashboard', 'alert', 'action'],
});
};

return (
<>
{notifications?.length > 0 && (
<OsdsMessage
className={`rounded ${notifications?.length > 1 ? '!pb-8' : ''}`}
role="alert"
color={getMessageColor(notifications[currentIndex].level)}
type={getMessageType(notifications[currentIndex].level)}
data-testid="notifications_carousel"
>
<OsdsText
data-testid="notification_content"
color={getMessageColor(notifications[currentIndex].level)}
level={ODS_TEXT_LEVEL.body}
size={ODS_TEXT_SIZE._500}
>
<span
dangerouslySetInnerHTML={{
__html: notifications[currentIndex].description,
}}
></span>
</OsdsText>
{notifications?.length > 1 && (
<>
<OsdsIcon
data-testid="next-notification-button"
className="absolute top-1/2 right-4 -mt-6 cursor-pointer"
name={ODS_ICON_NAME.ARROW_RIGHT}
size={ODS_ICON_SIZE.sm}
color={ODS_THEME_COLOR_INTENT.primary}
onClick={showNextNotification}
/>
<div
className="absolute block w-full text-center right-0 left-0 bottom-1"
data-testid="notification-navigation"
>
{notifications.map(
(notification: Notification, index: number) => (
<OsdsIcon
key={`notification_selector_${notification.id}`}
className={`inline-block cursor-pointer ${
index > 0 ? 'ml-2' : ''
}`}
name={ODS_ICON_NAME.SHAPE_DOT}
size={ODS_ICON_SIZE.xxs}
color={getTextColor(notification.level)}
contrasted={currentIndex === index || undefined}
onClick={() => setCurrentIndex((previousIndex) => index)}
/>
),
)}
</div>
</>
)}
</OsdsMessage>
)}
</>
);
}
40 changes: 38 additions & 2 deletions packages/manager/apps/hub/src/pages/layout/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import SiretModal from '@/pages/layout/SiretModal.component';
import KycIndiaBanner from '@/pages/layout/KycIndiaBanner.component';
import KycFraudBanner from '@/pages/layout/KycFraudBanner.component';
import { KycStatus } from '@/types/kyc.type';
import NotificationsCarousel from '@/pages/layout/NotificationsCarousel.component';
import { Notification, NotificationType } from '@/types/notifications.type';

const queryClient = new QueryClient();

Expand Down Expand Up @@ -255,6 +257,27 @@ vi.mock('@/data/hooks/kyc/useKyc', () => ({
}),
}));

vi.mock('@/data/hooks/notifications/useNotifications', () => ({
useFetchHubNotifications: (): {
data: Notification[];
isPending: boolean;
} => ({
data: [
{
data: {},
date: '2022-02-08',
description:
'Fraudulent emails circulate and direct to scam websites claiming to be OVHcloud. <a href="https://docs.ovh.com/us/en/customer/scams-fraud-phishing" target="_blank">Find out more</a>',
level: 'error' as NotificationType,
id: 'GLOBAL_COMMUNICATION_PHISHING',
status: 'acknowledged',
subject: 'General information',
},
],
isPending: false,
}),
}));

const useBillingServicesMockValue: any = {
data: null,
isLoading: true,
Expand Down Expand Up @@ -296,7 +319,7 @@ describe('Layout.page', () => {

expect(welcome).not.toBeNull();
expect(queryByText('Banner')).not.toBeInTheDocument();
expect(queryByText('ovh-manager-hub-carousel')).not.toBeInTheDocument();
expect(queryByTestId('notifications_carousel')).not.toBeInTheDocument();
expect(queryByTestId('siret_banner')).not.toBeInTheDocument();
expect(queryByTestId('siret_modal')).not.toBeInTheDocument();
expect(queryByText('Payment Status')).not.toBeInTheDocument();
Expand Down Expand Up @@ -344,6 +367,7 @@ describe('Layout.page', () => {
queryByText,
findByText,
findByTestId,
queryByTestId,
} = renderComponent(<Layout />);

expect(getByTestId('banner_skeleton')).not.toBeNull();
Expand All @@ -353,7 +377,7 @@ describe('Layout.page', () => {

expect(welcome).not.toBeNull();
expect(banner).not.toBeNull();
expect(getByText('ovh-manager-hub-carousel')).not.toBeNull();
expect(queryByTestId('notifications_carousel')).not.toBeNull();
expect(getByTestId('siret_banner')).not.toBeNull();
expect(getByTestId('siret_modal')).not.toBeNull();
expect(getByTestId('kyc_india_banner')).not.toBeNull();
Expand Down Expand Up @@ -946,4 +970,16 @@ describe('Layout.page', () => {
expect(queryByTestId('kyc_fraud_banner')).not.toBeInTheDocument();
});
});

describe('NotificationsCarousel component', () => {
it('should render a single notification without "navigation"', async () => {
const { getByTestId, queryByTestId } = renderComponent(
<NotificationsCarousel />,
);

expect(getByTestId('notification_content')).not.toBeNull();
expect(queryByTestId('next-notification-button')).not.toBeInTheDocument();
expect(queryByTestId('notification-navigation')).not.toBeInTheDocument();
});
});
});
Loading

0 comments on commit c2c7c82

Please sign in to comment.