diff --git a/.github/workflows/BuildAndDeploy.yml b/.github/workflows/BuildAndDeploy.yml index 8f37439d45..174c583157 100644 --- a/.github/workflows/BuildAndDeploy.yml +++ b/.github/workflows/BuildAndDeploy.yml @@ -118,7 +118,7 @@ jobs: uses: gradle/gradle-build-action@v2.10.0 - name: License check run: ./gradlew clean checkLicense - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: backend-license-report @@ -484,7 +484,7 @@ jobs: env: APP_ORIGIN: ${{ vars.APP_HTTP_SCHEDULE }}://${{ secrets.AWS_EC2_IP_E2E }}:${{ secrets.AWS_EC2_IP_E2E_FRONTEND_PORT }} run: pnpm run e2e - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: cypress-videos diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index 122d1d32ff..9e6a895e76 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -27,7 +27,7 @@ jobs: uses: gradle/gradle-build-action@v2.10.0 - name: Build run: ./gradlew clean build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: backend-app path: backend/build/libs/heartbeat-backend-0.0.1-SNAPSHOT.jar @@ -53,7 +53,7 @@ jobs: - name: Build run: | pnpm run build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: frontend-app path: frontend/dist @@ -98,12 +98,12 @@ jobs: steps: - uses: actions/checkout@v4 - name: Download frontend artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: frontend-app path: ./${{ env.REPO_NAME }}-frontend - name: Download backend artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: backend-app - name: List diff --git a/backend/src/main/java/heartbeat/config/CacheConfig.java b/backend/src/main/java/heartbeat/config/CacheConfig.java index e62ffcd8a3..aef139a6ed 100644 --- a/backend/src/main/java/heartbeat/config/CacheConfig.java +++ b/backend/src/main/java/heartbeat/config/CacheConfig.java @@ -37,7 +37,7 @@ public CacheManager ehCacheManager() { @SuppressWarnings("unchecked") private javax.cache.configuration.Configuration getCacheConfiguration(Class valueType) { val offHeap = ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(2, MemoryUnit.MB); - val timeToLive = Duration.ofSeconds(20); + val timeToLive = Duration.ofSeconds(180); CacheConfigurationBuilder configuration = CacheConfigurationBuilder .newCacheConfigurationBuilder((Class) String.class, valueType, offHeap) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(timeToLive)); diff --git a/backend/src/main/java/heartbeat/config/ThreadPoolConfig.java b/backend/src/main/java/heartbeat/config/ThreadPoolConfig.java index c63fd0f93a..896bdba278 100644 --- a/backend/src/main/java/heartbeat/config/ThreadPoolConfig.java +++ b/backend/src/main/java/heartbeat/config/ThreadPoolConfig.java @@ -10,7 +10,7 @@ public class ThreadPoolConfig { @Bean("customTaskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(10); + executor.setCorePoolSize(20); executor.setMaxPoolSize(100); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); diff --git a/frontend/__tests__/src/components/Metrics/MetricsStepper/MetricsStepper.test.tsx b/frontend/__tests__/src/components/Metrics/MetricsStepper/MetricsStepper.test.tsx index 081707b65e..38c1662d23 100644 --- a/frontend/__tests__/src/components/Metrics/MetricsStepper/MetricsStepper.test.tsx +++ b/frontend/__tests__/src/components/Metrics/MetricsStepper/MetricsStepper.test.tsx @@ -90,6 +90,16 @@ jest.mock('@src/utils/util', () => ({ filterAndMapCycleTimeSettings: jest.fn(), })) +jest.mock('@src/hooks/useGenerateReportEffect', () => ({ + useGenerateReportEffect: jest.fn().mockReturnValue({ + startPollingReports: jest.fn(), + stopPollingReports: jest.fn(), + isLoading: false, + isServerError: false, + errorMessage: '', + }), +})) + const server = setupServer(rest.post(MOCK_REPORT_URL, (_, res, ctx) => res(ctx.status(HttpStatusCode.Ok)))) const mockLocation = { reload: jest.fn() } @@ -342,6 +352,38 @@ describe('MetricsStepper', () => { expect(exportToJsonFile).toHaveBeenCalledWith(expectedFileName, expectedJson) }, 50000) + it('should export json file when click save button in report page given all content is empty', async () => { + const expectedFileName = 'config' + const expectedJson = { + assigneeFilter: ASSIGNEE_FILTER_TYPES.LAST_ASSIGNEE, + board: { boardId: '', email: '', projectKey: '', site: '', token: '', type: 'Jira' }, + calendarType: 'Regular Calendar(Weekend Considered)', + dateRange: { + endDate: dayjs().endOf('date').add(13, 'day').format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + startDate: dayjs().startOf('date').format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + }, + metrics: ['Velocity'], + pipelineTool: undefined, + projectName: 'test project Name', + sourceControl: undefined, + classification: undefined, + crews: undefined, + cycleTime: undefined, + deployment: undefined, + doneStatus: undefined, + leadTime: undefined, + } + + const { getByText } = setup() + await fillConfigPageData() + await userEvent.click(getByText(NEXT)) + await fillMetricsPageDate() + await userEvent.click(getByText(NEXT)) + await userEvent.click(getByText(SAVE)) + + expect(exportToJsonFile).toHaveBeenCalledWith(expectedFileName, expectedJson) + }, 50000) + it('should clean the config information that is hidden when click next button', async () => { const { getByText } = setup() diff --git a/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx b/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx index 0f2850358a..e6f6eb7ea7 100644 --- a/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/src/components/Metrics/ReportStep/ReportStep.test.tsx @@ -9,6 +9,7 @@ import { EXPORT_PIPELINE_DATA, MOCK_JIRA_VERIFY_RESPONSE, REQUIRED_DATA_LIST, + SAVE, } from '../../../fixtures' import { setupStore } from '../../../utils/setupStoreUtil' import { Provider } from 'react-redux' @@ -79,6 +80,7 @@ describe('Report Step', () => { reportHook.current.errorMessage = '' reportHook.current.reports = EXPECTED_REPORT_VALUES } + const handleSaveMock = jest.fn() const setup = (params: [string]) => { store = setupStore() store.dispatch( @@ -112,7 +114,7 @@ describe('Report Step', () => { ) return render( - + ) } @@ -153,6 +155,15 @@ describe('Report Step', () => { expect(backStep).toHaveBeenCalledTimes(1) }) + it('should call handleSaveMock method when click save button', async () => { + const { getByText } = setup(['']) + + const save = getByText(SAVE) + await userEvent.click(save) + + expect(handleSaveMock).toHaveBeenCalledTimes(1) + }) + it('should not show export pipeline button when not select deployment frequency', () => { const { queryByText } = setup([REQUIRED_DATA_LIST[1]]) diff --git a/frontend/cypress/e2e/importAProject.cy.ts b/frontend/cypress/e2e/importAProject.cy.ts index 4d03ca088d..ec506173fc 100644 --- a/frontend/cypress/e2e/importAProject.cy.ts +++ b/frontend/cypress/e2e/importAProject.cy.ts @@ -118,6 +118,18 @@ const checkRequiredFields = () => { metricsPage.nextButton.should('be.enabled') } +const checkProjectConfig = () => { + cy.wait(2000) + cy.fixture('config.json').then((localFileContent) => { + cy.readFile('cypress/downloads/config.json').then((fileContent) => { + expect(fileContent.sourceControl.token).to.eq(GITHUB_TOKEN) + for (const key in localFileContent) { + expect(fileContent[key]).to.deep.eq(localFileContent[key]) + } + }) + }) +} + describe('Import project from file', () => { beforeEach(() => { cy.waitForNetworkIdlePrepare({ @@ -152,6 +164,10 @@ describe('Import project from file', () => { checkMeanTimeToRecovery('[data-test-id="Mean Time To Recovery"]') + reportPage.exportProjectConfig() + + checkProjectConfig() + reportPage.backToMetricsStep() checkFieldsExist(metricsTextList) diff --git a/frontend/cypress/fixtures/config.json b/frontend/cypress/fixtures/config.json new file mode 100644 index 0000000000..3a62d2eef1 --- /dev/null +++ b/frontend/cypress/fixtures/config.json @@ -0,0 +1,101 @@ +{ + "projectName": "ConfigFileForImporting", + "dateRange": { + "startDate": "2022-09-01T00:00:00.000+08:00", + "endDate": "2022-09-14T23:59:59.999+08:00" + }, + "calendarType": "Calendar with Chinese Holiday", + "metrics": [ + "Velocity", + "Cycle time", + "Classification", + "Lead time for changes", + "Deployment frequency", + "Mean time to recovery" + ], + "board": { + "type": "Classic Jira", + "boardId": "1963", + "email": "test@test.com", + "projectKey": "PLL", + "site": "mockSite", + "token": "mockToken" + }, + "pipelineTool": { + "type": "BuildKite", + "token": "mockToken" + }, + "crews": ["Yu Zhang"], + "assigneeFilter": "lastAssignee", + "cycleTime": { + "jiraColumns": [ + { + "In Analysis": "Analysis" + }, + { + "Ready For Dev": "To do" + }, + { + "In Dev": "In Dev" + }, + { + "Blocked": "Block" + }, + { + "Ready For Test": "Waiting for testing" + }, + { + "In Test": "Testing" + }, + { + "Ready to Deploy": "Review" + }, + { + "Done": "Done" + } + ], + "treatFlagCardAsBlock": true + }, + "doneStatus": ["DONE", "CLOSED"], + "classification": [ + "issuetype", + "customfield_21212", + "customfield_22466", + "parent", + "components", + "project", + "reporter", + "customfield_16400", + "fixVersions", + "priority", + "customfield_16800", + "labels", + "customfield_10004", + "customfield_10007", + "customfield_10008", + "assignee", + "customfield_22203", + "customfield_21871", + "customfield_10009", + "customfield_11700", + "environment", + "versions", + "customfield_22213", + "customfield_22231", + "customfield_17000", + "customfield_16302", + "customfield_14603", + "customfield_22229", + "customfield_22228", + "customfield_22226" + ], + "deployment": [ + { + "id": 0, + "organization": "XXXX", + "pipelineName": "fs-platform-onboarding", + "step": " :docker: publish gradle-cache image to cloudsmith", + "branches": ["PLL-1326", "PLL-1406", "PLL-1406-1", "master"] + } + ] +} diff --git a/frontend/cypress/pages/metrics/report.ts b/frontend/cypress/pages/metrics/report.ts index f20f4edc87..0895a98009 100644 --- a/frontend/cypress/pages/metrics/report.ts +++ b/frontend/cypress/pages/metrics/report.ts @@ -22,6 +22,10 @@ class Report { return cy.contains('Previous') } + get saveButton() { + return cy.contains('Save') + } + get exportMetricDataButton() { return cy.contains('Export metric data') } @@ -43,7 +47,7 @@ class Report { } backToMetricsStep() { - this.backButton.click() + this.backButton.click({ force: true }) } exportMetricData() { @@ -57,6 +61,10 @@ class Report { exportBoardData() { this.exportBoardDataButton.click() } + + exportProjectConfig() { + this.saveButton.click({ force: true }) + } } const reportPage = new Report() diff --git a/frontend/src/components/HomeGuide/index.tsx b/frontend/src/components/HomeGuide/index.tsx index b6f3b9423a..87810e4d57 100644 --- a/frontend/src/components/HomeGuide/index.tsx +++ b/frontend/src/components/HomeGuide/index.tsx @@ -8,7 +8,7 @@ import { updateMetricsImportedData } from '@src/context/Metrics/metricsSlice' import { resetStep } from '@src/context/stepper/StepperSlice' import { WarningNotification } from '@src/components/Common/WarningNotification' import { convertToNewFileConfig, NewFileConfig, OldFileConfig } from '@src/fileConfig/fileConfig' -import { GuideButton, HomeGuideContainer } from '@src/components/HomeGuide/style' +import { GuideButton, HomeGuideContainer, StyledStack } from '@src/components/HomeGuide/style' import { MESSAGE } from '@src/constants/resources' import { ROUTE } from '@src/constants/router' @@ -70,11 +70,11 @@ export const HomeGuide = () => { return ( {!validConfig && } - + Import project from file Create a new project - + ) } diff --git a/frontend/src/components/HomeGuide/style.tsx b/frontend/src/components/HomeGuide/style.tsx index 338520bb56..4c5deb8e13 100644 --- a/frontend/src/components/HomeGuide/style.tsx +++ b/frontend/src/components/HomeGuide/style.tsx @@ -1,6 +1,7 @@ import { theme } from '@src/theme' import Button, { ButtonProps } from '@mui/material/Button' import styled from '@emotion/styled' +import Stack from '@mui/material/Stack' export const basicStyle = { backgroundColor: theme.main.backgroundColor, @@ -27,7 +28,14 @@ export const GuideButton = styled(Button)({ }, }) +export const StyledStack = styled(Stack)({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%,-50%)', +}) + export const HomeGuideContainer = styled.div({ height: '44rem', - display: 'flex', + position: 'relative', }) diff --git a/frontend/src/components/Metrics/MetricsStepper/index.tsx b/frontend/src/components/Metrics/MetricsStepper/index.tsx index f28cf4bacf..e183c9d57c 100644 --- a/frontend/src/components/Metrics/MetricsStepper/index.tsx +++ b/frontend/src/components/Metrics/MetricsStepper/index.tsx @@ -2,7 +2,6 @@ import React, { lazy, Suspense, useEffect, useState } from 'react' import { BackButton, ButtonContainer, - ButtonGroup, MetricsStepperContent, NextButton, SaveButton, @@ -22,7 +21,7 @@ import { SOURCE_CONTROL_TYPES, TIPS, } from '@src/constants/resources' -import { METRICS_STEPS, STEPS } from '@src/constants/commons' +import { COMMON_BUTTONS, METRICS_STEPS, STEPS } from '@src/constants/commons' import { ConfirmDialog } from '@src/components/Metrics/MetricsStepper/ConfirmDialog' import { useNavigate } from 'react-router-dom' import { @@ -278,7 +277,7 @@ const MetricsStepper = (props: useNotificationLayoutEffectInterface) => { {activeStep === METRICS_STEPS.CONFIG && } {activeStep === METRICS_STEPS.METRICS && } - {activeStep === METRICS_STEPS.REPORT && } + {activeStep === METRICS_STEPS.REPORT && } @@ -286,17 +285,17 @@ const MetricsStepper = (props: useNotificationLayoutEffectInterface) => { <> }> - Save + {COMMON_BUTTONS.SAVE} - +
- Previous + {COMMON_BUTTONS.BACK} - Next + {COMMON_BUTTONS.NEXT} - +
)}
diff --git a/frontend/src/components/Metrics/MetricsStepper/style.tsx b/frontend/src/components/Metrics/MetricsStepper/style.tsx index 35973ac943..8227593791 100644 --- a/frontend/src/components/Metrics/MetricsStepper/style.tsx +++ b/frontend/src/components/Metrics/MetricsStepper/style.tsx @@ -82,8 +82,6 @@ export const NextButton = styled(Button)({ }, }) -export const ButtonGroup = styled('div')({}) - export const ButtonContainer = styled('div')({ display: 'flex', alignItems: 'center', diff --git a/frontend/src/components/Metrics/ReportStep/index.tsx b/frontend/src/components/Metrics/ReportStep/index.tsx index 846bb1a4ff..a713972d62 100644 --- a/frontend/src/components/Metrics/ReportStep/index.tsx +++ b/frontend/src/components/Metrics/ReportStep/index.tsx @@ -3,14 +3,19 @@ import { useGenerateReportEffect } from '@src/hooks/useGenerateReportEffect' import { Loading } from '@src/components/Loading' import { useAppSelector } from '@src/hooks' import { selectConfig, selectJiraColumns, selectMetrics } from '@src/context/config/configSlice' -import { CALENDAR, PIPELINE_STEP, NAME, REQUIRED_DATA, MESSAGE } from '@src/constants/resources' -import { INIT_REPORT_DATA_WITH_THREE_COLUMNS, INIT_REPORT_DATA_WITH_TWO_COLUMNS } from '@src/constants/commons' +import { CALENDAR, PIPELINE_STEP, NAME, REQUIRED_DATA, MESSAGE, TIPS } from '@src/constants/resources' +import { + COMMON_BUTTONS, + DOWNLOAD_TYPES, + INIT_REPORT_DATA_WITH_THREE_COLUMNS, + INIT_REPORT_DATA_WITH_TWO_COLUMNS, +} from '@src/constants/commons' import ReportForTwoColumns from '@src/components/Common/ReportForTwoColumns' import ReportForThreeColumns from '@src/components/Common/ReportForThreeColumns' import { CSVReportRequestDTO, ReportRequestDTO } from '@src/clients/report/dto/request' import { IPipelineConfig, selectMetricsContent } from '@src/context/Metrics/metricsSlice' import dayjs from 'dayjs' -import { BackButton } from '@src/components/Metrics/MetricsStepper/style' +import { BackButton, SaveButton } from '@src/components/Metrics/MetricsStepper/style' import { useExportCsvEffect } from '@src/hooks/useExportCsvEffect' import { backStep, selectTimeStamp } from '@src/context/stepper/StepperSlice' import { useAppDispatch } from '@src/hooks/useAppDispatch' @@ -23,8 +28,14 @@ import { filterAndMapCycleTimeSettings, getJiraBoardToken } from '@src/utils/uti import { useNotificationLayoutEffectInterface } from '@src/hooks/useNotificationLayoutEffect' import { ReportResponse } from '@src/clients/report/dto/response' import { ROUTE } from '@src/constants/router' +import { Tooltip } from '@mui/material' +import SaveAltIcon from '@mui/icons-material/SaveAlt' -const ReportStep = ({ updateProps }: useNotificationLayoutEffectInterface) => { +interface ReportStepInterface extends useNotificationLayoutEffectInterface { + handleSave: () => void +} + +const ReportStep = ({ updateProps, handleSave }: ReportStepInterface) => { const dispatch = useAppDispatch() const navigate = useNavigate() const { @@ -155,7 +166,11 @@ const ReportStep = ({ updateProps }: useNotificationLayoutEffectInterface) => { csvTimeStamp: csvTimeStamp, }) - const getExportCSV = (dataType: string, startDate: string | null, endDate: string | null): CSVReportRequestDTO => ({ + const getExportCSV = ( + dataType: DOWNLOAD_TYPES, + startDate: string | null, + endDate: string | null + ): CSVReportRequestDTO => ({ dataType: dataType, csvTimeStamp: csvTimeStamp, startDate: startDate ?? '', @@ -238,7 +253,7 @@ const ReportStep = ({ updateProps }: useNotificationLayoutEffectInterface) => { res?.exportValidityTimeMin && setExportValidityTimeMin(res.exportValidityTimeMin) } - const handleDownload = (dataType: string, startDate: string | null, endDate: string | null) => { + const handleDownload = (dataType: DOWNLOAD_TYPES, startDate: string | null, endDate: string | null) => { fetchExportData(getExportCSV(dataType, startDate, endDate)) } @@ -308,18 +323,29 @@ const ReportStep = ({ updateProps }: useNotificationLayoutEffectInterface) => { /> )} - - Previous - - handleDownload('metric', startDate, endDate)}>Export metric data - {isShowExportBoardButton && ( - handleDownload('board', startDate, endDate)}>Export board data - )} - {isShowExportPipelineButton && ( - handleDownload('pipeline', startDate, endDate)}> - Export pipeline data + + }> + {COMMON_BUTTONS.SAVE} + + +
+ + {COMMON_BUTTONS.BACK} + + handleDownload(DOWNLOAD_TYPES.METRICS, startDate, endDate)}> + {COMMON_BUTTONS.EXPORT_METRIC_DATA} - )} + {isShowExportBoardButton && ( + handleDownload(DOWNLOAD_TYPES.BOARD, startDate, endDate)}> + {COMMON_BUTTONS.EXPORT_BOARD_DATA} + + )} + {isShowExportPipelineButton && ( + handleDownload(DOWNLOAD_TYPES.PIPELINE, startDate, endDate)}> + {COMMON_BUTTONS.EXPORT_PIPELINE_DATA} + + )} +
{} diff --git a/frontend/src/components/Metrics/ReportStep/style.tsx b/frontend/src/components/Metrics/ReportStep/style.tsx index 0a83cf6514..01f1810307 100644 --- a/frontend/src/components/Metrics/ReportStep/style.tsx +++ b/frontend/src/components/Metrics/ReportStep/style.tsx @@ -29,7 +29,7 @@ export const ButtonGroupStyle = styled('div')({ display: 'flex', textAlign: 'center', margin: '0 auto', - justifyContent: 'flex-end', + justifyContent: 'space-between', width: '100%', }) diff --git a/frontend/src/constants/commons.ts b/frontend/src/constants/commons.ts index c2def90602..4247e08430 100644 --- a/frontend/src/constants/commons.ts +++ b/frontend/src/constants/commons.ts @@ -55,8 +55,23 @@ export const Z_INDEX = { FIXED: 1070, } +export enum DOWNLOAD_TYPES { + METRICS = 'metric', + BOARD = 'board', + PIPELINE = 'pipeline', +} + export const METRICS_STEPS = { CONFIG: 0, METRICS: 1, REPORT: 2, } + +export const COMMON_BUTTONS = { + SAVE: 'Save', + BACK: 'Previous', + NEXT: 'Next', + EXPORT_PIPELINE_DATA: 'Export pipeline data', + EXPORT_BOARD_DATA: 'Export board data', + EXPORT_METRIC_DATA: 'Export metric data', +} diff --git a/frontend/src/layouts/style.tsx b/frontend/src/layouts/style.tsx index 19e3cd5d76..e5b4a47624 100644 --- a/frontend/src/layouts/style.tsx +++ b/frontend/src/layouts/style.tsx @@ -9,7 +9,7 @@ export const LogoWarp = styled.div({ padding: '0 1rem', alignItems: 'center', backgroundColor: theme.main.backgroundColor, - fontFamily: theme.typography.fontFamily, + fontFamily: 'Times', zIndex: Z_INDEX.STICKY, position: 'sticky', top: 0,