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) && (