diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 46b1c6c1a6..969401ce55 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -6,6 +6,8 @@ body: - type: markdown attributes: value: | + ## Request Detail + The issue list is reserved exclusively for bug reports and feature requests. For usage questions, please use the following resources: @@ -54,8 +56,51 @@ body: description: What tools will support your request feature? multiple: true options: - - Board - - Pipeline Tool - - Source Control + - Board (like Jira) + - Pipeline Tool (like buildkite) + - Source Control (like github) + validations: + required: true + + - type: markdown + attributes: + value: | + ## Account Detail + + Let's know more about you and your account. We will horizontally evaluate all received requests to adjust the priority. + + **Below information are important in terms of prioritization.** + + - type: input + id: account_info + attributes: + label: Account name + description: What's your account name? + placeholder: Make sure it could be found in jigsaw + validations: + required: true + + - type: input + id: account_location + attributes: + label: Account location + description: Which country you account locate at? + validations: + required: true + + - type: input + id: account_size + attributes: + label: Teams in Account + description: How many teams will adopt heartbeat after feature release? + validations: + required: true + + - type: input + id: expected_date + attributes: + label: Expected launch date + description: What is the latest possible launch date you can accept? + placeholder: 2024-12 validations: - required: true \ No newline at end of file + required: false diff --git a/backend/build.gradle b/backend/build.gradle index 46692478e5..db3dc15e7c 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -56,7 +56,7 @@ dependencies { tasks.named('test') { useJUnitPlatform() testLogging { - events "passed", "skipped", "failed" + events "skipped", "failed" } finalizedBy jacocoTestReport } @@ -72,6 +72,7 @@ sonar { property "sonar.projectKey", "au-heartbeat-heartbeat-backend" property "sonar.organization", "au-heartbeat" property "sonar.host.url", "https://sonarcloud.io" + property "sonar.exclusions", "src/main/java/heartbeat/HeartbeatApplication.java,src/main/java/heartbeat/config/**,src/main/java/heartbeat/util/SystemUtil.java" } } @@ -100,6 +101,36 @@ jacocoTestCoverageVerification { violationRules { rule { limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 1.0 + } + } + rule { + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 1.0 + } + } + rule { + limit { + counter = 'METHOD' + value = 'COVEREDRATIO' + minimum = 1.0 + } + } + rule { + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.90 + } + } + rule { + limit { + counter = 'CLASS' + value = 'COVEREDRATIO' minimum = 1.0 } } diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfo.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfo.java index 2b2f06ee99..e02e766260 100644 --- a/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfo.java +++ b/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfo.java @@ -18,6 +18,8 @@ public class PullRequestInfo implements Serializable { private Integer number; + private String url; + @JsonProperty("created_at") private String createdAt; diff --git a/backend/src/main/java/heartbeat/config/SwaggerConfig.java b/backend/src/main/java/heartbeat/config/SwaggerConfig.java index 2c6fe9c26e..b3a7e323b6 100644 --- a/backend/src/main/java/heartbeat/config/SwaggerConfig.java +++ b/backend/src/main/java/heartbeat/config/SwaggerConfig.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -12,11 +14,14 @@ @Configuration public class SwaggerConfig { + @Value("${heartbeat.swagger.host}") + private String swaggerHost; + @Bean public OpenAPI customOpenAPI() { return new OpenAPI().components(new Components()) .info(new Info().title("Backend API").version("1.0")) - .servers(List.of(new Server().url("http://13.214.14.43:4321/api/v1"))); + .servers(List.of(new Server().url(String.format("%s/api/v1", this.swaggerHost)))); } } diff --git a/backend/src/main/java/heartbeat/handler/AsyncExceptionHandler.java b/backend/src/main/java/heartbeat/handler/AsyncExceptionHandler.java index fd51251e4a..3cb5c4e8c8 100644 --- a/backend/src/main/java/heartbeat/handler/AsyncExceptionHandler.java +++ b/backend/src/main/java/heartbeat/handler/AsyncExceptionHandler.java @@ -16,14 +16,14 @@ public class AsyncExceptionHandler extends AsyncDataBaseHandler { public void put(String reportId, BaseException e) { - createFileByType(ERROR, reportId, new Gson().toJson(e)); + createFileByType(ERROR, reportId, new Gson().toJson(new AsyncExceptionDTO(e))); } - public BaseException get(String reportId) { + public AsyncExceptionDTO get(String reportId) { return readFileByType(ERROR, reportId, AsyncExceptionDTO.class); } - public BaseException remove(String reportId) { + public AsyncExceptionDTO remove(String reportId) { return readAndRemoveFileByType(ERROR, reportId, AsyncExceptionDTO.class); } diff --git a/backend/src/main/java/heartbeat/handler/base/AsyncExceptionDTO.java b/backend/src/main/java/heartbeat/handler/base/AsyncExceptionDTO.java index 8e39fb6e25..53a8095d84 100644 --- a/backend/src/main/java/heartbeat/handler/base/AsyncExceptionDTO.java +++ b/backend/src/main/java/heartbeat/handler/base/AsyncExceptionDTO.java @@ -1,11 +1,20 @@ package heartbeat.handler.base; import heartbeat.exception.BaseException; +import lombok.AllArgsConstructor; +import lombok.Data; -public class AsyncExceptionDTO extends BaseException { +@Data +@AllArgsConstructor +public class AsyncExceptionDTO { - public AsyncExceptionDTO(String message, int status) { - super(message, status); + private String message; + + private int status; + + public AsyncExceptionDTO(BaseException e) { + this.message = e.getMessage(); + this.status = e.getStatus(); } } diff --git a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java index 8791757500..9ba69b9f69 100644 --- a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java +++ b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java @@ -1,6 +1,5 @@ package heartbeat.service.report; -import heartbeat.client.dto.codebase.github.PipelineLeadTime; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.JiraBoardSetting; import heartbeat.controller.report.dto.response.ErrorInfo; @@ -15,6 +14,7 @@ import heartbeat.handler.AsyncExceptionHandler; import heartbeat.handler.AsyncMetricsDataHandler; import heartbeat.handler.AsyncReportRequestHandler; +import heartbeat.handler.base.AsyncExceptionDTO; import heartbeat.service.report.calculator.ChangeFailureRateCalculator; import heartbeat.service.report.calculator.ClassificationCalculator; import heartbeat.service.report.calculator.CycleTimeCalculator; @@ -102,12 +102,10 @@ public void generateDoraReport(GenerateReportRequest request) { FetchedData fetchedData = new FetchedData(); if (CollectionUtils.isNotEmpty(request.getPipelineMetrics())) { GenerateReportRequest pipelineRequest = request.toPipelineRequest(); - fetchOriginalData(pipelineRequest, fetchedData); generatePipelineReport(pipelineRequest, fetchedData); } if (CollectionUtils.isNotEmpty(request.getSourceControlMetrics())) { GenerateReportRequest sourceControlRequest = request.toSourceControlRequest(); - fetchOriginalData(sourceControlRequest, fetchedData); generateSourceControlReport(sourceControlRequest, fetchedData); } generateCSVForPipeline(request, fetchedData.getBuildKiteData()); @@ -121,6 +119,7 @@ private void generatePipelineReport(GenerateReportRequest request, FetchedData f request.getPipelineMetrics(), request.getConsiderHoliday(), request.getStartTime(), request.getEndTime(), pipelineReportId); try { + fetchOriginalData(request, fetchedData); saveReporterInHandler(generatePipelineReporter(request, fetchedData), pipelineReportId); log.info( "Successfully generate pipeline report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _pipelineReportId: {}", @@ -131,6 +130,9 @@ private void generatePipelineReport(GenerateReportRequest request, FetchedData f asyncExceptionHandler.put(pipelineReportId, e); if (List.of(401, 403, 404).contains(e.getStatus())) asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(request.getDoraReportId(), DORA); + if (Objects.equals(400, e.getStatus())) { + throw e; + } } } @@ -141,6 +143,7 @@ private void generateSourceControlReport(GenerateReportRequest request, FetchedD request.getSourceControlMetrics(), request.getConsiderHoliday(), request.getStartTime(), request.getEndTime(), sourceControlReportId); try { + fetchOriginalData(request, fetchedData); saveReporterInHandler(generateSourceControlReporter(request, fetchedData), sourceControlReportId); log.info( "Successfully generate source control report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _sourceControlReportId: {}", @@ -151,6 +154,9 @@ private void generateSourceControlReport(GenerateReportRequest request, FetchedD asyncExceptionHandler.put(sourceControlReportId, e); if (List.of(401, 403, 404).contains(e.getStatus())) asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(request.getDoraReportId(), DORA); + if (Objects.equals(400, e.getStatus())) { + throw e; + } } } @@ -265,7 +271,7 @@ private void saveReporterInHandler(ReportResponse reportContent, String reportId asyncReportRequestHandler.putReport(reportId, reportContent); } - private ErrorInfo handleAsyncExceptionAndGetErrorInfo(BaseException exception) { + private ErrorInfo handleAsyncExceptionAndGetErrorInfo(AsyncExceptionDTO exception) { if (Objects.nonNull(exception)) { int status = exception.getStatus(); final String errorMessage = exception.getMessage(); @@ -349,9 +355,9 @@ public ReportResponse getComposedReportResponse(String reportId) { } private ReportMetricsError getReportErrorAndHandleAsyncException(String reportId) { - BaseException boardException = asyncExceptionHandler.get(IdUtil.getBoardReportId(reportId)); - BaseException pipelineException = asyncExceptionHandler.get(IdUtil.getPipelineReportId(reportId)); - BaseException sourceControlException = asyncExceptionHandler.get(IdUtil.getSourceControlReportId(reportId)); + AsyncExceptionDTO boardException = asyncExceptionHandler.get(IdUtil.getBoardReportId(reportId)); + AsyncExceptionDTO pipelineException = asyncExceptionHandler.get(IdUtil.getPipelineReportId(reportId)); + AsyncExceptionDTO sourceControlException = asyncExceptionHandler.get(IdUtil.getSourceControlReportId(reportId)); return ReportMetricsError.builder() .boardMetricsError(handleAsyncExceptionAndGetErrorInfo(boardException)) .pipelineMetricsError(handleAsyncExceptionAndGetErrorInfo(pipelineException)) diff --git a/backend/src/main/java/heartbeat/service/report/WorkDay.java b/backend/src/main/java/heartbeat/service/report/WorkDay.java index 162b4871ab..94eee3c74f 100644 --- a/backend/src/main/java/heartbeat/service/report/WorkDay.java +++ b/backend/src/main/java/heartbeat/service/report/WorkDay.java @@ -21,7 +21,7 @@ @RequiredArgsConstructor public class WorkDay { - private static final long ONE_DAY = 1000 * 60 * 60 * 24; + private static final long ONE_DAY = 1000L * 60 * 60 * 24; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); diff --git a/backend/src/main/java/heartbeat/service/source/github/GitHubService.java b/backend/src/main/java/heartbeat/service/source/github/GitHubService.java index 445a87f3e1..171f270da3 100644 --- a/backend/src/main/java/heartbeat/service/source/github/GitHubService.java +++ b/backend/src/main/java/heartbeat/service/source/github/GitHubService.java @@ -163,7 +163,8 @@ private LeadTime getLeadTimeByPullRequest(String realToken, PipelineInfoOfReposi } Optional mergedPull = pullRequestInfos.stream() - .filter(gitHubPull -> gitHubPull.getMergedAt() != null) + .filter(gitHubPull -> gitHubPull.getMergedAt() != null + && gitHubPull.getUrl().contains(item.getRepository())) .min(Comparator.comparing(PullRequestInfo::getNumber)); if (mergedPull.isEmpty()) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 213bd8e508..1ed158cf6c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -29,5 +29,7 @@ springdoc: path: /api-docs heartbeat: + swagger: + host: ${SWAGGER_HOST:http://localhost:4322} version: 1.1.5 diff --git a/backend/src/test/java/heartbeat/TestFixtures.java b/backend/src/test/java/heartbeat/TestFixtures.java index 0b237754cd..422a00aac1 100644 --- a/backend/src/test/java/heartbeat/TestFixtures.java +++ b/backend/src/test/java/heartbeat/TestFixtures.java @@ -2,7 +2,7 @@ public class TestFixtures { - public static final String GITHUB_TOKEN = "ghp_12345jhgyui987654rdef43567yhu7654321"; // gitleaks:allow + public static final String GITHUB_TOKEN = "ghp_" + "12345j".repeat(6); public static final String BUILDKITE_TOKEN = "bkua_6xxxafcc3bxxxxxxb8xxx8d8dxxxf7897cc8b2f1"; diff --git a/backend/src/test/java/heartbeat/handler/AsyncExceptionHandlerTest.java b/backend/src/test/java/heartbeat/handler/AsyncExceptionHandlerTest.java index 4a9780cf7e..5b51da35a8 100644 --- a/backend/src/test/java/heartbeat/handler/AsyncExceptionHandlerTest.java +++ b/backend/src/test/java/heartbeat/handler/AsyncExceptionHandlerTest.java @@ -1,8 +1,8 @@ package heartbeat.handler; -import heartbeat.exception.BaseException; import heartbeat.exception.GenerateReportException; import heartbeat.exception.UnauthorizedException; +import heartbeat.handler.base.AsyncExceptionDTO; import heartbeat.util.IdUtil; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.AfterAll; @@ -162,7 +162,7 @@ void shouldPutAndRemoveAsyncException() { String boardReportId = IdUtil.getBoardReportId(currentTime); asyncExceptionHandler.put(boardReportId, new UnauthorizedException("test")); - BaseException baseException = asyncExceptionHandler.remove(boardReportId); + AsyncExceptionDTO baseException = asyncExceptionHandler.remove(boardReportId); assertEquals(HttpStatus.UNAUTHORIZED.value(), baseException.getStatus()); assertEquals("test", baseException.getMessage()); diff --git a/backend/src/test/java/heartbeat/handler/base/AsyncExceptionDTOTest.java b/backend/src/test/java/heartbeat/handler/base/AsyncExceptionTest.java similarity index 75% rename from backend/src/test/java/heartbeat/handler/base/AsyncExceptionDTOTest.java rename to backend/src/test/java/heartbeat/handler/base/AsyncExceptionTest.java index b0909820f8..4f90d61f5f 100644 --- a/backend/src/test/java/heartbeat/handler/base/AsyncExceptionDTOTest.java +++ b/backend/src/test/java/heartbeat/handler/base/AsyncExceptionTest.java @@ -1,6 +1,5 @@ package heartbeat.handler.base; -import heartbeat.exception.BaseException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -12,7 +11,7 @@ void testAsyncExceptionDTOInheritance() { String expectedMessage = "Test Message"; int expectedStatus = 404; - BaseException baseException = new AsyncExceptionDTO(expectedMessage, expectedStatus); + AsyncExceptionDTO baseException = new AsyncExceptionDTO(expectedMessage, expectedStatus); assertEquals(expectedMessage, baseException.getMessage()); assertEquals(expectedStatus, baseException.getStatus()); diff --git a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java index f2ba0c7af0..7e216fa5b9 100644 --- a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java @@ -26,6 +26,7 @@ import heartbeat.handler.AsyncExceptionHandler; import heartbeat.handler.AsyncMetricsDataHandler; import heartbeat.handler.AsyncReportRequestHandler; +import heartbeat.handler.base.AsyncExceptionDTO; import heartbeat.service.report.calculator.ChangeFailureRateCalculator; import heartbeat.service.report.calculator.ClassificationCalculator; import heartbeat.service.report.calculator.CycleTimeCalculator; @@ -34,8 +35,6 @@ import heartbeat.service.report.calculator.MeanToRecoveryCalculator; import heartbeat.service.report.calculator.VelocityCalculator; import heartbeat.service.report.calculator.model.FetchedData; -import heartbeat.util.ValueUtil; -import lombok.val; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; @@ -688,7 +687,7 @@ void shouldReturnErrorDataWhenExceptionIs404Or403Or401() { when(asyncReportRequestHandler.getReport(any())).thenReturn(ReportResponse.builder().build()); when(asyncMetricsDataHandler.getReportReadyStatusByTimeStamp(reportId)) .thenReturn(metricsDataDTO); - when(asyncExceptionHandler.get(any())).thenReturn(new NotFoundException("error")); + when(asyncExceptionHandler.get(any())).thenReturn(new AsyncExceptionDTO(new NotFoundException("error"))); ReportResponse res = generateReporterService.getComposedReportResponse(reportId); @@ -702,7 +701,7 @@ void shouldThrowGenerateReportExceptionWhenErrorIs500() { when(asyncReportRequestHandler.getReport(any())).thenReturn(ReportResponse.builder().build()); when(asyncMetricsDataHandler.getReportReadyStatusByTimeStamp(reportId)) .thenReturn(metricsDataDTO); - when(asyncExceptionHandler.get(any())).thenReturn(new GenerateReportException("errorMessage")); + when(asyncExceptionHandler.get(any())).thenReturn(new AsyncExceptionDTO(new GenerateReportException("errorMessage"))); try { generateReporterService.getComposedReportResponse(reportId); @@ -719,7 +718,7 @@ void shouldThrowServiceUnavailableExceptionWhenErrorIs503() { when(asyncReportRequestHandler.getReport(any())).thenReturn(ReportResponse.builder().build()); when(asyncMetricsDataHandler.getReportReadyStatusByTimeStamp(reportId)) .thenReturn(metricsDataDTO); - when(asyncExceptionHandler.get(any())).thenReturn(new ServiceUnavailableException("errorMessage")); + when(asyncExceptionHandler.get(any())).thenReturn(new AsyncExceptionDTO(new ServiceUnavailableException("errorMessage"))); try { generateReporterService.getComposedReportResponse(reportId); @@ -736,7 +735,7 @@ void shouldThrowRequestFailedExceptionWhenErrorIsDefault() { when(asyncReportRequestHandler.getReport(any())).thenReturn(ReportResponse.builder().build()); when(asyncMetricsDataHandler.getReportReadyStatusByTimeStamp(reportId)) .thenReturn(metricsDataDTO); - when(asyncExceptionHandler.get(any())).thenReturn(new BadRequestException("error")); + when(asyncExceptionHandler.get(any())).thenReturn(new AsyncExceptionDTO(new BadRequestException("error"))); try { generateReporterService.getComposedReportResponse(reportId); diff --git a/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java b/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java index 707a712344..2ac1c3bfd2 100644 --- a/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java +++ b/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java @@ -86,6 +86,7 @@ public void setUp() { .mergedAt("2022-07-23T04:04:00.000+00:00") .createdAt("2022-07-23T04:03:00.000+00:00") .mergeCommitSha("111") + .url("https://api.github.com/repos/XXXX-fs/fs-platform-onboarding/pulls/1") .number(1) .build(); deployInfo = DeployInfo.builder() @@ -364,6 +365,38 @@ void shouldReturnEmptyLeadTimeWhenDeployTimesIsEmpty() { assertEquals(expect, result); } + @Test + void shouldReturnEmptyLeadTimeGithubShaIsDifferent() { + String mockToken = "mockToken"; + List expect = List.of(PipelineLeadTime.builder() + .pipelineStep(PIPELINE_STEP) + .pipelineName("Name") + .leadTimes(List.of(LeadTime.builder() + .commitId("111") + .jobFinishTime(1658549160000L) + .pipelineCreateTime(1658549100000L) + .prLeadTime(0L) + .pipelineLeadTime(180000) + .totalTime(180000) + .build())) + .build()); + var pullRequestInfoWithDifferentSha = PullRequestInfo.builder() + .mergedAt("2022-07-23T04:04:00.000+00:00") + .createdAt("2022-07-23T04:03:00.000+00:00") + .mergeCommitSha("222") + .url("https://api.github.com/repos/XXXX-fs/fs-platform-onboarding/pulls/1") + .number(1) + .build(); + when(gitHubFeignClient.getPullRequestListInfo(any(), any(), any())) + .thenReturn(List.of(pullRequestInfoWithDifferentSha)); + when(gitHubFeignClient.getPullRequestCommitInfo(any(), any(), any())).thenReturn(List.of(commitInfo)); + when(gitHubFeignClient.getCommitInfo(any(), any(), any())).thenReturn(commitInfo); + + List result = githubService.fetchPipelinesLeadTime(deployTimes, repositoryMap, mockToken); + + assertEquals(expect, result); + } + @Test void shouldReturnEmptyMergeLeadTimeWhenPullRequestInfoIsEmpty() { String mockToken = "mockToken"; @@ -525,6 +558,7 @@ void shouldReturnPipeLineLeadTimeWhenDeployCommitShaIsDifferent() { .mergedAt("2022-07-23T04:04:00.000+00:00") .createdAt("2022-07-23T04:03:00.000+00:00") .mergeCommitSha("222") + .url("") .number(1) .build(); pipelineLeadTimes = List.of(PipelineLeadTime.builder() diff --git a/docs/src/content/docs/en/designs/e2e-testing.mdx b/docs/src/content/docs/en/designs/e2e-testing.mdx new file mode 100644 index 0000000000..8c8095afb3 --- /dev/null +++ b/docs/src/content/docs/en/designs/e2e-testing.mdx @@ -0,0 +1,211 @@ +--- +title: E2E Testing +description: E2E Testing +--- +## E2E Overview +We currently use Playwright as the testing framework for E2E testing. In order to ensure the credibility of the test, we no longer use stub/mock 3rd services, but directly test against 3rd services. +```plantuml +@startuml +set separator none +title Heartbeat - E2E Architecture + +left to right direction + +!include +!include +!include + +Person(E2ErunnerPlaywright, "E2E runner \n Playwright", $descr="", $tags="", $link="") +System(3rdApi, "3rdApi", $descr="", $tags="", $link="") + +System_Boundary("Heartbeat_boundary", "Heartbeat", $tags="") { + Container(Heartbeat.HBFrontend, "HB Frontend", $techn="", $descr="", $tags="", $link="") + Container(Heartbeat.HBBackend, "HB Backend", $techn="", $descr="", $tags="", $link="") +} + +Rel(E2ErunnerPlaywright, Heartbeat.HBFrontend, "Uses", $techn="", $tags="", $link="") +Rel(Heartbeat.HBFrontend, Heartbeat.HBBackend, "API call to", $techn="", $tags="", $link="") +Rel(Heartbeat.HBBackend, 3rdApi, "", $techn="", $tags="", $link="") + +SHOW_LEGEND(true) +@enduml +``` + +## What is Playwright? +Playwright is an open-source automation library for browser testing and web scraping developed by Microsoft. It provides a high-level API for automating browsers such as Chrome, Firefox, and WebKit. It's similar to other tools like Puppeteer but offers cross-browser support and additional features like being able to test on multiple browsers in parallel. + +## Why Playwright? +🔄 We decided to switch from Cypress to Playwright, mainly considering the following: + +1. **Cross-browser testing:** Playwright supports multiple browsers like Chrome, Firefox, and WebKit, making it easier to test the web application's compatibility across different browsers. Cypress primarily supports Chrome, although there are efforts to add support for other browsers as well. + +2. **Parallel testing:** Playwright allows for running tests in parallel across multiple browser instances, which can significantly reduce test execution time. This is particularly valuable when dealing with a large number of tests in a test suite. + +3. **Better automation capabilities:** Playwright provides more automation capabilities compared to Cypress, such as downloads automation, geolocation simulation, and network request interception. It offers more flexibility in how tests are written and executed. + +4. **Faster execution:** Playwright is known for its faster test execution speed due to its efficient architecture and features like the ability to interact with elements without the need for waiting timers. + +5. **Community and support:** Playwright is developed and maintained by Microsoft, which has a strong reputation in the software development industry. This can instill confidence in teams regarding the tool's long-term maintenance and support. + +In addition, playwright has some advantages for developers: + +1. **Programming language support:** Playwright supports multiple languages such as JavaScript, TypeScript, Python, and C#, providing more options for writing tests in a language preferred by the team. This allows developers to leverage their existing skills and knowledge. + +2. **Asynchronous handling:** Playwright uses a more traditional async/await syntax for handling asynchronous operations, which can be more familiar to developers who are used to working with promises and async/await in modern JavaScript. But Cypress actually returned something only pretending to be a Promise. + +3. **Page object model:** Playwright provides better support for the page object model, allowing for a more organized and maintainable test structure by separating page interactions into reusable components. + +4. **Debugging capabilities:** Playwright offers better debugging capabilities, such as the ability to pause and inspect the state of the application during test execution, making it easier to troubleshoot issues and write reliable tests. + +5. **Reporter:** Playwright's HTML reporter produces a self-contained folder that contains report for the test run that can be served as a web page. It's easy to read and retrace errors than Cypress. + +## E2E file structure +```sh +e2e +├── fixtures +├── pages +├── reports +├── specs +├── temp +├── test-results +└── utils +``` +- `fixtures`: This directory contain test fixtures or data used in the E2E tests, such as sample data, expect result, and [Playwright fixtures](https://playwright.dev/docs/test-fixtures). + +- `pages`: This directory typically includes page objects or modules that represent different pages of the application being tested. Page objects help in organizing and maintaining the code for interacting with specific pages. ref: [Playwright POM](https://playwright.dev/docs/pom) + +- `reports`: This directory store the generated test reports or logs after running the E2E tests, which provide insights into the test results, failures, and performance metrics. + +- `specs`: This directory contains the test specifications or scenarios that reflect the [E2E Test Case Summary](https://docs.google.com/spreadsheets/d/1vT5LnQb940HK12V0o0kZlWI9SJwY03SqZHmZ11B9f9Q/edit?usp=sharing) cases. + +- `temp`: This directory be used to store temporary files or data during the test execution. This folder is not defined by playwright, but specified by the programmer. + +- `test-results`: The output directory for files created during test execution. This directory is cleaned at the start. + +- `utils`: This directory hold utility functions or helper modules that provide common functionalities used across the E2E tests. + + +## How to use in the local environment +The following command working within the directory: `/frontend` + +### Initial setup +1. `pnpm install` +2. `pnpm exec playwright install --with-deps` +3. Create the `e2e/.env.local` by following the `e2e/.env.example` + +### Run E2E testing with local env +- Run E2E (You should start FE&BE services firstly): `pnpm run e2e:local` +- Run E2E with FE&BE services: `pnpm run e2e:with-server` + +### Serve HTML testing report +`pnpm run e2e:report PATH/TO/e2e-reports/html` + + +## Debugging tests +### Debug tests in UI mode +*Run E2E with debug UI*: `pnpm run e2e:ui` +It's highly recommend debugging your tests with UI Mode for a better developer experience where you can easily walk through each step of the test and visually see what was happening before, during and after each step. UI mode also comes with many other features such as the locator picker, watch mode and more. + +![UI mode](https://github.com/microsoft/playwright/assets/13063165/ffca2fd1-5349-41fb-ade9-ace143bb2c58) + +### Debug tests with the Playwright Inspector +*Run E2E with debug Inspector*: `pnpm run e2e:debug` +The Playwright Inspector provides a user-friendly interface where you can view the DOM, inspect elements, debug JavaScript code, interact with the page, and understand the state of the application during test execution. + +![Debug mode](https://github.com/microsoft/playwright/assets/13063165/6b3b3caa-d258-4cb8-aa05-cd407f501626) + +### Debug the pipeline E2E result with Playwright HTML report +- Download the report package from the pipeline build -> `🚀Run e2e` section -> Artifacts tab +- Unzip the report and run `pnpm run e2e:report PATH/TO/e2e-reports/html` + +## How to generate screenshots +The pipeline run in the linux environment, so the screenshots are slightly different, so we need to use the macOS and docker container to generate a set of screenshots during development. + +### Generate for macOS testing +`pnpm run e2e:updateSnapshots` + +### Generate for pipeline testing +1. Build image for the first run: `pnpm run e2e:build-docker-image` +2. `pnpm run e2e:updateSnapshots-docker` + +## How to add new testing env-var +If you need to add a new token or password to the test case, you should use the environment variable instead of plaintext to write it into the codebase. +1. Add env-var into `frontend/e2e/.env.local` +2. Add env-var sample into `frontend/e2e/.env.example` +3. Add env-var into `.github/workflows/build-and-deploy.yml` e2e section +4. Add env-var into `ops/check.sh` e2e_container_check function +5. For the pipeline, we currently use the Buildkite environment hook to inject environment variables + - SSH into the Buildkite Agent EC2 server + - Add env-var into `/etc/buildkite-agent/hooks/environment` + +## How the pipeline run E2E testing +Due to permission issues with the Buildkite Agent EC2 server installation Playwright dependencies, we use docker images to run tests instead of native environment. + +Pipeline config `.buildkite/pipeline.yml`: +```yaml + - label: ":rocket: Run e2e" + branches: main + key: "check-e2e" + depends_on: + - "deploy-e2e" + - "check-shell" + - "check-security" + - "check-frontend" + - "check-px" + - deny-css-rgba-check + - deny-css-hex-check + - "check-backend" + - "check-frontend-license" + - "check-backend-license" + command: ./ops/check.sh e2e-container + plugins: + - artifacts#v1.9.0: + upload: "./e2e-reports.tar.gz" + expire_in: "${RETENTION_DAYS} days" +``` + +E2E runner script `./ops/check.sh`: +```shell +e2e_container_check() { + docker build -t "heartbeat_e2e:latest" ./ -f ./ops/infra/Dockerfile.e2e + + set +e + local result + docker run \ + --name hb_e2e_runner \ + -e "APP_ORIGIN=${APP_HTTP_SCHEDULE:-}://${AWS_EC2_IP_E2E:-}:${AWS_EC2_IP_E2E_PORT:-}" \ + -e "E2E_TOKEN_JIRA=${E2E_TOKEN_JIRA:-}" \ + -e "E2E_TOKEN_BUILD_KITE=${E2E_TOKEN_BUILD_KITE:-}" \ + -e "E2E_TOKEN_GITHUB=${E2E_TOKEN_GITHUB:-}" \ + -e "E2E_TOKEN_FLAG_AS_BLOCK_JIRA=${E2E_TOKEN_FLAG_AS_BLOCK_JIRA:-}" \ + -e "CI=${CI:-}" \ + heartbeat_e2e:latest \ + pnpm run e2e:major-ci + result=$? + set -e + + docker cp hb_e2e_runner:/app/e2e/reports ./e2e-reports + docker rm hb_e2e_runner + tar -zcvf ./e2e-reports.tar.gz ./e2e-reports + exit $result +} +``` + +## FAQ +### `Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:xxxx/` +Start your local FE and BE services firstly + +### `Error: Failed to start E2E testing, please configure the env var APP_ORIGIN` +Please check if the `.env.local` file is configured + +### I encountered some assertion errors in the test +- Please make sure that you have rebased the latest code and restarted the local FE and BE services +- Please check whether the token in env file is invalid +- If you modify the relevant code, please check whether it has broken the test +- If your want or need to modify the assertion or screenshot, you should align with BA instead of working silently + - If the screenshot is incorrect, you need to update the screenshot of both macOS and linux + +## Know issues +- In headed mode, screenshot comparison fails +- On the pipeline, the E2E docker image needs to be built every time +- Currently, we only test the Chrome browser locally and on the pipeline diff --git a/docs/src/i18n/en/nav.ts b/docs/src/i18n/en/nav.ts index 5766cee1a8..368cdbe4c3 100644 --- a/docs/src/i18n/en/nav.ts +++ b/docs/src/i18n/en/nav.ts @@ -87,6 +87,11 @@ export default [ slug: 'designs/support-multiple-columns', key: 'designs/support-multiple-columns', }, + { + text: 'E2E Testing', + slug: 'designs/e2e-testing', + key: 'designs/e2e-testing', + }, { text: 'Issue Solutions', header: true, type: 'tech', key: 'Issue Solutions' }, { diff --git a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx index 9242fc8970..72522b511f 100644 --- a/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/DateRangePicker.test.tsx @@ -1,3 +1,9 @@ +import { + initDeploymentFrequencySettings, + saveUsers, + updateShouldGetBoardConfig, + updateShouldGetPipelineConfig, +} from '@src/context/Metrics/metricsSlice'; import { DateRangePicker } from '@src/containers/ConfigStep/DateRangePicker'; import { fireEvent, render, screen } from '@testing-library/react'; import { setupStore } from '../../utils/setupStoreUtil'; @@ -12,6 +18,14 @@ const TODAY = dayjs(); const INPUT_DATE_VALUE = TODAY.format('MM/DD/YYYY'); let store = setupStore(); +jest.mock('@src/context/Metrics/metricsSlice', () => ({ + ...jest.requireActual('@src/context/Metrics/metricsSlice'), + updateShouldGetBoardConfig: jest.fn().mockReturnValue({ type: 'SHOULD_UPDATE_BOARD_CONFIG' }), + updateShouldGetPipelineConfig: jest.fn().mockReturnValue({ type: 'SHOULD_UPDATE_PIPELINE_CONFIG' }), + initDeploymentFrequencySettings: jest.fn().mockReturnValue({ type: 'INIT_DEPLOYMENT_SETTINGS' }), + saveUsers: jest.fn().mockReturnValue({ type: 'SAVE_USERS' }), +})); + const setup = () => { store = setupStore(); return render( @@ -48,7 +62,6 @@ describe('DateRangePicker', () => { const endDateInput = screen.getByRole('textbox', { name: END_DATE_LABEL }) as HTMLInputElement; fireEvent.change(endDateInput, { target: { value: INPUT_DATE_VALUE } }); - expectDate(endDateInput); }); @@ -75,4 +88,24 @@ describe('DateRangePicker', () => { expect(startDateInput.valueAsDate).toEqual(null); expect(endDateInput.valueAsDate).toEqual(null); }); + + it('should dispatch update configuration when change startDate', () => { + setup(); + const startDateInput = screen.getByRole('textbox', { name: START_DATE_LABEL }) as HTMLInputElement; + fireEvent.change(startDateInput, { target: { value: INPUT_DATE_VALUE } }); + expect(updateShouldGetBoardConfig).toHaveBeenCalledWith(true); + expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); + expect(initDeploymentFrequencySettings).toHaveBeenCalled(); + expect(saveUsers).toHaveBeenCalledWith([]); + }); + + it('should dispatch update configuration when change endDate', () => { + setup(); + const endDateInput = screen.getByRole('textbox', { name: END_DATE_LABEL }) as HTMLInputElement; + fireEvent.change(endDateInput, { target: { value: INPUT_DATE_VALUE } }); + expect(updateShouldGetBoardConfig).toHaveBeenCalledWith(true); + expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); + expect(initDeploymentFrequencySettings).toHaveBeenCalled(); + expect(saveUsers).toHaveBeenCalledWith([]); + }); }); diff --git a/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx b/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx index de2d2a4df8..7b305e5779 100644 --- a/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx +++ b/frontend/__tests__/containers/ConfigStep/SourceControl.test.tsx @@ -9,6 +9,7 @@ import { VERIFIED, VERIFY, } from '../../fixtures'; +import { initDeploymentFrequencySettings, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { SourceControl } from '@src/containers/ConfigStep/SourceControl'; import { SOURCE_CONTROL_TYPES } from '@src/constants/resources'; @@ -35,6 +36,12 @@ let store = null; const server = setupServer(rest.post(MOCK_SOURCE_CONTROL_VERIFY_TOKEN_URL, (req, res, ctx) => res(ctx.status(204)))); +jest.mock('@src/context/Metrics/metricsSlice', () => ({ + ...jest.requireActual('@src/context/Metrics/metricsSlice'), + updateShouldGetPipelineConfig: jest.fn().mockReturnValue({ type: 'SHOULD_UPDATE_PIPELINE_CONFIG' }), + initDeploymentFrequencySettings: jest.fn().mockReturnValue({ type: 'INIT_DEPLOYMENT_SETTINGS' }), +})); + describe('SourceControl', () => { beforeAll(() => server.listen()); afterAll(() => server.close()); @@ -112,6 +119,20 @@ describe('SourceControl', () => { }); }); + it('should reload pipeline config when reset fields', async () => { + setup(); + fillSourceControlFieldsInformation(); + + await userEvent.click(screen.getByText(VERIFY)); + + await userEvent.click(screen.getByRole('button', { name: RESET })); + + fillSourceControlFieldsInformation(); + + expect(updateShouldGetPipelineConfig).toHaveBeenCalledWith(true); + expect(initDeploymentFrequencySettings).toHaveBeenCalled(); + }); + it('should show error message and error style when token is empty', () => { setup(); diff --git a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx index 6742f38d1c..42d9b687fd 100644 --- a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/DeploymentFrequencySettings.test.tsx @@ -40,6 +40,7 @@ jest.mock('@src/context/config/configSlice', () => ({ selectBranches: jest.fn().mockReturnValue(['']), selectPipelineCrews: jest.fn().mockReturnValue(['']), selectStepsParams: jest.fn().mockReturnValue(['']), + selectDateRange: jest.fn().mockReturnValue(['']), })); const mockValidationCheckContext = { diff --git a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection.test.tsx b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection.test.tsx index 8fb324eab9..8affbba0be 100644 --- a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection.test.tsx @@ -51,8 +51,28 @@ jest.mock('@src/context/config/configSlice', () => ({ updatePipelineToolVerifyResponseSteps: jest .fn() .mockReturnValue({ type: 'UPDATE_PIPELINE_TOOL_VERIFY_RESPONSE_STEPS' }), + selectPipelineList: jest.fn().mockReturnValue([ + { + id: 'mockPipelineId', + name: 'mockName', + orgId: 'mockOrgId', + orgName: 'mockOrgName', + repository: 'git@github.com:au-heartbeat/Heartbeat.git', + branches: ['branch1', 'branch2', 'branch3'], + }, + { + id: 'mockPipelineId2', + name: 'mockName2', + orgId: 'mockOrgId2', + orgName: 'mockOrgName2', + repository: 'git@github.com:au-heartbeat/Heartbeat.git', + branches: ['branch1', 'branch2', 'branch3', 'branch4'], + }, + ]), })); +const store = setupStore(); + describe('PipelineMetricSelection', () => { beforeAll(() => server.listen()); afterAll(() => server.close()); @@ -73,7 +93,6 @@ describe('PipelineMetricSelection', () => { isShowRemoveButton: boolean, isDuplicated: boolean, ) => { - const store = setupStore(); store.dispatch(updateShouldGetPipelineConfig(true)); return render( @@ -185,6 +204,8 @@ describe('PipelineMetricSelection', () => { 'There is no step during this period for this pipeline! Please change the search time in the Config page!', ), ).toBeInTheDocument(); + + expect(getByText('No steps for this pipeline!')).toBeInTheDocument(); }); }); @@ -255,6 +276,28 @@ describe('PipelineMetricSelection', () => { expect(getByRole('button', { name: 'branch2' })).toBeInTheDocument(); }); + it('should show not show branches when deployment setting has branches given branches does not match pipeline ', async () => { + metricsClient.getSteps = jest + .fn() + .mockReturnValue({ response: ['steps'], haveStep: true, branches: ['branch1', 'branch2'] }); + const { getByRole, queryByRole, getByText } = await setup( + { id: 0, organization: 'mockOrgName3', pipelineName: 'mockName3', step: '', branches: ['branch6', 'branch7'] }, + false, + false, + ); + + await waitFor(() => { + expect(getByText(BRANCH)).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.click(getByRole('combobox', { name: 'Branches' })); + }); + + expect(queryByRole('button', { name: 'branch6' })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: 'branch7' })).not.toBeInTheDocument(); + }); + it('should show duplicated message given duplicated id', async () => { metricsClient.getSteps = jest.fn().mockReturnValue({ response: ['steps'], haveStep: true }); const { getByText } = await setup( diff --git a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection.test.tsx b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection.test.tsx index 06803f28e0..7262668360 100644 --- a/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection.test.tsx @@ -17,6 +17,14 @@ jest.mock('react', () => ({ ...jest.requireActual('react'), useEffect: jest.fn(), })); +jest.mock('@src/context/Metrics/metricsSlice', () => ({ + ...jest.requireActual('@src/context/Metrics/metricsSlice'), + selectDeploymentFrequencySettings: jest.fn().mockReturnValue([]), +})); +jest.mock('@src/utils/util', () => ({ + ...jest.requireActual('@src/utils/util'), + getDisabledOptions: jest.fn(), +})); let store = setupStore(); describe('SingleSelection', () => { diff --git a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx index 3329fd3172..d92dadbb2b 100644 --- a/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/MetricsStep.test.tsx @@ -77,7 +77,7 @@ describe('MetricsStep', () => { setup(); expect(screen.getByText(CREWS_SETTING)).toBeInTheDocument(); - expect(screen.queryByText(CYCLE_TIME_SETTINGS)).not.toBeInTheDocument(); + expect(screen.queryByText(CYCLE_TIME_SETTINGS)).toBeInTheDocument(); expect(screen.queryByText(CLASSIFICATION_SETTING)).not.toBeInTheDocument(); expect(screen.getByText(REAL_DONE)).toBeInTheDocument(); }); @@ -88,7 +88,7 @@ describe('MetricsStep', () => { setup(); expect(screen.getByText(CREWS_SETTING)).toBeInTheDocument(); - expect(screen.queryByText(CYCLE_TIME_SETTINGS)).not.toBeInTheDocument(); + expect(screen.queryByText(CYCLE_TIME_SETTINGS)).toBeInTheDocument(); expect(screen.queryByText(CLASSIFICATION_SETTING)).not.toBeInTheDocument(); expect(screen.queryByText(REAL_DONE)).not.toBeInTheDocument(); }); diff --git a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx index 6aea818f3c..86f72a7f44 100644 --- a/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx +++ b/frontend/__tests__/containers/MetricsStepper/MetricsStepper.test.tsx @@ -133,7 +133,12 @@ const fillMetricsPageDate = async () => { store.dispatch(saveTargetFields([{ name: 'mockClassification', key: 'mockClassification', flag: true }])); store.dispatch(saveUsers(['mockUsers'])); store.dispatch(saveDoneColumn(['Done', 'Canceled'])), - store.dispatch(updateCycleTimeSettings([{ name: 'TODO', value: 'To do' }])); + store.dispatch( + updateCycleTimeSettings([ + { column: 'Testing', status: 'testing', value: 'Done' }, + { column: 'Testing', status: 'test', value: 'Done' }, + ]), + ); store.dispatch(updateTreatFlagCardAsBlock(false)), store.dispatch( updateDeploymentFrequencySettings({ updateId: 0, label: 'organization', value: 'mock new organization' }), diff --git a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx index 1d33fae4bd..d276d12dfc 100644 --- a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx +++ b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx @@ -57,7 +57,7 @@ describe('Report Card', () => { setup(); expect(screen.getByText(RETRY)).toBeInTheDocument(); - expect(screen.getByText('Failed to get Github info, status: 404')).toBeInTheDocument(); + expect(screen.getByText('Failed to get GitHub info, status: 404')).toBeInTheDocument(); await userEvent.click(screen.getByText(RETRY)); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index d2b0d9c47f..16710352b9 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -509,7 +509,7 @@ describe('Report Step', () => { setup(REQUIRED_DATA_LIST); expect(addNotification).toBeCalledWith({ - message: MESSAGE.FAILED_TO_GET_DATA('Github'), + message: MESSAGE.FAILED_TO_GET_DATA('GitHub'), type: 'error', }); }); diff --git a/frontend/__tests__/utils/Util.test.tsx b/frontend/__tests__/utils/Util.test.tsx index b9816dd45e..1c90402553 100644 --- a/frontend/__tests__/utils/Util.test.tsx +++ b/frontend/__tests__/utils/Util.test.tsx @@ -8,9 +8,12 @@ import { getRealDoneStatus, transformToCleanedBuildKiteEmoji, formatDuplicatedNameWithSuffix, + getDisabledOptions, + sortDisabledOptions, } from '@src/utils/util'; import { CleanedBuildKiteEmoji, OriginBuildKiteEmoji } from '@src/constants/emojis/emoji'; import { CYCLE_TIME_SETTINGS_TYPES } from '@src/constants/resources'; +import { IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { EMPTY_STRING } from '@src/constants/commons'; import { PIPELINE_TOOL_TYPES } from '../fixtures'; @@ -45,6 +48,71 @@ describe('transformToCleanedBuildKiteEmoji function', () => { }); }); +describe('getDisabledOptions function', () => { + it('should return true when option is includes', () => { + const mockDeploymentFrequencySettings: IPipelineConfig[] = [ + { id: 0, organization: '', pipelineName: 'mock 1', step: '', branches: [] }, + { id: 1, organization: '', pipelineName: 'mock 2', step: '', branches: [] }, + ]; + + const mockOption: string = 'mock 1'; + + const result = getDisabledOptions(mockDeploymentFrequencySettings, mockOption); + + expect(result).toBeTruthy(); + }); + + it('should return true when option is not includes', () => { + const mockDeploymentFrequencySettings: IPipelineConfig[] = [ + { id: 0, organization: '', pipelineName: 'mock 1', step: '', branches: [] }, + { id: 1, organization: '', pipelineName: 'mock 2', step: '', branches: [] }, + ]; + + const mockOption: string = 'mock 3'; + + const result = getDisabledOptions(mockDeploymentFrequencySettings, mockOption); + + expect(result).toBeFalsy(); + }); +}); + +describe('sortDisabledOptions function', () => { + it('should sort the mock3 is first when mock1 & mock2 is selected', () => { + const mockDeploymentFrequencySettings: IPipelineConfig[] = [ + { id: 0, organization: '', pipelineName: 'mock1', step: '', branches: [] }, + { id: 1, organization: '', pipelineName: 'mock2', step: '', branches: [] }, + ]; + + const mockOptions = ['mock1', 'mock2', 'mock3']; + + const result = sortDisabledOptions(mockDeploymentFrequencySettings, mockOptions); + + expect(result).toEqual(['mock3', 'mock1', 'mock2']); + }); + + it('should not sort when deploymentFrequencySettings is empty', () => { + const mockDeploymentFrequencySettings: IPipelineConfig[] = []; + + const mockOptions = ['mock1', 'mock2', 'mock3']; + + const result = sortDisabledOptions(mockDeploymentFrequencySettings, mockOptions); + + expect(result).toEqual(['mock1', 'mock2', 'mock3']); + }); + + it('should as is when selected option is last', () => { + const mockDeploymentFrequencySettings: IPipelineConfig[] = [ + { id: 0, organization: '', pipelineName: 'mock3', step: '', branches: [] }, + ]; + + const mockOptions = ['mock1', 'mock2', 'mock3']; + + const result = sortDisabledOptions(mockDeploymentFrequencySettings, mockOptions); + + expect(result).toEqual(['mock1', 'mock2', 'mock3']); + }); +}); + describe('getJiraToken function', () => { it('should return an valid string when token is not empty string', () => { const email = 'test@example.com'; diff --git a/frontend/e2e/fixtures/index.ts b/frontend/e2e/fixtures/index.ts index 206c15906f..a80af0ae8b 100644 --- a/frontend/e2e/fixtures/index.ts +++ b/frontend/e2e/fixtures/index.ts @@ -4,4 +4,5 @@ export const E2E_PROJECT_NAME = 'E2EProjectName'; export const CONFIG_STEP_SAVING_FILENAME = 'config-step.json'; export const METRICS_STEP_SAVING_FILENAME = 'metrics-step.json'; -export const E2E_EXPECT_TIMEOUT = 30 * 1000; +export const E2E_EXPECT_LOCAL_TIMEOUT = 30 * 1000; +export const E2E_EXPECT_CI_TIMEOUT = 30 * 3 * 1000; diff --git a/frontend/e2e/pages/metrics/MetricsStep.ts b/frontend/e2e/pages/metrics/MetricsStep.ts index f4e62c7bc1..56f67c3d2c 100644 --- a/frontend/e2e/pages/metrics/MetricsStep.ts +++ b/frontend/e2e/pages/metrics/MetricsStep.ts @@ -416,11 +416,14 @@ export class MetricsStep { await expect(this.pipelineDefaultSelectedBranchChips).toHaveCount(branches.length); } - async selectDefaultGivenPipelineSetting(pipelineSettings: typeof metricsStepData.deployment) { + async selectDefaultGivenPipelineSetting( + pipelineSettings: typeof metricsStepData.deployment, + shouldSelectPipelineName = true, + ) { const firstPipelineConfig = pipelineSettings[0]; await this.selectOrganization(firstPipelineConfig.organization); - await this.selectPipelineName(firstPipelineConfig.pipelineName); + shouldSelectPipelineName && (await this.selectPipelineName(firstPipelineConfig.pipelineName)); await this.selectStep(firstPipelineConfig.step); await this.selectBranch(firstPipelineConfig.branches); } diff --git a/frontend/e2e/pages/metrics/ReportStep.ts b/frontend/e2e/pages/metrics/ReportStep.ts index 653f95156c..d4bc6ccfe4 100644 --- a/frontend/e2e/pages/metrics/ReportStep.ts +++ b/frontend/e2e/pages/metrics/ReportStep.ts @@ -1,11 +1,16 @@ import { checkDownloadReport, downloadFileAndCheck } from 'e2e/utils/download'; import { expect, Locator, Page } from '@playwright/test'; -import { E2E_EXPECT_TIMEOUT } from '../../fixtures'; +import { E2E_EXPECT_CI_TIMEOUT } from '../../fixtures'; import { parse } from 'csv-parse/sync'; import path from 'path'; import fs from 'fs'; +export enum ProjectCreationType { + IMPORT_PROJECT_FROM_FILE, + CREATE_A_NEW_PROJECT, +} + export class ReportStep { readonly page: Page; readonly pageHeader: Locator; @@ -25,6 +30,12 @@ export class ReportStep { readonly exportBoardData: Locator; readonly exportMetricData: Locator; readonly homeIcon: Locator; + readonly velocityRows: Locator; + readonly cycleTimeRows: Locator; + readonly classificationRows: Locator; + readonly leadTimeForChangesRows: Locator; + readonly changeFailureRateRows: Locator; + readonly meanTimeToRecoveryRows: Locator; constructor(page: Page) { this.page = page; @@ -49,18 +60,51 @@ export class ReportStep { this.exportBoardData = this.page.getByText('Export board data'); this.exportPipelineDataButton = this.page.getByText('Export pipeline data'); this.homeIcon = page.getByLabel('Home'); + this.velocityRows = this.page.getByTestId('Velocity').locator('tbody').getByRole('row'); + this.cycleTimeRows = this.page.getByTestId('Cycle Time').locator('tbody').getByRole('row'); + this.classificationRows = this.page.getByTestId('Classification').locator('tbody').getByRole('row'); + this.leadTimeForChangesRows = this.page.getByTestId('Lead Time For Changes').getByRole('row'); + this.changeFailureRateRows = this.page.getByTestId('Change Failure Rate').getByRole('row'); + this.meanTimeToRecoveryRows = this.page.getByTestId('Mean Time To Recovery').getByRole('row'); + } + combineStrings(arr: string[]): string { + return arr.join(''); } async goToPreviousStep() { await this.previousButton.click(); } - async checkDoraMetricsDetails(snapshotPath: string) { + async checkDoraMetricsReportDetails() { + await expect(this.page.getByTestId('Deployment Frequency').getByRole('row').nth(2)).toContainText( + this.combineStrings(['Deployment frequency', '6.60']), + ); + + await expect(this.leadTimeForChangesRows.nth(2)).toContainText(this.combineStrings(['PR Lead Time', '6.12'])); + await expect(this.leadTimeForChangesRows.nth(3)).toContainText(this.combineStrings(['Pipeline Lead Time', '0.50'])); + await expect(this.leadTimeForChangesRows.nth(4)).toContainText(this.combineStrings(['Total Lead Time', '6.62'])); + + await expect(this.leadTimeForChangesRows.nth(4)).toContainText(this.combineStrings(['Total Lead Time', '6.62'])); + + await expect(this.changeFailureRateRows.nth(2)).toContainText( + this.combineStrings(['Failure rate', '17.50%(7/40)']), + ); + + await expect(this.meanTimeToRecoveryRows.nth(2)).toContainText( + this.combineStrings(['Mean Time To Recovery', '1.90']), + ); + } + + async checkDoraMetricsDetails(projectCreationType: ProjectCreationType) { await this.showMoreLinks.nth(1).click(); - await expect(this.page).toHaveScreenshot(snapshotPath, { - fullPage: true, - mask: [this.pageHeader], - }); + if ( + projectCreationType === ProjectCreationType.IMPORT_PROJECT_FROM_FILE || + projectCreationType === ProjectCreationType.CREATE_A_NEW_PROJECT + ) { + await this.checkDoraMetricsReportDetails(); + } else { + throw Error('The board detail type is not correct, please give a correct one.'); + } await downloadFileAndCheck(this.page, this.exportPipelineDataButton, 'pipelineData.csv', async (fileDataString) => { const localCsvFile = fs.readFileSync(path.resolve(__dirname, '../../fixtures/createNew/pipelineData.csv')); const localCsv = parse(localCsvFile); @@ -72,7 +116,7 @@ export class ReportStep { } async confirmGeneratedReport() { - await expect(this.page.getByRole('alert')).toContainText('Help Information', { timeout: E2E_EXPECT_TIMEOUT * 3 }); + await expect(this.page.getByRole('alert')).toContainText('Help Information', { timeout: E2E_EXPECT_CI_TIMEOUT }); await expect(this.page.getByRole('alert')).toContainText( 'The file will expire in 30 minutes, please download it in time.', ); @@ -90,12 +134,102 @@ export class ReportStep { await expect(this.averageCycleTimeForCard).toContainText(`${averageCycleTimeForCard}Average Cycle Time(Days/Card)`); } - async checkBoardMetricsDetails(snapshotPath: string, csvCompareLines: number) { + async checkBoardMetricsReportReportDetail() { + await expect(this.velocityRows.filter({ hasText: 'Velocity(Story Point)' }).getByRole('cell').nth(1)).toContainText( + '17', + ); + await expect( + this.velocityRows.filter({ hasText: 'Throughput(Cards Count)' }).getByRole('cell').nth(1), + ).toContainText('9'); + + await expect(this.cycleTimeRows.nth(0).getByRole('cell').nth(1)).toContainText('4.86(Days/SP)'); + await expect(this.cycleTimeRows.filter({ hasText: 'Average cycle time' }).getByRole('cell').nth(1)).toContainText( + '4.86(Days/SP)', + ); + await expect(this.cycleTimeRows.nth(1).getByRole('cell').nth(0)).toContainText('9.18(Days/Card)'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Total development time / Total cycle time' }).getByRole('cell').nth(1), + ).toContainText('37.55%'); + await expect( + this.cycleTimeRows + .filter({ hasText: 'Total waiting for testing time / Total cycle time' }) + .getByRole('cell') + .nth(1), + ).toContainText('10.92%'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Total block time / Total cycle time' }).getByRole('cell').nth(1), + ).toContainText('19.96%'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Total review time / Total cycle time' }).getByRole('cell').nth(1), + ).toContainText('22.47%'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Total testing time / Total cycle time' }).getByRole('cell').nth(1), + ).toContainText('9.1%'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Average development time' }).getByRole('cell').nth(1), + ).toContainText('1.83(Days/SP)'); + await expect(this.cycleTimeRows.nth(8).getByRole('cell').nth(0)).toContainText('3.45(Days/Card)'); + await expect( + this.cycleTimeRows.filter({ hasText: 'Average waiting for testing time' }).getByRole('cell').nth(1), + ).toContainText('0.53(Days/SP)'); + await expect(this.cycleTimeRows.nth(10).getByRole('cell').nth(0)).toContainText('1.00(Days/Card)'); + await expect(this.cycleTimeRows.filter({ hasText: 'Average block time' }).getByRole('cell').nth(1)).toContainText( + '0.97(Days/SP)', + ); + await expect(this.cycleTimeRows.nth(12).getByRole('cell').nth(0)).toContainText('1.83(Days/Card)'); + await expect(this.cycleTimeRows.filter({ hasText: 'Average review time' }).getByRole('cell').nth(1)).toContainText( + '1.09(Days/SP)', + ); + await expect(this.cycleTimeRows.nth(14).getByRole('cell').nth(0)).toContainText('2.06(Days/Card)'); + await expect(this.cycleTimeRows.filter({ hasText: 'Average testing time' }).getByRole('cell').nth(1)).toContainText( + '0.44(Days/SP)', + ); + await expect(this.cycleTimeRows.nth(16).getByRole('cell').nth(0)).toContainText('0.84(Days/Card)'); + + await expect(this.classificationRows.nth(1)).toContainText(this.combineStrings(['Spike', '11.11%'])); + await expect(this.classificationRows.nth(2)).toContainText(this.combineStrings(['Task', '88.89%'])); + await expect(this.classificationRows.nth(4)).toContainText(this.combineStrings(['ADM-322', '66.67%'])); + await expect(this.classificationRows.nth(5)).toContainText(this.combineStrings(['ADM-279', '22.22%'])); + await expect(this.classificationRows.nth(6)).toContainText(this.combineStrings(['ADM-319', '11.11%'])); + await expect(this.classificationRows.nth(8)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(10)).toContainText(this.combineStrings(['1.0', '88.89%'])); + await expect(this.classificationRows.nth(11)).toContainText(this.combineStrings(['None', '11.11%'])); + await expect(this.classificationRows.nth(13)).toContainText(this.combineStrings(['Sprint 26', '11.11%'])); + await expect(this.classificationRows.nth(14)).toContainText(this.combineStrings(['Sprint 27', '100.00%'])); + await expect(this.classificationRows.nth(15)).toContainText(this.combineStrings(['Sprint 28', '88.89%'])); + await expect(this.classificationRows.nth(17)).toContainText(this.combineStrings(['Auto Dora Metrics', '100.00%'])); + await expect(this.classificationRows.nth(19)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(21)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(23)).toContainText(this.combineStrings(['Medium', '100.00%'])); + await expect(this.classificationRows.nth(25)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(27)).toContainText(this.combineStrings(['Stream1', '44.44%'])); + await expect(this.classificationRows.nth(28)).toContainText(this.combineStrings(['Stream2', '55.56%'])); + await expect(this.classificationRows.nth(30)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(32)).toContainText(this.combineStrings(['1.0', '44.44%'])); + await expect(this.classificationRows.nth(33)).toContainText(this.combineStrings(['2.0', '22.22%'])); + await expect(this.classificationRows.nth(34)).toContainText(this.combineStrings(['3.0', '33.33%'])); + await expect(this.classificationRows.nth(36)).toContainText(this.combineStrings(['Weiran Sun', '11.11%'])); + await expect(this.classificationRows.nth(37)).toContainText(this.combineStrings(['None', '88.89%'])); + await expect(this.classificationRows.nth(39)).toContainText(this.combineStrings(['None', '100.00%'])); + await expect(this.classificationRows.nth(41)).toContainText(this.combineStrings(['heartbeat user', '44.44%'])); + await expect(this.classificationRows.nth(42)).toContainText(this.combineStrings(['Junbo Dai', '11.11%'])); + await expect(this.classificationRows.nth(43)).toContainText(this.combineStrings(['Xinyi Wang', '11.11%'])); + await expect(this.classificationRows.nth(44)).toContainText(this.combineStrings(['Weiran Sun', '11.11%'])); + await expect(this.classificationRows.nth(45)).toContainText(this.combineStrings(['Xuebing Li', '11.11%'])); + await expect(this.classificationRows.nth(46)).toContainText(this.combineStrings(['Yunsong Yang', '11.11%'])); + } + + async checkBoardMetricsDetails(boardDetailType: ProjectCreationType, csvCompareLines: number) { await this.showMoreLinks.first().click(); - await expect(this.page).toHaveScreenshot(snapshotPath, { - fullPage: true, - mask: [this.pageHeader], - }); + if ( + boardDetailType === ProjectCreationType.IMPORT_PROJECT_FROM_FILE || + boardDetailType === ProjectCreationType.CREATE_A_NEW_PROJECT + ) { + await this.checkBoardMetricsReportReportDetail(); + } else { + throw Error('The board detail type is not correct, please give a correct one.'); + } + await downloadFileAndCheck(this.page, this.exportBoardData, 'boardData.csv', async (fileDataString) => { const localCsvFile = fs.readFileSync(path.resolve(__dirname, '../../fixtures/createNew/boardData.csv')); const localCsv = parse(localCsvFile, { to: csvCompareLines }); diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts b/frontend/e2e/specs/create-a-new-project.spec.ts index 5b47b8f1ef..fbde2a2092 100644 --- a/frontend/e2e/specs/create-a-new-project.spec.ts +++ b/frontend/e2e/specs/create-a-new-project.spec.ts @@ -1,6 +1,7 @@ import { BOARD_METRICS_RESULT, DORA_METRICS_RESULT } from '../fixtures/createNew/reportResult'; import { config as metricsStepData } from '../fixtures/createNew/metricsStep'; import { config as configStepData } from '../fixtures/createNew/configStep'; +import { ProjectCreationType } from 'e2e/pages/metrics/ReportStep'; import { test } from '../fixtures/testWithExtendFixtures'; import { clearTempDir } from 'e2e/utils/clearTempDir'; import { format } from 'e2e/utils/dateTime'; @@ -64,7 +65,7 @@ test('Create a new project', async ({ homePage, configStep, metricsStep, reportS BOARD_METRICS_RESULT.AverageCycleTime4SP, BOARD_METRICS_RESULT.AverageCycleTime4Card, ); - await reportStep.checkBoardMetricsDetails('create-a-new-project-Board-Metrics.png', 9); + await reportStep.checkBoardMetricsDetails(ProjectCreationType.CREATE_A_NEW_PROJECT, 9); await reportStep.checkDoraMetrics( DORA_METRICS_RESULT.PrLeadTime, DORA_METRICS_RESULT.PipelineLeadTime, @@ -73,6 +74,6 @@ test('Create a new project', async ({ homePage, configStep, metricsStep, reportS DORA_METRICS_RESULT.FailureRate, DORA_METRICS_RESULT.MeanTimeToRecovery, ); - await reportStep.checkDoraMetricsDetails('create-a-new-project-DORA-Metrics.png'); + await reportStep.checkDoraMetricsDetails(ProjectCreationType.CREATE_A_NEW_PROJECT); await reportStep.checkMetricDownloadData(); }); diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-darwin.png deleted file mode 100644 index cfbd2ce42f..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Google-Chrome-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-darwin.png deleted file mode 100644 index 1a9dd5664b..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Microsoft-Edge-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-darwin.png deleted file mode 100644 index 0647c04dec..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-linux.png deleted file mode 100644 index dcce64c647..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-Tablet-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-darwin.png deleted file mode 100644 index 2508201756..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-chromium-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-darwin.png deleted file mode 100644 index 693567f7ce..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-linux.png deleted file mode 100644 index aa635a5e90..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-Board-Metrics-webkit-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-darwin.png deleted file mode 100644 index de35cbad38..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Google-Chrome-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-darwin.png deleted file mode 100644 index a7662da649..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Microsoft-Edge-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-darwin.png deleted file mode 100644 index cb4237536d..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-linux.png deleted file mode 100644 index 8fe27e0095..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-Tablet-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-darwin.png deleted file mode 100644 index a5a0bf03ce..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-chromium-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-darwin.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-darwin.png deleted file mode 100644 index df73d79709..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-linux.png b/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-linux.png deleted file mode 100644 index b5f894cd1e..0000000000 Binary files a/frontend/e2e/specs/create-a-new-project.spec.ts-snapshots/create-a-new-project-DORA-Metrics-webkit-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts b/frontend/e2e/specs/import-project-from-file.spec.ts index 3687959a2a..571559ae29 100644 --- a/frontend/e2e/specs/import-project-from-file.spec.ts +++ b/frontend/e2e/specs/import-project-from-file.spec.ts @@ -1,5 +1,6 @@ import { BOARD_METRICS_RESULT, FLAG_AS_BLOCK_PROJECT_BOARD_METRICS_RESULT } from '../fixtures/createNew/reportResult'; import { importMultipleDoneProjectFromFile } from '../fixtures/importFile/multiple-done-config-file'; +import { ProjectCreationType } from 'e2e/pages/metrics/ReportStep'; import { test } from '../fixtures/testWithExtendFixtures'; import { clearTempDir } from 'e2e/utils/clearTempDir'; @@ -36,8 +37,8 @@ test('Import project from file', async ({ homePage, configStep, metricsStep, rep BOARD_METRICS_RESULT.AverageCycleTime4SP, BOARD_METRICS_RESULT.AverageCycleTime4Card, ); - await reportStep.checkBoardMetricsDetails('import-project-from-file-Board-Metrics.png', 9); - await reportStep.checkDoraMetricsDetails('import-project-from-file-DORA-Metrics.png'); + await reportStep.checkBoardMetricsDetails(ProjectCreationType.IMPORT_PROJECT_FROM_FILE, 9); + await reportStep.checkDoraMetricsDetails(ProjectCreationType.IMPORT_PROJECT_FROM_FILE); await reportStep.checkDownloadReports(); }); diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-darwin.png deleted file mode 100644 index cfbd2ce42f..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Google-Chrome-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-darwin.png deleted file mode 100644 index 1a9dd5664b..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Microsoft-Edge-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-darwin.png deleted file mode 100644 index 134befc210..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-linux.png deleted file mode 100644 index dcce64c647..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-Tablet-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-darwin.png deleted file mode 100644 index 193117a378..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-linux.png deleted file mode 100644 index a2c2de3823..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-chromium-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-darwin.png deleted file mode 100644 index 693567f7ce..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-linux.png deleted file mode 100644 index aa635a5e90..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-Board-Metrics-webkit-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-darwin.png deleted file mode 100644 index 4bfe590760..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Google-Chrome-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-darwin.png deleted file mode 100644 index f992350e31..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Microsoft-Edge-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-darwin.png deleted file mode 100644 index eef81143d3..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-linux.png deleted file mode 100644 index 8fe27e0095..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-Tablet-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-darwin.png deleted file mode 100644 index f4aa046fb3..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-linux.png deleted file mode 100644 index 9c004b2a31..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-chromium-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-darwin.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-darwin.png deleted file mode 100644 index df73d79709..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-darwin.png and /dev/null differ diff --git a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-linux.png b/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-linux.png deleted file mode 100644 index b5f894cd1e..0000000000 Binary files a/frontend/e2e/specs/import-project-from-file.spec.ts-snapshots/import-project-from-file-DORA-Metrics-webkit-linux.png and /dev/null differ diff --git a/frontend/e2e/specs/page-jumps.spec.ts b/frontend/e2e/specs/page-jumps.spec.ts index ee004af4fc..898033f8ae 100644 --- a/frontend/e2e/specs/page-jumps.spec.ts +++ b/frontend/e2e/specs/page-jumps.spec.ts @@ -40,7 +40,7 @@ test('Page jump for import', async ({ homePage, configStep, metricsStep, reportS await metricsStep.checkCycleTimeConsiderAsBlockUnchecked(); await metricsStep.checkClassifications(modifiedMetricsStepData.classification); - await metricsStep.selectDefaultGivenPipelineSetting(modifiedMetricsStepData.deployment); + await metricsStep.selectDefaultGivenPipelineSetting(modifiedMetricsStepData.deployment, false); await metricsStep.selectGivenPipelineCrews(modifiedMetricsStepData.pipelineCrews); await metricsStep.goToPreviousStep(); await configStep.goToMetrics(); @@ -107,7 +107,7 @@ test('Page jump for create', async ({ homePage, configStep, metricsStep, reportS await metricsStep.checkBoardByStatusRadioBoxChecked(); await metricsStep.checkClassifications(modifiedMetricsStepData.classification); - await metricsStep.selectDefaultGivenPipelineSetting(modifiedMetricsStepData.deployment); + await metricsStep.selectDefaultGivenPipelineSetting(modifiedMetricsStepData.deployment, false); await metricsStep.selectGivenPipelineCrews(modifiedMetricsStepData.pipelineCrews); await metricsStep.goToPreviousStep(); await configStep.goToMetrics(); diff --git a/frontend/package.json b/frontend/package.json index 45b7cfa69b..432a08cd1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,12 +22,14 @@ "e2e:major-ci": "dotenvx run --env-file=./e2e/.env.ci -- pnpm run e2e:major", "e2e:local": "dotenvx run --env-file=./e2e/.env.local -- pnpm run e2e:major", "e2e:with-server": "ts-node --project tsconfig.scripts.json ./scripts/runE2eWithServer.ts 'pnpm run e2e:local'", - "e2e:ui": "dotenvx run --env-file=./e2e/.env.local -- pnpm exec playwright test --ui --headed", + "e2e:debug": "pnpm run e2e:local --debug", + "e2e:ui": "dotenvx run --env-file=./e2e/.env.local -- pnpm exec playwright test --ui", + "e2e:headed": "dotenvx run --env-file=./e2e/.env.local -- pnpm exec playwright test --ui --headed", "e2e:report": "pnpm exec playwright show-report", "e2e:codegen": "pnpm exec playwright codegen 13.215.41.120:4321", "e2e:build-docker-image": "docker build -t 'heartbeat_e2e:latest' ../ -f ../ops/infra/Dockerfile.e2e", "e2e:updateSnapshots": "pnpm run e2e:local --update-snapshots", - "e2e:updateSnapshots-docker": "docker run --rm --network=host -v $(pwd)/e2e:/app/e2e -w /app -it heartbeat_e2e:latest pnpm run e2e:updateSnapshots", + "e2e:updateSnapshots-docker": "docker run --rm --network=host -e APP_ORIGIN='http://host.docker.internal:4321' -v $(pwd)/e2e:/app/e2e -w /app -it heartbeat_e2e:latest pnpm run e2e:updateSnapshots", "prepare": "cd .. && husky install frontend/.husky", "license-compliance": "license-compliance -r detailed", "type-check": "tsc --noEmit" diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 94823198b0..07f40949ec 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,4 +1,4 @@ -import { E2E_EXPECT_TIMEOUT, VIEWPORT_DEFAULT } from 'e2e/fixtures'; +import { E2E_EXPECT_LOCAL_TIMEOUT, E2E_EXPECT_CI_TIMEOUT, VIEWPORT_DEFAULT } from 'e2e/fixtures'; import { defineConfig, devices } from '@playwright/test'; /** @@ -17,7 +17,7 @@ export default defineConfig({ timeout: 3 * 60 * 1000, testDir: './e2e', expect: { - timeout: E2E_EXPECT_TIMEOUT, + timeout: process.env.CI ? E2E_EXPECT_CI_TIMEOUT : E2E_EXPECT_LOCAL_TIMEOUT, }, /* Run tests in files in parallel */ fullyParallel: true, diff --git a/frontend/src/clients/board/dto/request.ts b/frontend/src/clients/board/dto/request.ts index fa452b69c4..9d04365139 100644 --- a/frontend/src/clients/board/dto/request.ts +++ b/frontend/src/clients/board/dto/request.ts @@ -3,8 +3,6 @@ export interface BoardRequestDTO { type: string; site: string; email: string; - startTime: number | null; - endTime: number | null; boardId: string; } diff --git a/frontend/src/components/Common/ReportForTwoColumns/style.tsx b/frontend/src/components/Common/ReportForTwoColumns/style.tsx index e0be3b5386..de8abb107b 100644 --- a/frontend/src/components/Common/ReportForTwoColumns/style.tsx +++ b/frontend/src/components/Common/ReportForTwoColumns/style.tsx @@ -10,12 +10,13 @@ export const Row = styled(TableRow)({}); export const StyledTableCell = styled(TableCell)(() => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.secondary.dark, + border: 'none', fontWeight: 600, }, })); export const BorderTableCell = styled(TableCell)(() => ({ - border: `0.06rem solid ${theme.palette.secondary.dark}`, + border: `0.07rem solid ${theme.main.boardColor}`, borderRight: 'none', color: theme.palette.secondary.contrastText, })); diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 1ddc218017..e12a51bf50 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -271,6 +271,8 @@ export enum CYCLE_TIME_SETTINGS_TYPES { export const AXIOS_NETWORK_ERROR_CODES = [AxiosError.ECONNABORTED, AxiosError.ETIMEDOUT, AxiosError.ERR_NETWORK]; +export const NO_PIPELINE_STEP_ERROR = 'No steps for this pipeline!'; + export enum HEARTBEAT_EXCEPTION_CODE { TIMEOUT = 'HB_TIMEOUT', } diff --git a/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx b/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx index 3f9357126f..76f9f052ee 100644 --- a/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx +++ b/frontend/src/containers/ConfigStep/DateRangePicker/index.tsx @@ -1,3 +1,9 @@ +import { + initDeploymentFrequencySettings, + saveUsers, + updateShouldGetBoardConfig, + updateShouldGetPipelineConfig, +} from '@src/context/Metrics/metricsSlice'; import { selectDateRange, updateDateRange } from '@src/context/config/configSlice'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { StyledDateRangePicker, StyledDateRangePickerContainer } from './style'; @@ -12,6 +18,12 @@ import isNull from 'lodash/isNull'; export const DateRangePicker = () => { const dispatch = useAppDispatch(); const { startDate, endDate } = useAppSelector(selectDateRange); + const dispatchUpdateConfig = () => { + dispatch(updateShouldGetBoardConfig(true)); + dispatch(updateShouldGetPipelineConfig(true)); + dispatch(initDeploymentFrequencySettings()); + dispatch(saveUsers([])); + }; const changeStartDate = (value: Nullable) => { dispatch( updateDateRange( @@ -26,6 +38,7 @@ export const DateRangePicker = () => { }, ), ); + dispatchUpdateConfig(); }; const changeEndDate = (value: Dayjs) => { @@ -35,6 +48,7 @@ export const DateRangePicker = () => { endDate: !isNull(value) ? value.endOf('date').format('YYYY-MM-DDTHH:mm:ss.SSSZ') : null, }), ); + dispatchUpdateConfig(); }; return ( diff --git a/frontend/src/containers/ConfigStep/SourceControl/index.tsx b/frontend/src/containers/ConfigStep/SourceControl/index.tsx index 3fa63a0009..0bd99decc5 100644 --- a/frontend/src/containers/ConfigStep/SourceControl/index.tsx +++ b/frontend/src/containers/ConfigStep/SourceControl/index.tsx @@ -11,6 +11,7 @@ import { updateSourceControl, updateSourceControlVerifyState, } from '@src/context/config/configSlice'; +import { initDeploymentFrequencySettings, updateShouldGetPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { useVerifySourceControlTokenEffect } from '@src/hooks/useVerifySourceControlTokenEffect'; import { CONFIG_TITLE, SOURCE_CONTROL_TYPES, TOKEN_HELPER_TEXT } from '@src/constants/resources'; import { ResetButton, VerifyButton } from '@src/components/Common/Buttons'; @@ -67,6 +68,8 @@ export const SourceControl = () => { token: fields[FIELD_KEY.TOKEN].value, }), ); + dispatch(updateShouldGetPipelineConfig(true)); + dispatch(initDeploymentFrequencySettings()); }; const getNewFields = (value: string) => diff --git a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/BranchSelection/index.tsx b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/BranchSelection/index.tsx index 8fce3f5dba..cf3269471a 100644 --- a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/BranchSelection/index.tsx +++ b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/BranchSelection/index.tsx @@ -13,6 +13,7 @@ import { Autocomplete, Checkbox, TextField } from '@mui/material'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { useAppSelector } from '@src/hooks'; +import { intersection } from 'lodash'; export interface BranchSelectionProps { id: number; @@ -34,6 +35,12 @@ export const BranchSelection = (props: BranchSelectionProps) => { [organization, pipelineList, pipelineName], ); + const validBranches = useMemo( + () => intersection(currentPipeline?.branches || [], branches), + [currentPipeline, branches], + ); + const repository = currentPipeline?.repository ?? ''; + const branchesOptions: FormFieldWithMeta[] = useMemo(() => { const branchesOptions = currentPipeline?.branches ?? []; return branchesOptions.map((item) => ({ value: item })); @@ -45,7 +52,7 @@ export const BranchSelection = (props: BranchSelectionProps) => { }, [formMeta.metrics.pipelines, id]); const selectedBranchesWithMeta = useMemo(() => { - return branches.map((item) => { + return validBranches.map((item) => { const metaInfo = branchesFormData.find((branch) => branch.value === item); const shouldVerifyBranches = sourceControlFields.token !== ''; @@ -56,7 +63,7 @@ export const BranchSelection = (props: BranchSelectionProps) => { needVerify: shouldVerifyBranches, }; }); - }, [branches, branchesFormData, sourceControlFields.token]); + }, [validBranches, branchesFormData, sourceControlFields.token]); const updateSingleBranchMeta = useCallback( (branchWithMeta: FormFieldWithMeta) => { @@ -135,7 +142,7 @@ export const BranchSelection = (props: BranchSelectionProps) => { {...props} {...option} key={key} - repository={currentPipeline?.repository ?? ''} + repository={repository} updateBranchMeta={updateSingleBranchMeta} /> ); diff --git a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/index.tsx b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/index.tsx index d67acd935d..d4264dd74d 100644 --- a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/index.tsx +++ b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/index.tsx @@ -19,12 +19,12 @@ import { BranchSelection } from '@src/containers/MetricsStep/DeploymentFrequency import { ButtonWrapper, PipelineMetricSelectionWrapper, RemoveButton, WarningMessage } from './style'; import { WarningNotification } from '@src/components/Common/WarningNotification'; import { useGetMetricsStepsEffect } from '@src/hooks/useGetMetricsStepsEffect'; +import { MESSAGE, NO_PIPELINE_STEP_ERROR } from '@src/constants/resources'; import { ErrorNotification } from '@src/components/ErrorNotification'; import { shouldMetricsLoad } from '@src/context/stepper/StepperSlice'; import { useAppDispatch, useAppSelector } from '@src/hooks'; -import { MESSAGE } from '@src/constants/resources'; +import { useEffect, useMemo, useState } from 'react'; import { Loading } from '@src/components/Loading'; -import { useEffect, useState } from 'react'; import { store } from '@src/store'; interface pipelineMetricSelectionProps { @@ -65,6 +65,8 @@ export const PipelineMetricSelection = ({ const shouldLoad = useAppSelector(shouldMetricsLoad); const shouldGetPipelineConfig = useAppSelector(selectShouldGetPipelineConfig); + const validStepValue = useMemo(() => (stepsOptions.includes(step) ? step : ''), [step, stepsOptions]); + const handleRemoveClick = () => { onRemovePipeline(id); }; @@ -134,7 +136,9 @@ export const PipelineMetricSelection = ({ id={id} options={stepsOptions} label={'Step'} - value={step} + value={validStepValue} + isError={isShowNoStepWarning} + errorText={NO_PIPELINE_STEP_ERROR} onUpDatePipeline={(id, label, value) => onUpdatePipeline(id, label, value)} /> )} diff --git a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection/index.tsx b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection/index.tsx index fcc6c7bf6f..5f2a43e800 100644 --- a/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection/index.tsx +++ b/frontend/src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection/index.tsx @@ -1,8 +1,11 @@ +import { selectDeploymentFrequencySettings } from '@src/context/Metrics/metricsSlice'; import { getEmojiUrls, removeExtraEmojiName } from '@src/constants/emojis/emoji'; import { Autocomplete, Box, ListItemText, TextField } from '@mui/material'; +import { getDisabledOptions, sortDisabledOptions } from '@src/utils/util'; import { EmojiWrap, StyledAvatar } from '@src/constants/emojis/style'; -import { Z_INDEX } from '@src/constants/commons'; +import { DEFAULT_HELPER_TEXT, Z_INDEX } from '@src/constants/commons'; import { FormControlWrapper } from './style'; +import { useAppSelector } from '@src/hooks'; import React, { useState } from 'react'; interface Props { @@ -10,13 +13,25 @@ interface Props { label: string; value: string; id: number; + isError?: boolean; + errorText?: string; onGetSteps?: (pipelineName: string) => void; onUpDatePipeline: (id: number, label: string, value: string) => void; } -export const SingleSelection = ({ options, label, value, id, onGetSteps, onUpDatePipeline }: Props) => { +export const SingleSelection = ({ + options, + label, + value, + id, + isError = false, + errorText, + onGetSteps, + onUpDatePipeline, +}: Props) => { const labelId = `single-selection-${label.toLowerCase().replace(' ', '-')}`; const [inputValue, setInputValue] = useState(value); + const deploymentFrequencySettings = useAppSelector(selectDeploymentFrequencySettings); const handleSelectedOptionsChange = (value: string) => { if (onGetSteps) { @@ -37,7 +52,10 @@ export const SingleSelection = ({ options, label, value, id, onGetSteps, onUpDat + label === 'Pipeline Name' && getDisabledOptions(deploymentFrequencySettings, option) + } getOptionLabel={(option: string) => removeExtraEmojiName(option).trim()} renderOption={(props, option: string) => ( @@ -55,7 +73,16 @@ export const SingleSelection = ({ options, label, value, id, onGetSteps, onUpDat onInputChange={(event, newInputValue) => { setInputValue(newInputValue); }} - renderInput={(params) => } + renderInput={(params) => ( + + )} slotProps={{ popper: { sx: { diff --git a/frontend/src/containers/MetricsStep/index.tsx b/frontend/src/containers/MetricsStep/index.tsx index 5f20b9b304..e767306899 100644 --- a/frontend/src/containers/MetricsStep/index.tsx +++ b/frontend/src/containers/MetricsStep/index.tsx @@ -19,14 +19,13 @@ import { MetricSelectionWrapper, MetricsSelectionTitle, } from '@src/containers/MetricsStep/style'; +import { CYCLE_TIME_SETTINGS_TYPES, DONE, REQUIRED_DATA, HEARTBEAT_EXCEPTION_CODE } from '@src/constants/resources'; import { DeploymentFrequencySettings } from '@src/containers/MetricsStep/DeploymentFrequencySettings'; import { StyledRetryButton, StyledErrorMessage } from '@src/containers/MetricsStep/style'; -import { CYCLE_TIME_SETTINGS_TYPES, DONE, REQUIRED_DATA } from '@src/constants/resources'; import { closeAllNotifications } from '@src/context/notification/NotificationSlice'; import { Classification } from '@src/containers/MetricsStep/Classification'; import { shouldMetricsLoad } from '@src/context/stepper/StepperSlice'; import DateRangeViewer from '@src/components/Common/DateRangeViewer'; -import { HEARTBEAT_EXCEPTION_CODE } from '@src/constants/resources'; import { useGetBoardInfoEffect } from '@src/hooks/useGetBoardInfo'; import { CycleTime } from '@src/containers/MetricsStep/CycleTime'; import { RealDone } from '@src/containers/MetricsStep/RealDone'; @@ -102,7 +101,7 @@ const MetricsStep = () => { <> - {requiredData.includes(REQUIRED_DATA.CYCLE_TIME) && } + {isShowRealDone && ( diff --git a/frontend/src/containers/MetricsStepper/index.tsx b/frontend/src/containers/MetricsStepper/index.tsx index b71159b509..a47a676fae 100644 --- a/frontend/src/containers/MetricsStepper/index.tsx +++ b/frontend/src/containers/MetricsStepper/index.tsx @@ -73,7 +73,10 @@ const MetricsStepper = () => { const { isShow: isShowBoard, isVerified: isBoardVerified } = config.board; const { isShow: isShowPipeline, isVerified: isPipelineToolVerified } = config.pipelineTool; const { isShow: isShowSourceControl, isVerified: isSourceControlVerified } = config.sourceControl; - const isShowCycleTimeSettings = requiredData.includes(REQUIRED_DATA.CYCLE_TIME); + const isShowCycleTimeSettings = + requiredData.includes(REQUIRED_DATA.CYCLE_TIME) || + requiredData.includes(REQUIRED_DATA.CLASSIFICATION) || + requiredData.includes(REQUIRED_DATA.VELOCITY); const isCycleTimeSettingsVerified = cycleTimeSettings.some((e) => e.value === DONE); const isShowClassificationSetting = requiredData.includes(REQUIRED_DATA.CLASSIFICATION); const isClassificationSettingVerified = metricsConfig.targetFields.some((item) => item.flag); diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index c77661bf47..d62d325fe4 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -191,7 +191,7 @@ const DoraMetrics = ({ const getErrorMessage4Github = () => _.get(doraReport, ['reportMetricsError', 'sourceControlMetricsError']) - ? `Failed to get Github info, status: ${_.get(doraReport, [ + ? `Failed to get GitHub info, status: ${_.get(doraReport, [ 'reportMetricsError', 'sourceControlMetricsError', 'status', diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index bc4be033fc..7c3d2937f5 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -152,7 +152,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { setNotifications4SummaryPage((prevState) => [ ...prevState, { - message: MESSAGE.FAILED_TO_GET_DATA('Github'), + message: MESSAGE.FAILED_TO_GET_DATA('GitHub'), type: 'error', }, ]); diff --git a/frontend/src/context/Metrics/metricsSlice.ts b/frontend/src/context/Metrics/metricsSlice.ts index b8dbd59e65..4c854cd97f 100644 --- a/frontend/src/context/Metrics/metricsSlice.ts +++ b/frontend/src/context/Metrics/metricsSlice.ts @@ -378,8 +378,9 @@ export const metricsSlice = createSlice({ pipelineList .filter((pipeline: pipeline) => pipeline.orgName.toLowerCase() === organization.toLowerCase()) .map((item: pipeline) => item.name); - const getValidPipelines = (pipelines: IPipelineConfig[]) => - pipelines.length > 0 + const getValidPipelines = (pipelines: IPipelineConfig[]) => { + const hasPipeline = pipelines.filter(({ id }) => id !== undefined).length; + return pipelines.length && hasPipeline ? pipelines.map(({ id, organization, pipelineName, step, branches }) => ({ id, organization: orgNames.find((i) => (i as string).toLowerCase() === organization.toLowerCase()) || '', @@ -388,7 +389,7 @@ export const metricsSlice = createSlice({ branches: branches || [], })) : [{ id: 0, organization: '', pipelineName: '', step: '', branches: [] }]; - + }; const createPipelineWarning = ({ id, organization, pipelineName }: IPipelineConfig) => { const orgWarning = orgNames.some((i) => (i as string).toLowerCase() === organization.toLowerCase()) ? null @@ -407,7 +408,8 @@ export const metricsSlice = createSlice({ }; const getPipelinesWarningMessage = (pipelines: IPipelineConfig[]) => { - if (!pipelines.length || isProjectCreated) { + const hasPipeline = pipelines.filter(({ id }) => id !== undefined).length; + if (!pipelines.length || isProjectCreated || !hasPipeline) { return []; } return pipelines.map((pipeline) => createPipelineWarning(pipeline)); @@ -415,6 +417,7 @@ export const metricsSlice = createSlice({ const deploymentSettings = state.deploymentFrequencySettings.length > 0 ? state.deploymentFrequencySettings : importedDeployment; + state.deploymentFrequencySettings = getValidPipelines(deploymentSettings); state.deploymentWarningMessage = getPipelinesWarningMessage(deploymentSettings); }, diff --git a/frontend/src/hooks/useGetPipelineToolInfoEffect.ts b/frontend/src/hooks/useGetPipelineToolInfoEffect.ts index 38074b7c1b..629904f64b 100644 --- a/frontend/src/hooks/useGetPipelineToolInfoEffect.ts +++ b/frontend/src/hooks/useGetPipelineToolInfoEffect.ts @@ -42,11 +42,14 @@ export const useGetPipelineToolInfoEffect = (): IUseVerifyPipeLineToolStateInter endTime: dateRange.endDate, }; setIsLoading(true); - const response = await pipelineToolClient.getInfo(params); - setInfo(response); - dispatch(updatePipelineToolVerifyResponse(response.data)); - pipelineToolVerified && dispatch(updatePipelineSettings({ ...response.data, isProjectCreated })); - setIsLoading(false); + try { + const response = await pipelineToolClient.getInfo(params); + setInfo(response); + dispatch(updatePipelineToolVerifyResponse(response.data)); + pipelineToolVerified && dispatch(updatePipelineSettings({ ...response.data, isProjectCreated })); + } finally { + setIsLoading(false); + } }, [ dispatch, isProjectCreated, diff --git a/frontend/src/hooks/useVerifyBoardEffect.ts b/frontend/src/hooks/useVerifyBoardEffect.ts index 7e178bb0e7..d4214435e0 100644 --- a/frontend/src/hooks/useVerifyBoardEffect.ts +++ b/frontend/src/hooks/useVerifyBoardEffect.ts @@ -1,4 +1,4 @@ -import { selectBoard, selectDateRange, updateBoard, updateBoardVerifyState } from '@src/context/config/configSlice'; +import { selectBoard, updateBoard, updateBoardVerifyState } from '@src/context/config/configSlice'; import { BOARD_TYPES, MESSAGE, UNKNOWN_ERROR_TITLE } from '@src/constants/resources'; import { updateTreatFlagCardAsBlock } from '@src/context/Metrics/metricsSlice'; import { findCaseInsensitiveType, getJiraBoardToken } from '@src/utils/util'; @@ -11,7 +11,6 @@ import { isHeartBeatException } from '@src/exceptions'; import { REGEX } from '@src/constants/regex'; import { HttpStatusCode } from 'axios'; import { useState } from 'react'; -import dayjs from 'dayjs'; export interface Field { key: string; @@ -62,7 +61,6 @@ const getValidatedError = (key: string, value: string, validateRule?: (value: st export const useVerifyBoardEffect = (): useVerifyBoardStateInterface => { const [isLoading, setIsLoading] = useState(false); const boardFields = useAppSelector(selectBoard); - const dateRange = useAppSelector(selectDateRange); const dispatch = useAppDispatch(); const type = findCaseInsensitiveType(Object.values(BOARD_TYPES), boardFields.type); const [fields, setFields] = useState([ @@ -184,8 +182,6 @@ export const useVerifyBoardEffect = (): useVerifyBoardStateInterface => { try { const res: { response: Record } = await boardClient.getVerifyBoard({ ...boardInfo, - startTime: dayjs(dateRange.startDate).valueOf(), - endTime: dayjs(dateRange.endDate).valueOf(), token: getJiraBoardToken(boardInfo.token, boardInfo.email), }); if (res?.response) { diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index e9eef495a7..44e5b16784 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -12,6 +12,7 @@ declare module '@mui/material/styles' { backgroundColor: string; color: string; secondColor: string; + boardColor: string; fontSize: string; boxShadow: string; cardShadow: string; @@ -93,6 +94,7 @@ export const theme = createTheme({ backgroundColor: indigo[FIVE_HUNDRED], color: '#fff', secondColor: 'black', + boardColor: '#efefef', fontSize: '1rem', boxShadow: '0 0.2rem 0.1rem -0.1rem rgb(0 0 0 / 20%), 0 0.1rem 0.1rem 0 rgb(0 0 0 / 14%), 0 0.1rem 0.3rem 0 rgb(0 0 0 / 12%)', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 1a944b41d7..8dfcd247e3 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -1,9 +1,10 @@ import { CleanedBuildKiteEmoji, OriginBuildKiteEmoji } from '@src/constants/emojis/emoji'; import { CYCLE_TIME_SETTINGS_TYPES, METRICS_CONSTANTS } from '@src/constants/resources'; +import { ICycleTimeSetting, IPipelineConfig } from '@src/context/Metrics/metricsSlice'; import { ITargetFieldType } from '@src/components/Common/MultiAutoComplete/styles'; -import { ICycleTimeSetting } from '@src/context/Metrics/metricsSlice'; import { DATE_FORMAT_TEMPLATE } from '@src/constants/template'; import duration from 'dayjs/plugin/duration'; +import { includes, sortBy } from 'lodash'; import dayjs from 'dayjs'; dayjs.extend(duration); @@ -72,6 +73,17 @@ export const findCaseInsensitiveType = (option: string[], value: string): string return newValue ? newValue : value; }; +export const getDisabledOptions = (deploymentFrequencySettings: IPipelineConfig[], option: string) => { + return includes( + deploymentFrequencySettings.map((item) => item.pipelineName), + option, + ); +}; + +export const sortDisabledOptions = (deploymentFrequencySettings: IPipelineConfig[], options: string[]) => { + return sortBy(options, (item: string) => getDisabledOptions(deploymentFrequencySettings, item)); +}; + export const formatDate = (date: Date | string) => { return dayjs(date).format(DATE_FORMAT_TEMPLATE); }; diff --git a/ops/deploy.sh b/ops/deploy.sh index a38bdb19fb..83376c4e89 100755 --- a/ops/deploy.sh +++ b/ops/deploy.sh @@ -20,6 +20,7 @@ deploy_infra() { deploy_e2e() { sed -i -e "s/heartbeat_backend:latest/${AWS_ECR_HOST}\/heartbeat_backend:latest/g" ./ops/infra/docker-compose.yml sed -i -e "s/heartbeat_frontend:latest/${AWS_ECR_HOST}\/heartbeat_frontend:latest/g" ./ops/infra/docker-compose.yml + sed -i -e "s/SWAGGER_HOST_PLACEHOLDER/http:\/\/${AWS_EC2_IP_E2E}:4321/g" ./ops/infra/docker-compose.yml scp -o StrictHostKeyChecking=no -i /var/lib/buildkite-agent/.ssh/HeartBeatKeyPair.pem -P "${AWS_SSH_PORT}" ./ops/infra/docker-compose.yml "${AWS_USERNAME}@${AWS_EC2_IP_E2E}:./" @@ -45,6 +46,7 @@ deploy_e2e() { deploy_prod() { sed -i -e "s/heartbeat_backend:latest/${AWS_ECR_HOST}\/heartbeat_backend:latest/g" ./ops/infra/docker-compose.yml sed -i -e "s/heartbeat_frontend:latest/${AWS_ECR_HOST}\/heartbeat_frontend:latest/g" ./ops/infra/docker-compose.yml + sed -i -e "s/SWAGGER_HOST_PLACEHOLDER/http:\/\/${AWS_EC2_IP}:4321/g" ./ops/infra/docker-compose.yml scp -o StrictHostKeyChecking=no -i /var/lib/buildkite-agent/.ssh/HeartBeatKeyPair.pem -P "${AWS_SSH_PORT}" ./ops/infra/docker-compose.yml "${AWS_USERNAME}@${AWS_EC2_IP}:./" diff --git a/ops/infra/cloudformation.yml b/ops/infra/cloudformation.yml index eb42af2f33..1818701f60 100644 --- a/ops/infra/cloudformation.yml +++ b/ops/infra/cloudformation.yml @@ -248,7 +248,7 @@ Resources: Effect: Allow Principal: AWS: - - "*" + - !Ref EC2Role Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage @@ -287,7 +287,7 @@ Resources: Effect: Allow Principal: AWS: - - "*" + - !Ref EC2Role Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage @@ -323,7 +323,6 @@ Resources: - !Ref EC2SSHSecurityGroup - !Ref EC2SSHSecurityGroupForBuildkite - !Ref EC2AppSecurityGroup - - !Ref EC2OldAppSecurityGroup IamInstanceProfile: !Ref EC2InstanceProfile KeyName: !Ref EC2KeyPair UserData: @@ -456,18 +455,6 @@ Resources: ToPort: 22 CidrIp: 13.214.134.0/24 - EC2OldAppSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupDescription: Enable access application - Tags: - - Key: "Name" - Value: "Old app port" - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 4325 - ToPort: 4325 - CidrIp: 0.0.0.0/0 EC2AppSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: diff --git a/ops/infra/docker-compose.yml b/ops/infra/docker-compose.yml index 7d59bf2cd8..8d857112c3 100644 --- a/ops/infra/docker-compose.yml +++ b/ops/infra/docker-compose.yml @@ -16,6 +16,7 @@ services: - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-default} - BACKEND_SECRET_KEY=fake - FIXED_SALT=fake + - SWAGGER_HOST=SWAGGER_HOST_PLACEHOLDER frontend: image: heartbeat_frontend:latest container_name: frontend