diff --git a/frontend/__tests__/client/ReportClient.test.ts b/frontend/__tests__/client/ReportClient.test.ts index 41066ff8dc..d75f5d0032 100644 --- a/frontend/__tests__/client/ReportClient.test.ts +++ b/frontend/__tests__/client/ReportClient.test.ts @@ -21,9 +21,8 @@ describe('report client', () => { afterAll(() => server.close()); it('should get response when generate report request status 202', async () => { - const excepted = { - response: MOCK_RETRIEVE_REPORT_RESPONSE, - }; + const excepted = MOCK_RETRIEVE_REPORT_RESPONSE; + server.use( rest.post(MOCK_REPORT_URL, (req, res, ctx) => res(ctx.status(HttpStatusCode.Accepted), ctx.json(MOCK_RETRIEVE_REPORT_RESPONSE)), diff --git a/frontend/__tests__/components/Common/DateRangeViewer/DateRangeViewer.test.tsx b/frontend/__tests__/components/Common/DateRangeViewer/DateRangeViewer.test.tsx index 9a3c22332f..879e0abe61 100644 --- a/frontend/__tests__/components/Common/DateRangeViewer/DateRangeViewer.test.tsx +++ b/frontend/__tests__/components/Common/DateRangeViewer/DateRangeViewer.test.tsx @@ -5,7 +5,7 @@ import { render } from '@testing-library/react'; describe('DateRangeViewer', () => { const setup = (dateRanges: DateRange) => { - return render(); + return render(); }; const mockDateRanges = [ { diff --git a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx index 1588c7214f..91970318a7 100644 --- a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx +++ b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx @@ -15,6 +15,7 @@ import { REQUIRED_DATA, MOCK_PIPELINE_VERIFY_URL, MOCK_BOARD_URL_FOR_JIRA, + MOCK_REPORT_RESPONSE, } from '../../fixtures'; import { updateCycleTimeSettings, @@ -24,9 +25,9 @@ import { updateDeploymentFrequencySettings, updateTreatFlagCardAsBlock, } from '@src/context/Metrics/metricsSlice'; +import { ASSIGNEE_FILTER_TYPES, DEFAULT_MESSAGE } from '@src/constants/resources'; +import { updateDateRange, updateMetrics } from '@src/context/config/configSlice'; import { act, render, screen, waitFor, within } from '@testing-library/react'; -import { ASSIGNEE_FILTER_TYPES } from '@src/constants/resources'; -import { updateMetrics } from '@src/context/config/configSlice'; import MetricsStepper from '@src/containers/MetricsStepper'; import { setupStore } from '../../utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; @@ -56,6 +57,11 @@ const mockValidationCheckContext = { getDuplicatedPipeLineIds: jest.fn().mockReturnValue([]), }; +const mockDateRange = { + startDate: '2024-02-18T00:00:00.000+08:00', + endDate: '2024-02-28T23:59:59.999+08:00', +}; + jest.mock('@src/hooks/useMetricsStepValidationCheckContext', () => ({ useMetricsStepValidationCheckContext: () => mockValidationCheckContext, })); @@ -88,12 +94,29 @@ jest.mock('@src/utils/util', () => ({ })); jest.mock('@src/hooks/useGenerateReportEffect', () => ({ + ...jest.requireActual('@src/hooks/useGenerateReportEffect'), useGenerateReportEffect: jest.fn().mockReturnValue({ startToRequestData: jest.fn(), - startToRequestDoraData: jest.fn(), stopPollingReports: jest.fn(), - isServerError: false, - errorMessage: '', + closeReportInfosErrorStatus: jest.fn(), + closeBoardMetricsError: jest.fn(), + closePipelineMetricsError: jest.fn(), + closeSourceControlMetricsError: jest.fn(), + reportInfos: [ + { + id: mockDateRange.startDate, + timeout4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + shouldShowBoardMetricsError: true, + shouldShowPipelineMetricsError: true, + shouldShowSourceControlMetricsError: true, + reportData: { ...MOCK_REPORT_RESPONSE, exportValidityTime: 30 }, + }, + ], }), })); @@ -160,6 +183,7 @@ const fillMetricsPageDate = async () => { updateDeploymentFrequencySettings({ updateId: 0, label: 'pipelineName', value: 'mock new pipelineName' }), ); store.dispatch(updateDeploymentFrequencySettings({ updateId: 0, label: 'step', value: 'mock new step' })); + store.dispatch(updateDateRange([mockDateRange])); }); }; @@ -396,8 +420,8 @@ describe('MetricsStepper', () => { calendarType: 'Regular Calendar(Weekend Considered)', dateRange: [ { - endDate: dayjs().endOf('date').add(0, 'day').format(COMMON_TIME_FORMAT), - startDate: dayjs().startOf('date').format(COMMON_TIME_FORMAT), + endDate: mockDateRange.endDate, + startDate: mockDateRange.startDate, }, ], metrics: ['Velocity'], diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 90033308b7..69373d4042 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -7,7 +7,6 @@ import { EXPORT_METRIC_DATA, EXPORT_PIPELINE_DATA, LEAD_TIME_FOR_CHANGES, - MOCK_DATE_RANGE, MOCK_JIRA_VERIFY_RESPONSE, MOCK_REPORT_RESPONSE, PREVIOUS, @@ -24,21 +23,23 @@ import { updatePipelineToolVerifyResponse, } from '@src/context/config/configSlice'; import { addADeploymentFrequencySetting, updateDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; +import { act, render, renderHook, screen, waitFor } from '@testing-library/react'; +import { closeNotification } from '@src/context/notification/NotificationSlice'; import { addNotification } from '@src/context/notification/NotificationSlice'; import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; -import { render, renderHook, screen, waitFor } from '@testing-library/react'; +import { DEFAULT_MESSAGE, MESSAGE } from '@src/constants/resources'; import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect'; import { backStep } from '@src/context/stepper/StepperSlice'; 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 { Provider } from 'react-redux'; -import React from 'react'; +import { ReactNode } from 'react'; jest.mock('@src/context/notification/NotificationSlice', () => ({ ...jest.requireActual('@src/context/notification/NotificationSlice'), addNotification: jest.fn().mockReturnValue({ type: 'ADD_NOTIFICATION' }), + closeNotification: jest.fn(), })); jest.mock('@src/context/stepper/StepperSlice', () => ({ @@ -54,12 +55,14 @@ jest.mock('@src/hooks/useExportCsvEffect', () => ({ })); jest.mock('@src/hooks/useGenerateReportEffect', () => ({ + ...jest.requireActual('@src/hooks/useGenerateReportEffect'), useGenerateReportEffect: jest.fn().mockReturnValue({ startToRequestData: jest.fn(), - startToRequestDoraData: jest.fn(), stopPollingReports: jest.fn(), - isServerError: false, - errorMessage: '', + closeReportInfosErrorStatus: jest.fn(), + closeBoardMetricsError: jest.fn(), + closePipelineMetricsError: jest.fn(), + closeSourceControlMetricsError: jest.fn(), }), })); @@ -77,20 +80,60 @@ jest.mock('@src/utils/util', () => ({ formatMillisecondsToHours: jest.fn().mockImplementation((time) => time / 60 / 60 / 1000), })); -let store = null; +let store = setupStore(); + +const emptyValueDateRange = { + startDate: '2024-02-04T00:00:00.000+08:00', + endDate: '2024-02-17T23:59:59.999+08:00', +}; + +const fullValueDateRange = { + startDate: '2024-02-18T00:00:00.000+08:00', + endDate: '2024-02-28T23:59:59.999+08:00', +}; + describe('Report Step', () => { - const { result: reportHook } = renderHook(() => useGenerateReportEffect()); + const { result: reportHook } = renderHook(() => useGenerateReportEffect(), { + wrapper: ({ children }: { children: ReactNode }) => { + return {children}; + }, + }); beforeEach(() => { + store = setupStore(); resetReportHook(); }); const resetReportHook = async () => { - reportHook.current.startToRequestData = jest.fn(); - reportHook.current.stopPollingReports = jest.fn(); - reportHook.current.reportData = { ...MOCK_REPORT_RESPONSE, exportValidityTime: 30 }; + reportHook.current.reportInfos = [ + { + id: fullValueDateRange.startDate, + timeout4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + shouldShowBoardMetricsError: true, + shouldShowPipelineMetricsError: true, + shouldShowSourceControlMetricsError: true, + reportData: { ...MOCK_REPORT_RESPONSE, exportValidityTime: 30 }, + }, + { + id: emptyValueDateRange.startDate, + timeout4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + shouldShowBoardMetricsError: true, + shouldShowPipelineMetricsError: true, + shouldShowSourceControlMetricsError: true, + reportData: { ...EMPTY_REPORT_VALUES }, + }, + ]; }; const handleSaveMock = jest.fn(); - const setup = (params: string[], dateRange?: DateRange) => { - store = setupStore(); + const setup = (params: string[], dateRange: DateRange = [fullValueDateRange]) => { dateRange && store.dispatch(updateDateRange(dateRange)); store.dispatch( updateJiraVerifyResponse({ @@ -129,13 +172,12 @@ describe('Report Step', () => { ); }; afterEach(() => { - store = null; jest.clearAllMocks(); }); describe('render correctly', () => { it('should render report page', () => { - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); expect(screen.getByText('Board Metrics')).toBeInTheDocument(); expect(screen.getByText('Velocity')).toBeInTheDocument(); @@ -148,9 +190,7 @@ describe('Report Step', () => { }); it('should render loading page when report data is empty', () => { - reportHook.current.reportData = EMPTY_REPORT_VALUES; - - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); expect(screen.getAllByTestId('loading-page')).toHaveLength(7); }); @@ -169,7 +209,6 @@ describe('Report Step', () => { it('should render the velocity component with correct props', async () => { setup([REQUIRED_DATA_LIST[1]]); - expect(screen.getByText('20')).toBeInTheDocument(); expect(screen.getByText('14')).toBeInTheDocument(); }); @@ -255,7 +294,7 @@ describe('Report Step', () => { it.each([[REQUIRED_DATA_LIST[2]], [REQUIRED_DATA_LIST[5]]])( 'should render detail page when clicking show more button given metric %s', async (requiredData) => { - setup([requiredData], MOCK_DATE_RANGE); + setup([requiredData]); await userEvent.click(screen.getByText(SHOW_MORE)); @@ -340,8 +379,8 @@ describe('Report Step', () => { expect(result.current.fetchExportData).toBeCalledWith({ csvTimeStamp: 0, dataType: 'pipeline', - endDate: '', - startDate: '', + endDate: '2024-02-28T23:59:59.999+08:00', + startDate: '2024-02-18T00:00:00.000+08:00', }); }); }); @@ -377,8 +416,8 @@ describe('Report Step', () => { expect(result.current.fetchExportData).toBeCalledWith({ csvTimeStamp: 0, dataType: 'board', - endDate: '', - startDate: '', + endDate: '2024-02-28T23:59:59.999+08:00', + startDate: '2024-02-18T00:00:00.000+08:00', }); }); }); @@ -403,8 +442,8 @@ describe('Report Step', () => { expect(result.current.fetchExportData).toBeCalledWith({ csvTimeStamp: 0, dataType: 'metric', - endDate: '', - startDate: '', + endDate: '2024-02-28T23:59:59.999+08:00', + startDate: '2024-02-18T00:00:00.000+08:00', }); }); @@ -419,40 +458,47 @@ describe('Report Step', () => { const error = 'error'; it('should call addNotification when having timeout4Board error', () => { - reportHook.current.timeout4Board = error; + reportHook.current.reportInfos = reportHook.current.reportInfos.slice(1); + reportHook.current.reportInfos[0].timeout4Board = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); + expect(addNotification).toHaveBeenCalledTimes(1); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.LOADING_TIMEOUT('Board metrics'), type: 'error', }); }); it('should call addNotification when having timeout4Dora error', () => { - reportHook.current.timeout4Dora = error; + reportHook.current.reportInfos = reportHook.current.reportInfos.slice(1); + reportHook.current.reportInfos[0].timeout4Dora = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), type: 'error', }); }); it('should call addNotification when having timeout4Report error', () => { - reportHook.current.timeout4Report = error; + reportHook.current.reportInfos = reportHook.current.reportInfos.slice(1); + reportHook.current.reportInfos[0].timeout4Report = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.LOADING_TIMEOUT('Report'), type: 'error', }); }); it('should call addNotification when having boardMetricsError', () => { - reportHook.current.reportData = { + reportHook.current.reportInfos[0].reportData = { ...MOCK_REPORT_RESPONSE, reportMetricsError: { boardMetricsError: { @@ -466,14 +512,15 @@ describe('Report Step', () => { setup(REQUIRED_DATA_LIST); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), type: 'error', }); }); it('should call addNotification when having pipelineMetricsError', () => { - reportHook.current.reportData = { + reportHook.current.reportInfos[0].reportData = { ...MOCK_REPORT_RESPONSE, reportMetricsError: { boardMetricsError: null, @@ -486,15 +533,16 @@ describe('Report Step', () => { }; setup(REQUIRED_DATA_LIST); - - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledTimes(2); + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), type: 'error', }); }); it('should call addNotification when having sourceControlMetricsError', () => { - reportHook.current.reportData = { + reportHook.current.reportInfos[0].reportData = { ...MOCK_REPORT_RESPONSE, reportMetricsError: { boardMetricsError: null, @@ -508,48 +556,52 @@ describe('Report Step', () => { setup(REQUIRED_DATA_LIST); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_GET_DATA('GitHub'), type: 'error', }); }); it('should call addNotification when having generalError4Board error', () => { - reportHook.current.generalError4Board = error; + reportHook.current.reportInfos[1].generalError4Board = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_REQUEST, type: 'error', }); }); it('should call addNotification when having generalError4Dora error', () => { - reportHook.current.generalError4Dora = error; + reportHook.current.reportInfos[1].generalError4Dora = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_REQUEST, type: 'error', }); }); it('should call addNotification when having generalError4Report error', () => { - reportHook.current.generalError4Report = error; + reportHook.current.reportInfos[1].generalError4Report = { message: error, shouldShow: true }; - setup(REQUIRED_DATA_LIST); + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); - expect(addNotification).toBeCalledWith({ + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), message: MESSAGE.FAILED_TO_REQUEST, type: 'error', }); }); it('should retry startToRequestData when click the retry button in Board Metrics', async () => { - reportHook.current.generalError4Report = error; - setup(REQUIRED_DATA_LIST); + reportHook.current.reportInfos[1].generalError4Report = { message: error, shouldShow: true }; + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); await userEvent.click(screen.getAllByText(RETRY)[0]); @@ -559,8 +611,8 @@ describe('Report Step', () => { }); it('should retry startToRequestData when click the retry button in Dora Metrics', async () => { - reportHook.current.generalError4Report = error; - setup(REQUIRED_DATA_LIST); + reportHook.current.reportInfos[1].generalError4Report = { message: error, shouldShow: true }; + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); await userEvent.click(screen.getAllByText(RETRY)[1]); @@ -568,5 +620,69 @@ describe('Report Step', () => { expect(useGenerateReportEffect().startToRequestData).toHaveBeenCalledTimes(2); }); }); + + it('should not show notification when sending request', async () => { + reportHook.current.hasPollingStarted = true; + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); + + expect(addNotification).toHaveBeenCalledTimes(0); + }); + + it('should not show notification given the requests all failed', () => { + reportHook.current.hasPollingStarted = false; + reportHook.current.reportInfos[0].reportData = undefined; + reportHook.current.reportInfos[1].reportData = undefined; + setup(REQUIRED_DATA_LIST, [fullValueDateRange]); + expect(addNotification).toHaveBeenCalledTimes(0); + }); + + it('should show "file will expire ..." notification given the request is successful', () => { + reportHook.current.hasPollingStarted = false; + setup(REQUIRED_DATA_LIST, [fullValueDateRange]); + expect(addNotification).toHaveBeenCalledWith({ + message: MESSAGE.EXPIRE_INFORMATION(30), + }); + }); + + it('should not show notifications given shown once', () => { + reportHook.current.reportInfos = reportHook.current.reportInfos.slice(1); + reportHook.current.reportInfos[0].generalError4Report = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].generalError4Dora = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].generalError4Board = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].timeout4Dora = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].timeout4Board = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].timeout4Report = { shouldShow: false, message: 'error' }; + reportHook.current.reportInfos[0].reportData!.reportMetricsError = { + boardMetricsError: { status: 400, message: 'error' }, + pipelineMetricsError: { status: 400, message: 'error' }, + sourceControlMetricsError: { status: 400, message: 'error' }, + }; + reportHook.current.reportInfos[0].shouldShowBoardMetricsError = false; + reportHook.current.reportInfos[0].shouldShowPipelineMetricsError = false; + reportHook.current.reportInfos[0].shouldShowSourceControlMetricsError = false; + setup(REQUIRED_DATA_LIST, [emptyValueDateRange]); + expect(addNotification).toHaveBeenCalledTimes(0); + }); + + it('should close error notification when change dateRange', async () => { + reportHook.current.reportInfos[1].timeout4Board = { shouldShow: true, message: 'error' }; + const { getByTestId, getByText } = setup(REQUIRED_DATA_LIST, [fullValueDateRange, emptyValueDateRange]); + const expandMoreIcon = getByTestId('ExpandMoreIcon'); + await act(async () => { + await userEvent.click(expandMoreIcon); + }); + const secondDateRange = await getByText(/2024\/02\/04/); + + await userEvent.click(secondDateRange); + await userEvent.click(expandMoreIcon); + const firstDateRange = screen.getByText(/2024\/02\/18/); + await userEvent.click(firstDateRange); + expect(addNotification).toHaveBeenCalledWith({ + id: expect.any(String), + message: MESSAGE.LOADING_TIMEOUT('Board metrics'), + type: 'error', + }); + expect(closeNotification).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts index 47d570ae23..aedd61ce43 100644 --- a/frontend/__tests__/fixtures.ts +++ b/frontend/__tests__/fixtures.ts @@ -288,10 +288,14 @@ export const MOCK_IMPORT_FILE = { metrics: [], }; -export const MOCK_DATE_RANGE = [ +export const MockedDateRanges = [ { - startDate: '2023-04-04T00:00:00+08:00', - endDate: '2023-04-18T00:00:00+08:00', + startDate: '2024-02-04T00:00:00.000+08:00', + endDate: '2024-02-17T23:59:59.999+08:00', + }, + { + startDate: '2024-02-18T00:00:00.000+08:00', + endDate: '2024-02-28T23:59:59.999+08:00', }, ]; diff --git a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx index cf7411582c..59474be55c 100644 --- a/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx +++ b/frontend/__tests__/hooks/useGenerateReportEffect.test.tsx @@ -1,13 +1,30 @@ -import { MOCK_GENERATE_REPORT_REQUEST_PARAMS, MOCK_REPORT_RESPONSE, MOCK_RETRIEVE_REPORT_RESPONSE } from '../fixtures'; -import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect'; +import { + GeneralErrorKey, + IReportError, + IReportInfo, + useGenerateReportEffect, + IUseGenerateReportEffect, + TimeoutErrorKey, +} from '@src/hooks/useGenerateReportEffect'; +import { + MOCK_GENERATE_REPORT_REQUEST_PARAMS, + MOCK_REPORT_RESPONSE, + MOCK_RETRIEVE_REPORT_RESPONSE, + MockedDateRanges, +} from '../fixtures'; +import { AXIOS_REQUEST_ERROR_CODE } from '@src/constants/resources'; +import { updateDateRange } from '@src/context/config/configSlice'; import { act, renderHook, waitFor } from '@testing-library/react'; import { reportClient } from '@src/clients/report/ReportClient'; +import { setupStore } from '@test/utils/setupStoreUtil'; import { TimeoutError } from '@src/errors/TimeoutError'; import { UnknownError } from '@src/errors/UnknownError'; +import { METRIC_TYPES } from '@src/constants/commons'; +import React, { ReactNode } from 'react'; +import { Provider } from 'react-redux'; import { HttpStatusCode } from 'axios'; import clearAllMocks = jest.clearAllMocks; import resetAllMocks = jest.resetAllMocks; -import { METRIC_TYPES } from '@src/constants/commons'; const MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE = { ...MOCK_GENERATE_REPORT_REQUEST_PARAMS, @@ -18,11 +35,24 @@ const MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE = { metricTypes: [METRIC_TYPES.DORA], }; +let store = setupStore(); + +const Wrapper = ({ children }: { children: ReactNode }) => { + return {children}; +}; + +const setup = () => + renderHook(() => useGenerateReportEffect(), { + wrapper: Wrapper, + }); + describe('use generate report effect', () => { afterAll(() => { clearAllMocks(); }); beforeEach(() => { + store = setupStore(); + store.dispatch(updateDateRange(MockedDateRanges)); jest.useFakeTimers(); }); afterEach(() => { @@ -30,200 +60,215 @@ describe('use generate report effect', () => { jest.useRealTimers(); }); - it('should set "Data loading failed" for board metrics when board data retrieval times out', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutError('5xx error', 503)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); - expect(result.current.timeout4Board).toEqual('Data loading failed'); - }); - }); - - it('should call polling report and setTimeout when request board data given pollingReport response return 204', async () => { - reportClient.polling = jest - .fn() - .mockImplementation(async () => ({ status: HttpStatusCode.NoContent, response: MOCK_REPORT_RESPONSE })); + it('should set "Data loading failed" for all board metrics when board data retrieval times out', async () => { reportClient.retrieveByUrl = jest .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); + .mockRejectedValue(new TimeoutError('timeout error', AXIOS_REQUEST_ERROR_CODE.TIMEOUT)); + reportClient.polling = jest.fn(); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); + await act(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); }); - jest.runOnlyPendingTimers(); - - await waitFor(() => { - expect(reportClient.polling).toHaveBeenCalledTimes(1); - }); + expect(result.current.reportInfos.length).toEqual(2); + expect(result.current.reportInfos[0].timeout4Board.message).toEqual('Data loading failed'); + expect(result.current.reportInfos[0].timeout4Board.shouldShow).toEqual(true); + expect(result.current.reportInfos[0].reportData).toEqual(undefined); + expect(result.current.reportInfos[1].timeout4Board.message).toEqual('Data loading failed'); + expect(result.current.reportInfos[1].timeout4Board.shouldShow).toEqual(true); + expect(result.current.reportInfos[1].reportData).toEqual(undefined); + expect(reportClient.polling).toHaveBeenCalledTimes(0); }); - it('should call polling report more than one time when metrics is loading', async () => { - reportClient.polling = jest.fn().mockImplementation(async () => ({ - status: HttpStatusCode.NoContent, - response: { ...MOCK_REPORT_RESPONSE, allMetricsCompleted: false }, - })); + it('should set "Data loading failed" for dora metrics when dora data retrieval times out', async () => { reportClient.retrieveByUrl = jest .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); + .mockRejectedValueOnce(new TimeoutError('timeout error', AXIOS_REQUEST_ERROR_CODE.TIMEOUT)) + .mockResolvedValueOnce(async () => MOCK_RETRIEVE_REPORT_RESPONSE); - const { result } = renderHook(() => useGenerateReportEffect()); + reportClient.polling = jest + .fn() + .mockImplementation(async () => ({ status: HttpStatusCode.Ok, response: MOCK_REPORT_RESPONSE })); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - }); - act(() => { - jest.advanceTimersByTime(10000); + await act(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); }); - await waitFor(() => { - expect(reportClient.polling).toHaveBeenCalledTimes(2); - }); + expect(result.current.reportInfos[0].timeout4Dora.message).toEqual('Data loading failed'); + expect(result.current.reportInfos[0].timeout4Dora.shouldShow).toEqual(true); + expect(result.current.reportInfos[0].reportData).toEqual(undefined); + expect(result.current.reportInfos[1].timeout4Dora.message).toEqual(''); + expect(result.current.reportInfos[1].reportData).toBeTruthy(); }); - it('should call polling report only once when request board data given dora data retrieval is called before', async () => { + it('should call polling report and setTimeout when request board data given pollingReport response return 200', async () => { reportClient.polling = jest .fn() - .mockImplementation(async () => ({ status: HttpStatusCode.NoContent, response: MOCK_REPORT_RESPONSE })); - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); + .mockImplementation(async () => ({ status: HttpStatusCode.Ok, response: MOCK_REPORT_RESPONSE })); + reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => MOCK_RETRIEVE_REPORT_RESPONSE); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = setup(); await waitFor(() => { result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); }); jest.runOnlyPendingTimers(); await waitFor(() => { - expect(reportClient.polling).toHaveBeenCalledTimes(1); - }); - }); - - it('should set "Data loading failed" for dora metrics when dora data retrieval times out', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutError('5xx error', 503)); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); - expect(result.current.timeout4Dora).toEqual('Data loading failed'); - }); - }); - - it('should set "Data loading failed" for report when polling times out', async () => { - reportClient.polling = jest.fn().mockImplementation(async () => { - throw new TimeoutError('5xx error', 503); - }); - - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - - const { result } = renderHook(() => useGenerateReportEffect()); - - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.timeout4Report).toEqual('Data loading failed'); + expect(reportClient.polling).toHaveBeenCalledTimes(2); }); }); - it('should call polling report and setTimeout when request dora data given pollingReport response return 204', async () => { + it('should call polling report more than one time when metrics is loading', async () => { reportClient.polling = jest .fn() - .mockImplementation(async () => ({ status: HttpStatusCode.NoContent, response: MOCK_REPORT_RESPONSE })); - + .mockReturnValueOnce({ + status: HttpStatusCode.Ok, + response: { ...MOCK_REPORT_RESPONSE, allMetricsCompleted: false }, + }) + .mockRejectedValue(new TimeoutError('timeout error', AXIOS_REQUEST_ERROR_CODE.TIMEOUT)) + .mockReturnValueOnce({ + status: HttpStatusCode.Ok, + response: { ...MOCK_REPORT_RESPONSE, allMetricsCompleted: true }, + }); reportClient.retrieveByUrl = jest .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); + .mockReturnValueOnce(MOCK_RETRIEVE_REPORT_RESPONSE) + .mockReturnValueOnce({ ...MOCK_RETRIEVE_REPORT_RESPONSE, callbackUrl: '/url/1234' }); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); + await act(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); + jest.advanceTimersByTime(10000); }); - jest.runOnlyPendingTimers(); - - await waitFor(() => { - expect(reportClient.polling).toHaveBeenCalledTimes(1); - }); + expect(reportClient.polling).toHaveBeenCalledTimes(3); + expect(result.current.reportInfos[0][TimeoutErrorKey[METRIC_TYPES.ALL] as keyof IReportError].message).toEqual( + 'Data loading failed', + ); + expect(result.current.reportInfos[0][TimeoutErrorKey[METRIC_TYPES.ALL] as keyof IReportError].shouldShow).toEqual( + true, + ); }); - it('should call polling report only once when request dora data given board data retrieval is called before', async () => { + it('should call polling report only once when request board data given dora data retrieval is called before', async () => { reportClient.polling = jest .fn() - .mockImplementation(async () => ({ status: HttpStatusCode.NoContent, response: MOCK_REPORT_RESPONSE })); + .mockImplementation(async () => ({ status: HttpStatusCode.Ok, response: MOCK_REPORT_RESPONSE })); + reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => MOCK_RETRIEVE_REPORT_RESPONSE); - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); + await waitFor(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); }); jest.runOnlyPendingTimers(); - await waitFor(() => { - expect(reportClient.polling).toHaveBeenCalledTimes(1); - }); + expect(reportClient.polling).toHaveBeenCalledTimes(2); }); - it('should set "Data loading failed" for board metric when request board data given UnknownException', async () => { + it.each([ + { + params: MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE, + errorKey: GeneralErrorKey[METRIC_TYPES.BOARD], + }, + { + params: MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE, + errorKey: GeneralErrorKey[METRIC_TYPES.DORA], + }, + { + params: MOCK_GENERATE_REPORT_REQUEST_PARAMS, + errorKey: GeneralErrorKey[METRIC_TYPES.ALL], + }, + ])('should set "Data loading failed" for board metric when request given UnknownException', async (_) => { reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownError()); - const { result } = renderHook(() => useGenerateReportEffect()); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); - expect(result.current.generalError4Board).toEqual('Data loading failed'); + await act(async () => { + await result.current.startToRequestData(_.params); }); - }); + const errorKey = _.errorKey as keyof IReportError; - it('should set "Data loading failed" for dora metric when request dora data given UnknownException', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new UnknownError()); + expect(result.current.reportInfos[0][errorKey].message).toEqual('Data loading failed'); + expect(result.current.reportInfos[0][errorKey].shouldShow).toEqual(true); + expect(result.current.reportInfos[1][errorKey].message).toEqual('Data loading failed'); + expect(result.current.reportInfos[1][errorKey].shouldShow).toEqual(true); + }); - const { result } = renderHook(() => useGenerateReportEffect()); + it.each([ + { + errorKey: 'boardMetricsError', + stateKey: 'shouldShowBoardMetricsError', + updateMethod: 'closeBoardMetricsError', + }, + { + errorKey: 'pipelineMetricsError', + stateKey: 'shouldShowPipelineMetricsError', + updateMethod: 'closePipelineMetricsError', + }, + { + errorKey: 'sourceControlMetricsError', + stateKey: 'shouldShowSourceControlMetricsError', + updateMethod: 'closeSourceControlMetricsError', + }, + ])('should update the report error status when call the update method', async (_) => { + reportClient.polling = jest.fn().mockImplementation(async () => ({ + status: HttpStatusCode.Ok, + response: { + ...MOCK_REPORT_RESPONSE, + reportMetricsError: { + [_.errorKey]: { + status: 400, + message: 'error', + }, + }, + }, + })); + reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => MOCK_RETRIEVE_REPORT_RESPONSE); + const { result } = setup(); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_DORA_METRIC_TYPE); - expect(result.current.generalError4Dora).toEqual('Data loading failed'); + await act(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); }); - }); - it('should set "Data loading failed" for report when polling given UnknownException', async () => { - reportClient.polling = jest.fn().mockRejectedValue(new UnknownError()); - reportClient.retrieveByUrl = jest - .fn() - .mockImplementation(async () => ({ response: MOCK_RETRIEVE_REPORT_RESPONSE })); - - const { result } = renderHook(() => useGenerateReportEffect()); + expect(result.current.reportInfos[0][_.stateKey as keyof IReportInfo]).toEqual(true); + expect(result.current.reportInfos[1][_.stateKey as keyof IReportInfo]).toEqual(true); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.generalError4Report).toEqual('Data loading failed'); + await act(async () => { + const updateMethod = result.current[_.updateMethod as keyof IUseGenerateReportEffect] as (id: string) => void; + updateMethod(MockedDateRanges[0].startDate); }); - }); - - it('should set "Data loading failed" for report when all data retrieval times out', async () => { - reportClient.retrieveByUrl = jest.fn().mockRejectedValue(new TimeoutError('5xx error', 503)); - const { result } = renderHook(() => useGenerateReportEffect()); + expect(result.current.reportInfos[0][_.stateKey as keyof IReportInfo]).toEqual(false); + expect(result.current.reportInfos[1][_.stateKey as keyof IReportInfo]).toEqual(true); + }); - await waitFor(() => { - result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS); - expect(result.current.timeout4Report).toEqual('Data loading failed'); + it('should update the network error status when call the update method', async () => { + reportClient.retrieveByUrl = jest.fn().mockImplementation(async () => MOCK_RETRIEVE_REPORT_RESPONSE); + reportClient.polling = jest + .fn() + .mockRejectedValue(new TimeoutError('timeout error', AXIOS_REQUEST_ERROR_CODE.TIMEOUT)); + const { result } = setup(); + await act(async () => { + await result.current.startToRequestData(MOCK_GENERATE_REPORT_REQUEST_PARAMS_WITH_BOARD_METRIC_TYPE); + }); + expect(result.current.reportInfos[0].timeout4Dora.shouldShow).toEqual(true); + expect(result.current.reportInfos[1].timeout4Dora.shouldShow).toEqual(true); + await act(async () => { + await result.current.closeReportInfosErrorStatus( + MockedDateRanges[0].startDate, + TimeoutErrorKey[METRIC_TYPES.DORA], + ); }); + expect(result.current.reportInfos[0].timeout4Dora.shouldShow).toEqual(false); + expect(result.current.reportInfos[1].timeout4Dora.shouldShow).toEqual(true); }); }); diff --git a/frontend/src/clients/report/ReportClient.ts b/frontend/src/clients/report/ReportClient.ts index 811352c415..a7dfd7579c 100644 --- a/frontend/src/clients/report/ReportClient.ts +++ b/frontend/src/clients/report/ReportClient.ts @@ -2,6 +2,11 @@ import { ReportCallbackResponse, ReportResponseDTO } from '@src/clients/report/d import { ReportRequestDTO } from '@src/clients/report/dto/request'; import { HttpClient } from '@src/clients/HttpClient'; +export interface IPollingRes { + status: number; + response: ReportResponseDTO; +} + export class ReportClient extends HttpClient { status = 0; reportCallbackResponse: ReportCallbackResponse = { @@ -94,12 +99,10 @@ export class ReportClient extends HttpClient { .catch((e) => { throw e; }); - return { - response: this.reportCallbackResponse, - }; + return this.reportCallbackResponse; }; - polling = async (url: string) => { + polling = async (url: string): Promise => { await this.axiosInstance .get(url) .then((res) => { diff --git a/frontend/src/components/Common/DateRangeViewer/index.tsx b/frontend/src/components/Common/DateRangeViewer/index.tsx index d90e08e488..0f5a3f69d4 100644 --- a/frontend/src/components/Common/DateRangeViewer/index.tsx +++ b/frontend/src/components/Common/DateRangeViewer/index.tsx @@ -13,18 +13,14 @@ import { formatDate } from '@src/utils/util'; import { theme } from '@src/theme'; type Props = { - dateRanges: DateRange; - expandColor?: string; - expandBackgroundColor?: string; + dateRangeList: DateRange; + selectedDateRange?: Record; + changeDateRange?: (dateRange: Record) => void; + disabledAll?: boolean; }; -const DateRangeViewer = ({ - dateRanges, - expandColor = theme.palette.text.disabled, - expandBackgroundColor = theme.palette.secondary.dark, -}: Props) => { +const DateRangeViewer = ({ dateRangeList, changeDateRange, selectedDateRange, disabledAll = true }: Props) => { const [showMoreDateRange, setShowMoreDateRange] = useState(false); - const datePick = dateRanges[0]; const DateRangeExpandRef = useRef(null); const handleClickOutside = useCallback((event: MouseEvent) => { @@ -33,6 +29,11 @@ const DateRangeViewer = ({ } }, []); + const handleClick = (key: string) => { + changeDateRange && changeDateRange(dateRangeList.find((dateRange) => dateRange.startDate === key)!); + setShowMoreDateRange(false); + }; + useEffect(() => { document.addEventListener('mousedown', handleClickOutside); return () => { @@ -43,9 +44,14 @@ const DateRangeViewer = ({ const DateRangeExpand = forwardRef((props, ref: React.ForwardedRef) => { return ( - {dateRanges.map((dateRange, index) => { + {dateRangeList.map((dateRange) => { + const disabled = dateRange.disabled || disabledAll; return ( - + handleClick(dateRange.startDate!)} + key={dateRange.startDate!} + > {formatDate(dateRange.startDate as string)} {formatDate(dateRange.endDate as string)} @@ -57,10 +63,13 @@ const DateRangeViewer = ({ }); return ( - - {formatDate(datePick.startDate as string)} + + {formatDate((selectedDateRange || dateRangeList[0]).startDate as string)} - {formatDate(datePick.endDate as string)} + {formatDate((selectedDateRange || dateRangeList[0]).endDate as string)} setShowMoreDateRange(true)} /> diff --git a/frontend/src/components/Common/DateRangeViewer/style.tsx b/frontend/src/components/Common/DateRangeViewer/style.tsx index 531eaad9a0..760df7b6a7 100644 --- a/frontend/src/components/Common/DateRangeViewer/style.tsx +++ b/frontend/src/components/Common/DateRangeViewer/style.tsx @@ -4,20 +4,19 @@ import { Divider } from '@mui/material'; import styled from '@emotion/styled'; import { theme } from '@src/theme'; -export const DateRangeContainer = styled.div({ +export const DateRangeContainer = styled('div')(({ color }) => ({ position: 'relative', display: 'flex', justifyContent: 'flex-start', alignItems: 'center', - backgroundColor: theme.palette.secondary.dark, borderRadius: '0.5rem', border: '0.07rem solid', borderColor: theme.palette.grey[400], width: 'fit-content', padding: '.75rem', - color: theme.palette.text.disabled, fontSize: '.875rem', -}); + color: color, +})); export const DateRangeExpandContainer = styled.div({ position: 'absolute', @@ -47,27 +46,30 @@ export const DateRangeExpandContainer = styled.div({ }); interface SingleDateRangeProps { - backgroundColor: string; - color: string; + disabled: boolean; } -export const SingleDateRange = styled.div((props) => ({ +export const SingleDateRange = styled('div')(({ disabled }: SingleDateRangeProps) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: props.backgroundColor, - color: props.color, + color: theme.palette.text.primary, fontSize: '.875rem', padding: '0.5rem', + cursor: 'pointer', + + ...(disabled && { + color: theme.palette.text.disabled, + cursor: 'default', + }), })); + export const StyledArrowForward = styled(ArrowForward)({ - color: theme.palette.text.disabled, margin: '0 .5rem', fontSize: '0.875rem', }); export const StyledCalendarToday = styled(CalendarToday)({ - color: theme.palette.text.disabled, marginLeft: '1rem', fontSize: '.875rem', }); diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index af189434ac..14e917095e 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -46,7 +46,6 @@ import { Loading } from '@src/components/Loading'; import ReworkSettings from './ReworkSettings'; import { Advance } from './Advance/Advance'; import isEmpty from 'lodash/isEmpty'; -import { theme } from '@src/theme'; import merge from 'lodash/merge'; const MetricsStep = () => { @@ -129,11 +128,7 @@ const MetricsStep = () => { <> {startDate && endDate && ( - + )} {isShowCrewsAndRealDone && ( diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index 2b98f4c5e6..6183fbe859 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -1,12 +1,25 @@ import { filterAndMapCycleTimeSettings, - formatDateToTimestampString, formatDuplicatedNameWithSuffix, getJiraBoardToken, getRealDoneStatus, onlyEmptyAndDoneState, sortDateRanges, } from '@src/utils/util'; +import { + GeneralErrorKey, + initReportInfo, + IReportError, + IReportInfo, + TimeoutErrorKey, + useGenerateReportEffect, +} from '@src/hooks/useGenerateReportEffect'; +import { + addNotification, + closeAllNotifications, + closeNotification, + Notification, +} from '@src/context/notification/NotificationSlice'; import { isOnlySelectClassification, isSelectBoardMetrics, @@ -21,46 +34,57 @@ import { REPORT_PAGE_TYPE, REQUIRED_DATA, } from '@src/constants/resources'; -import { addNotification, closeAllNotifications, Notification } from '@src/context/notification/NotificationSlice'; import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; 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 { ReportResponseDTO } from '@src/clients/report/dto/response'; import BoardMetrics from '@src/containers/ReportStep/BoardMetrics'; import DoraMetrics from '@src/containers/ReportStep/DoraMetrics'; +import React, { useEffect, useMemo, useState } from 'react'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { BoardDetail, DoraDetail } from './ReportDetail'; import { METRIC_TYPES } from '@src/constants/commons'; import { useAppSelector } from '@src/hooks'; +import { uniqueId } from 'lodash'; export interface ReportStepProps { handleSave: () => void; } +const timeoutNotificationMessages = { + [TimeoutErrorKey[METRIC_TYPES.BOARD]]: 'Board metrics', + [TimeoutErrorKey[METRIC_TYPES.DORA]]: 'DORA metrics', + [TimeoutErrorKey[METRIC_TYPES.ALL]]: 'Report', +}; + const ReportStep = ({ handleSave }: ReportStepProps) => { const dispatch = useAppDispatch(); + const configData = useAppSelector(selectConfig); + const descendingDateRanges = sortDateRanges(configData.basic.dateRange); + const [selectedDateRange, setSelectedDateRange] = useState>( + descendingDateRanges[0], + ); + const [currentDataInfo, setCurrentDataInfo] = useState(initReportInfo()); + const { startToRequestData, - reportData, + reportInfos, stopPollingReports, - timeout4Board, - timeout4Dora, - timeout4Report, - generalError4Board, - generalError4Dora, - generalError4Report, + closeReportInfosErrorStatus, + closeBoardMetricsError, + closePipelineMetricsError, + closeSourceControlMetricsError, + hasPollingStarted, } = useGenerateReportEffect(); const [exportValidityTimeMin, setExportValidityTimeMin] = useState(undefined); const [pageType, setPageType] = useState(REPORT_PAGE_TYPE.SUMMARY); const [isCsvFileGeneratedAtEnd, setIsCsvFileGeneratedAtEnd] = useState(false); const [notifications4SummaryPage, setNotifications4SummaryPage] = useState[]>([]); + const [notificationIds, setNotificationIds] = useState([]); - const configData = useAppSelector(selectConfig); const csvTimeStamp = useAppSelector(selectTimeStamp); const { cycleTimeSettingsType, @@ -76,9 +100,8 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { leadTimeForChanges, } = useAppSelector(selectMetricsContent); - const descendingDateRanges = sortDateRanges(configData.basic.dateRange); - const startDate = configData.basic.dateRange[0]?.startDate ?? ''; - const endDate = configData.basic.dateRange[0]?.endDate ?? ''; + const startDate = selectedDateRange?.startDate as string; + const endDate = selectedDateRange?.endDate as string; const { metrics, calendarType } = configData.basic; const boardingMappingStates = [...new Set(cycleTimeSettings.map((item) => item.value))]; const isOnlyEmptyAndDoneState = onlyEmptyAndDoneState(boardingMappingStates); @@ -89,10 +112,15 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { const isSummaryPage = useMemo(() => pageType === REPORT_PAGE_TYPE.SUMMARY, [pageType]); const getErrorMessage4Board = () => { - if (reportData?.reportMetricsError.boardMetricsError) { - return `Failed to get Jira info, status: ${reportData.reportMetricsError.boardMetricsError.status}`; + if (currentDataInfo.reportData?.reportMetricsError.boardMetricsError) { + return `Failed to get Jira info, status: ${currentDataInfo.reportData.reportMetricsError.boardMetricsError.status}`; } - return timeout4Board || timeout4Report || generalError4Board || generalError4Report; + return ( + currentDataInfo.timeout4Board.message || + currentDataInfo.timeout4Report.message || + currentDataInfo.generalError4Board.message || + currentDataInfo.generalError4Report.message + ); }; const getJiraBoardSetting = () => { @@ -172,8 +200,8 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { }); const basicReportRequestBody = { - startTime: formatDateToTimestampString(startDate), - endTime: formatDateToTimestampString(endDate), + startTime: null, + endTime: null, considerHoliday: calendarType === CALENDAR.CHINA, csvTimeStamp, metrics, @@ -200,6 +228,15 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { jiraBoardSetting: undefined, }; + useEffect(() => { + notificationIds.forEach((notificationId) => { + closeNotification(notificationId); + }); + setNotificationIds([]); + setCurrentDataInfo(reportInfos.find((singleResult) => singleResult.id === selectedDateRange.startDate)!); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportInfos, selectedDateRange]); + useEffect(() => { setPageType(onlySelectClassification ? REPORT_PAGE_TYPE.BOARD : REPORT_PAGE_TYPE.SUMMARY); return () => { @@ -208,7 +245,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useLayoutEffect(() => { + useEffect(() => { exportValidityTimeMin && isCsvFileGeneratedAtEnd && dispatch( @@ -218,7 +255,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { ); }, [dispatch, exportValidityTimeMin, isCsvFileGeneratedAtEnd]); - useLayoutEffect(() => { + useEffect(() => { if (exportValidityTimeMin && isCsvFileGeneratedAtEnd) { const startTime = Date.now(); const timer = setInterval(() => { @@ -243,14 +280,19 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { } }, [dispatch, exportValidityTimeMin, isCsvFileGeneratedAtEnd]); - useLayoutEffect(() => { + useEffect(() => { dispatch(closeAllNotifications()); }, [dispatch, pageType]); useEffect(() => { - setExportValidityTimeMin(reportData?.exportValidityTime); - reportData && setIsCsvFileGeneratedAtEnd(reportData.allMetricsCompleted && reportData.isSuccessfulCreateCsvFile); - }, [dispatch, reportData]); + if (hasPollingStarted) return; + const successfulReportInfos = reportInfos.filter((reportInfo) => reportInfo.reportData); + if (successfulReportInfos.length === 0) return; + setExportValidityTimeMin(successfulReportInfos[0].reportData?.exportValidityTime); + setIsCsvFileGeneratedAtEnd( + successfulReportInfos.some((reportInfo) => reportInfo.reportData?.isSuccessfulCreateCsvFile), + ); + }, [dispatch, reportInfos, hasPollingStarted]); useEffect(() => { if (isSummaryPage && notifications4SummaryPage.length > 0) { @@ -261,106 +303,71 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { }, [dispatch, notifications4SummaryPage, isSummaryPage]); useEffect(() => { - if (reportData?.reportMetricsError.boardMetricsError) { + if (!currentDataInfo.shouldShowBoardMetricsError) return; + if (currentDataInfo.reportData?.reportMetricsError.boardMetricsError) { + const notificationId = uniqueId(); + setNotificationIds((pre) => [...pre, notificationId]); setNotifications4SummaryPage((prevState) => [ ...prevState, { + id: notificationId, message: MESSAGE.FAILED_TO_GET_DATA('Board Metrics'), type: 'error', }, ]); } - }, [reportData?.reportMetricsError.boardMetricsError]); + closeBoardMetricsError(selectedDateRange.startDate as string); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentDataInfo.reportData?.reportMetricsError.boardMetricsError]); useEffect(() => { - if (reportData?.reportMetricsError.pipelineMetricsError) { + if (!currentDataInfo.shouldShowPipelineMetricsError) return; + if (currentDataInfo.reportData?.reportMetricsError.pipelineMetricsError) { + const notificationId = uniqueId(); + setNotificationIds((pre) => [...pre, notificationId]); setNotifications4SummaryPage((prevState) => [ ...prevState, { + id: notificationId, message: MESSAGE.FAILED_TO_GET_DATA('Buildkite'), type: 'error', }, ]); } - }, [reportData?.reportMetricsError.pipelineMetricsError]); + closePipelineMetricsError(selectedDateRange.startDate as string); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentDataInfo.reportData?.reportMetricsError.pipelineMetricsError]); useEffect(() => { - if (reportData?.reportMetricsError.sourceControlMetricsError) { + if (!currentDataInfo.shouldShowSourceControlMetricsError) return; + if (currentDataInfo.reportData?.reportMetricsError.sourceControlMetricsError) { + const notificationId = uniqueId(); + setNotificationIds((pre) => [...pre, notificationId]); setNotifications4SummaryPage((prevState) => [ ...prevState, { + id: notificationId, message: MESSAGE.FAILED_TO_GET_DATA('GitHub'), type: 'error', }, ]); } - }, [reportData?.reportMetricsError.sourceControlMetricsError]); - - useEffect(() => { - timeout4Report && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.LOADING_TIMEOUT('Report'), - type: 'error', - }, - ]); - }, [timeout4Report]); - - useEffect(() => { - timeout4Board && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.LOADING_TIMEOUT('Board metrics'), - type: 'error', - }, - ]); - }, [timeout4Board]); - - useEffect(() => { - timeout4Dora && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.LOADING_TIMEOUT('DORA metrics'), - type: 'error', - }, - ]); - }, [timeout4Dora]); - - useEffect(() => { - generalError4Board && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.FAILED_TO_REQUEST, - type: 'error', - }, - ]); - }, [generalError4Board]); - - useEffect(() => { - generalError4Dora && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.FAILED_TO_REQUEST, - type: 'error', - }, - ]); - }, [generalError4Dora]); + closeSourceControlMetricsError(selectedDateRange.startDate as string); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentDataInfo.reportData?.reportMetricsError.sourceControlMetricsError]); useEffect(() => { - generalError4Report && - setNotifications4SummaryPage((prevState) => [ - ...prevState, - { - message: MESSAGE.FAILED_TO_REQUEST, - type: 'error', - }, - ]); - }, [generalError4Report]); + Object.values(TimeoutErrorKey).forEach((value) => handleTimeoutAndGeneralError(value)); + Object.values(GeneralErrorKey).forEach((value) => handleTimeoutAndGeneralError(value)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentDataInfo.timeout4Board, + currentDataInfo.timeout4Report, + currentDataInfo.timeout4Dora, + currentDataInfo.generalError4Board, + currentDataInfo.generalError4Dora, + currentDataInfo.generalError4Report, + ]); useEffect(() => { startToRequestData(basicReportRequestBody); @@ -373,7 +380,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { startToRequestData(boardReportRequestBody)} onShowDetail={() => setPageType(REPORT_PAGE_TYPE.BOARD)} - boardReport={reportData} + boardReport={currentDataInfo.reportData} errorMessage={getErrorMessage4Board()} /> )} @@ -381,8 +388,13 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { startToRequestData(doraReportRequestBody)} onShowDetail={() => setPageType(REPORT_PAGE_TYPE.DORA)} - doraReport={reportData} - errorMessage={timeout4Dora || timeout4Report || generalError4Dora || generalError4Report} + doraReport={currentDataInfo.reportData} + errorMessage={ + currentDataInfo.timeout4Dora.message || + currentDataInfo.timeout4Report.message || + currentDataInfo.generalError4Dora.message || + currentDataInfo.generalError4Report.message + } /> )} @@ -400,18 +412,43 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { setPageType(REPORT_PAGE_TYPE.SUMMARY); }; + const handleTimeoutAndGeneralError = (value: string) => { + const errorKey = value as keyof IReportError; + if (!currentDataInfo[errorKey].shouldShow) return; + if (currentDataInfo[errorKey].message) { + const notificationId = uniqueId(); + setNotificationIds((pre) => [...pre, notificationId]); + setNotifications4SummaryPage((prevState) => [ + ...prevState, + { + id: notificationId, + message: timeoutNotificationMessages[errorKey] + ? MESSAGE.LOADING_TIMEOUT(timeoutNotificationMessages[errorKey]) + : MESSAGE.FAILED_TO_REQUEST, + type: 'error', + }, + ]); + } + closeReportInfosErrorStatus(selectedDateRange.startDate as string, errorKey); + }; + return ( <> {startDate && endDate && ( - + setSelectedDateRange(dateRange)} + disabledAll={false} + /> )} {isSummaryPage ? showSummary() : pageType === REPORT_PAGE_TYPE.BOARD - ? showBoardDetail(reportData) - : !!reportData && showDoraDetail(reportData)} + ? showBoardDetail(currentDataInfo.reportData) + : !!currentDataInfo.reportData && showDoraDetail(currentDataInfo.reportData)} { isShowExportPipelineButton={isSummaryPage ? shouldShowDoraMetrics : pageType === REPORT_PAGE_TYPE.DORA} handleBack={() => handleBack()} handleSave={() => handleSave()} - reportData={reportData} + reportData={currentDataInfo.reportData} startDate={startDate} endDate={endDate} csvTimeStamp={csvTimeStamp} diff --git a/frontend/src/context/config/configSlice.ts b/frontend/src/context/config/configSlice.ts index 24b7aff82c..0cf6021d40 100644 --- a/frontend/src/context/config/configSlice.ts +++ b/frontend/src/context/config/configSlice.ts @@ -22,6 +22,7 @@ import dayjs from 'dayjs'; export type DateRange = { startDate: string | null; endDate: string | null; + disabled?: boolean; }[]; export interface BasicConfigState { diff --git a/frontend/src/hooks/useGenerateReportEffect.ts b/frontend/src/hooks/useGenerateReportEffect.ts index 009da1163c..ba03c492d9 100644 --- a/frontend/src/hooks/useGenerateReportEffect.ts +++ b/frontend/src/hooks/useGenerateReportEffect.ts @@ -1,118 +1,327 @@ +import { ReportCallbackResponse, ReportResponseDTO } from '@src/clients/report/dto/response'; import { exportValidityTimeMapper } from '@src/hooks/reportMapper/exportValidityTime'; import { DATA_LOADING_FAILED, DEFAULT_MESSAGE } from '@src/constants/resources'; -import { ReportResponseDTO } from '@src/clients/report/dto/response'; +import { IPollingRes, reportClient } from '@src/clients/report/ReportClient'; +import { DateRange, selectConfig } from '@src/context/config/configSlice'; import { ReportRequestDTO } from '@src/clients/report/dto/request'; -import { reportClient } from '@src/clients/report/ReportClient'; +import { formatDateToTimestampString } from '@src/utils/util'; import { TimeoutError } from '@src/errors/TimeoutError'; import { METRIC_TYPES } from '@src/constants/commons'; +import { useAppSelector } from '@src/hooks/index'; import { useRef, useState } from 'react'; +import get from 'lodash/get'; -export interface useGenerateReportEffectInterface { - startToRequestData: (params: ReportRequestDTO) => void; +export type PromiseSettledResultWithId = PromiseSettledResult & { + id: string; +}; + +export interface IUseGenerateReportEffect { + startToRequestData: (params: ReportRequestDTO) => Promise; stopPollingReports: () => void; - timeout4Board: string; - timeout4Dora: string; - timeout4Report: string; - generalError4Board: string; - generalError4Dora: string; - generalError4Report: string; + reportInfos: IReportInfo[]; + closeReportInfosErrorStatus: (id: string, errorKey: string) => void; + closeBoardMetricsError: (id: string) => void; + closePipelineMetricsError: (id: string) => void; + closeSourceControlMetricsError: (id: string) => void; + hasPollingStarted: boolean; +} + +interface IErrorInfo { + message: string; + shouldShow: boolean; +} + +export interface IReportError { + timeout4Board: IErrorInfo; + timeout4Dora: IErrorInfo; + timeout4Report: IErrorInfo; + generalError4Board: IErrorInfo; + generalError4Dora: IErrorInfo; + generalError4Report: IErrorInfo; +} + +export interface IReportInfo extends IReportError { + id: string; reportData: ReportResponseDTO | undefined; + shouldShowBoardMetricsError: boolean; + shouldShowPipelineMetricsError: boolean; + shouldShowSourceControlMetricsError: boolean; } -export const useGenerateReportEffect = (): useGenerateReportEffectInterface => { +export const initReportInfo = (): IReportInfo => ({ + id: '', + timeout4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + timeout4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Board: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Dora: { message: DEFAULT_MESSAGE, shouldShow: true }, + generalError4Report: { message: DEFAULT_MESSAGE, shouldShow: true }, + shouldShowBoardMetricsError: true, + shouldShowPipelineMetricsError: true, + shouldShowSourceControlMetricsError: true, + reportData: undefined, +}); + +export const TimeoutErrorKey = { + [METRIC_TYPES.BOARD]: 'timeout4Board', + [METRIC_TYPES.DORA]: 'timeout4Dora', + [METRIC_TYPES.ALL]: 'timeout4Report', +}; + +export const GeneralErrorKey = { + [METRIC_TYPES.BOARD]: 'generalError4Board', + [METRIC_TYPES.DORA]: 'generalError4Dora', + [METRIC_TYPES.ALL]: 'generalError4Report', +}; + +const REJECTED = 'rejected'; +const FULFILLED = 'fulfilled'; + +const getErrorKey = (error: Error, source: METRIC_TYPES): string => { + return error instanceof TimeoutError ? TimeoutErrorKey[source] : GeneralErrorKey[source]; +}; + +export const useGenerateReportEffect = (): IUseGenerateReportEffect => { const reportPath = '/reports'; - const [timeout4Board, setTimeout4Board] = useState(DEFAULT_MESSAGE); - const [timeout4Dora, setTimeout4Dora] = useState(DEFAULT_MESSAGE); - const [timeout4Report, setTimeout4Report] = useState(DEFAULT_MESSAGE); - const [generalError4Board, setGeneralError4Board] = useState(DEFAULT_MESSAGE); - const [generalError4Dora, setGeneralError4Dora] = useState(DEFAULT_MESSAGE); - const [generalError4Report, setGeneralError4Report] = useState(DEFAULT_MESSAGE); - const [reportData, setReportData] = useState(); + const configData = useAppSelector(selectConfig); const timerIdRef = useRef(); - let hasPollingStarted = false; - - const startToRequestData = (params: ReportRequestDTO) => { + const dateRangeList: DateRange = get(configData, 'basic.dateRange', []); + const [reportInfos, setReportInfos] = useState( + dateRangeList.map((dateRange) => ({ ...initReportInfo(), id: dateRange.startDate as string })), + ); + const [hasPollingStarted, setHasPollingStarted] = useState(false); + let nextHasPollingStarted = false; + const startToRequestData = async (params: ReportRequestDTO) => { const { metricTypes } = params; resetTimeoutMessage(metricTypes); - reportClient - .retrieveByUrl(params, reportPath) - .then((res) => { - if (hasPollingStarted) return; - hasPollingStarted = true; - pollingReport(res.response.callbackUrl, res.response.interval); - }) - .catch((e) => { - const source: METRIC_TYPES = metricTypes.length === 2 ? METRIC_TYPES.ALL : metricTypes[0]; - handleError(e, source); - }); + const res: PromiseSettledResult[] = await Promise.allSettled( + dateRangeList.map(({ startDate, endDate }) => + reportClient.retrieveByUrl( + { + ...params, + startTime: formatDateToTimestampString(startDate!), + endTime: formatDateToTimestampString(endDate!), + }, + reportPath, + ), + ), + ); + + updateErrorAfterFetchReport(res, metricTypes); + + if (hasPollingStarted) return; + nextHasPollingStarted = true; + setHasPollingStarted(nextHasPollingStarted); + + const { pollingInfos, pollingInterval } = assemblePollingParams(res); + + await pollingReport({ pollingInfos, interval: pollingInterval }); }; - const resetTimeoutMessage = (metricTypes: string[]) => { - if (metricTypes.length === 2) { - setTimeout4Report(DEFAULT_MESSAGE); - } else if (metricTypes.includes(METRIC_TYPES.BOARD)) { - setTimeout4Board(DEFAULT_MESSAGE); - } else { - setTimeout4Dora(DEFAULT_MESSAGE); + function getReportInfosAfterPolling( + preReportInfos: IReportInfo[], + pollingResponsesWithId: PromiseSettledResultWithId[], + ) { + return preReportInfos.map((reportInfo) => { + const matchedRes = pollingResponsesWithId.find((singleRes) => singleRes.id === reportInfo.id); + if (!matchedRes) return reportInfo; + + if (matchedRes.status === FULFILLED) { + const { response } = matchedRes.value; + reportInfo.reportData = assembleReportData(response); + reportInfo.shouldShowBoardMetricsError = true; + reportInfo.shouldShowPipelineMetricsError = true; + reportInfo.shouldShowSourceControlMetricsError = true; + } else { + const errorKey = getErrorKey(matchedRes.reason, METRIC_TYPES.ALL) as keyof IReportError; + reportInfo[errorKey] = { message: DATA_LOADING_FAILED, shouldShow: true }; + } + return reportInfo; + }); + } + + const pollingReport = async ({ + pollingInfos, + interval, + }: { + pollingInfos: Record[]; + interval: number; + }) => { + const pollingIds: string[] = pollingInfos.map((pollingInfo) => pollingInfo.id); + initReportInfosTimeout4Report(pollingIds); + + const pollingQueue: Promise[] = pollingInfos.map((pollingInfo) => + reportClient.polling(pollingInfo.callbackUrl), + ); + const pollingResponses = await Promise.allSettled(pollingQueue); + const pollingResponsesWithId = assemblePollingResWithId(pollingResponses, pollingInfos); + + setReportInfos((preReportInfos) => getReportInfosAfterPolling(preReportInfos, pollingResponsesWithId)); + + const nextPollingInfos = getNextPollingInfos(pollingResponsesWithId, pollingInfos); + if (nextPollingInfos.length === 0) { + stopPollingReports(); + return; } + timerIdRef.current = window.setTimeout(() => { + pollingReport({ pollingInfos: nextPollingInfos, interval }); + }, interval * 1000); }; - const handleTimeoutError = { - [METRIC_TYPES.BOARD]: setTimeout4Board, - [METRIC_TYPES.DORA]: setTimeout4Dora, - [METRIC_TYPES.ALL]: setTimeout4Report, + const stopPollingReports = () => { + window.clearTimeout(timerIdRef.current); + setHasPollingStarted(false); }; - const handleGeneralError = { - [METRIC_TYPES.BOARD]: setGeneralError4Board, - [METRIC_TYPES.DORA]: setGeneralError4Dora, - [METRIC_TYPES.ALL]: setGeneralError4Report, + const assembleReportData = (response: ReportResponseDTO): ReportResponseDTO => { + const exportValidityTime = exportValidityTimeMapper(response.exportValidityTime); + return { ...response, exportValidityTime: exportValidityTime }; }; - const handleError = (error: Error, source: METRIC_TYPES) => { - return error instanceof TimeoutError - ? handleTimeoutError[source](DATA_LOADING_FAILED) - : handleGeneralError[source](DATA_LOADING_FAILED); + const resetTimeoutMessage = (metricTypes: string[]) => { + setReportInfos((preReportInfos) => { + return preReportInfos.map((reportInfo) => { + if (metricTypes.length === 2) { + reportInfo.timeout4Report = { message: DEFAULT_MESSAGE, shouldShow: true }; + } else if (metricTypes.includes(METRIC_TYPES.BOARD)) { + reportInfo.timeout4Board = { message: DEFAULT_MESSAGE, shouldShow: true }; + } else { + reportInfo.timeout4Dora = { message: DEFAULT_MESSAGE, shouldShow: true }; + } + return reportInfo; + }); + }); }; - const pollingReport = (url: string, interval: number) => { - setTimeout4Report(DEFAULT_MESSAGE); - reportClient - .polling(url) - .then((res: { status: number; response: ReportResponseDTO }) => { - const response = res.response; - handleAndUpdateData(response); - if (response.allMetricsCompleted || !hasPollingStarted) { - stopPollingReports(); - } else { - timerIdRef.current = window.setTimeout(() => pollingReport(url, interval), interval * 1000); + const updateErrorAfterFetchReport = ( + res: PromiseSettledResult[], + metricTypes: METRIC_TYPES[], + ) => { + if (res.filter(({ status }) => status === REJECTED).length === 0) return; + + setReportInfos((preReportInfos: IReportInfo[]) => { + return preReportInfos.map((resInfo, index) => { + const currentRes = res[index]; + if (currentRes.status === REJECTED) { + const source: METRIC_TYPES = metricTypes.length === 2 ? METRIC_TYPES.ALL : metricTypes[0]; + const errorKey = getErrorKey(currentRes.reason, source) as keyof IReportError; + resInfo[errorKey] = { message: DATA_LOADING_FAILED, shouldShow: true }; } - }) - .catch((e) => { - handleError(e, METRIC_TYPES.ALL); - stopPollingReports(); + return resInfo; }); + }); }; - const stopPollingReports = () => { - window.clearTimeout(timerIdRef.current); - hasPollingStarted = false; + const assemblePollingParams = (res: PromiseSettledResult[]) => { + const resWithIds: PromiseSettledResultWithId[] = res.map((item, index) => ({ + ...item, + id: reportInfos[index].id, + })); + + const fulfilledResponses: PromiseSettledResultWithId[] = resWithIds.filter( + ({ status }) => status === FULFILLED, + ); + + const pollingInfos: Record[] = fulfilledResponses.map((v) => { + return { callbackUrl: (v as PromiseFulfilledResult).value.callbackUrl, id: v.id }; + }); + + const pollingInterval = (fulfilledResponses[0] as PromiseFulfilledResult)?.value.interval; + return { pollingInfos, pollingInterval }; }; - const handleAndUpdateData = (response: ReportResponseDTO) => { - const exportValidityTime = exportValidityTimeMapper(response.exportValidityTime); - setReportData({ ...response, exportValidityTime: exportValidityTime }); + const assemblePollingResWithId = ( + pollingResponses: Array>>>, + pollingInfos: Record[], + ) => { + const pollingResponsesWithId: PromiseSettledResultWithId[] = pollingResponses.map( + (singleRes, index) => ({ + ...singleRes, + id: pollingInfos[index].id, + }), + ); + return pollingResponsesWithId; + }; + + const getNextPollingInfos = ( + pollingResponsesWithId: PromiseSettledResultWithId[], + pollingInfos: Record[], + ) => { + const nextPollingInfos: Record[] = pollingResponsesWithId + .filter( + (pollingResponseWithId) => + pollingResponseWithId.status === FULFILLED && + !pollingResponseWithId.value.response.allMetricsCompleted && + nextHasPollingStarted, + ) + .map((pollingResponseWithId) => pollingInfos.find((pollingInfo) => pollingInfo.id === pollingResponseWithId.id)!); + return nextPollingInfos; + }; + + const initReportInfosTimeout4Report = (pollingIds: string[]) => { + setReportInfos((preInfos) => { + return preInfos.map((info) => { + if (pollingIds.includes(info.id)) { + info.timeout4Report = { message: DEFAULT_MESSAGE, shouldShow: true }; + } + return info; + }); + }); + }; + + const shutReportInfosErrorStatus = (id: string, errorKey: string) => { + setReportInfos((preReportInfos) => { + return preReportInfos.map((reportInfo) => { + if (reportInfo.id === id) { + const key = errorKey as keyof IReportError; + reportInfo[key].shouldShow = false; + } + return reportInfo; + }); + }); + }; + + const shutBoardMetricsError = (id: string) => { + setReportInfos((preReportInfos) => { + return preReportInfos.map((reportInfo) => { + if (reportInfo.id === id) { + reportInfo.shouldShowBoardMetricsError = false; + } + return reportInfo; + }); + }); + }; + + const shutPipelineMetricsError = (id: string) => { + setReportInfos((preReportInfos) => { + return preReportInfos.map((reportInfo) => { + if (reportInfo.id === id) { + reportInfo.shouldShowPipelineMetricsError = false; + } + return reportInfo; + }); + }); + }; + + const shutSourceControlMetricsError = (id: string) => { + setReportInfos((preReportInfos) => { + return preReportInfos.map((reportInfo) => { + if (reportInfo.id === id) { + reportInfo.shouldShowSourceControlMetricsError = false; + } + return reportInfo; + }); + }); }; return { startToRequestData, stopPollingReports, - reportData, - timeout4Board, - timeout4Dora, - timeout4Report, - generalError4Board, - generalError4Dora, - generalError4Report, + reportInfos, + closeReportInfosErrorStatus: shutReportInfosErrorStatus, + closeBoardMetricsError: shutBoardMetricsError, + closePipelineMetricsError: shutPipelineMetricsError, + closeSourceControlMetricsError: shutSourceControlMetricsError, + hasPollingStarted, }; };