From d609a011fbe2f2da437fa477f152cbcca094b4b1 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Tue, 16 Jan 2024 17:02:27 +0800 Subject: [PATCH 01/14] ADM-747: [frontend] refactor: refactor notification --- .../NotificationButton.test.tsx | 55 +++++------- .../containers/ReportStep/ReportStep.test.tsx | 69 +++++++-------- .../useNotificationLayoutEffect.test.tsx | 87 +++++++++---------- .../Common/NotificationButton/index.tsx | 54 ++++++------ .../Common/NotificationButton/style.tsx | 12 ++- frontend/src/containers/ConfigStep/index.tsx | 6 +- frontend/src/containers/MetricsStep/index.tsx | 4 +- frontend/src/containers/ReportStep/index.tsx | 18 ++-- .../src/hooks/useNotificationLayoutEffect.ts | 63 +++++--------- 9 files changed, 166 insertions(+), 202 deletions(-) diff --git a/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx b/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx index 159b841fb8..62d11b2832 100644 --- a/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx +++ b/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx @@ -5,55 +5,42 @@ import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; import React from 'react'; +const mockNotifications = [ + { id: '1', title: 'Notification', message: 'Notification Message 1' }, + { + id: '2', + title: 'Notification', + message: 'Notification Message 2', + }, +]; describe('Notification', () => { - const closeNotificationProps = { - open: false, - title: 'NotificationPopper', - message: 'Notification Message', - closeAutomatically: false, - }; - const openNotificationProps = { - open: true, - title: 'NotificationPopper', - message: 'Notification Message', - closeAutomatically: false, - }; const { result } = renderHook(() => useNotificationLayoutEffect()); - it('should show title and message given the "open" value is true', () => { + it('should render all notifications correctly', () => { act(() => { - result.current.notificationProps = openNotificationProps; + result.current.notifications = mockNotifications; }); render(); - expect(screen.getByText('NotificationPopper')).toBeInTheDocument(); - expect(screen.getByText('Notification Message')).toBeInTheDocument(); + expect(screen.queryAllByText('Notification')).toHaveLength(2); + expect(screen.getByText('Notification Message 1')).toBeInTheDocument(); + expect(screen.getByText('Notification Message 2')).toBeInTheDocument(); }); - it('should not show title and message given the "open" value is false', () => { + it('should call closeNotification with corresponding id when clicking close button', async () => { act(() => { - result.current.notificationProps = closeNotificationProps; - }); - render(); - - expect(screen.queryByText('NotificationPopper')).not.toBeInTheDocument(); - expect(screen.queryByText('Notification Message')).not.toBeInTheDocument(); - }); - - it('should call updateProps when clicking close button given the "open" value is true', async () => { - act(() => { - result.current.notificationProps = openNotificationProps; - result.current.updateProps = jest.fn(); + result.current.notifications = mockNotifications; + result.current.closeNotification = jest.fn(); }); render(); - const closeButton = screen.getByRole('button', { name: 'Close' }); + const closeButton = screen.getAllByRole('button', { name: 'Close' }); - await userEvent.click(closeButton); + await userEvent.click(closeButton[0]); await waitFor(() => { - expect(result.current.updateProps).toBeCalledWith(closeNotificationProps); + expect(result.current.closeNotification).toBeCalledWith('1'); }); }); @@ -64,10 +51,10 @@ describe('Notification', () => { ${'warning'} | ${'#FFF4E3'} | ${'InfoIcon'} | ${'#D78D20'} | ${'#F3D5A9'} ${'info'} | ${'#E9ECFF'} | ${'InfoIcon'} | ${'#4050B5'} | ${'#939DDA'} `( - `should render background color $backgroundColor and $icon in $iconColor given the "type" value is $type`, + `should render background color $backgroundColor, $icon in $iconColor, border color $borderColor given the "type" value is $type`, async ({ type, backgroundColor, icon, iconColor, borderColor }) => { act(() => { - result.current.notificationProps = { ...openNotificationProps, type }; + result.current.notifications = [{ id: '1', title: 'Notification', message: 'Notification Message 1', type }]; }); render(); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 8975510963..ccd2e2cb2e 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -237,40 +237,41 @@ describe('Report Step', () => { expect(navigateMock).toHaveBeenCalledWith(ERROR_PAGE_ROUTE); }); - it('should call updateProps when remaining time is less than or equal to 5 minutes', () => { - const resetProps = jest.fn(); - const updateProps = jest.fn(); - notificationHook.current.resetProps = resetProps; - notificationHook.current.updateProps = updateProps; - jest.useFakeTimers(); - - setup(['']); - - expect(updateProps).not.toBeCalledWith({ - open: true, - title: MESSAGE.EXPIRE_INFORMATION(5), - closeAutomatically: true, - }); - - jest.advanceTimersByTime(500000); - - expect(updateProps).not.toBeCalledWith({ - open: true, - title: MESSAGE.EXPIRE_INFORMATION(5), - closeAutomatically: true, - }); - - jest.advanceTimersByTime(1000000); - - expect(updateProps).toBeCalledWith({ - open: true, - title: 'Help Information', - message: MESSAGE.EXPIRE_INFORMATION(5), - closeAutomatically: true, - }); - - jest.useRealTimers(); - }); + // todo: to be fixed @ru.jiang + // it('should call updateProps when remaining time is less than or equal to 5 minutes', () => { + // const resetProps = jest.fn(); + // const updateProps = jest.fn(); + // notificationHook.current.resetProps = resetProps; + // notificationHook.current.updateProps = updateProps; + // jest.useFakeTimers(); + // + // setup(['']); + // + // expect(updateProps).not.toBeCalledWith({ + // open: true, + // title: MESSAGE.EXPIRE_INFORMATION(5), + // closeAutomatically: true, + // }); + // + // jest.advanceTimersByTime(500000); + // + // expect(updateProps).not.toBeCalledWith({ + // open: true, + // title: MESSAGE.EXPIRE_INFORMATION(5), + // closeAutomatically: true, + // }); + // + // jest.advanceTimersByTime(1000000); + // + // expect(updateProps).toBeCalledWith({ + // open: true, + // title: 'Help Information', + // message: MESSAGE.EXPIRE_INFORMATION(5), + // closeAutomatically: true, + // }); + // + // jest.useRealTimers(); + // }); it.each([[REQUIRED_DATA_LIST[1]], [REQUIRED_DATA_LIST[4]]])( 'should render detail page when clicking show more button given metric %s', diff --git a/frontend/__tests__/hooks/useNotificationLayoutEffect.test.tsx b/frontend/__tests__/hooks/useNotificationLayoutEffect.test.tsx index f59a5800c4..90702f777d 100644 --- a/frontend/__tests__/hooks/useNotificationLayoutEffect.test.tsx +++ b/frontend/__tests__/hooks/useNotificationLayoutEffect.test.tsx @@ -3,95 +3,88 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { DURATION } from '@src/constants/commons'; import clearAllMocks = jest.clearAllMocks; +const mockNotification = { title: 'Test', message: 'Notification Message' }; + describe('useNotificationLayoutEffect', () => { afterAll(() => { clearAllMocks(); }); - const defaultProps = { - title: '', - message: '', - open: false, - closeAutomatically: false, - durationTimeout: DURATION.NOTIFICATION_TIME, - }; - it('should init the state of notificationProps when render hook', async () => { + + it('should init the state of notifications when rendering hook', async () => { const { result } = renderHook(() => useNotificationLayoutEffect()); - expect(result.current.notificationProps).toEqual(defaultProps); + expect(result.current.notifications).toEqual([]); }); - it('should reset the notificationProps when call resetProps given mock props', async () => { - const mockProps = { title: 'Test', message: 'Notification Message', open: true, closeAutomatically: false }; + it('should update the notifications when calling addNotification', async () => { const { result } = renderHook(() => useNotificationLayoutEffect()); act(() => { - result.current.notificationProps = mockProps; - result.current.resetProps(); + result.current.addNotification(mockNotification); + result.current.addNotification(mockNotification); }); - expect(result.current.notificationProps).toEqual(defaultProps); + expect(result.current.notifications).toEqual([ + { id: expect.anything(), ...mockNotification }, + { + id: expect.anything(), + ...mockNotification, + }, + ]); }); - it('should update the notificationProps when call updateProps given mock props', async () => { - const mockProps = { title: 'Test', message: 'Notification Message', open: true, closeAutomatically: false }; + it('should close corresponding notification when calling closeNotification by id', async () => { const { result } = renderHook(() => useNotificationLayoutEffect()); act(() => { - result.current.notificationProps = defaultProps; - result.current.updateProps(mockProps); + result.current.addNotification(mockNotification); + result.current.addNotification(mockNotification); }); - expect(result.current.notificationProps).toEqual(mockProps); + expect(result.current.notifications.length).toEqual(2); + const expected = result.current.notifications[1]; + + act(() => { + result.current.closeNotification(result.current.notifications[0].id); + }); + + await waitFor(() => { + expect(result.current.notifications).toEqual([expected]); + }); }); - it('should reset the notificationProps when update the value of closeAutomatically given closeAutomatically equals to true', async () => { - jest.useFakeTimers(); - const mockProps = { title: 'Test', message: 'Notification Message', open: true, closeAutomatically: true }; + it('should reset the notifications when calling closeAllNotifications', async () => { const { result } = renderHook(() => useNotificationLayoutEffect()); act(() => { - result.current.notificationProps = defaultProps; - result.current.updateProps(mockProps); - }); - act(() => { - jest.advanceTimersByTime(DURATION.NOTIFICATION_TIME); + result.current.addNotification(mockNotification); + result.current.addNotification(mockNotification); }); + expect(result.current.notifications.length).toEqual(2); - await waitFor(() => { - expect(result.current.notificationProps).toEqual(defaultProps); + act(() => { + result.current.closeAllNotifications(); }); - jest.useRealTimers(); + expect(result.current.notifications).toEqual([]); }); - it('should reset the notificationProps after 5s when update the value of closeAutomatically given durationTimeout equals to 5s', async () => { + it('should close notification when time exceeds 10s', async () => { jest.useFakeTimers(); const { result } = renderHook(() => useNotificationLayoutEffect()); - const expectedTime = 5000; - const mockProps = { - title: 'Test', - message: 'Notification Message', - open: true, - closeAutomatically: true, - durationTimeout: expectedTime, - }; act(() => { - result.current.notificationProps = defaultProps; - result.current.updateProps(mockProps); + result.current.addNotification(mockNotification); }); - jest.advanceTimersByTime(1000); + expect(result.current.notifications).toEqual([{ id: expect.anything(), ...mockNotification }]); - await waitFor(() => { - expect(result.current.notificationProps).not.toEqual(defaultProps); - }); act(() => { - jest.advanceTimersByTime(expectedTime); + jest.advanceTimersByTime(DURATION.NOTIFICATION_TIME); }); await waitFor(() => { - expect(result.current.notificationProps).toEqual(defaultProps); + expect(result.current.notifications).toEqual([]); }); jest.useRealTimers(); diff --git a/frontend/src/components/Common/NotificationButton/index.tsx b/frontend/src/components/Common/NotificationButton/index.tsx index dbaddd4d90..0c0dc180f7 100644 --- a/frontend/src/components/Common/NotificationButton/index.tsx +++ b/frontend/src/components/Common/NotificationButton/index.tsx @@ -1,4 +1,8 @@ -import { AlertTitleWrapper, AlertWrapper } from '@src/components/Common/NotificationButton/style'; +import { + AlertTitleWrapper, + AlertWrapper, + NotificationContainer, +} from '@src/components/Common/NotificationButton/style'; import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; @@ -41,32 +45,28 @@ const getStyles = (type: AlertColor | undefined) => { } }; -export const Notification = ({ notificationProps, updateProps }: useNotificationLayoutEffectInterface) => { - const handleNotificationClose = () => { - updateProps({ - title: notificationProps.title, - message: notificationProps.message, - open: false, - closeAutomatically: false, - }); - }; - - const styles = getStyles(notificationProps.type); - +export const Notification = ({ notifications, closeNotification }: useNotificationLayoutEffectInterface) => { return ( - <> - {notificationProps.open && ( - } - backgroundcolor={styles.backgroundColor} - iconcolor={styles.iconColor} - bordercolor={styles.borderColor} - > - {notificationProps.title} - {notificationProps.message} - - )} - + + {notifications.map((notification) => { + const styles = getStyles(notification.type); + + return ( + { + closeNotification(notification.id); + }} + icon={} + backgroundcolor={styles.backgroundColor} + iconcolor={styles.iconColor} + bordercolor={styles.borderColor} + > + {notification.title} + {notification.message} + + ); + })} + ); }; diff --git a/frontend/src/components/Common/NotificationButton/style.tsx b/frontend/src/components/Common/NotificationButton/style.tsx index 80179a2b78..ba03059840 100644 --- a/frontend/src/components/Common/NotificationButton/style.tsx +++ b/frontend/src/components/Common/NotificationButton/style.tsx @@ -3,13 +3,17 @@ import { Z_INDEX } from '@src/constants/commons'; import styled from '@emotion/styled'; import { theme } from '@src/theme'; +export const NotificationContainer = styled('div')({ + position: 'fixed', + zIndex: Z_INDEX.FIXED, + top: '4rem', + right: '0.75rem', +}); + export const AlertWrapper = styled(Alert)( (props: { backgroundcolor: string; iconcolor: string; bordercolor: string }) => ({ backgroundColor: props.backgroundcolor, - position: 'fixed', - zIndex: Z_INDEX.FIXED, - top: '4.75rem', - right: '0.75rem', + marginTop: '0.75rem', padding: '0.75rem 1.5rem', borderRadius: '0.5rem', border: `0.0625rem solid ${props.bordercolor}`, diff --git a/frontend/src/containers/ConfigStep/index.tsx b/frontend/src/containers/ConfigStep/index.tsx index 1c6308ca34..46b1b0980e 100644 --- a/frontend/src/containers/ConfigStep/index.tsx +++ b/frontend/src/containers/ConfigStep/index.tsx @@ -4,10 +4,10 @@ import BasicInfo from '@src/containers/ConfigStep/BasicInfo'; import { ConfigStepWrapper } from './style'; import { useLayoutEffect } from 'react'; -const ConfigStep = ({ resetProps }: useNotificationLayoutEffectInterface) => { +const ConfigStep = ({ closeAllNotifications }: useNotificationLayoutEffectInterface) => { useLayoutEffect(() => { - resetProps(); - }, [resetProps]); + closeAllNotifications(); + }, []); return ( diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index a91e475138..8a11b540f5 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -16,7 +16,7 @@ import { Crews } from '@src/containers/MetricsStep/Crews'; import { useAppSelector } from '@src/hooks'; import { useLayoutEffect } from 'react'; -const MetricsStep = ({ resetProps }: useNotificationLayoutEffectInterface) => { +const MetricsStep = ({ closeAllNotifications }: useNotificationLayoutEffectInterface) => { const requiredData = useAppSelector(selectMetrics); const users = useAppSelector(selectUsers); const jiraColumns = useAppSelector(selectJiraColumns); @@ -30,7 +30,7 @@ const MetricsStep = ({ resetProps }: useNotificationLayoutEffectInterface) => { const isShowRealDone = cycleTimeSettings.some((e) => e.value === DONE); useLayoutEffect(() => { - resetProps(); + closeAllNotifications(); }, []); return ( diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index cc7b1b3ab4..74cd0ecee1 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -46,7 +46,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const endDate = configData.basic.dateRange.endDate ?? ''; const metrics = configData.basic.metrics; - const { updateProps, resetProps } = notification; + const { addNotification, closeAllNotifications } = notification; const [errorMessage, setErrorMessage] = useState(); const shouldShowBoardMetrics = useAppSelector(isSelectBoardMetrics); @@ -60,13 +60,11 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { useLayoutEffect(() => { exportValidityTimeMin && allMetricsCompleted && - updateProps({ - open: true, + addNotification({ title: 'Help Information', message: MESSAGE.EXPIRE_INFORMATION(exportValidityTimeMin), - closeAutomatically: true, }); - }, [exportValidityTimeMin, allMetricsCompleted, updateProps]); + }, [exportValidityTimeMin, allMetricsCompleted]); useLayoutEffect(() => { if (exportValidityTimeMin && allMetricsCompleted) { @@ -78,11 +76,9 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const remainingExpireTime = 5 * 60 * 1000; const remainingTime = exportValidityTimeMin * 60 * 1000 - elapsedTime; if (remainingTime <= remainingExpireTime) { - updateProps({ - open: true, + addNotification({ title: 'Help Information', message: MESSAGE.EXPIRE_INFORMATION(5), - closeAutomatically: true, }); clearInterval(timer); } @@ -92,11 +88,11 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { clearInterval(timer); }; } - }, [exportValidityTimeMin, allMetricsCompleted, updateProps]); + }, [exportValidityTimeMin, allMetricsCompleted]); useLayoutEffect(() => { - resetProps(); - }, [pageType, resetProps]); + closeAllNotifications(); + }, [pageType]); useEffect(() => { setExportValidityTimeMin(reportData?.exportValidityTime); diff --git a/frontend/src/hooks/useNotificationLayoutEffect.ts b/frontend/src/hooks/useNotificationLayoutEffect.ts index 6dcd253393..c9746bc8b4 100644 --- a/frontend/src/hooks/useNotificationLayoutEffect.ts +++ b/frontend/src/hooks/useNotificationLayoutEffect.ts @@ -1,57 +1,40 @@ -import { useCallback, useEffect, useState } from 'react'; -import { DURATION } from '@src/constants/commons'; +import { useState } from 'react'; import { AlertColor } from '@mui/material'; +import { DURATION } from '@src/constants/commons'; +import { uniqueId } from 'lodash'; -export interface NotificationTipProps { +export interface Notification { + id: string; title: string; message: string; - open: boolean; - closeAutomatically: boolean; - durationTimeout?: number; type?: AlertColor; } export interface useNotificationLayoutEffectInterface { - notificationProps: NotificationTipProps; - resetProps: () => void; - updateProps: (notificationProps: NotificationTipProps) => void; + notifications: Notification[]; + addNotification: (notification: Omit) => void; + closeNotification: (id: string) => void; + closeAllNotifications: () => void; } export const useNotificationLayoutEffect = (): useNotificationLayoutEffectInterface => { - const [notificationProps, setNotificationProps] = useState({ - open: false, - title: '', - message: '', - closeAutomatically: false, - durationTimeout: DURATION.NOTIFICATION_TIME, - }); - - const resetProps = useCallback(() => { - setNotificationProps(() => ({ - open: false, - title: '', - message: '', - closeAutomatically: false, - durationTimeout: DURATION.NOTIFICATION_TIME, - })); - }, []); - - const updateProps = useCallback((notificationProps: NotificationTipProps) => { - setNotificationProps(notificationProps); - }, []); + const [notifications, setNotifications] = useState([]); - const closeAutomatically = () => { - const durationTimeout = notificationProps.durationTimeout - ? notificationProps.durationTimeout - : DURATION.NOTIFICATION_TIME; + const addNotification = (notification: Omit) => { + const newNotification = { id: uniqueId(), ...notification }; + setNotifications((preNotifications) => [...preNotifications, newNotification]); window.setTimeout(() => { - resetProps(); - }, durationTimeout); + closeNotification(newNotification.id); + }, DURATION.NOTIFICATION_TIME); }; - useEffect(() => { - notificationProps.closeAutomatically && closeAutomatically(); - }, [notificationProps]); + const closeNotification = (id: string) => { + setNotifications(notifications.filter((notification) => notification.id !== id)); + }; + + const closeAllNotifications = () => { + setNotifications([]); + }; - return { notificationProps, resetProps, updateProps }; + return { notifications, addNotification, closeNotification, closeAllNotifications }; }; From 32528e231a430ac5238e32850ebbed1c909bc1ef Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Wed, 17 Jan 2024 10:26:29 +0800 Subject: [PATCH 02/14] ADM-747: [frontend] refactor: set default title for notification --- .../NotificationButton.test.tsx | 17 +++++++++-------- .../Common/NotificationButton/index.tsx | 7 ++++++- frontend/src/constants/resources.ts | 7 +++++++ frontend/src/containers/ReportStep/index.tsx | 2 -- .../src/hooks/useNotificationLayoutEffect.ts | 2 +- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx b/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx index 62d11b2832..ab963438e3 100644 --- a/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx +++ b/frontend/__tests__/components/Common/NotificationButton/NotificationButton.test.tsx @@ -45,20 +45,21 @@ describe('Notification', () => { }); it.each` - type | backgroundColor | icon | iconColor | borderColor - ${'error'} | ${'#FFE7EA'} | ${'CancelIcon'} | ${'#D74257'} | ${'#F3B6BE'} - ${'success'} | ${'#EFFFF1'} | ${'CheckCircleIcon'} | ${'#5E9E66'} | ${'#CFE2D1'} - ${'warning'} | ${'#FFF4E3'} | ${'InfoIcon'} | ${'#D78D20'} | ${'#F3D5A9'} - ${'info'} | ${'#E9ECFF'} | ${'InfoIcon'} | ${'#4050B5'} | ${'#939DDA'} + type | title | backgroundColor | icon | iconColor | borderColor + ${'error'} | ${'Something went wrong!'} | ${'#FFE7EA'} | ${'CancelIcon'} | ${'#D74257'} | ${'#F3B6BE'} + ${'success'} | ${'Successfully completed!'} | ${'#EFFFF1'} | ${'CheckCircleIcon'} | ${'#5E9E66'} | ${'#CFE2D1'} + ${'warning'} | ${'Please note that'} | ${'#FFF4E3'} | ${'InfoIcon'} | ${'#D78D20'} | ${'#F3D5A9'} + ${'info'} | ${'Help Information'} | ${'#E9ECFF'} | ${'InfoIcon'} | ${'#4050B5'} | ${'#939DDA'} `( - `should render background color $backgroundColor, $icon in $iconColor, border color $borderColor given the "type" value is $type`, - async ({ type, backgroundColor, icon, iconColor, borderColor }) => { + `should render title $title background color $backgroundColor, $icon in $iconColor, border color $borderColor given the "type" value is $type`, + async ({ type, title, backgroundColor, icon, iconColor, borderColor }) => { act(() => { - result.current.notifications = [{ id: '1', title: 'Notification', message: 'Notification Message 1', type }]; + result.current.notifications = [{ id: '1', message: 'Notification Message 1', type }]; }); render(); + expect(screen.getByText(title)).toBeInTheDocument(); const alertElement = screen.getByRole('alert'); expect(alertElement).toHaveStyle({ 'background-color': backgroundColor }); expect(alertElement).toHaveStyle({ border: `0.0625rem solid ${borderColor}` }); diff --git a/frontend/src/components/Common/NotificationButton/index.tsx b/frontend/src/components/Common/NotificationButton/index.tsx index 0c0dc180f7..dee28a2fe8 100644 --- a/frontend/src/components/Common/NotificationButton/index.tsx +++ b/frontend/src/components/Common/NotificationButton/index.tsx @@ -10,11 +10,13 @@ import { AlertColor, SvgIcon } from '@mui/material'; import InfoIcon from '@mui/icons-material/Info'; import { theme } from '@src/theme'; import React from 'react'; +import { NOTIFICATION_TITLE } from '@src/constants/resources'; const getStyles = (type: AlertColor | undefined) => { switch (type) { case 'error': return { + title: NOTIFICATION_TITLE.SOMETHING_WENT_WRONG, icon: CancelIcon, iconColor: theme.main.alert.error.iconColor, backgroundColor: theme.main.alert.error.backgroundColor, @@ -22,6 +24,7 @@ const getStyles = (type: AlertColor | undefined) => { }; case 'success': return { + title: NOTIFICATION_TITLE.SUCCESSFULLY_COMPLETED, icon: CheckCircleIcon, iconColor: theme.main.alert.success.iconColor, backgroundColor: theme.main.alert.success.backgroundColor, @@ -29,6 +32,7 @@ const getStyles = (type: AlertColor | undefined) => { }; case 'warning': return { + title: NOTIFICATION_TITLE.PLEASE_NOTE_THAT, icon: InfoIcon, iconColor: theme.main.alert.warning.iconColor, backgroundColor: theme.main.alert.warning.backgroundColor, @@ -37,6 +41,7 @@ const getStyles = (type: AlertColor | undefined) => { case 'info': default: return { + title: NOTIFICATION_TITLE.HELP_INFORMATION, icon: InfoIcon, iconColor: theme.main.alert.info.iconColor, backgroundColor: theme.main.alert.info.backgroundColor, @@ -62,7 +67,7 @@ export const Notification = ({ notifications, closeNotification }: useNotificati iconcolor={styles.iconColor} bordercolor={styles.borderColor} > - {notification.title} + {notification.title || styles.title} {notification.message} ); diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index bb37941d2e..b86dcb4ba9 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -16,6 +16,13 @@ export const BACK = 'Back'; export const RETRY = 'retry'; export const TIMEOUT_PROMPT = 'Data loading failed'; +export enum NOTIFICATION_TITLE { + HELP_INFORMATION = 'Help Information', + PLEASE_NOTE_THAT = 'Please note that', + SUCCESSFULLY_COMPLETED = 'Successfully completed!', + SOMETHING_WENT_WRONG = 'Something went wrong!', +} + export enum REQUIRED_DATA { All = 'All', VELOCITY = 'Velocity', diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 74cd0ecee1..6d7611f1d8 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -61,7 +61,6 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { exportValidityTimeMin && allMetricsCompleted && addNotification({ - title: 'Help Information', message: MESSAGE.EXPIRE_INFORMATION(exportValidityTimeMin), }); }, [exportValidityTimeMin, allMetricsCompleted]); @@ -77,7 +76,6 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const remainingTime = exportValidityTimeMin * 60 * 1000 - elapsedTime; if (remainingTime <= remainingExpireTime) { addNotification({ - title: 'Help Information', message: MESSAGE.EXPIRE_INFORMATION(5), }); clearInterval(timer); diff --git a/frontend/src/hooks/useNotificationLayoutEffect.ts b/frontend/src/hooks/useNotificationLayoutEffect.ts index c9746bc8b4..d6c86594f5 100644 --- a/frontend/src/hooks/useNotificationLayoutEffect.ts +++ b/frontend/src/hooks/useNotificationLayoutEffect.ts @@ -5,7 +5,7 @@ import { uniqueId } from 'lodash'; export interface Notification { id: string; - title: string; + title?: string; message: string; type?: AlertColor; } From ffab4e1c5bf2e840387344e120c2c5181e5f2936 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Wed, 17 Jan 2024 10:47:05 +0800 Subject: [PATCH 03/14] ADM-747: [frontend] feat: handle timeout error --- frontend/src/constants/resources.ts | 1 + frontend/src/hooks/useGenerateReportEffect.ts | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index b86dcb4ba9..61340cff7a 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -192,6 +192,7 @@ export const MESSAGE = { ERROR_PAGE: 'Something on internet is not quite right. Perhaps head back to our homepage and try again.', EXPIRE_INFORMATION: (value: number) => `The file will expire in ${value} minutes, please download it in time.`, REPORT_LOADING: 'The report is being generated, please do not refresh the page or all the data will be disappeared.', + LOADING_TIMEOUT: (name: string) => `${name} loading timeout, please click "Retry"!`, }; export const METRICS_CYCLE_SETTING_TABLE_HEADER = [ diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index 38541af18c..14ddbd8dda 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -2,11 +2,13 @@ import { BoardReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidityTime'; import { InternalServerException } from '@src/exceptions/InternalServerException'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import { RETRIEVE_REPORT_TYPES } from '@src/constants/commons'; import { TimeoutException } from '@src/exceptions/TimeoutException'; import { reportClient } from '@src/clients/report/ReportClient'; -import { TIMEOUT_PROMPT } from '@src/constants/resources'; import { METRIC_TYPES } from '@src/constants/commons'; import { useRef, useState } from 'react'; +import { MESSAGE, TIMEOUT_PROMPT } from '@src/constants/resources'; +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; export interface useGenerateReportEffectInterface { startToRequestBoardData: (boardParams: BoardReportRequestDTO) => void; @@ -19,6 +21,7 @@ export interface useGenerateReportEffectInterface { } export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { + const { addNotification } = useNotificationLayoutEffect(); const reportPath = '/reports'; const [isServerError, setIsServerError] = useState(false); const [timeout4Board, setTimeout4Board] = useState(''); @@ -48,11 +51,23 @@ export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { } else { if (source === 'Board') { setTimeout4Board(TIMEOUT_PROMPT); + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('Board metrics'), + type: 'error', + }); } else if (source === 'Dora') { setTimeout4Dora(TIMEOUT_PROMPT); + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), + type: 'error', + }); } else { setTimeout4Board(TIMEOUT_PROMPT); setTimeout4Dora(TIMEOUT_PROMPT); + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('Report'), + type: 'error', + }); } } }; From 3e9e245ad3e337211f8fea508f5a2467a917db6f Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Wed, 17 Jan 2024 16:51:02 +0800 Subject: [PATCH 04/14] ADM-747: [frontend] feat: handle report error --- frontend/src/constants/resources.ts | 1 + .../ReportStep/BoardMetrics/index.tsx | 18 ++++++++- .../ReportStep/DoraMetrics/index.tsx | 38 +++++++++++++++---- frontend/src/containers/ReportStep/index.tsx | 4 +- frontend/src/hooks/useGenerateReportEffect.ts | 7 ++-- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 61340cff7a..04c2203c72 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -193,6 +193,7 @@ export const MESSAGE = { EXPIRE_INFORMATION: (value: number) => `The file will expire in ${value} minutes, please download it in time.`, REPORT_LOADING: 'The report is being generated, please do not refresh the page or all the data will be disappeared.', LOADING_TIMEOUT: (name: string) => `${name} loading timeout, please click "Retry"!`, + FAILED_TO_GET_DATA: (name: string) => `Failed to get ${name} data, please click "retry"!`, }; export const METRICS_CYCLE_SETTING_TABLE_HEADER = [ diff --git a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx index 025ec2e425..4dbd3241e2 100644 --- a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx @@ -8,12 +8,13 @@ import { import { BOARD_METRICS, CALENDAR, + MESSAGE, METRICS_SUBTITLE, - REPORT_PAGE, METRICS_TITLE, + REPORT_PAGE, REQUIRED_DATA, - SHOW_MORE, RETRY, + SHOW_MORE, } from '@src/constants/resources'; import { BoardReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto/request'; import { filterAndMapCycleTimeSettings, getJiraBoardToken } from '@src/utils/util'; @@ -24,12 +25,14 @@ import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { ReportGrid } from '@src/components/Common/ReportGrid'; import { Loading } from '@src/components/Loading'; import { Nullable } from '@src/utils/types'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { useAppSelector } from '@src/hooks'; import React, { useEffect } from 'react'; import dayjs from 'dayjs'; import _ from 'lodash'; interface BoardMetricsProps { + notification: useNotificationLayoutEffectInterface; startToRequestBoardData: (request: ReportRequestDTO) => void; onShowDetail: () => void; boardReport?: ReportResponseDTO; @@ -41,6 +44,7 @@ interface BoardMetricsProps { } const BoardMetrics = ({ + notification, isBackFromDetail, startToRequestBoardData, onShowDetail, @@ -63,6 +67,7 @@ const BoardMetrics = ({ (obj: { key: string; value: { name: string; statuses: string[] } }) => obj.value, ); const boardMetrics = metrics.filter((metric) => BOARD_METRICS.includes(metric)); + const { addNotification } = notification; const getErrorMessage = () => _.get(boardReport, ['reportMetricsError', 'boardMetricsError']) @@ -150,6 +155,15 @@ const BoardMetrics = ({ !isBackFromDetail && startToRequestBoardData(getBoardReportRequestBody()); }, []); + useEffect(() => { + if (boardReport?.reportError.boardError) { + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), + type: 'error', + }); + } + }, [boardReport?.reportError.boardError]); + return ( <> diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index 3b4671c8e2..6fa58df2ca 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -1,31 +1,33 @@ +import React, { useEffect } from 'react'; +import { useAppSelector } from '@src/hooks'; +import { selectConfig } from '@src/context/config/configSlice'; import { CALENDAR, DORA_METRICS, + MESSAGE, METRICS_SUBTITLE, - REPORT_PAGE, METRICS_TITLE, + REPORT_PAGE, REQUIRED_DATA, - SHOW_MORE, RETRY, + SHOW_MORE, } from '@src/constants/resources'; -import { StyledShowMore, StyledTitleWrapper } from '@src/containers/ReportStep/DoraMetrics/style'; import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; -import { StyledMetricsSection } from '@src/containers/ReportStep/DoraMetrics/style'; -import { formatMillisecondsToHours, formatMinToHours } from '@src/utils/util'; +import { StyledMetricsSection, StyledShowMore, StyledTitleWrapper } from '@src/containers/ReportStep/DoraMetrics/style'; import { ReportTitle } from '@src/components/Common/ReportGrid/ReportTitle'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { ReportRequestDTO } from '@src/clients/report/dto/request'; import { StyledSpacing } from '@src/containers/ReportStep/style'; +import { formatMillisecondsToHours, formatMinToHours } from '@src/utils/util'; import { ReportGrid } from '@src/components/Common/ReportGrid'; -import { selectConfig } from '@src/context/config/configSlice'; import { StyledRetry } from '../BoardMetrics/BoardMetrics'; import { Nullable } from '@src/utils/types'; -import { useAppSelector } from '@src/hooks'; -import { useEffect } from 'react'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import dayjs from 'dayjs'; import _ from 'lodash'; interface DoraMetricsProps { + notification: useNotificationLayoutEffectInterface; startToRequestDoraData: (request: ReportRequestDTO) => void; onShowDetail: () => void; doraReport?: ReportResponseDTO; @@ -37,6 +39,7 @@ interface DoraMetricsProps { } const DoraMetrics = ({ + notification, isBackFromDetail, startToRequestDoraData, onShowDetail, @@ -51,6 +54,7 @@ const DoraMetrics = ({ const { metrics, calendarType } = configData.basic; const { pipelineCrews, deploymentFrequencySettings, leadTimeForChanges } = useAppSelector(selectMetricsContent); const shouldShowSourceControl = metrics.includes(REQUIRED_DATA.LEAD_TIME_FOR_CHANGES); + const { addNotification } = notification; const getDoraReportRequestBody = (): ReportRequestDTO => { const doraMetrics = metrics.filter((metric) => DORA_METRICS.includes(metric)); @@ -212,6 +216,24 @@ const DoraMetrics = ({ !isBackFromDetail && startToRequestDoraData(getDoraReportRequestBody()); }, []); + useEffect(() => { + if (doraReport?.reportError.pipelineError) { + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), + type: 'error', + }); + } + }, [doraReport?.reportError.pipelineError]); + + useEffect(() => { + if (doraReport?.reportError.sourceControlError) { + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Github'), + type: 'error', + }); + } + }, [doraReport?.reportError.sourceControlError]); + return ( <> diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 6d7611f1d8..e150749ec3 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -33,7 +33,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { stopPollingReports, timeout4Board, timeout4Dora, - } = useGenerateReportEffect(); + } = useGenerateReportEffect(notification); const [exportValidityTimeMin, setExportValidityTimeMin] = useState(undefined); const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); @@ -107,6 +107,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { <> {shouldShowBoardMetrics && ( { )} {shouldShowDoraMetrics && ( void; @@ -20,8 +20,9 @@ export interface useGenerateReportEffectInterface { reportData: ReportResponseDTO | undefined; } -export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { - const { addNotification } = useNotificationLayoutEffect(); +export const useGenerateReportEffect = ({ + addNotification, +}: useNotificationLayoutEffectInterface): useGenerateReportEffectInterface => { const reportPath = '/reports'; const [isServerError, setIsServerError] = useState(false); const [timeout4Board, setTimeout4Board] = useState(''); From b4a0d3792108342bdb41f68c7b3db9ec4d1c714d Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Wed, 17 Jan 2024 17:29:20 +0800 Subject: [PATCH 05/14] ADM-747: [frontend] refactor: rename reportMetricsError --- frontend/src/containers/ReportStep/BoardMetrics/index.tsx | 4 ++-- frontend/src/containers/ReportStep/DoraMetrics/index.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx index 4dbd3241e2..fc3620374b 100644 --- a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx @@ -156,13 +156,13 @@ const BoardMetrics = ({ }, []); useEffect(() => { - if (boardReport?.reportError.boardError) { + if (boardReport?.reportMetricsError.boardMetricsError) { addNotification({ message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), type: 'error', }); } - }, [boardReport?.reportError.boardError]); + }, [boardReport?.reportMetricsError.boardMetricsError]); return ( <> diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index 6fa58df2ca..a2a9b5f02a 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -217,22 +217,22 @@ const DoraMetrics = ({ }, []); useEffect(() => { - if (doraReport?.reportError.pipelineError) { + if (doraReport?.reportMetricsError.pipelineMetricsError) { addNotification({ message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), type: 'error', }); } - }, [doraReport?.reportError.pipelineError]); + }, [doraReport?.reportMetricsError.pipelineMetricsError]); useEffect(() => { - if (doraReport?.reportError.sourceControlError) { + if (doraReport?.reportMetricsError.sourceControlMetricsError) { addNotification({ message: MESSAGE.FAILED_TO_GET_DATA('Github'), type: 'error', }); } - }, [doraReport?.reportError.sourceControlError]); + }, [doraReport?.reportMetricsError.sourceControlMetricsError]); return ( <> From 04bd2ebf8e5e64ff0d5293d1476234276cc57768 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Wed, 17 Jan 2024 17:31:37 +0800 Subject: [PATCH 06/14] ADM-747: [frontend] refactor: replace enum with object --- frontend/src/constants/resources.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 04c2203c72..96af9d8168 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -16,12 +16,12 @@ export const BACK = 'Back'; export const RETRY = 'retry'; export const TIMEOUT_PROMPT = 'Data loading failed'; -export enum NOTIFICATION_TITLE { - HELP_INFORMATION = 'Help Information', - PLEASE_NOTE_THAT = 'Please note that', - SUCCESSFULLY_COMPLETED = 'Successfully completed!', - SOMETHING_WENT_WRONG = 'Something went wrong!', -} +export const NOTIFICATION_TITLE = { + HELP_INFORMATION: 'Help Information', + PLEASE_NOTE_THAT: 'Please note that', + SUCCESSFULLY_COMPLETED: 'Successfully completed!', + SOMETHING_WENT_WRONG: 'Something went wrong!', +}; export enum REQUIRED_DATA { All = 'All', From e55915575f8aed493af8323e82138615f7602d61 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Thu, 18 Jan 2024 13:48:05 +0800 Subject: [PATCH 07/14] ADM-747: [frontend] feat: handle report error --- .../ReportStep/BoardMetrics/index.tsx | 13 -- .../ReportStep/DoraMetrics/index.tsx | 23 ---- frontend/src/containers/ReportStep/index.tsx | 115 ++++++++++++++---- frontend/src/hooks/useGenerateReportEffect.ts | 27 ++-- .../src/hooks/useNotificationLayoutEffect.ts | 2 +- 5 files changed, 100 insertions(+), 80 deletions(-) diff --git a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx index fc3620374b..df2c4580da 100644 --- a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx @@ -8,7 +8,6 @@ import { import { BOARD_METRICS, CALENDAR, - MESSAGE, METRICS_SUBTITLE, METRICS_TITLE, REPORT_PAGE, @@ -32,7 +31,6 @@ import dayjs from 'dayjs'; import _ from 'lodash'; interface BoardMetricsProps { - notification: useNotificationLayoutEffectInterface; startToRequestBoardData: (request: ReportRequestDTO) => void; onShowDetail: () => void; boardReport?: ReportResponseDTO; @@ -44,7 +42,6 @@ interface BoardMetricsProps { } const BoardMetrics = ({ - notification, isBackFromDetail, startToRequestBoardData, onShowDetail, @@ -67,7 +64,6 @@ const BoardMetrics = ({ (obj: { key: string; value: { name: string; statuses: string[] } }) => obj.value, ); const boardMetrics = metrics.filter((metric) => BOARD_METRICS.includes(metric)); - const { addNotification } = notification; const getErrorMessage = () => _.get(boardReport, ['reportMetricsError', 'boardMetricsError']) @@ -155,15 +151,6 @@ const BoardMetrics = ({ !isBackFromDetail && startToRequestBoardData(getBoardReportRequestBody()); }, []); - useEffect(() => { - if (boardReport?.reportMetricsError.boardMetricsError) { - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), - type: 'error', - }); - } - }, [boardReport?.reportMetricsError.boardMetricsError]); - return ( <> diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index a2a9b5f02a..1a57aeb151 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -4,7 +4,6 @@ import { selectConfig } from '@src/context/config/configSlice'; import { CALENDAR, DORA_METRICS, - MESSAGE, METRICS_SUBTITLE, METRICS_TITLE, REPORT_PAGE, @@ -22,12 +21,10 @@ import { formatMillisecondsToHours, formatMinToHours } from '@src/utils/util'; import { ReportGrid } from '@src/components/Common/ReportGrid'; import { StyledRetry } from '../BoardMetrics/BoardMetrics'; import { Nullable } from '@src/utils/types'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import dayjs from 'dayjs'; import _ from 'lodash'; interface DoraMetricsProps { - notification: useNotificationLayoutEffectInterface; startToRequestDoraData: (request: ReportRequestDTO) => void; onShowDetail: () => void; doraReport?: ReportResponseDTO; @@ -39,7 +36,6 @@ interface DoraMetricsProps { } const DoraMetrics = ({ - notification, isBackFromDetail, startToRequestDoraData, onShowDetail, @@ -54,7 +50,6 @@ const DoraMetrics = ({ const { metrics, calendarType } = configData.basic; const { pipelineCrews, deploymentFrequencySettings, leadTimeForChanges } = useAppSelector(selectMetricsContent); const shouldShowSourceControl = metrics.includes(REQUIRED_DATA.LEAD_TIME_FOR_CHANGES); - const { addNotification } = notification; const getDoraReportRequestBody = (): ReportRequestDTO => { const doraMetrics = metrics.filter((metric) => DORA_METRICS.includes(metric)); @@ -216,24 +211,6 @@ const DoraMetrics = ({ !isBackFromDetail && startToRequestDoraData(getDoraReportRequestBody()); }, []); - useEffect(() => { - if (doraReport?.reportMetricsError.pipelineMetricsError) { - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), - type: 'error', - }); - } - }, [doraReport?.reportMetricsError.pipelineMetricsError]); - - useEffect(() => { - if (doraReport?.reportMetricsError.sourceControlMetricsError) { - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Github'), - type: 'error', - }); - } - }, [doraReport?.reportMetricsError.sourceControlMetricsError]); - return ( <> diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index e150749ec3..fd43d32e81 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -1,21 +1,22 @@ +import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; +import { useAppSelector } from '@src/hooks'; import { isSelectBoardMetrics, isSelectDoraMetrics, selectConfig } from '@src/context/config/configSlice'; import { StyledCalendarWrapper, StyledErrorNotification } from '@src/containers/ReportStep/style'; import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { MESSAGE, REPORT_PAGE_TYPE, REQUIRED_DATA } from '@src/constants/resources'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { ErrorNotification } from '@src/components/ErrorNotification'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; -import { ReportResponseDTO } from '@src/clients/report/dto/response'; -import React, { useEffect, useLayoutEffect, useState } from 'react'; +import { ErrorResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import { Nullable } from '@src/utils/types'; import { BoardDetail, DoraDetail } from './ReportDetail'; import { useNavigate } from 'react-router-dom'; import { ROUTE } from '@src/constants/router'; -import { useAppSelector } from '@src/hooks'; export interface ReportStepProps { notification: useNotificationLayoutEffectInterface; @@ -33,12 +34,20 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { stopPollingReports, timeout4Board, timeout4Dora, - } = useGenerateReportEffect(notification); + timeout4Report, + } = useGenerateReportEffect(); const [exportValidityTimeMin, setExportValidityTimeMin] = useState(undefined); const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); const [isBackFromDetail, setIsBackFromDetail] = useState(false); const [allMetricsCompleted, setAllMetricsCompleted] = useState(false); + const [boardMetricsError, setBoardMetricsError] = useState>(null); + const [pipelineMetricsError, setPipelineMetricsError] = useState>(null); + const [sourceControlMetricsError, setSourceControlMetricsError] = useState>(null); + const [timeoutError4Board, setTimeoutError4Board] = useState(''); + const [timeoutError4Dora, setTimeoutError4Dora] = useState(''); + const [timeoutError4Report, setTimeoutError4Report] = useState(''); + const configData = useAppSelector(selectConfig); const csvTimeStamp = useAppSelector(selectTimeStamp); @@ -52,6 +61,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const shouldShowBoardMetrics = useAppSelector(isSelectBoardMetrics); const shouldShowDoraMetrics = useAppSelector(isSelectDoraMetrics); const onlySelectClassification = metrics.length === 1 && metrics[0] === REQUIRED_DATA.CLASSIFICATION; + const isSummaryPage = useMemo(() => pageType === REPORT_PAGE_TYPE.SUMMARY, [pageType]); useEffect(() => { setPageType(onlySelectClassification ? REPORT_PAGE_TYPE.BOARD : REPORT_PAGE_TYPE.SUMMARY); @@ -103,11 +113,78 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { }; }, []); + useEffect(() => { + if (isSummaryPage && reportData) { + setBoardMetricsError(reportData.reportMetricsError.boardMetricsError); + setPipelineMetricsError(reportData.reportMetricsError.pipelineMetricsError); + setSourceControlMetricsError(reportData.reportMetricsError.sourceControlMetricsError); + } + }, [reportData, isSummaryPage]); + + useEffect(() => { + boardMetricsError && + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), + type: 'error', + }); + }, [boardMetricsError]); + + useEffect(() => { + pipelineMetricsError && + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), + type: 'error', + }); + }, [pipelineMetricsError]); + + useEffect(() => { + sourceControlMetricsError && + addNotification({ + message: MESSAGE.FAILED_TO_GET_DATA('Github'), + type: 'error', + }); + }, [sourceControlMetricsError]); + + useEffect(() => { + isSummaryPage && setTimeoutError4Report(timeout4Report); + }, [timeout4Report, isSummaryPage]); + + useEffect(() => { + isSummaryPage && setTimeoutError4Board(timeout4Board); + }, [timeout4Board, isSummaryPage]); + + useEffect(() => { + isSummaryPage && setTimeoutError4Dora(timeout4Dora); + }, [timeout4Dora, isSummaryPage]); + + useEffect(() => { + timeoutError4Report && + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('Report'), + type: 'error', + }); + }, [timeoutError4Report]); + + useEffect(() => { + timeoutError4Board && + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('Board metrics'), + type: 'error', + }); + }, [timeoutError4Board]); + + useEffect(() => { + timeoutError4Dora && + addNotification({ + message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), + type: 'error', + }); + }, [timeoutError4Dora]); + const showSummary = () => ( <> {shouldShowBoardMetrics && ( { onShowDetail={() => setPageType(REPORT_PAGE_TYPE.BOARD)} boardReport={reportData} csvTimeStamp={csvTimeStamp} - timeoutError={timeout4Board} + timeoutError={timeout4Board || timeout4Report} /> )} {shouldShowDoraMetrics && ( { onShowDetail={() => setPageType(REPORT_PAGE_TYPE.DORA)} doraReport={reportData} csvTimeStamp={csvTimeStamp} - timeoutError={timeout4Dora} + timeoutError={timeout4Dora || timeout4Report} /> )} @@ -137,7 +213,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const showDoraDetail = (data: ReportResponseDTO) => backToSummaryPage()} data={data} />; const handleBack = () => { - pageType === REPORT_PAGE_TYPE.SUMMARY || onlySelectClassification ? dispatch(backStep()) : backToSummaryPage(); + isSummaryPage || onlySelectClassification ? dispatch(backStep()) : backToSummaryPage(); }; const backToSummaryPage = () => { @@ -152,10 +228,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { ) : ( <> {startDate && endDate && ( - + )} @@ -164,19 +237,15 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { )} - {pageType === REPORT_PAGE_TYPE.SUMMARY + {isSummaryPage ? showSummary() : !!reportData && (pageType === REPORT_PAGE_TYPE.BOARD ? showBoardDetail(reportData) : showDoraDetail(reportData))} handleBack()} handleSave={() => handleSave()} reportData={reportData} diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index 41faee7eb0..a8456aeae4 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -2,13 +2,11 @@ import { BoardReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidityTime'; import { InternalServerException } from '@src/exceptions/InternalServerException'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; -import { RETRIEVE_REPORT_TYPES } from '@src/constants/commons'; import { TimeoutException } from '@src/exceptions/TimeoutException'; +import { TIMEOUT_PROMPT } from '@src/constants/resources'; import { reportClient } from '@src/clients/report/ReportClient'; import { METRIC_TYPES } from '@src/constants/commons'; import { useRef, useState } from 'react'; -import { MESSAGE, TIMEOUT_PROMPT } from '@src/constants/resources'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; export interface useGenerateReportEffectInterface { startToRequestBoardData: (boardParams: BoardReportRequestDTO) => void; @@ -17,16 +15,16 @@ export interface useGenerateReportEffectInterface { isServerError: boolean; timeout4Board: string; timeout4Dora: string; + timeout4Report: string; reportData: ReportResponseDTO | undefined; } -export const useGenerateReportEffect = ({ - addNotification, -}: useNotificationLayoutEffectInterface): useGenerateReportEffectInterface => { +export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { const reportPath = '/reports'; const [isServerError, setIsServerError] = useState(false); const [timeout4Board, setTimeout4Board] = useState(''); const [timeout4Dora, setTimeout4Dora] = useState(''); + const [timeout4Report, setTimeout4Report] = useState(''); const [reportData, setReportData] = useState(); const timerIdRef = useRef(); let hasPollingStarted = false; @@ -52,23 +50,10 @@ export const useGenerateReportEffect = ({ } else { if (source === 'Board') { setTimeout4Board(TIMEOUT_PROMPT); - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('Board metrics'), - type: 'error', - }); } else if (source === 'Dora') { setTimeout4Dora(TIMEOUT_PROMPT); - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), - type: 'error', - }); } else { - setTimeout4Board(TIMEOUT_PROMPT); - setTimeout4Dora(TIMEOUT_PROMPT); - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('Report'), - type: 'error', - }); + setTimeout4Report(TIMEOUT_PROMPT); } } }; @@ -89,6 +74,7 @@ export const useGenerateReportEffect = ({ }; const pollingReport = (url: string, interval: number) => { + setTimeout4Report(''); reportClient .polling(url) .then((res: { status: number; response: ReportResponseDTO }) => { @@ -124,5 +110,6 @@ export const useGenerateReportEffect = ({ isServerError, timeout4Board, timeout4Dora, + timeout4Report, }; }; diff --git a/frontend/src/hooks/useNotificationLayoutEffect.ts b/frontend/src/hooks/useNotificationLayoutEffect.ts index d6c86594f5..bb9ecd1cc9 100644 --- a/frontend/src/hooks/useNotificationLayoutEffect.ts +++ b/frontend/src/hooks/useNotificationLayoutEffect.ts @@ -29,7 +29,7 @@ export const useNotificationLayoutEffect = (): useNotificationLayoutEffectInterf }; const closeNotification = (id: string) => { - setNotifications(notifications.filter((notification) => notification.id !== id)); + setNotifications((preNotifications) => preNotifications.filter((notification) => notification.id !== id)); }; const closeAllNotifications = () => { From b946dd894b377bfb652fd2b316b4cc82beff6efa Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Thu, 18 Jan 2024 16:51:21 +0800 Subject: [PATCH 08/14] ADM-747: [frontend] feat: handle export error --- frontend/src/constants/resources.ts | 1 + .../containers/ReportButtonGroup/index.tsx | 19 ++++++++----------- frontend/src/containers/ReportStep/index.tsx | 17 ++++------------- frontend/src/containers/ReportStep/style.tsx | 5 ----- frontend/src/hooks/useExportCsvEffect.ts | 19 ++++++++++--------- 5 files changed, 23 insertions(+), 38 deletions(-) diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 96af9d8168..c35705a5d5 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -194,6 +194,7 @@ export const MESSAGE = { REPORT_LOADING: 'The report is being generated, please do not refresh the page or all the data will be disappeared.', LOADING_TIMEOUT: (name: string) => `${name} loading timeout, please click "Retry"!`, FAILED_TO_GET_DATA: (name: string) => `Failed to get ${name} data, please click "retry"!`, + FAILED_TO_EXPORT_CSV: 'Failed to export csv.', }; export const METRICS_CYCLE_SETTING_TABLE_HEADER = [ diff --git a/frontend/src/containers/ReportButtonGroup/index.tsx b/frontend/src/containers/ReportButtonGroup/index.tsx index 2ecc102182..8e52c1780b 100644 --- a/frontend/src/containers/ReportButtonGroup/index.tsx +++ b/frontend/src/containers/ReportButtonGroup/index.tsx @@ -1,22 +1,23 @@ import { StyledButtonGroup, StyledExportButton, StyledRightButtonGroup } from '@src/containers/ReportButtonGroup/style'; import { BackButton, SaveButton } from '@src/containers/MetricsStepper/style'; -import { ExpiredDialog } from '@src/containers/ReportStep/ExpiredDialog'; +import SaveAltIcon from '@mui/icons-material/SaveAlt'; +import React from 'react'; import { CSVReportRequestDTO } from '@src/clients/report/dto/request'; +import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; +import { ExpiredDialog } from '@src/containers/ReportStep/ExpiredDialog'; import { COMMON_BUTTONS, REPORT_TYPES } from '@src/constants/commons'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; -import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; -import SaveAltIcon from '@mui/icons-material/SaveAlt'; import { TIPS } from '@src/constants/resources'; -import React, { useEffect } from 'react'; import { Tooltip } from '@mui/material'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; interface ReportButtonGroupProps { + notification: useNotificationLayoutEffectInterface; handleSave?: () => void; handleBack: () => void; csvTimeStamp: number; startDate: string; endDate: string; - setErrorMessage: (message: string) => void; reportData: ReportResponseDTO | undefined; isShowSave: boolean; isShowExportBoardButton: boolean; @@ -25,23 +26,19 @@ interface ReportButtonGroupProps { } export const ReportButtonGroup = ({ + notification, handleSave, handleBack, csvTimeStamp, startDate, endDate, - setErrorMessage, reportData, isShowSave, isShowExportMetrics, isShowExportBoardButton, isShowExportPipelineButton, }: ReportButtonGroupProps) => { - const { fetchExportData, errorMessage, isExpired } = useExportCsvEffect(); - - useEffect(() => { - setErrorMessage(errorMessage); - }, [errorMessage]); + const { fetchExportData, isExpired } = useExportCsvEffect(notification); const exportCSV = (dataType: REPORT_TYPES, startDate: string, endDate: string): CSVReportRequestDTO => ({ dataType: dataType, diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index fd43d32e81..c7707ab644 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -2,11 +2,11 @@ import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { useAppSelector } from '@src/hooks'; import { isSelectBoardMetrics, isSelectDoraMetrics, selectConfig } from '@src/context/config/configSlice'; -import { StyledCalendarWrapper, StyledErrorNotification } from '@src/containers/ReportStep/style'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { MESSAGE, REPORT_PAGE_TYPE, REQUIRED_DATA } from '@src/constants/resources'; +import { StyledCalendarWrapper } from '@src/containers/ReportStep/style'; +import { useNavigate } from 'react-router-dom'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; -import { ErrorNotification } from '@src/components/ErrorNotification'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; import { ErrorResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; @@ -15,7 +15,6 @@ import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { Nullable } from '@src/utils/types'; import { BoardDetail, DoraDetail } from './ReportDetail'; -import { useNavigate } from 'react-router-dom'; import { ROUTE } from '@src/constants/router'; export interface ReportStepProps { @@ -56,7 +55,6 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const metrics = configData.basic.metrics; const { addNotification, closeAllNotifications } = notification; - const [errorMessage, setErrorMessage] = useState(); const shouldShowBoardMetrics = useAppSelector(isSelectBoardMetrics); const shouldShowDoraMetrics = useAppSelector(isSelectDoraMetrics); @@ -232,16 +230,12 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { )} - {errorMessage && ( - - - - )} {isSummaryPage ? showSummary() : !!reportData && (pageType === REPORT_PAGE_TYPE.BOARD ? showBoardDetail(reportData) : showDoraDetail(reportData))} { startDate={startDate} endDate={endDate} csvTimeStamp={csvTimeStamp} - setErrorMessage={(message) => { - setErrorMessage(message); - }} /> )} diff --git a/frontend/src/containers/ReportStep/style.tsx b/frontend/src/containers/ReportStep/style.tsx index 915134679e..9186edec3f 100644 --- a/frontend/src/containers/ReportStep/style.tsx +++ b/frontend/src/containers/ReportStep/style.tsx @@ -1,11 +1,6 @@ -import { Z_INDEX } from '@src/constants/commons'; import { styled } from '@mui/material/styles'; import { theme } from '@src/theme'; -export const StyledErrorNotification = styled('div')({ - zIndex: Z_INDEX.MODAL_BACKDROP, -}); - export const StyledSpacing = styled('div')({ height: '1.5rem', }); diff --git a/frontend/src/hooks/useExportCsvEffect.ts b/frontend/src/hooks/useExportCsvEffect.ts index 04bebe48bd..132d67ef06 100644 --- a/frontend/src/hooks/useExportCsvEffect.ts +++ b/frontend/src/hooks/useExportCsvEffect.ts @@ -1,17 +1,18 @@ import { NotFoundException } from '@src/exceptions/NotFoundException'; import { CSVReportRequestDTO } from '@src/clients/report/dto/request'; import { csvClient } from '@src/clients/report/CSVClient'; -import { DURATION } from '@src/constants/commons'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; +import { MESSAGE } from '@src/constants/resources'; import { useState } from 'react'; export interface useExportCsvEffectInterface { fetchExportData: (params: CSVReportRequestDTO) => void; - errorMessage: string; isExpired: boolean; } -export const useExportCsvEffect = (): useExportCsvEffectInterface => { - const [errorMessage, setErrorMessage] = useState(''); +export const useExportCsvEffect = ({ + addNotification, +}: useNotificationLayoutEffectInterface): useExportCsvEffectInterface => { const [isExpired, setIsExpired] = useState(false); const fetchExportData = async (params: CSVReportRequestDTO) => { @@ -23,13 +24,13 @@ export const useExportCsvEffect = (): useExportCsvEffectInterface => { if (err instanceof NotFoundException) { setIsExpired(true); } else { - setErrorMessage(`failed to export csv: ${err.message}`); - setTimeout(() => { - setErrorMessage(''); - }, DURATION.ERROR_MESSAGE_TIME); + addNotification({ + message: MESSAGE.FAILED_TO_EXPORT_CSV, + type: 'error', + }); } } }; - return { fetchExportData, errorMessage, isExpired }; + return { fetchExportData, isExpired }; }; From 4b87a42172e81bc5fbc3c0eef97ebb03ba46a4f5 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Fri, 19 Jan 2024 10:16:23 +0800 Subject: [PATCH 09/14] ADM-747: [frontend] test: add tests for error notifications --- .../MetricsStep/MetricsStep.test.tsx | 10 +- .../containers/ReportButtonGroup.test.tsx | 6 +- .../containers/ReportStep/ReportStep.test.tsx | 175 ++++++++++++++---- .../hooks/useExportCsvEffect.test.tsx | 40 ++-- .../hooks/useGenerateReportEffect.test.tsx | 7 +- 5 files changed, 163 insertions(+), 75 deletions(-) diff --git a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx index 6cc72f68a3..06947934d8 100644 --- a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx @@ -12,13 +12,13 @@ import { CYCLE_TIME_SETTINGS_SECTION, DEPLOYMENT_FREQUENCY_SETTINGS, LIST_OPEN, + MOCK_BUILD_KITE_GET_INFO_RESPONSE, MOCK_JIRA_VERIFY_RESPONSE, + MOCK_PIPELINE_GET_INFO_URL, REAL_DONE, REAL_DONE_SETTING_SECTION, REQUIRED_DATA_LIST, SELECT_CONSIDER_AS_DONE_MESSAGE, - MOCK_PIPELINE_GET_INFO_URL, - MOCK_BUILD_KITE_GET_INFO_RESPONSE, } from '../../fixtures'; import { saveCycleTimeSettings, saveDoneColumn } from '@src/context/Metrics/metricsSlice'; import { updateJiraVerifyResponse, updateMetrics } from '@src/context/config/configSlice'; @@ -93,9 +93,9 @@ describe('MetricsStep', () => { expect(getByText(DEPLOYMENT_FREQUENCY_SETTINGS)).toBeInTheDocument(); }); - it('should call resetProps when resetProps is not undefined', async () => { + it('should call closeAllNotifications', async () => { act(() => { - result.current.resetProps = jest.fn(); + result.current.closeAllNotifications = jest.fn(); }); await waitFor(() => @@ -106,7 +106,7 @@ describe('MetricsStep', () => { ), ); - expect(result.current.resetProps).toBeCalled(); + expect(result.current.closeAllNotifications).toBeCalled(); }); describe('with pre-filled cycle time data', () => { diff --git a/frontend/__tests__/containers/ReportButtonGroup.test.tsx b/frontend/__tests__/containers/ReportButtonGroup.test.tsx index ed708c768d..cc0a93166f 100644 --- a/frontend/__tests__/containers/ReportButtonGroup.test.tsx +++ b/frontend/__tests__/containers/ReportButtonGroup.test.tsx @@ -1,8 +1,10 @@ import { EXPORT_METRIC_DATA, MOCK_REPORT_RESPONSE } from '../fixtures'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; -import { render, screen } from '@testing-library/react'; +import { render, renderHook, screen } from '@testing-library/react'; +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; describe('test', () => { + const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); const mockHandler = jest.fn(); const mockData = { ...MOCK_REPORT_RESPONSE, @@ -25,6 +27,7 @@ describe('test', () => { it('test', () => { render( { startDate={''} endDate={''} csvTimeStamp={1239013} - setErrorMessage={mockHandler} />, ); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index ccd2e2cb2e..4b94a6df1f 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -44,7 +44,6 @@ jest.mock('@src/context/stepper/StepperSlice', () => ({ jest.mock('@src/hooks/useExportCsvEffect', () => ({ useExportCsvEffect: jest.fn().mockReturnValue({ fetchExportData: jest.fn(), - errorMessage: 'failed export csv', isExpired: false, }), })); @@ -229,7 +228,7 @@ describe('Report Step', () => { expect(handleSaveMock).toHaveBeenCalledTimes(1); }); - it('should check error page show when isreportMetricsError is true', () => { + it('should call navigate show when isServerError is true', () => { reportHook.current.isServerError = true; setup([REQUIRED_DATA_LIST[1]]); @@ -237,41 +236,33 @@ describe('Report Step', () => { expect(navigateMock).toHaveBeenCalledWith(ERROR_PAGE_ROUTE); }); - // todo: to be fixed @ru.jiang - // it('should call updateProps when remaining time is less than or equal to 5 minutes', () => { - // const resetProps = jest.fn(); - // const updateProps = jest.fn(); - // notificationHook.current.resetProps = resetProps; - // notificationHook.current.updateProps = updateProps; - // jest.useFakeTimers(); - // - // setup(['']); - // - // expect(updateProps).not.toBeCalledWith({ - // open: true, - // title: MESSAGE.EXPIRE_INFORMATION(5), - // closeAutomatically: true, - // }); - // - // jest.advanceTimersByTime(500000); - // - // expect(updateProps).not.toBeCalledWith({ - // open: true, - // title: MESSAGE.EXPIRE_INFORMATION(5), - // closeAutomatically: true, - // }); - // - // jest.advanceTimersByTime(1000000); - // - // expect(updateProps).toBeCalledWith({ - // open: true, - // title: 'Help Information', - // message: MESSAGE.EXPIRE_INFORMATION(5), - // closeAutomatically: true, - // }); - // - // jest.useRealTimers(); - // }); + it('should call addNotification when remaining time is less than or equal to 5 minutes', () => { + const closeAllNotifications = jest.fn(); + const addNotification = jest.fn(); + notificationHook.current.closeAllNotifications = closeAllNotifications; + notificationHook.current.addNotification = addNotification; + jest.useFakeTimers(); + + setup(['']); + + expect(addNotification).not.toBeCalledWith({ + title: MESSAGE.EXPIRE_INFORMATION(5), + }); + + jest.advanceTimersByTime(500000); + + expect(addNotification).not.toBeCalledWith({ + title: MESSAGE.EXPIRE_INFORMATION(5), + }); + + jest.advanceTimersByTime(1000000); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.EXPIRE_INFORMATION(5), + }); + + jest.useRealTimers(); + }); it.each([[REQUIRED_DATA_LIST[1]], [REQUIRED_DATA_LIST[4]]])( 'should render detail page when clicking show more button given metric %s', @@ -351,7 +342,7 @@ describe('Report Step', () => { ); it('should call fetchExportData when clicking "Export pipeline data"', async () => { - const { result } = renderHook(() => useExportCsvEffect()); + const { result } = renderHook(() => useExportCsvEffect(notificationHook.current)); setup([REQUIRED_DATA_LIST[6]]); const exportButton = screen.getByText(EXPORT_PIPELINE_DATA); @@ -388,7 +379,7 @@ describe('Report Step', () => { ); it('should call fetchExportData when clicking "Export board data"', async () => { - const { result } = renderHook(() => useExportCsvEffect()); + const { result } = renderHook(() => useExportCsvEffect(notificationHook.current)); setup([REQUIRED_DATA_LIST[2]]); const exportButton = screen.getByText(EXPORT_BOARD_DATA); @@ -414,7 +405,7 @@ describe('Report Step', () => { }); it('should call fetchExportData when clicking "Export metric data"', async () => { - const { result } = renderHook(() => useExportCsvEffect()); + const { result } = renderHook(() => useExportCsvEffect(notificationHook.current)); setup(['']); const exportButton = screen.getByText(EXPORT_METRIC_DATA); @@ -435,4 +426,108 @@ describe('Report Step', () => { expect(screen.getByText('Export metric data')).toBeInTheDocument(); }); }); + + describe('error notification', () => { + const addNotification = jest.fn(); + const timeoutError = 'time out error'; + beforeEach(() => { + notificationHook.current.addNotification = addNotification; + }); + + it('should call addNotification when having timeout4Board error', () => { + reportHook.current.timeout4Board = timeoutError; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.LOADING_TIMEOUT('Board metrics'), + type: 'error', + }); + }); + + it('should call addNotification when having timeout4Dora error', () => { + reportHook.current.timeout4Dora = timeoutError; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), + type: 'error', + }); + }); + + it('should call addNotification when having timeout4Report error', () => { + reportHook.current.timeout4Report = timeoutError; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.LOADING_TIMEOUT('Report'), + type: 'error', + }); + }); + + it('should call addNotification when having boardMetricsError', () => { + reportHook.current.reportData = { + ...MOCK_REPORT_RESPONSE, + reportMetricsError: { + boardMetricsError: { + status: 400, + message: 'Board metrics error', + }, + pipelineMetricsError: null, + sourceControlMetricsError: null, + }, + }; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), + type: 'error', + }); + }); + + it('should call addNotification when having pipelineMetricsError', () => { + reportHook.current.reportData = { + ...MOCK_REPORT_RESPONSE, + reportMetricsError: { + boardMetricsError: null, + pipelineMetricsError: { + status: 400, + message: 'Pipeline metrics error', + }, + sourceControlMetricsError: null, + }, + }; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), + type: 'error', + }); + }); + + it('should call addNotification when having sourceControlMetricsError', () => { + reportHook.current.reportData = { + ...MOCK_REPORT_RESPONSE, + reportMetricsError: { + boardMetricsError: null, + pipelineMetricsError: null, + sourceControlMetricsError: { + status: 400, + message: 'source control metrics error', + }, + }, + }; + + setup(REQUIRED_DATA_LIST); + + expect(addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_GET_DATA('Github'), + type: 'error', + }); + }); + }); }); diff --git a/frontend/__tests__/hooks/useExportCsvEffect.test.tsx b/frontend/__tests__/hooks/useExportCsvEffect.test.tsx index 0e4453aade..11c9958ca4 100644 --- a/frontend/__tests__/hooks/useExportCsvEffect.test.tsx +++ b/frontend/__tests__/hooks/useExportCsvEffect.test.tsx @@ -1,49 +1,41 @@ -import { ERROR_MESSAGE_TIME_DURATION, MOCK_EXPORT_CSV_REQUEST_PARAMS } from '../fixtures'; +import { MOCK_EXPORT_CSV_REQUEST_PARAMS } from '../fixtures'; +import { act, renderHook } from '@testing-library/react'; +import { csvClient } from '@src/clients/report/CSVClient'; +import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; import { InternalServerException } from '@src/exceptions/InternalServerException'; import { NotFoundException } from '@src/exceptions/NotFoundException'; -import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; -import { csvClient } from '@src/clients/report/CSVClient'; -import { act, renderHook } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; describe('use export csv effect', () => { + const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); + afterEach(() => { jest.resetAllMocks(); }); - it('should set error message empty when export csv throw error and last for 4 seconds', async () => { - jest.useFakeTimers(); - csvClient.exportCSVData = jest.fn().mockImplementation(() => { - throw new Error('error'); - }); - const { result } = renderHook(() => useExportCsvEffect()); - - act(() => { - result.current.fetchExportData(MOCK_EXPORT_CSV_REQUEST_PARAMS); - jest.advanceTimersByTime(ERROR_MESSAGE_TIME_DURATION); - }); - - expect(result.current.errorMessage).toEqual(''); - }); - - it('should set error message when export csv response status 500', async () => { + it('should call addNotification when export csv response status 500', async () => { csvClient.exportCSVData = jest.fn().mockImplementation(() => { throw new InternalServerException('error message', HttpStatusCode.InternalServerError); }); - const { result } = renderHook(() => useExportCsvEffect()); + notificationHook.current.addNotification = jest.fn(); + const { result } = renderHook(() => useExportCsvEffect(notificationHook.current)); act(() => { result.current.fetchExportData(MOCK_EXPORT_CSV_REQUEST_PARAMS); }); - expect(result.current.errorMessage).toEqual('failed to export csv: error message'); + expect(notificationHook.current.addNotification).toBeCalledWith({ + message: 'Failed to export csv.', + type: 'error', + }); }); - it('should set error message when export csv response status 404', async () => { + it('should set isExpired true when export csv response status 404', async () => { csvClient.exportCSVData = jest.fn().mockImplementation(() => { throw new NotFoundException('error message', HttpStatusCode.NotFound); }); - const { result } = renderHook(() => useExportCsvEffect()); + const { result } = renderHook(() => useExportCsvEffect(notificationHook.current)); act(() => { result.current.fetchExportData(MOCK_EXPORT_CSV_REQUEST_PARAMS); diff --git a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx index 310decb688..0b01fb22f3 100644 --- a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx @@ -10,9 +10,9 @@ import { UnknownException } from '@src/exceptions/UnknownException'; import { act, renderHook, waitFor } from '@testing-library/react'; import { reportClient } from '@src/clients/report/ReportClient'; import { HttpStatusCode } from 'axios'; +import { TimeoutException } from '@src/exceptions/TimeoutException'; import clearAllMocks = jest.clearAllMocks; import resetAllMocks = jest.resetAllMocks; -import { TimeoutException } from '@src/exceptions/TimeoutException'; jest.mock('@src/hooks/reportMapper/report', () => ({ pipelineReportMapper: jest.fn(), @@ -208,7 +208,7 @@ describe('use generate report effect', () => { }); }); - it('should set timeout4Dora and timeout4Board is "Data loading failed" when polling timeout', async () => { + it('should set timeout4Report is "Data loading failed" when polling timeout', async () => { reportClient.polling = jest.fn().mockImplementation(async () => { throw new UnknownException(); }); @@ -221,8 +221,7 @@ describe('use generate report effect', () => { await waitFor(() => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.timeout4Dora).toEqual('Data loading failed'); - expect(result.current.timeout4Board).toEqual('Data loading failed'); + expect(result.current.timeout4Report).toEqual('Data loading failed'); }); }); From bf0e95bfdff6d2424a1cc6a04be62f669c4aa296 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Fri, 19 Jan 2024 15:21:57 +0800 Subject: [PATCH 10/14] ADM-747: [frontend] refactor: refactor report useEffect --- frontend/src/containers/ReportStep/index.tsx | 144 +++++++++---------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index c7707ab644..57386d75ce 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -5,17 +5,16 @@ import { isSelectBoardMetrics, isSelectDoraMetrics, selectConfig } from '@src/co import { MESSAGE, REPORT_PAGE_TYPE, REQUIRED_DATA } from '@src/constants/resources'; import { StyledCalendarWrapper } from '@src/containers/ReportStep/style'; import { useNavigate } from 'react-router-dom'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; +import { Notification, useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; +import { ROUTE } from '@src/constants/router'; +import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; +import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; -import { ErrorResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; -import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; -import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; -import { useAppDispatch } from '@src/hooks/useAppDispatch'; -import { Nullable } from '@src/utils/types'; import { BoardDetail, DoraDetail } from './ReportDetail'; -import { ROUTE } from '@src/constants/router'; +import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import { useAppDispatch } from '@src/hooks/useAppDispatch'; export interface ReportStepProps { notification: useNotificationLayoutEffectInterface; @@ -40,12 +39,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); const [isBackFromDetail, setIsBackFromDetail] = useState(false); const [allMetricsCompleted, setAllMetricsCompleted] = useState(false); - const [boardMetricsError, setBoardMetricsError] = useState>(null); - const [pipelineMetricsError, setPipelineMetricsError] = useState>(null); - const [sourceControlMetricsError, setSourceControlMetricsError] = useState>(null); - const [timeoutError4Board, setTimeoutError4Board] = useState(''); - const [timeoutError4Dora, setTimeoutError4Dora] = useState(''); - const [timeoutError4Report, setTimeoutError4Report] = useState(''); + const [notifications4SummaryPage, setNotifications4SummaryPage] = useState[]>([]); const configData = useAppSelector(selectConfig); const csvTimeStamp = useAppSelector(selectTimeStamp); @@ -63,6 +57,9 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { useEffect(() => { setPageType(onlySelectClassification ? REPORT_PAGE_TYPE.BOARD : REPORT_PAGE_TYPE.SUMMARY); + return () => { + stopPollingReports(); + }; }, []); useLayoutEffect(() => { @@ -105,79 +102,82 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { reportData && setAllMetricsCompleted(reportData.allMetricsCompleted); }, [reportData]); - useLayoutEffect(() => { - return () => { - stopPollingReports(); - }; - }, []); - useEffect(() => { - if (isSummaryPage && reportData) { - setBoardMetricsError(reportData.reportMetricsError.boardMetricsError); - setPipelineMetricsError(reportData.reportMetricsError.pipelineMetricsError); - setSourceControlMetricsError(reportData.reportMetricsError.sourceControlMetricsError); + if (isSummaryPage && notifications4SummaryPage.length > 0) { + const notification = notifications4SummaryPage[0]; + notification && addNotification(notification); + setNotifications4SummaryPage(notifications4SummaryPage.slice(1)); } - }, [reportData, isSummaryPage]); - - useEffect(() => { - boardMetricsError && - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), - type: 'error', - }); - }, [boardMetricsError]); - - useEffect(() => { - pipelineMetricsError && - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), - type: 'error', - }); - }, [pipelineMetricsError]); - - useEffect(() => { - sourceControlMetricsError && - addNotification({ - message: MESSAGE.FAILED_TO_GET_DATA('Github'), - type: 'error', - }); - }, [sourceControlMetricsError]); + }, [notifications4SummaryPage, isSummaryPage]); useEffect(() => { - isSummaryPage && setTimeoutError4Report(timeout4Report); - }, [timeout4Report, isSummaryPage]); + if (reportData?.reportMetricsError.boardMetricsError) { + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), + type: 'error', + }, + ]); + } + }, [reportData?.reportMetricsError.boardMetricsError]); useEffect(() => { - isSummaryPage && setTimeoutError4Board(timeout4Board); - }, [timeout4Board, isSummaryPage]); + if (reportData?.reportMetricsError.pipelineMetricsError) { + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), + type: 'error', + }, + ]); + } + }, [reportData?.reportMetricsError.pipelineMetricsError]); useEffect(() => { - isSummaryPage && setTimeoutError4Dora(timeout4Dora); - }, [timeout4Dora, isSummaryPage]); + if (reportData?.reportMetricsError.sourceControlMetricsError) { + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.FAILED_TO_GET_DATA('Github'), + type: 'error', + }, + ]); + } + }, [reportData?.reportMetricsError.sourceControlMetricsError]); useEffect(() => { - timeoutError4Report && - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('Report'), - type: 'error', - }); - }, [timeoutError4Report]); + timeout4Report && + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.LOADING_TIMEOUT('Report'), + type: 'error', + }, + ]); + }, [timeout4Report]); useEffect(() => { - timeoutError4Board && - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('Board metrics'), - type: 'error', - }); - }, [timeoutError4Board]); + timeout4Board && + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.LOADING_TIMEOUT('Board metrics'), + type: 'error', + }, + ]); + }, [timeout4Board]); useEffect(() => { - timeoutError4Dora && - addNotification({ - message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), - type: 'error', - }); - }, [timeoutError4Dora]); + timeout4Dora && + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), + type: 'error', + }, + ]); + }, [timeout4Dora]); const showSummary = () => ( <> From 8072fea8690610eeb295abc2741147955b1a53fa Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Fri, 19 Jan 2024 15:49:16 +0800 Subject: [PATCH 11/14] ADM-747: [frontend] refactor: refactor TimeoutException --- .../hooks/useGenerateReportEffect.test.tsx | 29 ++----------------- frontend/src/hooks/useGenerateReportEffect.ts | 4 +-- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx index 0b01fb22f3..64548c1da6 100644 --- a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx @@ -6,7 +6,6 @@ import { } from '../fixtures'; import { InternalServerException } from '@src/exceptions/InternalServerException'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; -import { UnknownException } from '@src/exceptions/UnknownException'; import { act, renderHook, waitFor } from '@testing-library/react'; import { reportClient } from '@src/clients/report/ReportClient'; import { HttpStatusCode } from 'axios'; @@ -57,19 +56,8 @@ describe('use generate report effect', () => { }); }); - it('should set isServerError is true when throw TimeoutException', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - it('should set timeout4Board is "Data loading failed" when timeout', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownException()); + reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); const { result } = renderHook(() => useGenerateReportEffect()); @@ -186,19 +174,8 @@ describe('use generate report effect', () => { }); }); - it('should set isServerError is true when throw TimeoutException', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - it('should set timeout4Dora is "Data loading failed" when timeout', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownException()); + reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); const { result } = renderHook(() => useGenerateReportEffect()); @@ -210,7 +187,7 @@ describe('use generate report effect', () => { it('should set timeout4Report is "Data loading failed" when polling timeout', async () => { reportClient.polling = jest.fn().mockImplementation(async () => { - throw new UnknownException(); + throw new TimeoutException('5xx error', 503); }); reportClient.retrieveByUrl = jest diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index a8456aeae4..a9adac7969 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -45,9 +45,9 @@ export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { }; const handleError = (error: Error, source: string) => { - if (error instanceof InternalServerException || error instanceof TimeoutException) { + if (error instanceof InternalServerException) { setIsServerError(true); - } else { + } else if (error instanceof TimeoutException) { if (source === 'Board') { setTimeout4Board(TIMEOUT_PROMPT); } else if (source === 'Dora') { From d436556ec7c703b3e49b4651bdcfc1193d4c3290 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Mon, 22 Jan 2024 10:15:20 +0800 Subject: [PATCH 12/14] ADM-747: [frontend] refactor: delete useless isServerError --- .../containers/ReportStep/ReportStep.test.tsx | 15 +- .../hooks/useGenerateReportEffect.test.tsx | 171 +++++++----------- frontend/src/constants/resources.ts | 2 + frontend/src/containers/ReportStep/index.tsx | 54 +++--- frontend/src/hooks/useGenerateReportEffect.ts | 20 +- 5 files changed, 101 insertions(+), 161 deletions(-) diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 4b94a6df1f..bd943fb70d 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -3,7 +3,6 @@ import { BOARD_METRICS_TITLE, CLASSIFICATION, EMPTY_REPORT_VALUES, - ERROR_PAGE_ROUTE, EXPORT_BOARD_DATA, EXPORT_METRIC_DATA, EXPORT_PIPELINE_DATA, @@ -22,9 +21,9 @@ import { updateMetrics, updatePipelineToolVerifyResponse, } from '@src/context/config/configSlice'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { updateDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { render, renderHook, screen, waitFor } from '@testing-library/react'; import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; import { backStep } from '@src/context/stepper/StepperSlice'; @@ -32,7 +31,6 @@ import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import ReportStep from '@src/containers/ReportStep'; import { MESSAGE } from '@src/constants/resources'; -import { navigateMock } from '../../setupTests'; import { Provider } from 'react-redux'; import React from 'react'; @@ -75,7 +73,7 @@ jest.mock('@src/utils/util', () => ({ let store = null; describe('Report Step', () => { const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); - const { result: reportHook } = renderHook(() => useGenerateReportEffect()); + const { result: reportHook } = renderHook(() => useGenerateReportEffect(notificationHook.current)); beforeEach(() => { resetReportHook(); }); @@ -86,7 +84,6 @@ describe('Report Step', () => { reportHook.current.startToRequestBoardData = jest.fn(); reportHook.current.startToRequestDoraData = jest.fn(); reportHook.current.stopPollingReports = jest.fn(); - reportHook.current.isServerError = false; reportHook.current.reportData = { ...MOCK_REPORT_RESPONSE, exportValidityTime: 30 }; }; const handleSaveMock = jest.fn(); @@ -228,14 +225,6 @@ describe('Report Step', () => { expect(handleSaveMock).toHaveBeenCalledTimes(1); }); - it('should call navigate show when isServerError is true', () => { - reportHook.current.isServerError = true; - - setup([REQUIRED_DATA_LIST[1]]); - - expect(navigateMock).toHaveBeenCalledWith(ERROR_PAGE_ROUTE); - }); - it('should call addNotification when remaining time is less than or equal to 5 minutes', () => { const closeAllNotifications = jest.fn(); const addNotification = jest.fn(); diff --git a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx index 64548c1da6..8144d371e2 100644 --- a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx @@ -1,24 +1,18 @@ -import { - INTERNAL_SERVER_ERROR_MESSAGE, - MOCK_GENERATE_REPORT_REQUEST_PARAMS, - MOCK_REPORT_RESPONSE, - MOCK_RETRIEVE_REPORT_RESPONSE, -} from '../fixtures'; -import { InternalServerException } from '@src/exceptions/InternalServerException'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { reportClient } from '@src/clients/report/ReportClient'; +import { MOCK_GENERATE_REPORT_REQUEST_PARAMS, MOCK_REPORT_RESPONSE, MOCK_RETRIEVE_REPORT_RESPONSE } from '../fixtures'; import { HttpStatusCode } from 'axios'; import { TimeoutException } from '@src/exceptions/TimeoutException'; +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; +import { UnknownException } from '@src/exceptions/UnknownException'; +import { MESSAGE } from '@src/constants/resources'; import clearAllMocks = jest.clearAllMocks; import resetAllMocks = jest.resetAllMocks; -jest.mock('@src/hooks/reportMapper/report', () => ({ - pipelineReportMapper: jest.fn(), - sourceControlReportMapper: jest.fn(), -})); - describe('use generate report effect', () => { + const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); + afterAll(() => { clearAllMocks(); }); @@ -30,36 +24,10 @@ describe('use generate report effect', () => { jest.useRealTimers(); }); - it('should set error message when generate report response status 500', async () => { - reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => { - throw new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE, HttpStatusCode.InternalServerError); - }); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - - it('should set isServerError is true when throw InternalServerException', async () => { - reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => { - throw new InternalServerException('5xx error', 500); - }); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - it('should set timeout4Board is "Data loading failed" when timeout', async () => { reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -67,23 +35,6 @@ describe('use generate report effect', () => { }); }); - it('should return error message when calling startToRequestBoardData given pollingReport response return 5xx ', async () => { - reportClient.polling = jest.fn().mockImplementation(async () => { - throw new InternalServerException('error', HttpStatusCode.InternalServerError); - }); - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(reportClient.polling).toBeCalledTimes(1); - expect(result.current.isServerError).toEqual(true); - }); - }); - it('should call polling report and setTimeout when calling startToRequestBoardData given pollingReport response return 204 ', async () => { reportClient.polling = jest .fn() @@ -92,7 +43,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -114,7 +65,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -136,7 +87,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -150,34 +101,10 @@ describe('use generate report effect', () => { }); }); - it('should set error message when generate report response status 500', async () => { - reportClient.retrieveByUrl = jest - .fn() - .mockRejectedValue(new InternalServerException(INTERNAL_SERVER_ERROR_MESSAGE, HttpStatusCode.NotFound)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - - it('should set isServerError is true when throw InternalServerException', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new InternalServerException('5xx error', 500)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.isServerError).toEqual(true); - }); - }); - - it('should set timeout4Dora is "Data loading failed" when timeout', async () => { + it('should set timeout4Dora is "Data loading failed" when startToRequestDoraData timeout', async () => { reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutException('5xx error', 503)); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -194,7 +121,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -202,24 +129,6 @@ describe('use generate report effect', () => { }); }); - it('should return error message when calling startToRequestDoraData given pollingReport response return 5xx ', async () => { - reportClient.polling = jest.fn().mockImplementation(async () => { - throw new InternalServerException('error', HttpStatusCode.InternalServerError); - }); - - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(reportClient.polling).toBeCalledTimes(1); - expect(result.current.isServerError).toEqual(true); - }); - }); - it('should call polling report and setTimeout when calling startToRequestDoraData given pollingReport response return 204 ', async () => { reportClient.polling = jest .fn() @@ -229,7 +138,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -251,7 +160,7 @@ describe('use generate report effect', () => { .fn() .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); await waitFor(() => { result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); @@ -264,4 +173,52 @@ describe('use generate report effect', () => { expect(reportClient.polling).toHaveBeenCalledTimes(1); }); }); + + it('should call addNotification when startToRequestBoardData given UnknownException', async () => { + reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownException()); + notificationHook.current.addNotification = jest.fn(); + + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); + + await waitFor(() => { + result.current.startToRequestBoardData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); + expect(notificationHook.current.addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_REQUEST, + type: 'error', + }); + }); + }); + + it('should call addNotification when startToRequestDoraData given UnknownException', async () => { + reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownException()); + notificationHook.current.addNotification = jest.fn(); + + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); + + await waitFor(() => { + result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); + expect(notificationHook.current.addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_REQUEST, + type: 'error', + }); + }); + }); + + it('should call addNotification when polling given UnknownException', async () => { + reportClient.polling = jest.fn().mockRejectedValue(new UnknownException()); + reportClient.retrieveByUrl = jest + .fn() + .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); + notificationHook.current.addNotification = jest.fn(); + + const { result } = renderHook(() => useGenerateReportEffect(notificationHook.current)); + + await waitFor(() => { + result.current.startToRequestDoraData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); + expect(notificationHook.current.addNotification).toBeCalledWith({ + message: MESSAGE.FAILED_TO_REQUEST, + type: 'error', + }); + }); + }); }); diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index c35705a5d5..1e9d5717dc 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -195,6 +195,7 @@ export const MESSAGE = { LOADING_TIMEOUT: (name: string) => `${name} loading timeout, please click "Retry"!`, FAILED_TO_GET_DATA: (name: string) => `Failed to get ${name} data, please click "retry"!`, FAILED_TO_EXPORT_CSV: 'Failed to export csv.', + FAILED_TO_REQUEST: 'Failed to request !', }; export const METRICS_CYCLE_SETTING_TABLE_HEADER = [ @@ -222,6 +223,7 @@ export const REPORT_PAGE = { }; export const AXIOS_NETWORK_ERROR_CODES = [AxiosError.ECONNABORTED, AxiosError.ETIMEDOUT, AxiosError.ERR_NETWORK]; + export enum HEARTBEAT_EXCEPTION_CODE { TIMEOUT = 'HB_TIMEOUT', } diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 57386d75ce..c02549d3db 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -4,9 +4,7 @@ import { useAppSelector } from '@src/hooks'; import { isSelectBoardMetrics, isSelectDoraMetrics, selectConfig } from '@src/context/config/configSlice'; import { MESSAGE, REPORT_PAGE_TYPE, REQUIRED_DATA } from '@src/constants/resources'; import { StyledCalendarWrapper } from '@src/containers/ReportStep/style'; -import { useNavigate } from 'react-router-dom'; import { Notification, useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; -import { ROUTE } from '@src/constants/router'; import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; @@ -22,10 +20,8 @@ export interface ReportStepProps { } const ReportStep = ({ notification, handleSave }: ReportStepProps) => { - const navigate = useNavigate(); const dispatch = useAppDispatch(); const { - isServerError, startToRequestBoardData, startToRequestDoraData, reportData, @@ -33,7 +29,7 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { timeout4Board, timeout4Dora, timeout4Report, - } = useGenerateReportEffect(); + } = useGenerateReportEffect(notification); const [exportValidityTimeMin, setExportValidityTimeMin] = useState(undefined); const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); @@ -221,34 +217,28 @@ const ReportStep = ({ notification, handleSave }: ReportStepProps) => { return ( <> - {isServerError ? ( - navigate(ROUTE.ERROR_PAGE) - ) : ( - <> - {startDate && endDate && ( - - - - )} - {isSummaryPage - ? showSummary() - : !!reportData && - (pageType === REPORT_PAGE_TYPE.BOARD ? showBoardDetail(reportData) : showDoraDetail(reportData))} - handleBack()} - handleSave={() => handleSave()} - reportData={reportData} - startDate={startDate} - endDate={endDate} - csvTimeStamp={csvTimeStamp} - /> - + {startDate && endDate && ( + + + )} + {isSummaryPage + ? showSummary() + : !!reportData && + (pageType === REPORT_PAGE_TYPE.BOARD ? showBoardDetail(reportData) : showDoraDetail(reportData))} + handleBack()} + handleSave={() => handleSave()} + reportData={reportData} + startDate={startDate} + endDate={endDate} + csvTimeStamp={csvTimeStamp} + /> ); }; diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index a9adac7969..73503c5a67 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -1,9 +1,9 @@ import { BoardReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto/request'; import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidityTime'; -import { InternalServerException } from '@src/exceptions/InternalServerException'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { TimeoutException } from '@src/exceptions/TimeoutException'; -import { TIMEOUT_PROMPT } from '@src/constants/resources'; +import { MESSAGE, TIMEOUT_PROMPT } from '@src/constants/resources'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { reportClient } from '@src/clients/report/ReportClient'; import { METRIC_TYPES } from '@src/constants/commons'; import { useRef, useState } from 'react'; @@ -12,16 +12,16 @@ export interface useGenerateReportEffectInterface { startToRequestBoardData: (boardParams: BoardReportRequestDTO) => void; startToRequestDoraData: (doraParams: ReportRequestDTO) => void; stopPollingReports: () => void; - isServerError: boolean; timeout4Board: string; timeout4Dora: string; timeout4Report: string; reportData: ReportResponseDTO | undefined; } -export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { +export const useGenerateReportEffect = ({ + addNotification, +}: useNotificationLayoutEffectInterface): useGenerateReportEffectInterface => { const reportPath = '/reports'; - const [isServerError, setIsServerError] = useState(false); const [timeout4Board, setTimeout4Board] = useState(''); const [timeout4Dora, setTimeout4Dora] = useState(''); const [timeout4Report, setTimeout4Report] = useState(''); @@ -45,9 +45,7 @@ export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { }; const handleError = (error: Error, source: string) => { - if (error instanceof InternalServerException) { - setIsServerError(true); - } else if (error instanceof TimeoutException) { + if (error instanceof TimeoutException) { if (source === 'Board') { setTimeout4Board(TIMEOUT_PROMPT); } else if (source === 'Dora') { @@ -55,6 +53,11 @@ export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { } else { setTimeout4Report(TIMEOUT_PROMPT); } + } else { + addNotification({ + message: MESSAGE.FAILED_TO_REQUEST, + type: 'error', + }); } }; @@ -107,7 +110,6 @@ export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { startToRequestDoraData, stopPollingReports, reportData, - isServerError, timeout4Board, timeout4Dora, timeout4Report, From aae7ac61e748f8b75a77f0302a75f9ca766d21b3 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Mon, 22 Jan 2024 11:02:00 +0800 Subject: [PATCH 13/14] ADM-747: [frontend] test: add e2e tests for date picker --- frontend/cypress/e2e/createANewProject.cy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/cypress/e2e/createANewProject.cy.ts b/frontend/cypress/e2e/createANewProject.cy.ts index 7031ff9ed6..d4c5d672f0 100644 --- a/frontend/cypress/e2e/createANewProject.cy.ts +++ b/frontend/cypress/e2e/createANewProject.cy.ts @@ -143,6 +143,7 @@ const checkMetricsCalculation = (testId: string, boardData: MetricsDataItem[]) = const checkBoardShowMore = () => { reportPage.showMoreBoardButton.should('exist'); reportPage.goToBoardDetailPage(); + reportPage.checkDateRange(); cy.get(`[data-test-id="${METRICS_TITLE.VELOCITY}"]`).find('tbody > tr').should('have.length', 2); cy.get(`[data-test-id="${METRICS_TITLE.CYCLE_TIME}"]`).find('tbody > tr').should('have.length', 17); cy.get(`[data-test-id="${METRICS_TITLE.CLASSIFICATION}"]`).find('tbody > tr').should('have.length', 122); @@ -156,6 +157,7 @@ const checkBoardShowMore = () => { const checkDoraShowMore = () => { reportPage.showMoreDoraButton.should('exist'); reportPage.goToDoraDetailPage(); + reportPage.checkDateRange(); cy.get(`[data-test-id="${METRICS_TITLE.DEPLOYMENT_FREQUENCY}"]`).find('tbody > tr').should('have.length', 2); cy.get(`[data-test-id="${METRICS_TITLE.LEAD_TIME_FOR_CHANGES}"]`).find('tbody > tr').should('have.length', 4); From 9be13436f33c2da86ac9bcb170f9bab3ca70c8c3 Mon Sep 17 00:00:00 2001 From: JiangRu1 <3246736839@qq.com> Date: Mon, 22 Jan 2024 13:09:15 +0800 Subject: [PATCH 14/14] ADM-747: [frontend] fix: fix import --- .../containers/ReportButtonGroup.test.tsx | 2 +- .../containers/ReportStep/ReportStep.test.tsx | 2 +- .../__tests__/hooks/useExportCsvEffect.test.tsx | 10 +++++----- .../hooks/useGenerateReportEffect.test.tsx | 10 +++++----- .../Common/NotificationButton/index.tsx | 2 +- .../src/containers/ReportButtonGroup/index.tsx | 10 +++++----- .../containers/ReportStep/BoardMetrics/index.tsx | 1 - .../containers/ReportStep/DoraMetrics/index.tsx | 10 +++++----- frontend/src/containers/ReportStep/index.tsx | 16 ++++++++-------- frontend/src/hooks/useExportCsvEffect.ts | 2 +- frontend/src/hooks/useGenerateReportEffect.ts | 2 +- .../src/hooks/useNotificationLayoutEffect.ts | 4 ++-- 12 files changed, 35 insertions(+), 36 deletions(-) diff --git a/frontend/__tests__/containers/ReportButtonGroup.test.tsx b/frontend/__tests__/containers/ReportButtonGroup.test.tsx index cc0a93166f..6e04581d9e 100644 --- a/frontend/__tests__/containers/ReportButtonGroup.test.tsx +++ b/frontend/__tests__/containers/ReportButtonGroup.test.tsx @@ -1,7 +1,7 @@ +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; import { EXPORT_METRIC_DATA, MOCK_REPORT_RESPONSE } from '../fixtures'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; import { render, renderHook, screen } from '@testing-library/react'; -import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; describe('test', () => { const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index bd943fb70d..4bb5681d58 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -21,9 +21,9 @@ import { updateMetrics, updatePipelineToolVerifyResponse, } from '@src/context/config/configSlice'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { updateDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; import { render, renderHook, screen, waitFor } from '@testing-library/react'; import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; import { backStep } from '@src/context/stepper/StepperSlice'; diff --git a/frontend/__tests__/hooks/useExportCsvEffect.test.tsx b/frontend/__tests__/hooks/useExportCsvEffect.test.tsx index 11c9958ca4..07da8b7aad 100644 --- a/frontend/__tests__/hooks/useExportCsvEffect.test.tsx +++ b/frontend/__tests__/hooks/useExportCsvEffect.test.tsx @@ -1,11 +1,11 @@ -import { MOCK_EXPORT_CSV_REQUEST_PARAMS } from '../fixtures'; -import { act, renderHook } from '@testing-library/react'; -import { csvClient } from '@src/clients/report/CSVClient'; -import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; +import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; import { InternalServerException } from '@src/exceptions/InternalServerException'; import { NotFoundException } from '@src/exceptions/NotFoundException'; +import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; +import { MOCK_EXPORT_CSV_REQUEST_PARAMS } from '../fixtures'; +import { csvClient } from '@src/clients/report/CSVClient'; +import { act, renderHook } from '@testing-library/react'; import { HttpStatusCode } from 'axios'; -import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; describe('use export csv effect', () => { const { result: notificationHook } = renderHook(() => useNotificationLayoutEffect()); diff --git a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx index 8144d371e2..1d984abd85 100644 --- a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx @@ -1,12 +1,12 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; -import { reportClient } from '@src/clients/report/ReportClient'; import { MOCK_GENERATE_REPORT_REQUEST_PARAMS, MOCK_REPORT_RESPONSE, MOCK_RETRIEVE_REPORT_RESPONSE } from '../fixtures'; -import { HttpStatusCode } from 'axios'; -import { TimeoutException } from '@src/exceptions/TimeoutException'; import { useNotificationLayoutEffect } from '@src/hooks/useNotificationLayoutEffect'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; +import { TimeoutException } from '@src/exceptions/TimeoutException'; import { UnknownException } from '@src/exceptions/UnknownException'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { reportClient } from '@src/clients/report/ReportClient'; import { MESSAGE } from '@src/constants/resources'; +import { HttpStatusCode } from 'axios'; import clearAllMocks = jest.clearAllMocks; import resetAllMocks = jest.resetAllMocks; diff --git a/frontend/src/components/Common/NotificationButton/index.tsx b/frontend/src/components/Common/NotificationButton/index.tsx index dee28a2fe8..55bee6c314 100644 --- a/frontend/src/components/Common/NotificationButton/index.tsx +++ b/frontend/src/components/Common/NotificationButton/index.tsx @@ -5,12 +5,12 @@ import { } from '@src/components/Common/NotificationButton/style'; import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { NOTIFICATION_TITLE } from '@src/constants/resources'; import CancelIcon from '@mui/icons-material/Cancel'; import { AlertColor, SvgIcon } from '@mui/material'; import InfoIcon from '@mui/icons-material/Info'; import { theme } from '@src/theme'; import React from 'react'; -import { NOTIFICATION_TITLE } from '@src/constants/resources'; const getStyles = (type: AlertColor | undefined) => { switch (type) { diff --git a/frontend/src/containers/ReportButtonGroup/index.tsx b/frontend/src/containers/ReportButtonGroup/index.tsx index 8e52c1780b..766af4c9eb 100644 --- a/frontend/src/containers/ReportButtonGroup/index.tsx +++ b/frontend/src/containers/ReportButtonGroup/index.tsx @@ -1,15 +1,15 @@ import { StyledButtonGroup, StyledExportButton, StyledRightButtonGroup } from '@src/containers/ReportButtonGroup/style'; +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { BackButton, SaveButton } from '@src/containers/MetricsStepper/style'; -import SaveAltIcon from '@mui/icons-material/SaveAlt'; -import React from 'react'; -import { CSVReportRequestDTO } from '@src/clients/report/dto/request'; -import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; import { ExpiredDialog } from '@src/containers/ReportStep/ExpiredDialog'; +import { CSVReportRequestDTO } from '@src/clients/report/dto/request'; import { COMMON_BUTTONS, REPORT_TYPES } from '@src/constants/commons'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; +import SaveAltIcon from '@mui/icons-material/SaveAlt'; import { TIPS } from '@src/constants/resources'; import { Tooltip } from '@mui/material'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; +import React from 'react'; interface ReportButtonGroupProps { notification: useNotificationLayoutEffectInterface; diff --git a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx index df2c4580da..370f1c75cf 100644 --- a/frontend/src/containers/ReportStep/BoardMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/BoardMetrics/index.tsx @@ -24,7 +24,6 @@ import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { ReportGrid } from '@src/components/Common/ReportGrid'; import { Loading } from '@src/components/Loading'; import { Nullable } from '@src/utils/types'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { useAppSelector } from '@src/hooks'; import React, { useEffect } from 'react'; import dayjs from 'dayjs'; diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index 1a57aeb151..6dd33397a3 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -1,6 +1,3 @@ -import React, { useEffect } from 'react'; -import { useAppSelector } from '@src/hooks'; -import { selectConfig } from '@src/context/config/configSlice'; import { CALENDAR, DORA_METRICS, @@ -11,16 +8,19 @@ import { RETRY, SHOW_MORE, } from '@src/constants/resources'; -import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; import { StyledMetricsSection, StyledShowMore, StyledTitleWrapper } from '@src/containers/ReportStep/DoraMetrics/style'; +import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; +import { formatMillisecondsToHours, formatMinToHours } from '@src/utils/util'; import { ReportTitle } from '@src/components/Common/ReportGrid/ReportTitle'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { ReportRequestDTO } from '@src/clients/report/dto/request'; import { StyledSpacing } from '@src/containers/ReportStep/style'; -import { formatMillisecondsToHours, formatMinToHours } from '@src/utils/util'; import { ReportGrid } from '@src/components/Common/ReportGrid'; +import { selectConfig } from '@src/context/config/configSlice'; import { StyledRetry } from '../BoardMetrics/BoardMetrics'; import { Nullable } from '@src/utils/types'; +import { useAppSelector } from '@src/hooks'; +import React, { useEffect } from 'react'; import dayjs from 'dayjs'; import _ from 'lodash'; diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index c02549d3db..ece42588f6 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -1,18 +1,18 @@ -import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; -import { useAppSelector } from '@src/hooks'; +import { Notification, useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { isSelectBoardMetrics, isSelectDoraMetrics, selectConfig } from '@src/context/config/configSlice'; import { MESSAGE, REPORT_PAGE_TYPE, REQUIRED_DATA } from '@src/constants/resources'; -import { StyledCalendarWrapper } from '@src/containers/ReportStep/style'; -import { Notification, useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; -import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; -import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice'; +import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; +import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { StyledCalendarWrapper } from '@src/containers/ReportStep/style'; import { ReportButtonGroup } from '@src/containers/ReportButtonGroup'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; -import { BoardDetail, DoraDetail } from './ReportDetail'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; +import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; +import { BoardDetail, DoraDetail } from './ReportDetail'; +import { useAppSelector } from '@src/hooks'; export interface ReportStepProps { notification: useNotificationLayoutEffectInterface; diff --git a/frontend/src/hooks/useExportCsvEffect.ts b/frontend/src/hooks/useExportCsvEffect.ts index 132d67ef06..e1f265b432 100644 --- a/frontend/src/hooks/useExportCsvEffect.ts +++ b/frontend/src/hooks/useExportCsvEffect.ts @@ -1,7 +1,7 @@ +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { NotFoundException } from '@src/exceptions/NotFoundException'; import { CSVReportRequestDTO } from '@src/clients/report/dto/request'; import { csvClient } from '@src/clients/report/CSVClient'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { MESSAGE } from '@src/constants/resources'; import { useState } from 'react'; diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index 73503c5a67..334768b25a 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -1,9 +1,9 @@ +import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { BoardReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto/request'; import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidityTime'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; import { TimeoutException } from '@src/exceptions/TimeoutException'; import { MESSAGE, TIMEOUT_PROMPT } from '@src/constants/resources'; -import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect'; import { reportClient } from '@src/clients/report/ReportClient'; import { METRIC_TYPES } from '@src/constants/commons'; import { useRef, useState } from 'react'; diff --git a/frontend/src/hooks/useNotificationLayoutEffect.ts b/frontend/src/hooks/useNotificationLayoutEffect.ts index bb9ecd1cc9..0e37e059e9 100644 --- a/frontend/src/hooks/useNotificationLayoutEffect.ts +++ b/frontend/src/hooks/useNotificationLayoutEffect.ts @@ -1,7 +1,7 @@ -import { useState } from 'react'; -import { AlertColor } from '@mui/material'; import { DURATION } from '@src/constants/commons'; +import { AlertColor } from '@mui/material'; import { uniqueId } from 'lodash'; +import { useState } from 'react'; export interface Notification { id: string;