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,
};
};