From e4010f49c6cd689e081022797c35e0a90b63349d Mon Sep 17 00:00:00 2001
From: zhou-yinyuan <147299494+zhou-yinyuan@users.noreply.github.com>
Date: Fri, 16 Aug 2024 11:27:54 +0800
Subject: [PATCH] ADM-997 [frontend]: add new label for new functions (#1577)
* ADM-997 [frontend]: add new label for new functions
* ADM-997 [frontend]: responsive for mobile
* ADM-997 [frontend]: fix comments
* ADM-997 [frontend]: responsive for mobile
---
.../Common/NewFunctionsLabel.test.tsx | 57 ++++++++++++++
.../MetricsStep/Classification.test.tsx | 78 ++++++++++++++++---
.../containers/MetricsStep/CycleTime.test.tsx | 4 +-
.../MetricsStep/MetricsStep.test.tsx | 3 +
.../ReportStep/ChartAndTitleWrapper.test.tsx | 76 ++++++++++++++++--
.../ReportStep/DoraMetrics.test.tsx | 25 +++++-
.../containers/ReportStep/ReportStep.test.tsx | 14 +++-
.../ShareReport/ShareReport.test.tsx | 3 +
frontend/package.json | 2 +
frontend/pnpm-lock.yaml | 49 +++++-------
.../Common/NewFunctionsLabel/index.tsx | 37 +++++++++
.../Common/NewFunctionsLabel/style.tsx | 25 ++++++
.../Common/ReportGrid/ReportCard/index.tsx | 9 ++-
frontend/src/constants/commons.ts | 5 ++
.../MetricsStep/Classification/index.tsx | 9 ++-
.../CycleTime/Table/CellAutoComplete.tsx | 12 ++-
.../ReportStep/ChartAndTitleWrapper/index.tsx | 29 ++++---
.../ReportStep/ChartAndTitleWrapper/style.tsx | 9 ++-
.../PipelineSelector/index.tsx | 1 -
.../PipelineSelector/style.tsx | 1 +
.../ReportStep/DoraMetricsChart/index.tsx | 19 +++--
frontend/src/theme.ts | 8 ++
22 files changed, 395 insertions(+), 80 deletions(-)
create mode 100644 frontend/__tests__/components/Common/NewFunctionsLabel.test.tsx
create mode 100644 frontend/src/components/Common/NewFunctionsLabel/index.tsx
create mode 100644 frontend/src/components/Common/NewFunctionsLabel/style.tsx
diff --git a/frontend/__tests__/components/Common/NewFunctionsLabel.test.tsx b/frontend/__tests__/components/Common/NewFunctionsLabel.test.tsx
new file mode 100644
index 0000000000..ae5a5261dd
--- /dev/null
+++ b/frontend/__tests__/components/Common/NewFunctionsLabel.test.tsx
@@ -0,0 +1,57 @@
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
+import { saveVersion } from '@src/context/meta/metaSlice';
+import { setupStore } from '@test/utils/setupStoreUtil';
+import { render, screen } from '@testing-library/react';
+import { NewLabelType } from '@src/constants/commons';
+import { Provider } from 'react-redux';
+
+const store = setupStore();
+
+describe('NewFunctionsLabel', () => {
+ const setup = (version: string, initVersion: string, newLabelType: NewLabelType = NewLabelType.General) => {
+ store.dispatch(saveVersion(version));
+ return render(
+
+
+ test
+
+ ,
+ );
+ };
+
+ it('should show new label when version from store is less than version that functions create', () => {
+ setup('1.2.1', '1.3.0');
+
+ expect(screen.queryByLabelText('new label')).toBeInTheDocument();
+ });
+
+ it('should show new label when version from store is equal to version that functions create', () => {
+ setup('1.3.0', '1.3.0');
+
+ expect(screen.queryByLabelText('new label')).toBeInTheDocument();
+ });
+
+ it('should not show new label when version from store is more than version that functions create', () => {
+ setup('1.3.1', '1.3.0');
+
+ expect(screen.queryByLabelText('new label')).not.toBeInTheDocument();
+ });
+
+ it('should not show new label when init version exist alpha', () => {
+ setup('1.3.0', '1.3.0-alpha');
+
+ expect(screen.queryByLabelText('new label')).not.toBeInTheDocument();
+ });
+
+ it('should not show new label when init version exist pre-release', () => {
+ setup('1.3.0', '1.3.0-0.3.1');
+
+ expect(screen.queryByLabelText('new label')).not.toBeInTheDocument();
+ });
+
+ it('should show customize new label when version from store is equal to version that functions create and type is CustomizeMarginAndHeight', () => {
+ setup('1.3.0', '1.3.0', NewLabelType.CustomizeMarginAndHeight);
+
+ expect(screen.queryByLabelText('new label')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/__tests__/containers/MetricsStep/Classification.test.tsx b/frontend/__tests__/containers/MetricsStep/Classification.test.tsx
index dd99630681..76099c9cd5 100644
--- a/frontend/__tests__/containers/MetricsStep/Classification.test.tsx
+++ b/frontend/__tests__/containers/MetricsStep/Classification.test.tsx
@@ -3,6 +3,7 @@ import { act, render, waitFor, within, screen } from '@testing-library/react';
import { Classification } from '@src/containers/MetricsStep/Classification';
import { saveTargetFields } from '@src/context/Metrics/metricsSlice';
import { ERROR_MESSAGE_TIME_DURATION } from '../../fixtures';
+import { saveVersion } from '@src/context/meta/metaSlice';
import { setupStore } from '../../utils/setupStoreUtil';
import userEvent from '@testing-library/user-event';
import { Provider, useSelector } from 'react-redux';
@@ -35,9 +36,10 @@ const RenderComponent = () => {
return ;
};
-const setup = async (initField: ITargetFieldType[]) => {
+const setup = async (initField: ITargetFieldType[], version: string = '1.2.1') => {
const store = setupStore();
store.dispatch(saveTargetFields(initField));
+ store.dispatch(saveVersion(version));
return render(
@@ -67,18 +69,26 @@ describe('Classification', () => {
{ flag: false, key: 'custom_field10061', name: 'Story testing' },
]);
- expect(screen.getByRole('combobox', { name: mockClassificationChartLabel })).toBeDisabled();
+ expect(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ ).toBeDisabled();
+ expect(screen.getByLabelText('new label')).toBeInTheDocument();
});
it('should enable classification charts when classification is selected', async () => {
await setup(mockTargetFields);
- expect(screen.getByRole('combobox', { name: mockClassificationChartLabel })).not.toBeDisabled();
+ expect(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ ).not.toBeDisabled();
+ expect(screen.getByLabelText('new label')).toBeInTheDocument();
});
it('should show selected classification options when classification is selected', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
await waitFor(() => {
const listBox = within(screen.getByRole('listbox'));
@@ -90,13 +100,17 @@ describe('Classification', () => {
it('should show selected classification options when select a new classification', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
expect(screen.queryAllByRole('option')).toHaveLength(3);
await userEvent.click(screen.getByRole('combobox', { name: mockClassificationLabel }));
await userEvent.click(screen.getByRole('option', { name: /Type/i }));
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
await waitFor(() => {
expect(screen.queryAllByRole('option')).toHaveLength(4);
@@ -105,7 +119,9 @@ describe('Classification', () => {
it('should remove selected classification charts when remove a classification', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
const chartFormItem = within(screen.getByLabelText('Classification Visible Charts'));
await userEvent.click(listBox.getByLabelText(`${classificationChartOptionLabelPrefix} Issue`));
@@ -126,7 +142,9 @@ describe('Classification', () => {
it('should select classification charts correctly', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
const chartFormItem = within(screen.getByLabelText('Classification Visible Charts'));
await userEvent.click(listBox.getByLabelText(`${classificationChartOptionLabelPrefix} Issue`));
@@ -144,7 +162,9 @@ describe('Classification', () => {
it('should select all classification charts correctly', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
const chartFormItem = within(screen.getByLabelText('Classification Visible Charts'));
await userEvent.click(listBox.getByLabelText(`${classificationChartOptionLabelPrefix} All`));
@@ -165,7 +185,9 @@ describe('Classification', () => {
it('should enable and display all option given selected classification is less than 4', async () => {
await setup(mockTargetFields);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
await waitFor(() => {
@@ -185,7 +207,9 @@ describe('Classification', () => {
{ flag: true, key: 'more_than_four', name: 'Test' },
]);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
await waitFor(() => {
@@ -203,7 +227,9 @@ describe('Classification', () => {
];
await setup(mockStatus);
- await userEvent.click(screen.getByRole('combobox', { name: mockClassificationChartLabel }));
+ await userEvent.click(
+ screen.getByRole('combobox', { name: (content) => content.includes(mockClassificationChartLabel) }),
+ );
const listBox = within(screen.getByRole('listbox'));
await userEvent.click(listBox.getByLabelText(`${classificationChartOptionLabelPrefix} ${mockStatus[0].name}`));
await userEvent.click(listBox.getByLabelText(`${classificationChartOptionLabelPrefix} ${mockStatus[1].name}`));
@@ -217,6 +243,34 @@ describe('Classification', () => {
);
});
});
+
+ it('should not show new label when version is more than 1.3.0', async () => {
+ await setup(
+ [
+ { flag: false, key: 'issue', name: 'Issue' },
+ { flag: false, key: 'type', name: 'Type' },
+ { flag: false, key: 'custom_field10060', name: 'Story testing' },
+ { flag: false, key: 'custom_field10061', name: 'Story testing' },
+ ],
+ '1.3.1',
+ );
+
+ expect(screen.queryByLabelText('new label')).not.toBeInTheDocument();
+ });
+
+ it('should show new label when version is equal to 1.3.0', async () => {
+ await setup(
+ [
+ { flag: false, key: 'issue', name: 'Issue' },
+ { flag: false, key: 'type', name: 'Type' },
+ { flag: false, key: 'custom_field10060', name: 'Story testing' },
+ { flag: false, key: 'custom_field10061', name: 'Story testing' },
+ ],
+ '1.3.0',
+ );
+
+ expect(screen.queryByLabelText('new label')).toBeInTheDocument();
+ });
});
describe('Classification Distinguished by', () => {
diff --git a/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx b/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx
index 0999aea40b..592d116dcf 100644
--- a/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx
+++ b/frontend/__tests__/containers/MetricsStep/CycleTime.test.tsx
@@ -60,7 +60,9 @@ const mockedUseAppDispatch = jest.fn();
jest.mock('@src/hooks/useAppDispatch', () => ({
useAppDispatch: () => mockedUseAppDispatch,
}));
-
+jest.mock('semver', () => ({
+ gt: jest.fn((version, initVersion) => version > initVersion),
+}));
const setup = () =>
render(
diff --git a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx
index 534628ed0e..db7434ce67 100644
--- a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx
+++ b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx
@@ -38,6 +38,9 @@ jest.mock('@src/context/notification/NotificationSlice', () => ({
...jest.requireActual('@src/context/notification/NotificationSlice'),
closeAllNotifications: jest.fn().mockReturnValue({ type: 'CLOSE_ALL_NOTIFICATIONS' }),
}));
+jest.mock('semver', () => ({
+ gt: jest.fn((version, initVersion) => version > initVersion),
+}));
let store = setupStore();
const server = setupServer(
diff --git a/frontend/__tests__/containers/ReportStep/ChartAndTitleWrapper.test.tsx b/frontend/__tests__/containers/ReportStep/ChartAndTitleWrapper.test.tsx
index 2496bea65d..fcb3f88529 100644
--- a/frontend/__tests__/containers/ReportStep/ChartAndTitleWrapper.test.tsx
+++ b/frontend/__tests__/containers/ReportStep/ChartAndTitleWrapper.test.tsx
@@ -1,7 +1,10 @@
import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper';
import { ChartType, TrendIcon, TrendType } from '@src/constants/resources';
+import { saveVersion } from '@src/context/meta/metaSlice';
+import { setupStore } from '@test/utils/setupStoreUtil';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { Provider } from 'react-redux';
import { theme } from '@src/theme';
describe('ChartAndTitleWrapper', () => {
@@ -14,7 +17,9 @@ describe('ChartAndTitleWrapper', () => {
};
render();
const icon = screen.getByTestId('TrendingUpSharpIcon');
+ const newLabel = screen.queryByLabelText('new label');
+ expect(newLabel).not.toBeInTheDocument();
expect(icon).toBeInTheDocument();
expect(icon.parentElement?.parentElement).toHaveStyle({ color: theme.main.chartTrend.betterColor });
});
@@ -28,7 +33,9 @@ describe('ChartAndTitleWrapper', () => {
};
render();
const icon = screen.getByTestId('TrendingDownSharpIcon');
+ const newLabel = screen.queryByLabelText('new label');
+ expect(newLabel).not.toBeInTheDocument();
expect(screen.getByTestId('TrendingDownSharpIcon')).toBeInTheDocument();
expect(icon.parentElement?.parentElement).toHaveStyle({ color: theme.main.chartTrend.worseColor });
});
@@ -42,34 +49,89 @@ describe('ChartAndTitleWrapper', () => {
};
render();
+ const newLabel = screen.queryByLabelText('new label');
+
expect(screen.getByLabelText('trend number')).toHaveTextContent('83.72%');
+ expect(newLabel).not.toBeInTheDocument();
});
- it('should show the switch button group when chart type is classification', async () => {
+ it('should show the switch button group and show new label when chart type is classification and version is less than 1.3.0', async () => {
+ const store = setupStore();
+ store.dispatch(saveVersion('1.2.1'));
const testedTrendInfo = {
type: ChartType.Classification,
};
const clickSwitchClassificationModel = jest.fn();
render(
- ,
+
+
+ ,
);
expect(screen.getByLabelText('classification test switch model button group')).toBeInTheDocument();
const cardCountSwitchButton = screen.getByLabelText('classification test switch card count model button');
const storyPointsSwitchButton = screen.getByLabelText('classification test switch story points model button');
+ const newLabel = screen.getByLabelText('new label');
expect(cardCountSwitchButton).toBeInTheDocument();
expect(storyPointsSwitchButton).toBeInTheDocument();
+ expect(newLabel).toBeInTheDocument();
await userEvent.click(cardCountSwitchButton);
await userEvent.click(storyPointsSwitchButton);
expect(clickSwitchClassificationModel).toHaveBeenCalledTimes(2);
});
+
+ it('should show new label when version is equal to 1.3.0', () => {
+ const store = setupStore();
+ store.dispatch(saveVersion('1.3.0'));
+ const testedTrendInfo = {
+ type: ChartType.Classification,
+ };
+ const clickSwitchClassificationModel = jest.fn();
+ render(
+
+
+ ,
+ );
+
+ const newLabel = screen.getByLabelText('new label');
+
+ expect(newLabel).toBeInTheDocument();
+ });
+
+ it('should not show new label when version is more than 1.3.0', () => {
+ const store = setupStore();
+ store.dispatch(saveVersion('2.3.0'));
+ const testedTrendInfo = {
+ type: ChartType.Classification,
+ };
+ const clickSwitchClassificationModel = jest.fn();
+ render(
+
+
+ ,
+ );
+
+ const newLabel = screen.queryByLabelText('new label');
+
+ expect(newLabel).not.toBeInTheDocument();
+ });
});
diff --git a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx
index 567ec1cce1..4bbcd4bc19 100644
--- a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx
+++ b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx
@@ -7,12 +7,17 @@ import { RETRY } from '@src/constants/resources';
import { Provider } from 'react-redux';
import React from 'react';
import clearAllMocks = jest.clearAllMocks;
+import { saveVersion } from '@src/context/meta/metaSlice';
jest.mock('@src/utils/util', () => ({
...jest.requireActual('@src/utils/util'),
getDeviceSize: jest.fn().mockReturnValue('lg'),
}));
+jest.mock('semver', () => ({
+ gt: jest.fn((version, initVersion) => version > initVersion),
+}));
+
describe('Report Card', () => {
afterEach(() => {
clearAllMocks();
@@ -38,8 +43,9 @@ describe('Report Card', () => {
const mockHandleRetry = jest.fn();
const onShowDetail = jest.fn();
- const setup = () => {
+ const setup = (version: string = '1.2.1') => {
store = setupStore();
+ store.dispatch(saveVersion(version));
return render(
{
expect(screen.queryByLabelText('dora metrics dialog')).not.toBeInTheDocument();
});
+
+ it('should show four new labels in the dora metrics when version is less than 1.3.0', () => {
+ setup();
+
+ const newLabels = screen.queryAllByLabelText('new label');
+
+ expect(newLabels.length).toEqual(4);
+ newLabels.forEach((it) => expect(it).toBeInTheDocument());
+ });
+
+ it('should not show any new label in the dora metrics when version is more than 1.3.0', () => {
+ setup('1.3.0.1');
+
+ const newLabels = screen.queryAllByLabelText('new label');
+
+ expect(newLabels.length).toEqual(0);
+ });
});
diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx
index c683457e67..6af03981df 100644
--- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx
+++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx
@@ -42,6 +42,7 @@ import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect';
import { backStep, updateReportId } from '@src/context/stepper/StepperSlice';
import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect';
import ReportStep, { showChart } from '@src/containers/ReportStep';
+import { saveVersion } from '@src/context/meta/metaSlice';
import { setupStore } from '../../utils/setupStoreUtil';
import userEvent from '@testing-library/user-event';
import React, { ReactNode } from 'react';
@@ -156,8 +157,9 @@ describe('Report Step', () => {
];
};
const handleSaveMock = jest.fn();
- const setup = (params: string[], dateRange: DateRangeList = [fullValueDateRange]) => {
+ const setup = (params: string[], dateRange: DateRangeList = [fullValueDateRange], version: string = '1.2.1') => {
dateRange && store.dispatch(updateDateRange(dateRange));
+ store.dispatch(saveVersion(version));
store.dispatch(
updateJiraVerifyResponse({
jiraColumns: MOCK_JIRA_VERIFY_RESPONSE.jiraColumns,
@@ -869,7 +871,7 @@ describe('Report Step', () => {
jest.spyOn(echarts, 'init').mockImplementation(() => chart as unknown as echarts.ECharts);
});
- it('should correctly render dora chart', async () => {
+ it('should correctly render dora chart and show new label when version is less than 1.3.0', async () => {
reportHook.current.reportInfos[0].reportData = { ...MOCK_REPORT_MOCK_PIPELINE_RESPONSE };
setup(REQUIRED_DATA_LIST, [fullValueDateRange, emptyValueDateRange]);
@@ -877,6 +879,11 @@ describe('Report Step', () => {
const switchChartButton = screen.getByText(DISPLAY_TYPE.CHART);
await userEvent.click(switchChartButton);
+ const classificationNewLabels = screen.getAllByLabelText('new label');
+
+ expect(classificationNewLabels.length).toEqual(2);
+ classificationNewLabels.forEach((it) => expect(it).toBeInTheDocument());
+
const switchDoraChartButton = screen.getByText(CHART_TYPE.DORA);
await userEvent.click(switchDoraChartButton);
@@ -904,6 +911,9 @@ describe('Report Step', () => {
await userEvent.click(pipelineSelector);
});
expect(pipelineSelectorInput).toHaveValue('mock pipeline name/mock step1');
+ const pipelineNewLabels = screen.getAllByLabelText('new label');
+ expect(pipelineNewLabels.length).toEqual(1);
+ pipelineNewLabels.forEach((it) => expect(it).toBeInTheDocument());
expect(addNotification).toHaveBeenCalledWith({
message: MESSAGE.EXPIRE_INFORMATION,
diff --git a/frontend/__tests__/containers/ShareReport/ShareReport.test.tsx b/frontend/__tests__/containers/ShareReport/ShareReport.test.tsx
index 83c1b0b73f..9d760cb6c1 100644
--- a/frontend/__tests__/containers/ShareReport/ShareReport.test.tsx
+++ b/frontend/__tests__/containers/ShareReport/ShareReport.test.tsx
@@ -13,6 +13,9 @@ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
+jest.mock('semver', () => ({
+ gt: jest.fn((version, initVersion) => version > initVersion),
+}));
let store = setupStore();
diff --git a/frontend/package.json b/frontend/package.json
index 338baf3b7b..cea2e76cbb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -65,6 +65,7 @@
"react-hook-form": "^7.51.3",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.0",
+ "semver": "^7.6.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-pwa": "^0.20.0",
@@ -85,6 +86,7 @@
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.3.0",
"@types/react-redux": "^7.1.33",
+ "@types/semver": "^7.5.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@vitejs/plugin-react-swc": "^3.6.0",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 08f8e5a1e3..251e5a89a1 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -71,6 +71,9 @@ importers:
react-router-dom:
specifier: ^6.23.0
version: 6.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ semver:
+ specifier: ^7.6.3
+ version: 7.6.3
typescript:
specifier: ^5.4.5
version: 5.4.5
@@ -126,6 +129,9 @@ importers:
'@types/react-redux':
specifier: ^7.1.33
version: 7.1.33
+ '@types/semver':
+ specifier: ^7.5.8
+ version: 7.5.8
'@typescript-eslint/eslint-plugin':
specifier: ^7.8.0
version: 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
@@ -3671,10 +3677,6 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
- lru-cache@6.0.0:
- resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
- engines: {node: '>=10'}
-
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -4287,8 +4289,8 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
- semver@7.6.0:
- resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
+ semver@7.6.3:
+ resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
hasBin: true
@@ -4981,9 +4983,6 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
- yallist@4.0.0:
- resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
-
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
@@ -5905,7 +5904,7 @@ snapshots:
object-treeify: 1.1.33
open: 8.4.2
ora: 5.4.1
- semver: 7.6.0
+ semver: 7.6.3
undici: 5.28.4
which: 4.0.0
winston: 3.12.0
@@ -6949,7 +6948,7 @@ snapshots:
graphemer: 1.4.0
ignore: 5.3.1
natural-compare: 1.4.0
- semver: 7.6.0
+ semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.4.5)
optionalDependencies:
typescript: 5.4.5
@@ -6996,7 +6995,7 @@ snapshots:
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
- semver: 7.6.0
+ semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.4.5)
optionalDependencies:
typescript: 5.4.5
@@ -7012,7 +7011,7 @@ snapshots:
'@typescript-eslint/types': 7.8.0
'@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5)
eslint: 8.57.0
- semver: 7.6.0
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
- typescript
@@ -7209,7 +7208,7 @@ snapshots:
event-stream: 4.0.1
jju: 1.4.0
readline-transform: 1.0.0
- semver: 7.6.0
+ semver: 7.6.3
yargs: 17.7.2
autoprefixer@10.4.19(postcss@8.4.38):
@@ -7488,7 +7487,7 @@ snapshots:
json-schema-typed: 7.0.3
onetime: 5.1.2
pkg-up: 3.1.0
- semver: 7.6.0
+ semver: 7.6.3
convert-source-map@1.9.0: {}
@@ -7969,7 +7968,7 @@ snapshots:
globals: 15.1.0
ignore: 5.3.1
minimatch: 9.0.3
- semver: 7.6.0
+ semver: 7.6.3
eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5):
dependencies:
@@ -8602,7 +8601,7 @@ snapshots:
'@babel/parser': 7.24.1
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
- semver: 7.6.0
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
@@ -8918,7 +8917,7 @@ snapshots:
jest-util: 29.7.0
natural-compare: 1.4.0
pretty-format: 29.7.0
- semver: 7.6.0
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
@@ -9180,10 +9179,6 @@ snapshots:
dependencies:
yallist: 3.1.1
- lru-cache@6.0.0:
- dependencies:
- yallist: 4.0.0
-
lz-string@1.5.0: {}
magic-string@0.25.9:
@@ -9192,7 +9187,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.6.0
+ semver: 7.6.3
make-error@1.3.6: {}
@@ -9783,9 +9778,7 @@ snapshots:
semver@6.3.1: {}
- semver@7.6.0:
- dependencies:
- lru-cache: 6.0.0
+ semver@7.6.3: {}
serialize-javascript@6.0.2:
dependencies:
@@ -10105,7 +10098,7 @@ snapshots:
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
- semver: 7.6.0
+ semver: 7.6.3
typescript: 5.4.5
yargs-parser: 21.1.1
optionalDependencies:
@@ -10549,8 +10542,6 @@ snapshots:
yallist@3.1.1: {}
- yallist@4.0.0: {}
-
yaml@1.10.2: {}
yaml@2.3.4: {}
diff --git a/frontend/src/components/Common/NewFunctionsLabel/index.tsx b/frontend/src/components/Common/NewFunctionsLabel/index.tsx
new file mode 100644
index 0000000000..ec3fc4c56a
--- /dev/null
+++ b/frontend/src/components/Common/NewFunctionsLabel/index.tsx
@@ -0,0 +1,37 @@
+import {
+ NewFunctionsContent,
+ NewLabel,
+ NewLabelWithCustomizeMarginAndHeight,
+} from '@src/components/Common/NewFunctionsLabel/style';
+import { getVersion } from '@src/context/meta/metaSlice';
+import { NewLabelType } from '@src/constants/commons';
+import { useAppSelector } from '@src/hooks';
+import React, { ReactNode } from 'react';
+import semver from 'semver';
+
+interface NewFunctionsLabelProps {
+ children: ReactNode;
+ initVersion: string;
+ newLabelType?: NewLabelType;
+}
+
+export default function ({ children, newLabelType = NewLabelType.General, initVersion }: NewFunctionsLabelProps) {
+ const version = useAppSelector(getVersion);
+ const showNewLabel = () => {
+ if (!semver.gt(version, initVersion)) {
+ if (newLabelType === NewLabelType.CustomizeMarginAndHeight) {
+ return (
+ NEW
+ );
+ } else {
+ return NEW;
+ }
+ }
+ };
+ return (
+
+ {children}
+ {showNewLabel()}
+
+ );
+}
diff --git a/frontend/src/components/Common/NewFunctionsLabel/style.tsx b/frontend/src/components/Common/NewFunctionsLabel/style.tsx
new file mode 100644
index 0000000000..7bcc0448bb
--- /dev/null
+++ b/frontend/src/components/Common/NewFunctionsLabel/style.tsx
@@ -0,0 +1,25 @@
+import { styled } from '@mui/material/styles';
+import { theme } from '@src/theme';
+
+export const NewFunctionsContent = styled('div')({
+ display: 'flex',
+ gap: '1rem',
+ [theme.breakpoints.down('md')]: {
+ gap: '0.2rem',
+ },
+});
+
+export const NewLabel = styled('div')({
+ padding: '0 0.3rem',
+ fontSize: '1rem',
+ height: '1.5rem',
+ lineHeight: '1.5rem',
+ color: theme.components?.newFunctionsLabel.color,
+ backgroundColor: theme.components?.newFunctionsLabel.backgroundColor,
+});
+
+export const NewLabelWithCustomizeMarginAndHeight = styled(NewLabel)({
+ margin: '1rem 0 0',
+ height: '2rem',
+ lineHeight: '2rem',
+});
diff --git a/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx b/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx
index cadfd4b3c2..c19783cd37 100644
--- a/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx
+++ b/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx
@@ -6,6 +6,7 @@ import {
import { ReportCardItem, ReportCardItemProps } from '@src/components/Common/ReportGrid/ReportCardItem';
import { DoraMetricsDialog } from '@src/components/Common/ReportGrid/ReportCard/DoraMetricsDialog';
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined';
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
import { ErrorMessagePrompt } from '@src/components/ErrorMessagePrompt';
import { StyledLink } from '@src/containers/MetricsStep/style';
import { DORA_METRICS } from '@src/constants/resources';
@@ -94,9 +95,11 @@ export const ReportCard = ({ title, items, xs, errorMessage }: ReportCardProps)
{title}
{DORA_METRICS.some((it) => it.toLowerCase() === title.toLowerCase()) && (
-
-
-
+
+
+
+
+
)}
{errorMessage && }
diff --git a/frontend/src/constants/commons.ts b/frontend/src/constants/commons.ts
index 446b5ccf24..3dc903772f 100644
--- a/frontend/src/constants/commons.ts
+++ b/frontend/src/constants/commons.ts
@@ -107,3 +107,8 @@ export const ANIMATION_SECONDS = 0.5;
export const EVERY_FRAME_MILLISECOND = 17;
export const MILLISECONDS_PER_SECOND = 1000;
+
+export enum NewLabelType {
+ General = 'general',
+ CustomizeMarginAndHeight = 'customize margin and height',
+}
diff --git a/frontend/src/containers/MetricsStep/Classification/index.tsx b/frontend/src/containers/MetricsStep/Classification/index.tsx
index ff163771e7..413c2c4743 100644
--- a/frontend/src/containers/MetricsStep/Classification/index.tsx
+++ b/frontend/src/containers/MetricsStep/Classification/index.tsx
@@ -9,6 +9,7 @@ import { Checkbox, createFilterOptions, FilterOptionsState, TextField } from '@m
import { MetricsSettingTitle } from '@src/components/Common/MetricsSettingTitle';
import { WarningNotification } from '@src/components/Common/WarningNotification';
import { FormGroupWrapper } from '@src/components/Common/FormGroupWrapper';
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
import { formatDuplicatedNameWithSuffix } from '@src/utils/util';
import { useAppDispatch } from '@src/hooks/useAppDispatch';
import { ALL_OPTION_META } from '@src/constants/resources';
@@ -157,7 +158,13 @@ export const Classification = ({ targetFields, title, label }: classificationPro
);
}}
- renderInput={(params) => }
+ renderInput={(params) => (
+ Visible in charts (optional)}
+ />
+ )}
slotProps={{
popper: {
sx: {
diff --git a/frontend/src/containers/MetricsStep/CycleTime/Table/CellAutoComplete.tsx b/frontend/src/containers/MetricsStep/CycleTime/Table/CellAutoComplete.tsx
index 054cfc24d0..75588e7385 100644
--- a/frontend/src/containers/MetricsStep/CycleTime/Table/CellAutoComplete.tsx
+++ b/frontend/src/containers/MetricsStep/CycleTime/Table/CellAutoComplete.tsx
@@ -1,6 +1,7 @@
import { StyledTextField } from '@src/containers/MetricsStep/CycleTime/Table/style';
import React, { useState, useCallback, SyntheticEvent, useEffect } from 'react';
-import { CYCLE_TIME_LIST } from '@src/constants/resources';
+import { CYCLE_TIME_LIST, METRICS_CONSTANTS } from '@src/constants/resources';
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
import { Z_INDEX } from '@src/constants/commons';
import { Autocomplete } from '@mui/material';
@@ -44,6 +45,15 @@ const CellAutoComplete = ({ name, defaultSelected, onSelect, customRenderInput }
onChange={handleSelectOnChange}
inputValue={inputValue}
onInputChange={handleInputOnChange}
+ renderOption={(props, option: string) => {
+ const value =
+ option === METRICS_CONSTANTS.designValue || option === METRICS_CONSTANTS.waitingForDeploymentValue ? (
+ {option}
+ ) : (
+ option
+ );
+ return {value};
+ }}
renderInput={renderInput}
slotProps={{
popper: {
diff --git a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/index.tsx b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/index.tsx
index 809f4dda69..d784e079a1 100644
--- a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/index.tsx
+++ b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/index.tsx
@@ -12,6 +12,7 @@ import {
import { ClassificationChartModelType } from '@src/containers/ReportStep/BoardMetricsChart/ClassificationChart';
import { CHART_TREND_TIP, ChartType, TrendIcon, TrendType, UP_TREND_IS_BETTER } from '@src/constants/resources';
import TrendingDownSharpIcon from '@mui/icons-material/TrendingDownSharp';
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
import TrendingUpSharpIcon from '@mui/icons-material/TrendingUpSharp';
import { ChartWrapper } from '@src/containers/MetricsStep/style';
import { convertNumberToPercent } from '@src/utils/util';
@@ -105,18 +106,14 @@ const ChartAndTitleWrapper = forwardRef(
}}
>
{isLoading && }
- {isShowSwitch && (
-
-
-
- )}
- {trendInfo.type} {subTitle && `: ${subTitle}`}
+ {subTitle === undefined ? (
+ trendInfo.type
+ ) : (
+
+ {trendInfo.type} {`: ${subTitle}`}
+
+ )}
{trendInfo.trendNumber !== undefined && !isLoading && (
)}
+ {isShowSwitch && (
+
+
+
+ )}
{trendInfo.type === ChartType.Classification && (
diff --git a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx
index 551580b817..e110b93da5 100644
--- a/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx
+++ b/frontend/src/containers/ReportStep/ChartAndTitleWrapper/style.tsx
@@ -6,9 +6,6 @@ export const SwitchIconWrapper = styled('img')(
({ disabledClickRepeatButton }: { disabledClickRepeatButton: boolean }) => ({
width: '1.5rem',
color: theme.main.backgroundColor,
- position: 'absolute',
- right: '1.75rem',
- top: '1.75rem',
cursor: disabledClickRepeatButton ? 'not-allowed' : 'pointer',
zIndex: Z_INDEX.BUTTONS,
}),
@@ -30,10 +27,16 @@ export const StyledChartAndTitleWrapper = styled('div')({
export const ChartTitle = styled('div')({
display: 'flex',
+ justifyContent: 'space-between',
alignItems: 'center',
position: 'absolute',
top: '1.75rem',
left: '1.75rem',
+ right: '1.75rem',
+ [theme.breakpoints.down('md')]: {
+ left: '0.75rem',
+ right: '0.75rem',
+ },
zIndex: '1',
fontSize: '1.2rem',
fontWeight: 'bold',
diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx
index 6f65dfea74..4851e1aff0 100644
--- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx
+++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/index.tsx
@@ -28,7 +28,6 @@ export default function PipelineSelector({ options, value, onUpDatePipeline, tit
disableClearable
sx={{
flex: 1,
- paddingRight: '1.25rem',
marginLeft: '1rem',
minWidth: '22rem',
}}
diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx
index 95ba510e92..4fea8de048 100644
--- a/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx
+++ b/frontend/src/containers/ReportStep/DoraMetricsChart/PipelineSelector/style.tsx
@@ -9,5 +9,6 @@ export const PipelinesSelectContainer = styled('div')({
lineHeight: '2rem',
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
+ width: '7rem',
},
});
diff --git a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx
index 9cc1a0b0a7..8b515a2000 100644
--- a/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx
+++ b/frontend/src/containers/ReportStep/DoraMetricsChart/index.tsx
@@ -10,8 +10,8 @@ import {
import {
oneLineOptionMapper,
Series,
- stackedBarOptionMapper,
stackedAreaOptionMapper,
+ stackedBarOptionMapper,
} from '@src/containers/ReportStep/ChartOption';
import {
AREA_STYLE,
@@ -23,10 +23,11 @@ import PipelineSelector from '@src/containers/ReportStep/DoraMetricsChart/Pipeli
import { ReportResponse, ReportResponseDTO } from '@src/clients/report/dto/response';
import ChartAndTitleWrapper from '@src/containers/ReportStep/ChartAndTitleWrapper';
import { calculateTrendInfo, percentageFormatter } from '@src/utils/util';
+import NewFunctionsLabel from '@src/components/Common/NewFunctionsLabel';
+import { EMPTY_STRING, NewLabelType } from '@src/constants/commons';
import { ChartContainer } from '@src/containers/MetricsStep/style';
import { reportMapper } from '@src/hooks/reportMapper/report';
import { showChart } from '@src/containers/ReportStep';
-import { EMPTY_STRING } from '@src/constants/commons';
import { theme } from '@src/theme';
interface DoraMetricsChartProps {
@@ -325,12 +326,14 @@ export const DoraMetricsChart = ({
return (
<>
- onUpdatePipeline(value)}
- title={'Pipeline/Step'}
- />
+
+ onUpdatePipeline(value)}
+ title={'Pipeline/Step'}
+ />
+
{metrics.includes(RequiredData.LeadTimeForChanges) && (