diff --git a/backend/src/main/java/heartbeat/client/GitHubFeignClient.java b/backend/src/main/java/heartbeat/client/GitHubFeignClient.java index 7ac8d44a13..609fdc6af3 100644 --- a/backend/src/main/java/heartbeat/client/GitHubFeignClient.java +++ b/backend/src/main/java/heartbeat/client/GitHubFeignClient.java @@ -4,9 +4,8 @@ import heartbeat.client.dto.codebase.github.BranchesInfoDTO; import heartbeat.client.dto.codebase.github.CommitInfo; import heartbeat.client.dto.codebase.github.OrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PullRequestInfo; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PullRequestInfo; import heartbeat.client.dto.codebase.github.ReposInfoDTO; import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.openfeign.FeignClient; @@ -66,7 +65,7 @@ ResponseEntity> getAllBranches(@RequestHeader("Authorizati @GetMapping(path = "/repos/{org}/{repo}/pulls") @ResponseStatus(HttpStatus.OK) - ResponseEntity> getAllPullRequests(@RequestHeader("Authorization") String token, + ResponseEntity> getAllPullRequests(@RequestHeader("Authorization") String token, @PathVariable("org") String org, @PathVariable("repo") String repo, @RequestParam("per_page") int perPage, @RequestParam("page") int page, @RequestParam("base") String base, @RequestParam("state") String state); diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/CommitInfo.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/CommitInfo.java index f3b05d35b3..b08dc6dc13 100644 --- a/backend/src/main/java/heartbeat/client/dto/codebase/github/CommitInfo.java +++ b/backend/src/main/java/heartbeat/client/dto/codebase/github/CommitInfo.java @@ -1,5 +1,6 @@ package heartbeat.client.dto.codebase.github; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -13,6 +14,9 @@ @AllArgsConstructor public class CommitInfo implements Serializable { + @JsonProperty("sha") + private String commitId; + private Commit commit; } diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/LeadTime.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/LeadTime.java index bd065b1e2f..4d9d00f8c1 100644 --- a/backend/src/main/java/heartbeat/client/dto/codebase/github/LeadTime.java +++ b/backend/src/main/java/heartbeat/client/dto/codebase/github/LeadTime.java @@ -14,6 +14,10 @@ public class LeadTime { private String commitId; + private String committer; + + private Integer pullNumber; + @Nullable private Long prCreatedTime; @@ -23,9 +27,9 @@ public class LeadTime { @Nullable private Long firstCommitTimeInPr; - private long jobFinishTime; + private Long jobFinishTime; - private long jobStartTime; + private Long jobStartTime; @Nullable private Long noPRCommitTime; @@ -33,7 +37,7 @@ public class LeadTime { @Nullable private Long firstCommitTime; - private long pipelineCreateTime; + private Long pipelineCreateTime; @Nullable private Boolean isRevert; diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfoDTO.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfo.java similarity index 79% rename from backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfoDTO.java rename to backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfo.java index ab51311f57..437557ff5a 100644 --- a/backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfoDTO.java +++ b/backend/src/main/java/heartbeat/client/dto/codebase/github/PagePullRequestInfo.java @@ -14,10 +14,10 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class PagePullRequestInfoDTO implements Serializable { +public class PagePullRequestInfo implements Serializable { private int totalPage; - private List pageInfo; + private List pageInfo; } 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 5572cf6cd9..5853fc905b 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 @@ -1,8 +1,10 @@ package heartbeat.client.dto.codebase.github; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -14,6 +16,7 @@ @NoArgsConstructor @AllArgsConstructor @Getter +@JsonIgnoreProperties(ignoreUnknown = true) public class PullRequestInfo implements Serializable { private Integer number; @@ -29,4 +32,17 @@ public class PullRequestInfo implements Serializable { @JsonProperty("merge_commit_sha") private String mergeCommitSha; + private PullRequestUser user; + + @AllArgsConstructor + @NoArgsConstructor + @Builder + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class PullRequestUser implements Serializable { + + private String login; + + } + } diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfoDTO.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfoDTO.java deleted file mode 100644 index 5277b7976a..0000000000 --- a/backend/src/main/java/heartbeat/client/dto/codebase/github/PullRequestInfoDTO.java +++ /dev/null @@ -1,40 +0,0 @@ -package heartbeat.client.dto.codebase.github; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Data -@JsonIgnoreProperties(ignoreUnknown = true) -public class PullRequestInfoDTO implements Serializable { - - private int number; - - @JsonProperty("created_at") - private String createdAt; - - @JsonProperty("merged_at") - private String mergedAt; - - private PullRequestUser user; - - @AllArgsConstructor - @NoArgsConstructor - @Builder - @Data - @JsonIgnoreProperties(ignoreUnknown = true) - public static class PullRequestUser implements Serializable { - - private String login; - - } - -} diff --git a/backend/src/main/java/heartbeat/client/dto/codebase/github/SourceControlLeadTime.java b/backend/src/main/java/heartbeat/client/dto/codebase/github/SourceControlLeadTime.java new file mode 100644 index 0000000000..30fe2f4074 --- /dev/null +++ b/backend/src/main/java/heartbeat/client/dto/codebase/github/SourceControlLeadTime.java @@ -0,0 +1,24 @@ +package heartbeat.client.dto.codebase.github; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SourceControlLeadTime { + + private String organization; + + private String repo; + + private String branch; + + private List leadTimes; + +} diff --git a/backend/src/main/java/heartbeat/config/CacheConfig.java b/backend/src/main/java/heartbeat/config/CacheConfig.java index bfedfa504d..167c470d06 100644 --- a/backend/src/main/java/heartbeat/config/CacheConfig.java +++ b/backend/src/main/java/heartbeat/config/CacheConfig.java @@ -1,7 +1,6 @@ package heartbeat.config; import heartbeat.client.dto.board.jira.CardHistoryResponseDTO; -import heartbeat.client.dto.board.jira.CalendarificHolidayResponseDTO; import heartbeat.client.dto.board.jira.FieldResponseDTO; import heartbeat.client.dto.board.jira.HolidaysResponseDTO; import heartbeat.client.dto.board.jira.JiraBoardConfigDTO; @@ -11,7 +10,7 @@ import heartbeat.client.dto.codebase.github.CommitInfo; import heartbeat.client.dto.codebase.github.PageBranchesInfoDTO; import heartbeat.client.dto.codebase.github.PageOrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PagePullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PagePullRequestInfo; import heartbeat.client.dto.codebase.github.PageReposInfoDTO; import heartbeat.client.dto.pipeline.buildkite.BuildKiteTokenInfo; import heartbeat.client.dto.pipeline.buildkite.PageBuildKitePipelineInfoDTO; @@ -64,7 +63,7 @@ public CacheManager ehCacheManager() { cacheManager.createCache("pageOrganization", getCacheConfiguration(PageOrganizationsInfoDTO.class)); cacheManager.createCache("pageRepo", getCacheConfiguration(PageReposInfoDTO.class)); cacheManager.createCache("pageBranch", getCacheConfiguration(PageBranchesInfoDTO.class)); - cacheManager.createCache("pagePullRequest", getCacheConfiguration(PagePullRequestInfoDTO.class)); + cacheManager.createCache("pagePullRequest", getCacheConfiguration(PagePullRequestInfo.class)); return cacheManager; } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/CodeBase.java b/backend/src/main/java/heartbeat/controller/report/dto/request/CodeBase.java new file mode 100644 index 0000000000..091178981c --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/report/dto/request/CodeBase.java @@ -0,0 +1,22 @@ +package heartbeat.controller.report.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CodeBase { + + private List branches; + + private String repo; + + private String organization; + +} diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/CodebaseSetting.java b/backend/src/main/java/heartbeat/controller/report/dto/request/CodebaseSetting.java index 56a8caca08..dedc935ecc 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/request/CodebaseSetting.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/request/CodebaseSetting.java @@ -20,4 +20,8 @@ public class CodebaseSetting { private List leadTime; + private List crews; + + private List codebases; + } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/AvgLeadTimeForChanges.java b/backend/src/main/java/heartbeat/controller/report/dto/response/AvgLeadTimeForChanges.java index f18d1c6b6e..155587031c 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/AvgLeadTimeForChanges.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/AvgLeadTimeForChanges.java @@ -1,10 +1,17 @@ package heartbeat.controller.report.dto.response; +import heartbeat.util.DecimalUtil; +import io.micrometer.core.instrument.util.TimeUtils; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + +import static java.util.concurrent.TimeUnit.HOURS; + @Data @Builder @NoArgsConstructor @@ -20,4 +27,15 @@ public class AvgLeadTimeForChanges { private Double totalDelayTime; + public List getMetricsCsvRowData(String leadTimeForChangesTitle) { + List rows = new ArrayList<>(); + rows.add(new String[] { leadTimeForChangesTitle, name + " / PR Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(prLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, name + " / Pipeline Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipelineLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, name + " / Total Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(totalDelayTime, HOURS)) }); + return rows; + } + } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChanges.java b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChanges.java index 347d5481e2..f03e862a8a 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChanges.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChanges.java @@ -15,6 +15,8 @@ public class LeadTimeForChanges { private List leadTimeForChangesOfPipelines; + private List leadTimeForChangesOfSourceControls; + private AvgLeadTimeForChanges avgLeadTimeForChanges; } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfPipelines.java b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfPipelines.java index b6d4e2707d..5fe19d0bca 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfPipelines.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfPipelines.java @@ -1,10 +1,17 @@ package heartbeat.controller.report.dto.response; +import heartbeat.util.DecimalUtil; +import io.micrometer.core.instrument.util.TimeUtils; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + +import static java.util.concurrent.TimeUnit.HOURS; + @Data @NoArgsConstructor @AllArgsConstructor @@ -21,4 +28,20 @@ public class LeadTimeForChangesOfPipelines { private Double totalDelayTime; + private String extractPipelineStep(String step) { + return step.replaceAll(":\\w+: ", ""); + } + + public List getMetricsCsvRowData(String leadTimeForChangesTitle) { + List rows = new ArrayList<>(); + String pipelineStep = extractPipelineStep(this.step); + rows.add(new String[] { leadTimeForChangesTitle, this.name + " / " + pipelineStep + " / PR Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(prLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, this.name + " / " + pipelineStep + " / Pipeline Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipelineLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, this.name + " / " + pipelineStep + " / Total Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(totalDelayTime, HOURS)) }); + return rows; + } + } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfSourceControl.java b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfSourceControl.java new file mode 100644 index 0000000000..4d6a03da1b --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeForChangesOfSourceControl.java @@ -0,0 +1,42 @@ +package heartbeat.controller.report.dto.response; + +import heartbeat.util.DecimalUtil; +import io.micrometer.core.instrument.util.TimeUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.concurrent.TimeUnit.HOURS; + +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Data +public class LeadTimeForChangesOfSourceControl { + + private String organization; + + private String repo; + + private Double prLeadTime; + + private Double pipelineLeadTime; + + private Double totalDelayTime; + + public List getMetricsCsvRowData(String leadTimeForChangesTitle) { + List rows = new ArrayList<>(); + rows.add(new String[] { leadTimeForChangesTitle, organization + " / " + repo + " / PR Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(prLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, organization + " / " + repo + " / Pipeline Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipelineLeadTime, HOURS)) }); + rows.add(new String[] { leadTimeForChangesTitle, organization + " / " + repo + " / Total Lead Time", + DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(totalDelayTime, HOURS)) }); + return rows; + } + +} diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeInfo.java b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeInfo.java index 4a3e651e3e..f4f41e0f6b 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeInfo.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/LeadTimeInfo.java @@ -14,6 +14,8 @@ @AllArgsConstructor public class LeadTimeInfo { + private Integer pullNumber; + @Nullable private String prCreatedTime; @@ -43,10 +45,13 @@ public class LeadTimeInfo { private long nonWorkdays; + private String committer; + public LeadTimeInfo(LeadTime leadTime) { if (leadTime == null) { return; } + this.pullNumber = leadTime.getPullNumber(); this.firstCommitTimeInPr = convertToISOFormat(leadTime.getFirstCommitTimeInPr()); this.prCreatedTime = convertToISOFormat(leadTime.getPrCreatedTime()); this.prMergedTime = convertToISOFormat(leadTime.getPrMergedTime()); @@ -54,6 +59,7 @@ public LeadTimeInfo(LeadTime leadTime) { this.jobStartTime = convertToISOFormat(leadTime.getJobStartTime()); this.firstCommitTime = convertToISOFormat(leadTime.getFirstCommitTime()); this.noPRCommitTime = convertToISOFormat(leadTime.getNoPRCommitTime()); + this.committer = leadTime.getCommitter(); this.pipelineLeadTime = TimeUtil.msToHMS(leadTime.getPipelineLeadTime()); this.isRevert = leadTime.getIsRevert(); diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/PipelineCSVInfo.java b/backend/src/main/java/heartbeat/controller/report/dto/response/PipelineCSVInfo.java index ff384cd4de..2c3a865105 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/PipelineCSVInfo.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/PipelineCSVInfo.java @@ -18,6 +18,10 @@ public class PipelineCSVInfo { private String pipeLineName; + private String branchName; + + private String repoName; + private String stepName; private Boolean valid; diff --git a/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java b/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java index 8e270ad70c..453f23ebc0 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/response/ShareApiDetailsResponse.java @@ -19,6 +19,8 @@ public class ShareApiDetailsResponse { private List pipelines; + private List sourceControls; + private List classificationNames; } diff --git a/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java b/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java index 01e57c9846..288b433d1e 100644 --- a/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java +++ b/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java @@ -6,18 +6,20 @@ import heartbeat.client.dto.codebase.github.OrganizationsInfoDTO; import heartbeat.client.dto.codebase.github.PageBranchesInfoDTO; import heartbeat.client.dto.codebase.github.PageOrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PagePullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PagePullRequestInfo; import heartbeat.client.dto.codebase.github.PageReposInfoDTO; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PullRequestInfo; import heartbeat.client.dto.codebase.github.ReposInfoDTO; import heartbeat.client.dto.pipeline.buildkite.BuildKiteBuildInfo; import heartbeat.client.dto.pipeline.buildkite.PageBuildKitePipelineInfoDTO; import heartbeat.client.dto.pipeline.buildkite.PageStepsInfoDto; +import heartbeat.exception.BaseException; import heartbeat.exception.InternalServerErrorException; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; @@ -81,10 +83,13 @@ public PageOrganizationsInfoDTO getGitHubOrganizations(String token, int page, i int totalPage = parseTotalPage(allOrganizations.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageOrganizationsInfoDTO.builder().pageInfo(allOrganizations.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github organization info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github organization info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException(String + .format("Error to get paginated github organization info, page: %s, exception: %s", page, e)); + } + throw e; } } @@ -98,10 +103,13 @@ public PageReposInfoDTO getGitHubRepos(String token, String organization, int pa int totalPage = parseTotalPage(allRepos.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageReposInfoDTO.builder().pageInfo(allRepos.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github repo info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github repo info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException( + String.format("Error to get paginated github repo info, page: %s, exception: %s", page, e)); + } + throw e; } } @@ -116,29 +124,35 @@ public PageBranchesInfoDTO getGitHubBranches(String token, String organization, int totalPage = parseTotalPage(allRepos.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageBranchesInfoDTO.builder().pageInfo(allRepos.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github branch info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github branch info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException( + String.format("Error to get paginated github branch info, page: %s, exception: %s", page, e)); + } + throw e; } } @Cacheable(cacheNames = "pagePullRequest", key = "#token+'-'+#organization+'-'+#repo+'-'+#branch+'-'+#page+'-'+#perPage") - public PagePullRequestInfoDTO getGitHubPullRequest(String token, String organization, String repo, String branch, + public PagePullRequestInfo getGitHubPullRequest(String token, String organization, String repo, String branch, int page, int perPage) { try { log.info("Start to get paginated github pull request info, page: {}", page); - ResponseEntity> allPullRequests = gitHubFeignClient.getAllPullRequests(token, + ResponseEntity> allPullRequests = gitHubFeignClient.getAllPullRequests(token, organization, repo, perPage, page, branch, "all"); log.info("Successfully get paginated github pull request info, page: {}", page); int totalPage = parseTotalPage(allPullRequests.getHeaders().get(BUILD_KITE_LINK_HEADER)); - return PagePullRequestInfoDTO.builder().pageInfo(allPullRequests.getBody()).totalPage(totalPage).build(); + return PagePullRequestInfo.builder().pageInfo(allPullRequests.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github pull request info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github pull request info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException(String + .format("Error to get paginated github pull request info, page: %s, exception: %s", page, e)); + } + throw e; } } diff --git a/backend/src/main/java/heartbeat/service/report/CSVFileGenerator.java b/backend/src/main/java/heartbeat/service/report/CSVFileGenerator.java index 85ea2dc46a..7281598d55 100644 --- a/backend/src/main/java/heartbeat/service/report/CSVFileGenerator.java +++ b/backend/src/main/java/heartbeat/service/report/CSVFileGenerator.java @@ -3,7 +3,9 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import heartbeat.client.dto.pipeline.buildkite.BuildKiteBuildInfo; +import heartbeat.client.dto.pipeline.buildkite.DeployInfo; import heartbeat.controller.board.dto.request.CardStepsEnum; +import heartbeat.controller.report.dto.response.LeadTimeForChangesOfSourceControl; import heartbeat.controller.report.dto.response.PipelineChangeFailureRateOfPipeline; import heartbeat.controller.report.dto.response.PipelineMeanTimeToRecovery; import heartbeat.repository.FilePrefixType; @@ -73,8 +75,8 @@ private static Map getCustomFields(JiraCardDTO perRowCardDT } public void convertPipelineDataToCSV(String uuid, List leadTimeData, String csvTimeStamp) { - String[] headers = { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", - "Code Committer", "Build Creator", "First Code Committed Time In PR", "PR Created Time", + String[] headers = { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }; @@ -88,22 +90,28 @@ public void convertPipelineDataToCSV(String uuid, List leadTime } private String[] getRowData(PipelineCSVInfo csvInfo) { - String committerName = ofNullable(csvInfo.getBuildInfo().getAuthor()) + String committerName = ofNullable(csvInfo.getBuildInfo()).map(BuildKiteBuildInfo::getAuthor) .map(BuildKiteBuildInfo.Author::getUsername) - .orElse(null); + .orElse(csvInfo.getLeadTimeInfo().getCommitter()); - String creatorName = ofNullable(csvInfo.getBuildInfo().getCreator()).map(BuildKiteBuildInfo.Creator::getName) + String creatorName = ofNullable(csvInfo.getBuildInfo()).map(BuildKiteBuildInfo::getCreator) + .map(BuildKiteBuildInfo.Creator::getName) .orElse(null); String organization = csvInfo.getOrganizationName(); String pipelineName = csvInfo.getPipeLineName(); + String repoName = csvInfo.getRepoName(); String stepName = csvInfo.getStepName(); - String valid = String.valueOf(csvInfo.getValid()).toLowerCase(); - String buildNumber = String.valueOf(csvInfo.getBuildInfo().getNumber()); + String valid = ofNullable(csvInfo.getValid()).map(it -> String.valueOf(it).toLowerCase()).orElse(null); + String buildNumber = ofNullable(csvInfo.getBuildInfo()).map(it -> String.valueOf(it.getNumber())).orElse(null); + String pullNumber = ofNullable(csvInfo.getLeadTimeInfo().getPullNumber()).map(String::valueOf).orElse(null); + + String state = ofNullable(csvInfo.getPiplineStatus()).map(it -> it.equals(CANCELED_STATUS) ? CANCELED_STATUS + : ofNullable(csvInfo.getDeployInfo()).map(DeployInfo::getState).orElse(null)) + .orElse(null); - String state = csvInfo.getPiplineStatus().equals(CANCELED_STATUS) ? CANCELED_STATUS - : csvInfo.getDeployInfo().getState(); - String branch = csvInfo.getBuildInfo().getBranch(); + String branch = ofNullable(csvInfo.getBuildInfo()).map(BuildKiteBuildInfo::getBranch) + .orElse(csvInfo.getBranchName()); LeadTimeInfo leadTimeInfo = csvInfo.getLeadTimeInfo(); String firstCommitTimeInPr = leadTimeInfo.getFirstCommitTimeInPr(); @@ -112,16 +120,18 @@ private String[] getRowData(PipelineCSVInfo csvInfo) { String noPRCommitTime = leadTimeInfo.getNoPRCommitTime(); String jobStartTime = leadTimeInfo.getJobStartTime(); String pipelineStartTime = leadTimeInfo.getFirstCommitTime(); - String pipelineFinishTime = csvInfo.getDeployInfo().getJobFinishTime(); + String pipelineFinishTime = ofNullable(csvInfo.getDeployInfo()).map(DeployInfo::getJobFinishTime) + .orElse(leadTimeInfo.getJobFinishTime()); String nonWorkdays = String.valueOf(leadTimeInfo.getNonWorkdays() * 24); String totalTime = leadTimeInfo.getTotalTime(); String prLeadTime = leadTimeInfo.getPrLeadTime(); String pipelineLeadTime = leadTimeInfo.getPipelineLeadTime(); String isRevert = leadTimeInfo.getIsRevert() == null ? "" : String.valueOf(leadTimeInfo.getIsRevert()); - return new String[] { organization, pipelineName, stepName, valid, buildNumber, committerName, creatorName, - firstCommitTimeInPr, prCreatedTime, prMergedTime, noPRCommitTime, jobStartTime, pipelineStartTime, - pipelineFinishTime, nonWorkdays, totalTime, prLeadTime, pipelineLeadTime, state, branch, isRevert }; + return new String[] { organization, pipelineName, repoName, stepName, valid, buildNumber, pullNumber, + committerName, creatorName, firstCommitTimeInPr, prCreatedTime, prMergedTime, noPRCommitTime, + jobStartTime, pipelineStartTime, pipelineFinishTime, nonWorkdays, totalTime, prLeadTime, + pipelineLeadTime, state, branch, isRevert }; } public InputStreamResource getDataFromCSV(ReportType reportDataType, String uuid, String timeRangeAndTimeStamp) { @@ -489,34 +499,21 @@ private String extractPipelineStep(String step) { private List getRowsFromLeadTimeForChanges(LeadTimeForChanges leadTimeForChanges) { List rows = new ArrayList<>(); + String leadTimeForChangesTitle = "Lead time for changes"; List leadTimeForChangesOfPipelines = leadTimeForChanges .getLeadTimeForChangesOfPipelines(); - String leadTimeForChangesTitle = "Lead time for changes"; - leadTimeForChangesOfPipelines.forEach(pipeline -> { - String pipelineStep = extractPipelineStep(pipeline.getStep()); - rows.add(new String[] { leadTimeForChangesTitle, - pipeline.getName() + " / " + pipelineStep + " / PR Lead Time", - DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipeline.getPrLeadTime(), HOURS)) }); - rows.add(new String[] { leadTimeForChangesTitle, - pipeline.getName() + " / " + pipelineStep + " / Pipeline Lead Time", - DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipeline.getPipelineLeadTime(), HOURS)) }); - rows.add(new String[] { leadTimeForChangesTitle, - pipeline.getName() + " / " + pipelineStep + " / Total Lead Time", - DecimalUtil.formatDecimalTwo(TimeUtils.minutesToUnit(pipeline.getTotalDelayTime(), HOURS)) }); - }); + leadTimeForChangesOfPipelines + .forEach(pipeline -> rows.addAll(pipeline.getMetricsCsvRowData(leadTimeForChangesTitle))); + + List leadTimeForChangesOfSourceControls = leadTimeForChanges + .getLeadTimeForChangesOfSourceControls(); + leadTimeForChangesOfSourceControls + .forEach(sourceControl -> rows.addAll(sourceControl.getMetricsCsvRowData(leadTimeForChangesTitle))); AvgLeadTimeForChanges avgLeadTimeForChanges = leadTimeForChanges.getAvgLeadTimeForChanges(); - if (leadTimeForChangesOfPipelines.size() > 1) { - rows.add(new String[] { leadTimeForChangesTitle, avgLeadTimeForChanges.getName() + " / PR Lead Time", - DecimalUtil - .formatDecimalTwo(TimeUtils.minutesToUnit(avgLeadTimeForChanges.getPrLeadTime(), HOURS)) }); - rows.add(new String[] { leadTimeForChangesTitle, avgLeadTimeForChanges.getName() + " / Pipeline Lead Time", - DecimalUtil.formatDecimalTwo( - TimeUtils.minutesToUnit(avgLeadTimeForChanges.getPipelineLeadTime(), HOURS)) }); - rows.add(new String[] { leadTimeForChangesTitle, avgLeadTimeForChanges.getName() + " / Total Lead Time", - DecimalUtil - .formatDecimalTwo(TimeUtils.minutesToUnit(avgLeadTimeForChanges.getTotalDelayTime(), HOURS)) }); + if (leadTimeForChangesOfPipelines.size() + leadTimeForChangesOfSourceControls.size() > 1) { + rows.addAll(avgLeadTimeForChanges.getMetricsCsvRowData(leadTimeForChangesTitle)); } return rows; diff --git a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java index 1b5524ae4a..c8f140c371 100644 --- a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java +++ b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java @@ -29,6 +29,7 @@ import heartbeat.service.report.calculator.model.FetchedData; import heartbeat.service.report.calculator.model.FetchedData.BuildKiteData; import heartbeat.repository.FileRepository; +import heartbeat.service.source.github.GitHubService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.collections.CollectionUtils; @@ -56,6 +57,8 @@ public class GenerateReporterService { private final PipelineService pipelineService; + private final GitHubService gitHubService; + private final ClassificationCalculator classificationCalculator; private final DeploymentFrequencyCalculator deploymentFrequency; @@ -121,7 +124,7 @@ public void generateDoraReport(String uuid, GenerateReportRequest request) { uuid, timeRangeAndTimeStamp, MetricsDataCompleted.class, FilePrefixType.DATA_COMPLETED_PREFIX); if (previousMetricsCompleted != null && Boolean.FALSE.equals(previousMetricsCompleted.doraMetricsCompleted())) { - CompletableFuture.runAsync(() -> generateCSVForPipeline(uuid, request, fetchedData.getBuildKiteData())); + CompletableFuture.runAsync(() -> generateCSVForPipeline(uuid, request, fetchedData)); } } @@ -252,9 +255,8 @@ private synchronized ReportResponse generateSourceControlReporter(GenerateReport ReportResponse reportResponse = new ReportResponse(fileRepository.getExpiredTime()); request.getSourceControlMetrics() - .forEach(metric -> reportResponse.setLeadTimeForChanges( - leadTimeForChangesCalculator.calculate(fetchedData.getBuildKiteData().getPipelineLeadTimes(), - request.getBuildKiteSetting().getDeploymentEnvList()))); + .forEach(metric -> reportResponse + .setLeadTimeForChanges(leadTimeForChangesCalculator.calculate(fetchedData, request))); return reportResponse; } @@ -269,6 +271,7 @@ private void fetchGitHubData(GenerateReportRequest request, FetchedData fetchedD if (request.getCodebaseSetting() == null) throw new BadRequestException("Failed to fetch Github info due to code base setting is null."); fetchedData.setBuildKiteData(pipelineService.fetchGitHubData(request)); + fetchedData.setRepoData(gitHubService.fetchRepoData(request)); } private FetchedData fetchJiraBoardData(GenerateReportRequest request, FetchedData fetchedData) { @@ -280,9 +283,13 @@ private FetchedData fetchJiraBoardData(GenerateReportRequest request, FetchedDat return fetchedData; } - private void generateCSVForPipeline(String uuid, GenerateReportRequest request, BuildKiteData buildKiteData) { + private void generateCSVForPipeline(String uuid, GenerateReportRequest request, FetchedData fetchedData) { List pipelineData = pipelineService.generateCSVForPipeline(request.getStartTime(), - request.getEndTime(), buildKiteData, request.getBuildKiteSetting().getDeploymentEnvList()); + request.getEndTime(), fetchedData.getBuildKiteData(), + request.getBuildKiteSetting().getDeploymentEnvList()); + + pipelineData.addAll(gitHubService.generateCSVForSourceControl(fetchedData.getRepoData(), + request.getCodebaseSetting().getCodebases())); csvFileGenerator.convertPipelineDataToCSV(uuid, pipelineData, request.getTimeRangeAndTimeStamp()); asyncMetricsDataHandler.updateMetricsDataCompletedInHandler(uuid, request.getTimeRangeAndTimeStamp(), DORA, diff --git a/backend/src/main/java/heartbeat/service/report/ReportService.java b/backend/src/main/java/heartbeat/service/report/ReportService.java index 6f02231ccf..7dd533fa14 100644 --- a/backend/src/main/java/heartbeat/service/report/ReportService.java +++ b/backend/src/main/java/heartbeat/service/report/ReportService.java @@ -1,6 +1,7 @@ package heartbeat.service.report; import heartbeat.controller.report.dto.request.BuildKiteSetting; +import heartbeat.controller.report.dto.request.CodebaseSetting; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.JiraBoardSetting; import heartbeat.controller.report.dto.request.MetricType; @@ -160,10 +161,17 @@ public ShareApiDetailsResponse getShareReportInfo(String uuid) { .map(it -> String.format("%s/%s", it.getName(), it.getStep())) .distinct() .toList(); + List sourceControls = savedRequestInfoList.stream() + .map(SavedRequestInfo::getSourceControl) + .flatMap(Collection::stream) + .map(it -> String.format("%s/%s", it.getOrganization(), it.getRepo())) + .distinct() + .toList(); return ShareApiDetailsResponse.builder() .metrics(metrics) .classificationNames(classificationCharts) .pipelines(pipelines) + .sourceControls(sourceControls) .reportURLs(reportUrls) .build(); } @@ -181,6 +189,8 @@ public void saveRequestInfo(GenerateReportRequest request, String uuid) { .metrics(request.getMetrics()) .pipelines(ofNullable(request.getBuildKiteSetting()).map(BuildKiteSetting::getDeploymentEnvList) .orElse(List.of())) + .sourceControl( + ofNullable(request.getCodebaseSetting()).map(CodebaseSetting::getCodebases).orElse(List.of())) .classificationNames(ofNullable(request.getJiraBoardSetting()).map(JiraBoardSetting::getClassificationNames) .orElse(List.of())) .build(); diff --git a/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java index 5f0036e51d..282d03145b 100644 --- a/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java +++ b/backend/src/main/java/heartbeat/service/report/SavedRequestInfo.java @@ -1,6 +1,7 @@ package heartbeat.service.report; import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; +import heartbeat.controller.report.dto.request.CodeBase; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,6 +19,8 @@ public class SavedRequestInfo { private List pipelines; + private List sourceControl; + private List classificationNames; } diff --git a/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java b/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java index 78a0a8df81..2c8444922a 100644 --- a/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java +++ b/backend/src/main/java/heartbeat/service/report/calculator/LeadTimeForChangesCalculator.java @@ -2,17 +2,23 @@ import heartbeat.client.dto.codebase.github.LeadTime; import heartbeat.client.dto.codebase.github.PipelineLeadTime; +import heartbeat.client.dto.codebase.github.SourceControlLeadTime; import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; +import heartbeat.controller.report.dto.request.CodeBase; +import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.response.AvgLeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChangesOfPipelines; +import heartbeat.controller.report.dto.response.LeadTimeForChangesOfSourceControl; +import heartbeat.service.report.calculator.model.AverageLeadTime; +import heartbeat.service.report.calculator.model.FetchedData; import heartbeat.util.TimeUtil; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Component; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.stream.LongStream; @@ -22,39 +28,41 @@ @Component public class LeadTimeForChangesCalculator { - public LeadTimeForChanges calculate(List pipelineLeadTime, - List deploymentEnvironmentList) { - int pipelineCount = pipelineLeadTime.size(); - List leadTimeForChangesOfPipelines = new ArrayList<>(); - AvgLeadTimeForChanges avgLeadTimeForChanges = new AvgLeadTimeForChanges(); + public LeadTimeForChanges calculate(FetchedData fetchedData, GenerateReportRequest request) { + List pipelineLeadTime = fetchedData.getBuildKiteData().getPipelineLeadTimes(); + List deploymentEnvironmentList = request.getBuildKiteSetting().getDeploymentEnvList(); - if (pipelineLeadTime.isEmpty()) { - return new LeadTimeForChanges(leadTimeForChangesOfPipelines, avgLeadTimeForChanges); - } + Pair, List> pipelinePair = calculateLeadTimeForChangesOfPipelines( + pipelineLeadTime, deploymentEnvironmentList); + List leadTimeForChangesOfPipelines = pipelinePair.getLeft(); + List avgDelayTimesOfPipelines = pipelinePair.getRight(); + + List sourceControlLeadTimes = fetchedData.getRepoData().getSourceControlLeadTimes(); + List sourceControlCodeBase = request.getCodebaseSetting().getCodebases(); + + Pair, List> sourceControlPair = calculateLeadTimeForChangesOfSourceControl( + sourceControlLeadTimes, sourceControlCodeBase); + List leadTimeForChangesOfSourceControls = sourceControlPair.getLeft(); + List avgDelayTimesOfSourceControls = sourceControlPair.getRight(); + + AvgLeadTimeForChanges avgLeadTimeForChanges = calculateAverageLeadTimeForChanges(avgDelayTimesOfPipelines, + avgDelayTimesOfSourceControls); + + return new LeadTimeForChanges(leadTimeForChangesOfPipelines, leadTimeForChangesOfSourceControls, + avgLeadTimeForChanges); + } + + private Pair, List> calculateLeadTimeForChangesOfPipelines( + List pipelineLeadTime, List deploymentEnvironmentList) { + List leadTimeForChangesOfPipelines = new ArrayList<>(); - List> avgDelayTimeMapList = pipelineLeadTime.stream().map(item -> { - if (item.getLeadTimes() == null || item.getLeadTimes().isEmpty()) { - return new HashMap(); + List avgDelayTimeList = pipelineLeadTime.stream().map(item -> { + Pair leadTimePair = calculateLeadTimeForChanges(item.getLeadTimes()); + if (leadTimePair == null) { + return new AverageLeadTime(0d, 0d); } - int buildNumber = item.getLeadTimes().size(); - // 过滤掉noPr的数据 - List noPrLeadTime = item.getLeadTimes() - .stream() - .filter(leadTime -> leadTime.getPrMergedTime() != null && leadTime.getPrMergedTime() != 0) - .filter(leadTime -> leadTime.getPrLeadTime() != null && leadTime.getPrLeadTime() != 0) - .toList(); - // 通过noPrLeadTimeList去计算totalPrLeadTime - double totalPrLeadTime = noPrLeadTime.stream() - .flatMapToLong(leadTime -> LongStream.of(leadTime.getPrLeadTime())) - .sum(); - // 通过PipelineLeadTime去计算totalPipelineLeadTime - double totalPipelineLeadTime = item.getLeadTimes() - .stream() - .flatMapToLong(leadTime -> LongStream.of(leadTime.getPipelineLeadTime())) - .sum(); - - double avgPrLeadTime = TimeUtil.convertMillisecondToMinutes(totalPrLeadTime / buildNumber); - double avgPipelineLeadTime = TimeUtil.convertMillisecondToMinutes(totalPipelineLeadTime / buildNumber); + double avgPrLeadTime = leadTimePair.getLeft(); + double avgPipelineLeadTime = leadTimePair.getRight(); leadTimeForChangesOfPipelines.add(LeadTimeForChangesOfPipelines.builder() .name(item.getPipelineName()) @@ -64,26 +72,9 @@ public LeadTimeForChanges calculate(List pipelineLeadTime, .totalDelayTime(avgPrLeadTime + avgPipelineLeadTime) .build()); - HashMap avgTotalDelayTime = new HashMap<>(); - avgTotalDelayTime.put("avgPrLeadTime", avgPrLeadTime); - avgTotalDelayTime.put("avgPipelineLeadTime", avgPipelineLeadTime); - - return avgTotalDelayTime; + return new AverageLeadTime(avgPrLeadTime, avgPipelineLeadTime); }).toList(); - Double avgPrLeadTimeOfAllPipeline = avgDelayTimeMapList.stream() - .map(item -> item.getOrDefault("avgPrLeadTime", 0d)) - .reduce(0.0, Double::sum); - Double avgPipeDelayTimeOfAllPipeline = avgDelayTimeMapList.stream() - .map(item -> item.getOrDefault("avgPipelineLeadTime", 0d)) - .reduce(0.0, Double::sum); - Double avgPrLeadTime = avgPrLeadTimeOfAllPipeline / pipelineCount; - Double avgPipelineLeadTime = avgPipeDelayTimeOfAllPipeline / pipelineCount; - - avgLeadTimeForChanges.setPrLeadTime(avgPrLeadTime); - avgLeadTimeForChanges.setPipelineLeadTime(avgPipelineLeadTime); - avgLeadTimeForChanges.setTotalDelayTime(avgPrLeadTime + avgPipelineLeadTime); - List leftOverPipelines = deploymentEnvironmentList.stream() .filter(deploymentEnvironment -> leadTimeForChangesOfPipelines.stream() .noneMatch(leadTimeForChangesOfPipeline -> Objects.equals(deploymentEnvironment.getName(), @@ -99,8 +90,104 @@ public LeadTimeForChanges calculate(List pipelineLeadTime, .toList(); leadTimeForChangesOfPipelines.addAll(leftOverPipelines); + return Pair.of(leadTimeForChangesOfPipelines, avgDelayTimeList); + } + + private Pair, List> calculateLeadTimeForChangesOfSourceControl( + List sourceControlLeadTimes, List codeBases) { + List leadTimeForChangesOfSourceControls = new ArrayList<>(); + + List avgDelayTimeList = sourceControlLeadTimes.stream().map(item -> { + Pair leadTimePair = calculateLeadTimeForChanges(item.getLeadTimes()); + if (leadTimePair == null) { + return new AverageLeadTime(0d, 0d); + } + double avgPrLeadTime = leadTimePair.getLeft(); + double avgPipelineLeadTime = leadTimePair.getRight(); + + leadTimeForChangesOfSourceControls.add(LeadTimeForChangesOfSourceControl.builder() + .organization(item.getOrganization()) + .repo(item.getRepo()) + .prLeadTime(avgPrLeadTime) + .pipelineLeadTime(avgPipelineLeadTime) + .totalDelayTime(avgPrLeadTime + avgPipelineLeadTime) + .build()); - return new LeadTimeForChanges(leadTimeForChangesOfPipelines, avgLeadTimeForChanges); + return new AverageLeadTime(avgPrLeadTime, avgPipelineLeadTime); + }).toList(); + + List leftOverSourceControl = codeBases.stream() + .filter(codeBase -> leadTimeForChangesOfSourceControls.stream() + .noneMatch(leadTimeForChangesOfSourceControl -> Objects.equals(codeBase.getOrganization(), + leadTimeForChangesOfSourceControl.getOrganization()) + && Objects.equals(codeBase.getRepo(), leadTimeForChangesOfSourceControl.getRepo()))) + .map(it -> LeadTimeForChangesOfSourceControl.builder() + .organization(it.getOrganization()) + .repo(it.getRepo()) + .pipelineLeadTime(0.0) + .prLeadTime(0.0) + .totalDelayTime(0.0) + .build()) + .toList(); + + leadTimeForChangesOfSourceControls.addAll(leftOverSourceControl); + return Pair.of(leadTimeForChangesOfSourceControls, avgDelayTimeList); + } + + private AvgLeadTimeForChanges calculateAverageLeadTimeForChanges(List avgOfPipelines, + List avgOfSourceControls) { + + int count = avgOfPipelines.size() + avgOfSourceControls.size(); + AvgLeadTimeForChanges avgLeadTimeForChanges = new AvgLeadTimeForChanges(); + if (count == 0) { + return avgLeadTimeForChanges; + } + + // get average pr lead time and pipeline lead time + Double avgPrLeadTimeOfAllPipeline = avgOfPipelines.stream() + .map(AverageLeadTime::getAvgPrLeadTime) + .reduce(0.0, Double::sum); + Double avgPrLeadTimeOfAllSourceControl = avgOfSourceControls.stream() + .map(AverageLeadTime::getAvgPrLeadTime) + .reduce(0.0, Double::sum); + Double avgPipeDelayTimeOfAllPipeline = avgOfPipelines.stream() + .map(AverageLeadTime::getAvgPipelineLeadTime) + .reduce(0.0, Double::sum); + Double avgPipeDelayTimeOfAllSourceControl = avgOfSourceControls.stream() + .map(AverageLeadTime::getAvgPipelineLeadTime) + .reduce(0.0, Double::sum); + Double avgPrLeadTime = (avgPrLeadTimeOfAllPipeline + avgPrLeadTimeOfAllSourceControl) / count; + Double avgPipelineLeadTime = (avgPipeDelayTimeOfAllPipeline + avgPipeDelayTimeOfAllSourceControl) / count; + + avgLeadTimeForChanges.setPrLeadTime(avgPrLeadTime); + avgLeadTimeForChanges.setPipelineLeadTime(avgPipelineLeadTime); + avgLeadTimeForChanges.setTotalDelayTime(avgPrLeadTime + avgPipelineLeadTime); + return avgLeadTimeForChanges; + } + + private Pair calculateLeadTimeForChanges(List leadTimes) { + if (leadTimes == null || leadTimes.isEmpty()) { + return null; + } + int buildNumber = leadTimes.size(); + // 过滤掉noPr的数据 + List noPrLeadTime = leadTimes.stream() + .filter(leadTime -> leadTime.getPrMergedTime() != null && leadTime.getPrMergedTime() != 0) + .filter(leadTime -> leadTime.getPrLeadTime() != null && leadTime.getPrLeadTime() != 0) + .toList(); + // 通过noPrLeadTimeList去计算totalPrLeadTime + double totalPrLeadTime = noPrLeadTime.stream() + .flatMapToLong(leadTime -> LongStream.of(leadTime.getPrLeadTime())) + .sum(); + // 通过PipelineLeadTime去计算totalPipelineLeadTime + double totalPipelineLeadTime = leadTimes.stream() + .flatMapToLong(leadTime -> LongStream.of(leadTime.getPipelineLeadTime())) + .sum(); + + double avgPrLeadTime = TimeUtil.convertMillisecondToMinutes(totalPrLeadTime / buildNumber); + double avgPipelineLeadTime = TimeUtil.convertMillisecondToMinutes(totalPipelineLeadTime / buildNumber); + + return Pair.of(avgPrLeadTime, avgPipelineLeadTime); } } diff --git a/backend/src/main/java/heartbeat/service/report/calculator/model/AverageLeadTime.java b/backend/src/main/java/heartbeat/service/report/calculator/model/AverageLeadTime.java new file mode 100644 index 0000000000..2cd4622882 --- /dev/null +++ b/backend/src/main/java/heartbeat/service/report/calculator/model/AverageLeadTime.java @@ -0,0 +1,16 @@ +package heartbeat.service.report.calculator.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AverageLeadTime { + + private double avgPrLeadTime; + + private double avgPipelineLeadTime; + +} diff --git a/backend/src/main/java/heartbeat/service/report/calculator/model/FetchedData.java b/backend/src/main/java/heartbeat/service/report/calculator/model/FetchedData.java index d476aef838..ca56a0407f 100644 --- a/backend/src/main/java/heartbeat/service/report/calculator/model/FetchedData.java +++ b/backend/src/main/java/heartbeat/service/report/calculator/model/FetchedData.java @@ -1,6 +1,7 @@ package heartbeat.service.report.calculator.model; import heartbeat.client.dto.codebase.github.PipelineLeadTime; +import heartbeat.client.dto.codebase.github.SourceControlLeadTime; import heartbeat.client.dto.pipeline.buildkite.BuildKiteBuildInfo; import heartbeat.client.dto.pipeline.buildkite.DeployTimes; import heartbeat.controller.board.dto.response.CardCollection; @@ -20,6 +21,8 @@ public class FetchedData { private BuildKiteData buildKiteData; + private RepoData repoData; + @Data @Builder public static class CardCollectionInfo { @@ -57,4 +60,13 @@ public void addBuildKiteBuildInfos(String key, List buildKit } + @Data + @Builder + @AllArgsConstructor + public static class RepoData { + + private List sourceControlLeadTimes; + + } + } 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 9922339de7..2c45cb9c1a 100644 --- a/backend/src/main/java/heartbeat/service/source/github/GitHubService.java +++ b/backend/src/main/java/heartbeat/service/source/github/GitHubService.java @@ -7,15 +7,18 @@ import heartbeat.client.dto.codebase.github.OrganizationsInfoDTO; import heartbeat.client.dto.codebase.github.PageBranchesInfoDTO; import heartbeat.client.dto.codebase.github.PageOrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PagePullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PagePullRequestInfo; import heartbeat.client.dto.codebase.github.PageReposInfoDTO; import heartbeat.client.dto.codebase.github.PipelineLeadTime; import heartbeat.client.dto.codebase.github.PullRequestInfo; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; import heartbeat.client.dto.codebase.github.ReposInfoDTO; +import heartbeat.client.dto.codebase.github.SourceControlLeadTime; import heartbeat.client.dto.pipeline.buildkite.DeployInfo; import heartbeat.client.dto.pipeline.buildkite.DeployTimes; +import heartbeat.controller.report.dto.request.CodeBase; import heartbeat.controller.report.dto.request.GenerateReportRequest; +import heartbeat.controller.report.dto.response.LeadTimeInfo; +import heartbeat.controller.report.dto.response.PipelineCSVInfo; import heartbeat.exception.BadRequestException; import heartbeat.exception.BaseException; import heartbeat.exception.InternalServerErrorException; @@ -24,6 +27,7 @@ import heartbeat.exception.UnauthorizedException; import heartbeat.service.pipeline.buildkite.CachePageService; import heartbeat.service.report.WorkDay; +import heartbeat.service.report.calculator.model.FetchedData; import heartbeat.service.report.model.WorkInfo; import heartbeat.service.source.github.model.PipelineInfoOfRepository; import heartbeat.service.source.github.model.PullRequestFinishedInfo; @@ -31,6 +35,7 @@ import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; @@ -46,6 +51,8 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.IntStream; +import static java.util.Optional.ofNullable; + @Service @RequiredArgsConstructor @Log4j2 @@ -80,7 +87,7 @@ public void verifyToken(String githubToken) { log.info("Successfully verify token from github"); } catch (RuntimeException e) { - Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + Throwable cause = ofNullable(e.getCause()).orElse(e); log.error("Failed to call GitHub with token_error: {} ", cause.getMessage()); if (cause instanceof BaseException baseException) { throw baseException; @@ -110,7 +117,7 @@ public void verifyCanReadTargetBranch(String repository, String branch, String g throw new BadRequestException(String.format("Unable to read target branch: %s, with token error", branch)); } catch (RuntimeException e) { - Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + Throwable cause = ofNullable(e.getCause()).orElse(e); log.error("Failed to call GitHub branch:{} with error: {} ", branch, cause.getMessage()); if (cause instanceof BaseException baseException) { throw baseException; @@ -153,7 +160,7 @@ public List fetchPipelinesLeadTime(List deployTim return pipelineLeadTimeFutures.stream().map(CompletableFuture::join).toList(); } catch (RuntimeException e) { - Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + Throwable cause = ofNullable(e.getCause()).orElse(e); log.error("Failed to get pipeline leadTimes_error: {}", cause.getMessage()); if (cause instanceof BaseException baseException) { throw baseException; @@ -274,14 +281,20 @@ private Boolean isRevert(CommitInfo commitInfo) { public LeadTime mapLeadTimeWithInfo(PullRequestInfo pullRequestInfo, DeployInfo deployInfo, CommitInfo commitInfo, GenerateReportRequest request) { + log.info("Start to calculate base lead time"); if (pullRequestInfo.getMergedAt() == null) { return null; } long prCreatedTime = Instant.parse(pullRequestInfo.getCreatedAt()).toEpochMilli(); long prMergedTime = Instant.parse(pullRequestInfo.getMergedAt()).toEpochMilli(); - long jobFinishTime = Instant.parse(deployInfo.getJobFinishTime()).toEpochMilli(); - long jobStartTime = Instant.parse(deployInfo.getJobStartTime()).toEpochMilli(); - long pipelineCreateTime = Instant.parse(deployInfo.getPipelineCreateTime()).toEpochMilli(); + Long jobFinishTime = ofNullable(deployInfo.getJobFinishTime()).map(it -> Instant.parse(it).toEpochMilli()) + .orElse(null); + Long jobStartTime = ofNullable(deployInfo.getJobStartTime()).map(it -> Instant.parse(it).toEpochMilli()) + .orElse(null); + Long pipelineCreateTime = ofNullable(deployInfo.getPipelineCreateTime()) + .map(it -> Instant.parse(it).toEpochMilli()) + .orElse(null); + String commitId = ofNullable(deployInfo.getCommitId()).orElse(commitInfo.getCommitId()); long firstCommitTimeInPr; if (commitInfo.getCommit() != null && commitInfo.getCommit().getCommitter() != null && commitInfo.getCommit().getCommitter().getDate() != null) { @@ -291,7 +304,7 @@ public LeadTime mapLeadTimeWithInfo(PullRequestInfo pullRequestInfo, DeployInfo firstCommitTimeInPr = 0; } - long pipelineLeadTime = jobFinishTime - prMergedTime; + long pipelineLeadTime = ofNullable(jobFinishTime).map(it -> it - prMergedTime).orElse(0L); long prLeadTime; long totalTime; long holidays = 0; @@ -316,6 +329,7 @@ public LeadTime mapLeadTimeWithInfo(PullRequestInfo pullRequestInfo, DeployInfo } totalTime = prLeadTime + pipelineLeadTime; + log.info("Successfully to calculate base lead time"); return LeadTime.builder() .pipelineLeadTime(pipelineLeadTime) .prLeadTime(prLeadTime) @@ -323,7 +337,9 @@ public LeadTime mapLeadTimeWithInfo(PullRequestInfo pullRequestInfo, DeployInfo .prMergedTime(prMergedTime) .totalTime(totalTime) .prCreatedTime(prCreatedTime) - .commitId(deployInfo.getCommitId()) + .commitId(commitId) + .pullNumber(pullRequestInfo.getNumber()) + .committer(pullRequestInfo.getUser().getLogin()) .jobFinishTime(jobFinishTime) .jobStartTime(jobStartTime) .firstCommitTime(prMergedTime) @@ -347,7 +363,7 @@ public CommitInfo fetchCommitInfo(String commitId, String repositoryId, String t return commitInfo; } catch (RuntimeException e) { - Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + Throwable cause = ofNullable(e.getCause()).orElse(e); log.error("Failed to get commit info_repoId: {},commitId: {}, error: {}", repositoryId, commitId, cause.getMessage()); if (cause instanceof NotFoundException) { @@ -463,23 +479,122 @@ public List getAllCrews(String token, String organization, String repo, long endTime) { log.info("Start to get all crews, organization: {}, repo: {}, branch: {}, startTime: {}, endTime: {}", organization, repo, branch, startTime, endTime); - int initPage = 1; String realToken = BEARER_TITLE + token; - PagePullRequestInfoDTO pageBranchesInfoDTO = cachePageService.getGitHubPullRequest(realToken, organization, - repo, branch, initPage, PER_PAGE); - List firstPageStepsInfo = pageBranchesInfoDTO.getPageInfo(); - int totalPage = pageBranchesInfoDTO.getTotalPage(); + List validPullRequestInfo = getValidPullRequestInfo(realToken, organization, repo, branch, + startTime, endTime); + List crews = validPullRequestInfo.stream() + .map(PullRequestInfo::getUser) + .map(PullRequestInfo.PullRequestUser::getLogin) + .distinct() + .toList(); + log.info("Successfully to get all crews, organization: {}, repo: {}, branch: {}, startTime: {}, endTime: {}", + organization, repo, branch, startTime, endTime); + return crews; + } + + private PullRequestFinishedInfo filterPullRequestByTimeRange(List pullRequestInfoList, + long startTime, long endTime) { + log.info("Start to filter pull request, startTime: {}, endTime: {}", startTime, endTime); + Instant startTimeInstant = Instant.ofEpochMilli(startTime); + Instant endTimeInstant = Instant.ofEpochMilli(endTime); + List validPullRequestList = new ArrayList<>(); + boolean isGetNextPage = true; + for (PullRequestInfo PullRequestInfo : pullRequestInfoList) { + if (!Objects.nonNull(PullRequestInfo.getMergedAt())) { + continue; + } + Instant createdAt = Instant.parse(PullRequestInfo.getCreatedAt()); + Instant mergedAt = Instant.parse(PullRequestInfo.getMergedAt()); + if (createdAt.isAfter(startTimeInstant) && !createdAt.isAfter(endTimeInstant) + && mergedAt.isAfter(startTimeInstant) && !mergedAt.isAfter(endTimeInstant)) { + validPullRequestList.add(PullRequestInfo); + } + if (createdAt.isBefore(startTimeInstant)) { + isGetNextPage = false; + } + } + log.info( + "Successfully to filter pull request, startTime: {}, endTime: {}, should get next page pull request: {}", + startTime, endTime, isGetNextPage); + return PullRequestFinishedInfo.builder() + .isGetNextPage(isGetNextPage) + .pullRequestInfoList(validPullRequestList) + .build(); + } + + public FetchedData.RepoData fetchRepoData(GenerateReportRequest request) { + log.info("Start to fetch repo data"); + long startTime = Long.parseLong(request.getStartTime()); + long endTime = Long.parseLong(request.getEndTime()); + String token = ofNullable(request.getCodebaseSetting().getToken()).orElse(""); + String realToken = BEARER_TITLE + token; + List crews = request.getCodebaseSetting().getCrews(); + + List sourceControlLeadTimes = request.getCodebaseSetting() + .getCodebases() + .stream() + .map(codeBase -> { + String organization = codeBase.getOrganization(); + String repo = codeBase.getRepo(); + List branches = codeBase.getBranches(); + return branches.stream().map(branch -> { + List leadTimes = getValidPullRequestInfo(realToken, organization, repo, branch, startTime, + endTime) + .stream() + .filter(pullRequestInfo -> { + if (crews.isEmpty()) { + return true; + } + return crews.stream() + .anyMatch(crew -> Objects.equals(pullRequestInfo.getUser().getLogin(), crew)); + }) + .map(pullRequestInfo -> { + String pullNumber = pullRequestInfo.getNumber().toString(); + log.info("Start to get first code commit, organization: {}, repo: {}, pull number: {}", + organization, repo, pullNumber); + CommitInfo firstCodeCommit = gitHubFeignClient + .getPullRequestCommitInfo(String.format("%s/%s", organization, repo), pullNumber, + realToken) + .get(0); + log.info( + "Successfully to get first code commit, organization: {}, repo: {}, pull number: {}", + organization, repo, pullNumber); + return Pair.of(pullRequestInfo, firstCodeCommit); + }) + .map(pair -> mapLeadTimeWithInfo(pair.getLeft(), new DeployInfo(), pair.getRight(), request)) + .toList(); + return SourceControlLeadTime.builder() + .repo(repo) + .organization(organization) + .branch(branch) + .leadTimes(leadTimes) + .build(); + }).toList(); + }) + .flatMap(Collection::stream) + .toList(); + + log.info("Successfully fetch repo data"); + return FetchedData.RepoData.builder().sourceControlLeadTimes(sourceControlLeadTimes).build(); + } + + private List getValidPullRequestInfo(String realToken, String organization, String repo, + String branch, long startTime, long endTime) { + log.info("Start to get all pull request, organization: {}, repo: {}, branch: {}, startTime: {}, endTime: {}", + organization, repo, branch, startTime, endTime); + int initPage = 1; + + List pullRequestInfoList = new ArrayList<>(); + PagePullRequestInfo pagePullRequestInfo = cachePageService.getGitHubPullRequest(realToken, organization, repo, + branch, initPage, PER_PAGE); + List firstPageStepsInfo = pagePullRequestInfo.getPageInfo(); + int totalPage = pagePullRequestInfo.getTotalPage(); log.info("Successfully parse the total page_total page of pull requests: {}", totalPage); - List pullRequestNames = new ArrayList<>(); if (Objects.nonNull(firstPageStepsInfo)) { PullRequestFinishedInfo pullRequestFinishedInfo = filterPullRequestByTimeRange(firstPageStepsInfo, startTime, endTime); + pullRequestInfoList.addAll(pullRequestFinishedInfo.getPullRequestInfoList()); boolean isGetNextPage = pullRequestFinishedInfo.isGetNextPage(); - List firstPagePullRequestInfo = pullRequestFinishedInfo.getPullRequestInfoDTOList(); - pullRequestNames.addAll(firstPagePullRequestInfo.stream() - .map(PullRequestInfoDTO::getUser) - .map(PullRequestInfoDTO.PullRequestUser::getLogin) - .toList()); if (totalPage > 1 && isGetNextPage) { for (int i = initPage + 1; i < totalPage + 1; i = i + BATCH_SIZE) { List pullRequestFinishedInfoList = IntStream @@ -490,12 +605,7 @@ public List getAllCrews(String token, String organization, String repo, .getPageInfo()) .map(it -> filterPullRequestByTimeRange(it, startTime, endTime)) .toList(); - List crews = pullRequestFinishedInfoList.stream() - .map(PullRequestFinishedInfo::getPullRequestInfoDTOList) - .flatMap(Collection::stream) - .map(it -> it.getUser().getLogin()) - .toList(); - pullRequestNames.addAll(crews); + pullRequestFinishedInfoList.forEach(it -> pullRequestInfoList.addAll(it.getPullRequestInfoList())); boolean isGoToNextBatch = pullRequestFinishedInfoList.stream().anyMatch(it -> !it.isGetNextPage()); if (isGoToNextBatch) { break; @@ -503,39 +613,32 @@ public List getAllCrews(String token, String organization, String repo, } } } - log.info("Successfully to get all crews, organization: {}, repo: {}, branch: {}, startTime: {}, endTime: {}", + log.info( + "Successfully to get all pull request, organization: {}, repo: {}, branch: {}, startTime: {}, endTime: {}", organization, repo, branch, startTime, endTime); - return pullRequestNames.stream().distinct().toList(); + return pullRequestInfoList; } - private PullRequestFinishedInfo filterPullRequestByTimeRange(List pullRequestInfoDTOList, - long startTime, long endTime) { - log.info("Start to filter pull request, startTime: {}, endTime: {}", startTime, endTime); - Instant startTimeInstant = Instant.ofEpochMilli(startTime); - Instant endTimeInstant = Instant.ofEpochMilli(endTime); - List validPullRequestList = new ArrayList<>(); - boolean isGetNextPage = true; - for (PullRequestInfoDTO pullRequestInfoDTO : pullRequestInfoDTOList) { - if (!Objects.nonNull(pullRequestInfoDTO.getMergedAt())) { - continue; - } - Instant createdAt = Instant.parse(pullRequestInfoDTO.getCreatedAt()); - Instant mergedAt = Instant.parse(pullRequestInfoDTO.getMergedAt()); - if (createdAt.isAfter(startTimeInstant) && !createdAt.isAfter(endTimeInstant) - && mergedAt.isAfter(startTimeInstant) && !mergedAt.isAfter(endTimeInstant)) { - validPullRequestList.add(pullRequestInfoDTO); - } - if (createdAt.isBefore(startTimeInstant)) { - isGetNextPage = false; - } - } - log.info( - "Successfully to filter pull request, startTime: {}, endTime: {}, should get next page pull request: {}", - startTime, endTime, isGetNextPage); - return PullRequestFinishedInfo.builder() - .isGetNextPage(isGetNextPage) - .pullRequestInfoDTOList(validPullRequestList) - .build(); + public List generateCSVForSourceControl(FetchedData.RepoData repoData, List codeBases) { + return codeBases.stream().parallel().map(codeBase -> { + String organization = codeBase.getOrganization(); + String repo = codeBase.getRepo(); + return repoData.getSourceControlLeadTimes() + .stream() + .filter(sourceControlLeadTime -> Objects.equals(sourceControlLeadTime.getRepo(), repo) + && Objects.equals(sourceControlLeadTime.getOrganization(), organization)) + .map(sourceControlLeadTime -> sourceControlLeadTime.getLeadTimes() + .stream() + .map(leadTime -> PipelineCSVInfo.builder() + .organizationName(organization) + .repoName(repo) + .branchName(sourceControlLeadTime.getBranch()) + .leadTimeInfo(new LeadTimeInfo(leadTime)) + .build()) + .toList()) + .flatMap(Collection::stream) + .toList(); + }).flatMap(Collection::stream).toList(); } } diff --git a/backend/src/main/java/heartbeat/service/source/github/model/PullRequestFinishedInfo.java b/backend/src/main/java/heartbeat/service/source/github/model/PullRequestFinishedInfo.java index 62428a21ac..a661ff0daa 100644 --- a/backend/src/main/java/heartbeat/service/source/github/model/PullRequestFinishedInfo.java +++ b/backend/src/main/java/heartbeat/service/source/github/model/PullRequestFinishedInfo.java @@ -1,6 +1,6 @@ package heartbeat.service.source.github.model; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PullRequestInfo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -16,6 +16,6 @@ public class PullRequestFinishedInfo { private boolean isGetNextPage; - private List pullRequestInfoDTOList; + private List pullRequestInfoList; } diff --git a/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java b/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java index b79a838ab7..a2755a5b7c 100644 --- a/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java +++ b/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java @@ -8,17 +8,25 @@ import heartbeat.client.dto.codebase.github.OrganizationsInfoDTO; import heartbeat.client.dto.codebase.github.PageBranchesInfoDTO; import heartbeat.client.dto.codebase.github.PageOrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PagePullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PagePullRequestInfo; import heartbeat.client.dto.codebase.github.PageReposInfoDTO; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PullRequestInfo; import heartbeat.client.dto.codebase.github.ReposInfoDTO; import heartbeat.client.dto.pipeline.buildkite.BuildKiteBuildInfo; import heartbeat.client.dto.pipeline.buildkite.BuildKiteJob; import heartbeat.client.dto.pipeline.buildkite.BuildKitePipelineDTO; import heartbeat.client.dto.pipeline.buildkite.PageStepsInfoDto; +import heartbeat.exception.BaseException; import heartbeat.exception.InternalServerErrorException; +import heartbeat.exception.NotFoundException; +import heartbeat.exception.PermissionDenyException; +import heartbeat.exception.RequestFailedException; +import heartbeat.exception.UnauthorizedException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,6 +40,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.validator.internal.util.Contracts.assertNotNull; @@ -218,11 +227,27 @@ void shouldReturnPageOrganizationsInfoDtoWhenFetchPageOrganizationsInfoSuccessGi @Test void shouldThrowExceptionWhenFetchPageOrganizationsInfoThrow500() { - when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)).thenThrow(RuntimeException.class); + when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)).thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubOrganizations(MOCK_TOKEN, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github organization info, page: 1, exception: java.lang.RuntimeException", internalServerErrorException.getMessage()); + assertEquals("Error to get paginated github organization info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", + internalServerErrorException.getMessage()); + } + + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageOrganizationInfoThrow4xx(BaseException e, int errorCode) { + when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)) + .thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubOrganizations(MOCK_TOKEN, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", + baseException.getMessage()); } @Test @@ -243,15 +268,31 @@ void shouldReturnPageReposInfoDtoWhenFetchPageReposInfoSuccessGivenExist() throw @Test void shouldThrowExceptionWhenFetchPageRepoInfoThrow500() { String organization = "test-org"; - when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)).thenThrow(RuntimeException.class); + when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)) + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubRepos(MOCK_TOKEN, organization, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github repo info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github repo info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageRepoInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubRepos(MOCK_TOKEN, organization, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + @Test void shouldReturnPageBranchesInfoDtoWhenFetchPageBranchesInfoSuccessGivenExist() throws IOException { String organization = "test-org"; @@ -274,32 +315,48 @@ void shouldThrowExceptionWhenFetchPageBranchInfoThrow500() { String organization = "test-org"; String repo = "test-repo"; when(gitHubFeignClient.getAllBranches(MOCK_TOKEN, organization, repo, 100, 1)) - .thenThrow(RuntimeException.class); + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubBranches(MOCK_TOKEN, organization, repo, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github branch info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github branch info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageBranchInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + String repo = "test-repo"; + when(gitHubFeignClient.getAllBranches(MOCK_TOKEN, organization, repo, 100, 1)).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubBranches(MOCK_TOKEN, organization, repo, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + @Test void shouldReturnPagePullRequestInfoDtoWhenFetchPullRequestInfoSuccessGivenExist() throws IOException { String organization = "test-org"; String repo = "test-repo"; String branch = "test-branch"; HttpHeaders httpHeaders = buildGitHubHttpHeaders(); - ResponseEntity> responseEntity = getResponseEntity(httpHeaders, + ResponseEntity> responseEntity = getResponseEntity(httpHeaders, "src/test/java/heartbeat/controller/pipeline/githubPullRequest.json"); when(gitHubFeignClient.getAllPullRequests(MOCK_TOKEN, organization, repo, 100, 1, branch, "all")) .thenReturn(responseEntity); - PagePullRequestInfoDTO pagePullRequestInfoDTO = cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, - repo, branch, 1, 100); + PagePullRequestInfo pagePullRequestInfo = cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, repo, + branch, 1, 100); - assertNotNull(pagePullRequestInfoDTO); - assertThat(pagePullRequestInfoDTO.getPageInfo()).isEqualTo(responseEntity.getBody()); - assertThat(pagePullRequestInfoDTO.getTotalPage()).isEqualTo(2); + assertNotNull(pagePullRequestInfo); + assertThat(pagePullRequestInfo.getPageInfo()).isEqualTo(responseEntity.getBody()); + assertThat(pagePullRequestInfo.getTotalPage()).isEqualTo(2); } @Test @@ -308,15 +365,31 @@ void shouldThrowExceptionWhenFetchPagePullRequestInfoThrow500() { String repo = "test-repo"; String branch = "test-branch"; when(gitHubFeignClient.getAllPullRequests(MOCK_TOKEN, organization, repo, 100, 1, branch, "all")) - .thenThrow(RuntimeException.class); + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, repo, branch, 1, 100)); assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github pull request info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github pull request info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPagePullRequestInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + String repo = "test-repo"; + String branch = "test-branch"; + when(gitHubFeignClient.getAllPullRequests(MOCK_TOKEN, organization, repo, 100, 1, branch, "all")).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, repo, branch, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + private static ResponseEntity> getResponseEntity(HttpHeaders httpHeaders, String pathname) throws IOException { ObjectMapper mapper = new ObjectMapper(); @@ -337,4 +410,11 @@ private HttpHeaders buildGitHubHttpHeaders() { return buildHttpHeaders(GITHUB_TOTAL_PAGE_HEADER); } + static Stream baseExceptionProvider() { + return Stream.of(Arguments.of(new PermissionDenyException("error"), 403), + Arguments.of(new UnauthorizedException("error"), 401), Arguments.of(new NotFoundException("error"), 404) + + ); + } + } diff --git a/backend/src/test/java/heartbeat/service/report/CSVFileGeneratorTest.java b/backend/src/test/java/heartbeat/service/report/CSVFileGeneratorTest.java index 182c031487..2eeddf8d32 100644 --- a/backend/src/test/java/heartbeat/service/report/CSVFileGeneratorTest.java +++ b/backend/src/test/java/heartbeat/service/report/CSVFileGeneratorTest.java @@ -106,12 +106,12 @@ class ConvertPipelineDataToCSV { @MethodSource("generatePipelineCSVInfos") void shouldConvertPipelineDataToCsvGivenCommitInfoNotNull(List pipelineCSVInfos, String[] respectedData) { - String[][] expectedSavedData = new String[][] { { "Organization", "Pipeline Name", "Pipeline Step", "Valid", - "Build Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", - "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", - "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", - "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - respectedData }; + String[][] expectedSavedData = new String[][] { { "Organization", "Pipeline Name", "Repo Name", + "Pipeline Step", "Valid", "Build Number", "Pull Number", "Code Committer", "Build Creator", + "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", "No PR Committed Time", + "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", "Status", + "Branch", "Revert" }, respectedData }; csvFileGenerator.convertPipelineDataToCSV(TEST_UUID, pipelineCSVInfos, mockTimeStamp); verify(fileRepository, times(1)).createCSVFileByType(any(), any(), eq(expectedSavedData), any()); @@ -121,13 +121,14 @@ void shouldConvertPipelineDataToCsvGivenCommitInfoNotNull(List void shouldConvertPipelineDataToCsvWithoutCreatorName() { List pipelineCSVInfos = PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITHOUT_CREATOR_NAME(); String[][] expectedSavedData = new String[][] { - { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", "Code Committer", - "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", - "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", - "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", - "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "null", "880", null, null, - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", + "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", + "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", + "Status", "Branch", "Revert" }, + { "Thoughtworks-Heartbeat", "Heartbeat", null, ":rocket: Deploy prod", null, "880", null, null, + null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" } }; @@ -140,13 +141,14 @@ void shouldConvertPipelineDataToCsvWithoutCreatorName() { void shouldConvertPipelineDataToCsvGivenNullCommitInfo() { List pipelineCSVInfos = PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITH_NULL_COMMIT_INFO(); String[][] expectedSavedData = new String[][] { - { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", "Code Committer", - "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", - "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", - "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", - "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "true", "880", "XXXX", null, - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", + "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", + "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", + "Status", "Branch", "Revert" }, + { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", "true", "880", null, + "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" } }; @@ -159,13 +161,14 @@ void shouldConvertPipelineDataToCsvGivenNullCommitInfo() { void shouldConvertPipelineDataToCsvGivenCommitMessageIsRevert() { List pipelineCSVInfos = PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITH_MESSAGE_IS_REVERT(); String[][] expectedSavedData = new String[][] { - { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", "Code Committer", - "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", - "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", - "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", - "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "null", "880", "XXXX", null, - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", + "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", + "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", + "Status", "Branch", "Revert" }, + { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", null, "880", null, + "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "true" } }; @@ -178,13 +181,14 @@ void shouldConvertPipelineDataToCsvGivenCommitMessageIsRevert() { void shouldConvertPipelineDataToCsvGivenAuthorIsNull() { List pipelineCSVInfos = PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITHOUT_Author_NAME(); String[][] expectedSavedData = new String[][] { - { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", "Code Committer", - "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", - "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", - "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", - "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "null", "880", null, "XXX", - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", + "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", + "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", + "Status", "Branch", "Revert" }, + { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", null, "880", null, + null, "XXX", "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" } }; @@ -198,21 +202,22 @@ void shouldConvertPipelineDataToCsvGivenTwoOrganizationsPipeline() { List pipelineCSVInfos = PipelineCsvFixture.MOCK_TWO_ORGANIZATIONS_PIPELINE_CSV_DATA(); String[][] expectedSavedData = new String[][] { - { "Organization", "Pipeline Name", "Pipeline Step", "Valid", "Build Number", "Code Committer", - "Build Creator", "First Code Committed Time In PR", "PR Created Time", "PR Merged Time", - "No PR Committed Time", "Job Start Time", "Pipeline Start Time", "Pipeline Finish Time", - "Non-Workdays (Hours)", "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", - "Pipeline Lead Time (HH:mm:ss)", "Status", "Branch", "Revert" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "true", "880", null, "XXXX", - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Organization", "Pipeline Name", "Repo Name", "Pipeline Step", "Valid", "Build Number", + "Pull Number", "Code Committer", "Build Creator", "First Code Committed Time In PR", + "PR Created Time", "PR Merged Time", "No PR Committed Time", "Job Start Time", + "Pipeline Start Time", "Pipeline Finish Time", "Non-Workdays (Hours)", + "Total Lead Time (HH:mm:ss)", "PR Lead Time (HH:mm:ss)", "Pipeline Lead Time (HH:mm:ss)", + "Status", "Branch", "Revert" }, + { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", "true", "880", "1", + "test-committer", "XXXX", "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, + "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", + "passed", "branch", "" }, + { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", "true", "880", null, + "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" }, - { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "true", "880", "XXXX", null, - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", - "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", - "" }, - { "Thoughtworks-Foxtel", "Heartbeat1", ":rocket: Deploy prod", "true", "880", null, "XXXX", - "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", + { "Thoughtworks-Foxtel", "Heartbeat1", "test-repo", ":rocket: Deploy prod", "true", "880", null, + null, "XXXX", "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" } }; @@ -224,19 +229,19 @@ void shouldConvertPipelineDataToCsvGivenTwoOrganizationsPipeline() { private static Stream generatePipelineCSVInfos() { return Stream.of( Arguments.of(PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA(), - new String[] { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "true", "880", - "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, - "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", - "653037000", "passed", "branch", "" }), + new String[] { "Thoughtworks-Heartbeat", "Heartbeat", "test-repo", ":rocket: Deploy prod", + "true", "880", "1", "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", + "1683793037000", null, "168369327000", "168369327000", "1684793037000", "240", + "8379303", "16837", "653037000", "passed", "branch", "" }), Arguments.of(PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITH_PIPELINE_STATUS_IS_CANCELED(), - new String[] { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "true", "880", - "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, - "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", + new String[] { "Thoughtworks-Heartbeat", "Heartbeat", null, ":rocket: Deploy prod", "true", + "880", null, "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", + null, "168369327000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "canceled", "branch", "" }), Arguments.of(PipelineCsvFixture.MOCK_PIPELINE_CSV_DATA_WITHOUT_CREATOR(), - new String[] { "Thoughtworks-Heartbeat", "Heartbeat", ":rocket: Deploy prod", "null", "880", - "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", null, - "1683793037000", "168369327000", "1684793037000", "240", "8379303", "16837", + new String[] { "Thoughtworks-Heartbeat", "Heartbeat", null, ":rocket: Deploy prod", null, + "880", null, "XXXX", null, "2023-05-08T07:18:18Z", "168369327000", "1683793037000", + null, "1683793037000", "168369327000", "1684793037000", "240", "8379303", "16837", "653037000", "passed", "branch", "" })); } @@ -587,6 +592,12 @@ void shouldConvertMetricDataToCsv() { { "Lead time for changes", "Heartbeat / Check Frontend License / PR Lead Time", "0" }, { "Lead time for changes", "Heartbeat / Check Frontend License / Pipeline Lead Time", "0.09" }, { "Lead time for changes", "Heartbeat / Check Frontend License / Total Lead Time", "0.09" }, + { "Lead time for changes", "organization1 / repo1 / PR Lead Time", "0" }, + { "Lead time for changes", "organization1 / repo1 / Pipeline Lead Time", "0.02" }, + { "Lead time for changes", "organization1 / repo1 / Total Lead Time", "0.02" }, + { "Lead time for changes", "organization2 / repo2 / PR Lead Time", "0" }, + { "Lead time for changes", "organization2 / repo2 / Pipeline Lead Time", "0.09" }, + { "Lead time for changes", "organization2 / repo2 / Total Lead Time", "0.09" }, { "Lead time for changes", "Average / PR Lead Time", "0" }, { "Lead time for changes", "Average / Pipeline Lead Time", "0.05" }, { "Lead time for changes", "Average / Total Lead Time", "0.05" }, @@ -765,6 +776,7 @@ void shouldHasContentWhenGetDataFromCsvGivenDataTypeIsMetric() { .pipelineLeadTime(5.18) .totalDelayTime(5.18) .build())) + .leadTimeForChangesOfSourceControls(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") .prLeadTime(0.0) @@ -912,6 +924,7 @@ void shouldHasNoContentForAveragesWhenGetDataFromCsvGivenDataTypeIsMetricAndTheQ .pipelineLeadTime(1.01) .totalDelayTime(1.01) .build())) + .leadTimeForChangesOfSourceControls(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") .prLeadTime(0.0) diff --git a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java index 54660a76c1..4baf743eaa 100644 --- a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java @@ -39,6 +39,7 @@ import heartbeat.service.report.calculator.ReworkCalculator; import heartbeat.service.report.calculator.VelocityCalculator; import heartbeat.service.report.calculator.model.FetchedData; +import heartbeat.service.source.github.GitHubService; import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -54,6 +55,7 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -94,6 +96,9 @@ class GenerateReporterServiceTest { @Mock PipelineService pipelineService; + @Mock + GitHubService gitHubService; + @Mock ClassificationCalculator classificationCalculator; @@ -522,18 +527,20 @@ void shouldGenerateCsvFile() { .calendarType(CalendarTypeEnum.REGULAR) .metrics(List.of()) .buildKiteSetting(BuildKiteSetting.builder().build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) .csvTimeStamp(TIMESTAMP) .startTime("1710000000000") .endTime("1712678399999") .timezone("Asia/Shanghai") .build(); - List pipelineCSVInfos = List.of(); + List pipelineCSVInfos = new ArrayList<>(); String timeRangeAndTimeStamp = request.getTimeRangeAndTimeStamp(); when(fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX)) .thenReturn(MetricsDataCompleted.builder().doraMetricsCompleted(false).build()); when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); + when(gitHubService.generateCSVForSourceControl(any(), any())).thenReturn(List.of()); generateReporterService.generateDoraReport(TEST_UUID, request); @@ -544,6 +551,7 @@ void shouldGenerateCsvFile() { FilePrefixType.SOURCE_CONTROL_PREFIX); verify(fileRepository, times(1)).readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX); + verify(gitHubService, times(1)).generateCSVForSourceControl(any(), any()); Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { verify(pipelineService, times(1)).generateCSVForPipeline(any(), any(), any(), any()); @@ -682,6 +690,7 @@ void shouldGenerateCsvWithPipelineReportWhenPipeLineMetricIsNotEmpty() { .metrics(List.of("deployment frequency", "pipeline change failure rate", "pipeline mean time to recovery")) .buildKiteSetting(BuildKiteSetting.builder().build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) .csvTimeStamp(TIMESTAMP) .timezone("Asia/Shanghai") .build(); @@ -690,7 +699,7 @@ void shouldGenerateCsvWithPipelineReportWhenPipeLineMetricIsNotEmpty() { when(fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX)) .thenReturn(MetricsDataCompleted.builder().doraMetricsCompleted(false).build()); - List pipelineCSVInfos = List.of(); + List pipelineCSVInfos = new ArrayList<>(); when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); when(pipelineService.fetchBuildKiteInfo(request)) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); @@ -700,6 +709,7 @@ void shouldGenerateCsvWithPipelineReportWhenPipeLineMetricIsNotEmpty() { when(deploymentFrequency.calculate(any(), any(), any(), any(), any())).thenReturn(fakeDeploymentFrequency); when(pipelineChangeFailureRate.calculate(any())).thenReturn(fakePipelineChangeFailureRate); when(pipelineMeanToRecoveryCalculator.calculate(any(), any())).thenReturn(fakeMeantime); + when(gitHubService.generateCSVForSourceControl(any(), any())).thenReturn(List.of()); generateReporterService.generateDoraReport(TEST_UUID, request); @@ -714,6 +724,7 @@ void shouldGenerateCsvWithPipelineReportWhenPipeLineMetricIsNotEmpty() { timeRangeAndTimeStamp, DORA, false); verify(fileRepository, times(1)).createFileByType(eq(REPORT), eq(TEST_UUID), eq(timeRangeAndTimeStamp), responseArgumentCaptor.capture(), eq(FilePrefixType.PIPELINE_REPORT_PREFIX)); + verify(gitHubService, times(1)).generateCSVForSourceControl(any(), any()); ReportResponse response = responseArgumentCaptor.getValue(); @@ -789,6 +800,7 @@ void shouldGenerateCsvWithSourceControlReportWhenSourceControlMetricIsNotEmpty() .metrics(List.of("lead time for changes")) .codebaseSetting(CodebaseSetting.builder().build()) .buildKiteSetting(BuildKiteSetting.builder().build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) .csvTimeStamp(TIMESTAMP) .timezone("Asia/Shanghai") .build(); @@ -797,12 +809,15 @@ void shouldGenerateCsvWithSourceControlReportWhenSourceControlMetricIsNotEmpty() when(fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX)) .thenReturn(MetricsDataCompleted.builder().doraMetricsCompleted(false).build()); - List pipelineCSVInfos = List.of(); + List pipelineCSVInfos = new ArrayList<>(); when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); when(pipelineService.fetchGitHubData(request)) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); + when(gitHubService.fetchRepoData(any())) + .thenReturn(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); LeadTimeForChanges fakeLeadTimeForChange = LeadTimeForChanges.builder().build(); when(leadTimeForChangesCalculator.calculate(any(), any())).thenReturn(fakeLeadTimeForChange); + when(gitHubService.generateCSVForSourceControl(any(), any())).thenReturn(List.of()); generateReporterService.generateDoraReport(TEST_UUID, request); @@ -813,11 +828,11 @@ void shouldGenerateCsvWithSourceControlReportWhenSourceControlMetricIsNotEmpty() FilePrefixType.PIPELINE_REPORT_PREFIX); verify(fileRepository, times(1)).readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX); - verify(asyncMetricsDataHandler, never()).updateMetricsDataCompletedInHandler(TEST_UUID, timeRangeAndTimeStamp, DORA, false); verify(fileRepository, times(1)).createFileByType(eq(REPORT), eq(TEST_UUID), eq(timeRangeAndTimeStamp), responseArgumentCaptor.capture(), eq(FilePrefixType.SOURCE_CONTROL_PREFIX)); + verify(gitHubService, times(1)).generateCSVForSourceControl(any(), any()); ReportResponse response = responseArgumentCaptor.getValue(); assertEquals(fakeLeadTimeForChange, response.getLeadTimeForChanges()); @@ -840,7 +855,7 @@ void shouldGenerateCsvWithCachedDataWhenBuildKiteDataAlreadyExisted() { .endTime("20000") .metrics(List.of(MetricEnum.LEAD_TIME_FOR_CHANGES.getValue(), MetricEnum.PIPELINE_CHANGE_FAILURE_RATE.getValue())) - .codebaseSetting(CodebaseSetting.builder().build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) .buildKiteSetting(BuildKiteSetting.builder().build()) .csvTimeStamp(TIMESTAMP) .timezone("Asia/Shanghai") @@ -850,12 +865,15 @@ void shouldGenerateCsvWithCachedDataWhenBuildKiteDataAlreadyExisted() { when(fileRepository.readFileByType(FileType.METRICS_DATA_COMPLETED, TEST_UUID, timeRangeAndTimeStamp, MetricsDataCompleted.class, DATA_COMPLETED_PREFIX)) .thenReturn(MetricsDataCompleted.builder().doraMetricsCompleted(false).build()); - List pipelineCSVInfos = List.of(); + List pipelineCSVInfos = new ArrayList<>(); when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); when(pipelineService.fetchGitHubData(any())) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); + when(gitHubService.fetchRepoData(any())) + .thenReturn(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); when(pipelineService.fetchBuildKiteInfo(any())) .thenReturn(FetchedData.BuildKiteData.builder().buildInfosList(List.of()).build()); + when(gitHubService.generateCSVForSourceControl(any(), any())).thenReturn(List.of()); LeadTimeForChanges fakeLeadTimeForChange = LeadTimeForChanges.builder().build(); when(leadTimeForChangesCalculator.calculate(any(), any())).thenReturn(fakeLeadTimeForChange); @@ -872,6 +890,7 @@ void shouldGenerateCsvWithCachedDataWhenBuildKiteDataAlreadyExisted() { timeRangeAndTimeStamp, DORA, false); verify(fileRepository, times(1)).createFileByType(eq(REPORT), eq(TEST_UUID), eq(timeRangeAndTimeStamp), responseArgumentCaptor.capture(), eq(FilePrefixType.SOURCE_CONTROL_PREFIX)); + verify(gitHubService, times(1)).generateCSVForSourceControl(any(), any()); ReportResponse response = responseArgumentCaptor.getValue(); assertEquals(fakeLeadTimeForChange, response.getLeadTimeForChanges()); @@ -907,9 +926,12 @@ void shouldUpdateMetricCompletedWhenGenerateCsvWithSourceControlReportFailed() { when(pipelineService.generateCSVForPipeline(any(), any(), any(), any())).thenReturn(pipelineCSVInfos); when(pipelineService.fetchGitHubData(request)).thenReturn( FetchedData.BuildKiteData.builder().pipelineLeadTimes(List.of()).buildInfosList(List.of()).build()); + when(gitHubService.fetchRepoData(any())) + .thenReturn(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); doThrow(new NotFoundException("")).when(leadTimeForChangesCalculator).calculate(any(), any()); generateReporterService.generateDoraReport(TEST_UUID, request); + verify(kanbanService, never()).fetchDataFromKanban(request); verify(leadTimeForChangesCalculator, times(1)).calculate(any(), any()); verify(fileRepository, times(1)).removeFileByType(ERROR, TEST_UUID, timeRangeAndTimeStamp, diff --git a/backend/src/test/java/heartbeat/service/report/MetricCsvFixture.java b/backend/src/test/java/heartbeat/service/report/MetricCsvFixture.java index c2d1b3fdc3..fc13661aab 100644 --- a/backend/src/test/java/heartbeat/service/report/MetricCsvFixture.java +++ b/backend/src/test/java/heartbeat/service/report/MetricCsvFixture.java @@ -4,6 +4,7 @@ import heartbeat.controller.report.dto.response.AvgPipelineMeanTimeToRecovery; import heartbeat.controller.report.dto.response.Classification; import heartbeat.controller.report.dto.response.ClassificationInfo; +import heartbeat.controller.report.dto.response.LeadTimeForChangesOfSourceControl; import heartbeat.controller.report.dto.response.PipelineChangeFailureRate; import heartbeat.controller.report.dto.response.PipelineChangeFailureRateOfPipeline; import heartbeat.controller.report.dto.response.PipelineMeanTimeToRecovery; @@ -159,6 +160,21 @@ public static ReportResponse MOCK_METRIC_CSV_DATA() { .pipelineLeadTime(5.18) .totalDelayTime(5.18) .build())) + .leadTimeForChangesOfSourceControls(List.of( + LeadTimeForChangesOfSourceControl.builder() + .organization("organization1") + .repo("repo1") + .prLeadTime(0.0) + .pipelineLeadTime(1.01) + .totalDelayTime(1.01) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("organization2") + .repo("repo2") + .prLeadTime(0.0) + .pipelineLeadTime(5.18) + .totalDelayTime(5.18) + .build())) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") .prLeadTime(0.0) diff --git a/backend/src/test/java/heartbeat/service/report/PipelineCsvFixture.java b/backend/src/test/java/heartbeat/service/report/PipelineCsvFixture.java index 223bb13f5a..2a7fbb2a3a 100644 --- a/backend/src/test/java/heartbeat/service/report/PipelineCsvFixture.java +++ b/backend/src/test/java/heartbeat/service/report/PipelineCsvFixture.java @@ -18,6 +18,7 @@ public static List MOCK_PIPELINE_CSV_DATA() { PipelineCSVInfo pipelineCsvInfo = PipelineCSVInfo.builder() .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .piplineStatus("passed") .valid(true) @@ -50,6 +51,7 @@ public static List MOCK_PIPELINE_CSV_DATA() { .build()) .build()) .leadTimeInfo(LeadTimeInfo.builder() + .pullNumber(1) .firstCommitTimeInPr("2023-05-08T07:18:18Z") .totalTime("8379303") .prMergedTime("1683793037000") @@ -186,6 +188,7 @@ public static List MOCK_PIPELINE_CSV_DATA_WITH_MESSAGE_IS_REVER .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") .piplineStatus("passed") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .buildInfo(BuildKiteBuildInfo.builder() .commit("713b31878c756c205a6c03eac5be3ac7c7e6a227") @@ -244,6 +247,7 @@ public static List MOCK_PIPELINE_CSV_DATA_WITHOUT_Author_NAME() .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") .piplineStatus("passed") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .buildInfo(BuildKiteBuildInfo.builder() .commit("713b31878c756c205a6c03eac5be3ac7c7e6a227") @@ -356,6 +360,7 @@ public static List MOCK_PIPELINE_CSV_DATA_WITH_NULL_COMMIT_INFO .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") .piplineStatus("passed") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .valid(true) .buildInfo(BuildKiteBuildInfo.builder() @@ -397,6 +402,7 @@ public static List MOCK_TWO_ORGANIZATIONS_PIPELINE_CSV_DATA() { PipelineCSVInfo pipelineCsvInfo1 = PipelineCSVInfo.builder() .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .piplineStatus("passed") .valid(true) @@ -430,6 +436,8 @@ public static List MOCK_TWO_ORGANIZATIONS_PIPELINE_CSV_DATA() { .build()) .build()) .leadTimeInfo(LeadTimeInfo.builder() + .committer("test-committer") + .pullNumber(1) .firstCommitTimeInPr("2023-05-08T07:18:18Z") .totalTime("8379303") .prMergedTime("1683793037000") @@ -450,6 +458,7 @@ public static List MOCK_TWO_ORGANIZATIONS_PIPELINE_CSV_DATA() { PipelineCSVInfo pipelineCsvInfo2 = PipelineCSVInfo.builder() .organizationName("Thoughtworks-Heartbeat") .pipeLineName("Heartbeat") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .piplineStatus("passed") .valid(true) @@ -502,6 +511,7 @@ public static List MOCK_TWO_ORGANIZATIONS_PIPELINE_CSV_DATA() { PipelineCSVInfo pipelineCsvInfo3 = PipelineCSVInfo.builder() .organizationName("Thoughtworks-Foxtel") .pipeLineName("Heartbeat1") + .repoName("test-repo") .stepName(":rocket: Deploy prod") .piplineStatus("passed") .valid(true) diff --git a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java index 559a8dc064..63a195e965 100644 --- a/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/ReportServiceTest.java @@ -2,6 +2,8 @@ import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; import heartbeat.controller.report.dto.request.BuildKiteSetting; +import heartbeat.controller.report.dto.request.CodeBase; +import heartbeat.controller.report.dto.request.CodebaseSetting; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.JiraBoardSetting; import heartbeat.controller.report.dto.request.MetricType; @@ -393,7 +395,11 @@ void shouldGetReportUrlsSuccessfully() { DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), DeploymentEnvironment.builder().id("1").name("pipeline1").step("step2").build(), DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build() - )).build()); + )).sourceControl(List.of( + CodeBase.builder().repo("repo1").organization("org1").branches(List.of("branch1", "branch2")).build(), + CodeBase.builder().repo("repo2").organization("org2").branches(List.of("branch3", "branch2")).build(), + CodeBase.builder().repo("repo3").organization("org3").branches(List.of("branch1", "branch3")).build() + )).build()); when(fileRepository.readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("9-9-9"), any(), any())) .thenReturn(SavedRequestInfo.builder().metrics(List.of("test-metrics1", "test-metrics3")) .classificationNames(List.of("test-classification-chart1", "test-classification-chart3")) @@ -401,7 +407,10 @@ void shouldGetReportUrlsSuccessfully() { DeploymentEnvironment.builder().id("1").name("pipeline1").step("step1").build(), DeploymentEnvironment.builder().id("1").name("pipeline2").step("step1").build(), DeploymentEnvironment.builder().id("1").name("pipeline2").step("step2").build() - )).build()); + )).sourceControl(List.of( + CodeBase.builder().repo("repo1").organization("org1").branches(List.of("branch1", "branch2")).build(), + CodeBase.builder().repo("repo2").organization("org2").branches(List.of("branch3", "branch2")).build(), + CodeBase.builder().repo("repo3").organization("org3").branches(List.of("branch1", "branch3")).build())).build()); when(fileRepository.isExpired(anyLong(), anyLong())).thenReturn(false); ShareApiDetailsResponse shareReportInfo = reportService.getShareReportInfo(TEST_UUID); @@ -432,6 +441,12 @@ void shouldGetReportUrlsSuccessfully() { assertEquals("pipeline2/step1", pipelines.get(2)); assertEquals("pipeline2/step2", pipelines.get(3)); + List sourceControls = shareReportInfo.getSourceControls(); + assertEquals(3, sourceControls.size()); + assertEquals("org1/repo1", sourceControls.get(0)); + assertEquals("org2/repo2", sourceControls.get(1)); + assertEquals("org3/repo3", sourceControls.get(2)); + verify(fileRepository).getFiles(FileType.REPORT, TEST_UUID); verify(fileRepository).getFiles(FileType.CONFIGS, TEST_UUID); verify(fileRepository).readFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), eq("0-0-0"), any(), any()); @@ -524,6 +539,11 @@ void shouldSaveRequestInfoSuccessfully() { .jiraBoardSetting(JiraBoardSetting.builder() .classificationNames(List.of("test-classification-chart1", "test-classification-chart2")) .build()) + .codebaseSetting(CodebaseSetting.builder() + .codebases(List.of( + CodeBase.builder().repo("repo1").organization("org1").branches(List.of("branch1")).build(), + CodeBase.builder().repo("repo2").organization("org2").branches(List.of("branch2")).build())) + .build()) .timezone("Asia/Shanghai") .build(); @@ -549,6 +569,15 @@ void shouldSaveRequestInfoSuccessfully() { assertEquals(2, classificationCharts.size()); assertEquals("test-classification-chart1", classificationCharts.get(0)); assertEquals("test-classification-chart2", classificationCharts.get(1)); + + List sourceControl = savedRequestInfo.getSourceControl(); + assertEquals(2, sourceControl.size()); + assertEquals("repo1", sourceControl.get(0).getRepo()); + assertEquals("org1", sourceControl.get(0).getOrganization()); + assertEquals(List.of("branch1"), sourceControl.get(0).getBranches()); + assertEquals("repo2", sourceControl.get(1).getRepo()); + assertEquals("org2", sourceControl.get(1).getOrganization()); + assertEquals(List.of("branch2"), sourceControl.get(1).getBranches()); } @Test @@ -581,6 +610,41 @@ void shouldSaveRequestInfoSuccessfullyWhenBuildKiteSettingIsNull() { assertEquals(2, metrics.size()); assertEquals("test-metrics1", metrics.get(0)); assertEquals("test-metrics2", metrics.get(1)); + + } + + @Test + void shouldSaveRequestInfoSuccessfullyWhenCodeBaseSettingIsNull() { + String timeStamp = String.valueOf(mockTimeStamp(2023, 5, 10, 0, 0, 0)); + String startTimeStamp = String.valueOf(mockTimeStamp(2024, 3, 10, 0, 0, 0)); + String endTimeStamp = String.valueOf(mockTimeStamp(2024, 4, 9, 0, 0, 0)); + + GenerateReportRequest request = GenerateReportRequest.builder() + .csvTimeStamp(timeStamp) + .startTime(startTimeStamp) + .endTime(endTimeStamp) + .metrics(List.of("test-metrics1", "test-metrics2")) + .timezone("Asia/Shanghai") + .build(); + + reportService.saveRequestInfo(request, TEST_UUID); + + verify(fileRepository).createFileByType(eq(FileType.CONFIGS), eq(TEST_UUID), + eq(request.getTimeRangeAndTimeStamp()), argumentCaptor.capture(), eq(USER_CONFIG_REPORT_PREFIX)); + + SavedRequestInfo savedRequestInfo = argumentCaptor.getValue(); + + List pipelines = savedRequestInfo.getPipelines(); + assertEquals(0, pipelines.size()); + + List metrics = savedRequestInfo.getMetrics(); + assertEquals(2, metrics.size()); + assertEquals("test-metrics1", metrics.get(0)); + assertEquals("test-metrics2", metrics.get(1)); + + List sourceControl = savedRequestInfo.getSourceControl(); + assertEquals(0, sourceControl.size()); + } @Test diff --git a/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java b/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java index da174f2ae1..e5ad63f5a6 100644 --- a/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java +++ b/backend/src/test/java/heartbeat/service/report/calculator/CalculateLeadTimeForChangesTest.java @@ -4,13 +4,19 @@ import heartbeat.client.dto.codebase.github.LeadTime; import heartbeat.client.dto.codebase.github.PipelineLeadTime; +import heartbeat.client.dto.codebase.github.SourceControlLeadTime; import heartbeat.controller.pipeline.dto.request.DeploymentEnvironment; +import heartbeat.controller.report.dto.request.BuildKiteSetting; +import heartbeat.controller.report.dto.request.CodeBase; +import heartbeat.controller.report.dto.request.CodebaseSetting; +import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.response.AvgLeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChanges; import heartbeat.controller.report.dto.response.LeadTimeForChangesOfPipelines; import java.util.List; -import heartbeat.service.report.calculator.LeadTimeForChangesCalculator; +import heartbeat.controller.report.dto.response.LeadTimeForChangesOfSourceControl; +import heartbeat.service.report.calculator.model.FetchedData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,24 +54,42 @@ void setup() { @Test void shouldReturnEmptyWhenPipelineLeadTimeIsEmpty() { - LeadTimeForChanges result = calculator.calculate(List.of(), List.of()); + List pipelineLeadTimes = List.of(); + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(pipelineLeadTimes).build()); + fetchedData.setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) + .build(); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder().name("Average").build()) + .leadTimeForChangesOfSourceControls(List.of()) .build(); + + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + assertEquals(expect, result); } @Test void shouldReturnLeadTimeForChangesPipelineIsNotEmpty() { - List pipelineLeadTimes = List.of(pipelineLeadTime); List deploymentEnvironmentList = List.of( DeploymentEnvironment.builder().id("1").name("Name").step("Step").build(), DeploymentEnvironment.builder().id("2").name("Pipeline Name").step("Pipeline Step").build(), DeploymentEnvironment.builder().id("3").name("Name").step("Pipeline Step").build()); + List pipelineLeadTimes = List.of(pipelineLeadTime); - LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes, deploymentEnvironmentList); + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(pipelineLeadTimes).build()); + fetchedData.setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(deploymentEnvironmentList).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) + .build(); LeadTimeForChanges expect = LeadTimeForChanges.builder() + .leadTimeForChangesOfSourceControls(List.of()) .leadTimeForChangesOfPipelines(List.of( LeadTimeForChangesOfPipelines.builder() .name("Name") @@ -96,6 +120,8 @@ void shouldReturnLeadTimeForChangesPipelineIsNotEmpty() { .build()) .build(); + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + assertEquals(expect, result); } @@ -113,9 +139,18 @@ void shouldReturnEmptyWhenLeadTimeIsNull() { .pipelineLeadTime(0.0) .totalDelayTime(0.0) .build()) + .leadTimeForChangesOfSourceControls(List.of()) + .build(); + List pipelineLeadTimes = List.of(mockPipelineLeadTime); + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(pipelineLeadTimes).build()); + fetchedData.setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) .build(); - LeadTimeForChanges result = calculator.calculate(List.of(mockPipelineLeadTime), List.of()); + LeadTimeForChanges result = calculator.calculate(fetchedData, request); assertEquals(expect, result); } @@ -125,7 +160,13 @@ void shouldReturnEmptyWhenLeadTimeIsEmpty() { pipelineLeadTime.setLeadTimes(List.of()); List pipelineLeadTimes = List.of(pipelineLeadTime); - LeadTimeForChanges result = calculator.calculate(pipelineLeadTimes, List.of()); + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(pipelineLeadTimes).build()); + fetchedData.setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) + .build(); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() @@ -134,8 +175,11 @@ void shouldReturnEmptyWhenLeadTimeIsEmpty() { .pipelineLeadTime(0.0) .totalDelayTime(0.0) .build()) + .leadTimeForChangesOfSourceControls(List.of()) .build(); + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + assertEquals(expect, result); } @@ -159,8 +203,15 @@ void shouldReturnFilteredResultWhenPrMergedTimeOrPrLeadTimeIsNull() { .totalTime(120000) .build())) .build(); + List pipelineLeadTimes = List.of(noMergedTime); - LeadTimeForChanges result = calculator.calculate(List.of(noMergedTime), List.of()); + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(pipelineLeadTimes).build()); + fetchedData.setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of()).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(List.of()).build()) + .build(); LeadTimeForChanges expect = LeadTimeForChanges.builder() .leadTimeForChangesOfPipelines(List.of(LeadTimeForChangesOfPipelines.builder() .name("Name") @@ -169,6 +220,7 @@ void shouldReturnFilteredResultWhenPrMergedTimeOrPrLeadTimeIsNull() { .pipelineLeadTime(0.2) .totalDelayTime(0.4) .build())) + .leadTimeForChangesOfSourceControls(List.of()) .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() .name("Average") .prLeadTime(0.2) @@ -177,6 +229,227 @@ void shouldReturnFilteredResultWhenPrMergedTimeOrPrLeadTimeIsNull() { .build()) .build(); + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + + assertEquals(expect, result); + } + + @Test + void shouldReturnLeadTimeForChangesWhenSourceControlIsNotEmpty() { + List codeBaseList = List.of( + CodeBase.builder() + .organization("test-org1") + .branches(List.of("test-branch1")) + .repo("test-repo1") + .build(), + CodeBase.builder() + .organization("test-org2") + .branches(List.of("test-branch2")) + .repo("test-repo2") + .build(), + CodeBase.builder() + .organization("test-org1") + .branches(List.of("test-branch3")) + .repo("test-repo3") + .build()); + + SourceControlLeadTime sourceControlLeadTime = SourceControlLeadTime.builder() + .organization("test-org1") + .repo("test-repo1") + .leadTimes(List.of(LeadTime.builder() + .commitId("111") + .prCreatedTime(165854910000L) + .prMergedTime(1658549160000L) + .firstCommitTimeInPr(165854910000L) + .jobFinishTime(1658549160000L) + .pipelineCreateTime(165854910000L) + .prLeadTime(60000L) + .pipelineLeadTime(60000) + .totalTime(120000) + .build())) + .build(); + + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(List.of()).build()); + fetchedData + .setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of(sourceControlLeadTime)).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(codeBaseList).build()) + .build(); + LeadTimeForChanges expect = LeadTimeForChanges.builder() + .leadTimeForChangesOfPipelines(List.of()) + .leadTimeForChangesOfSourceControls(List.of( + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org1") + .repo("test-repo1") + .prLeadTime(1.0) + .pipelineLeadTime(1.0) + .totalDelayTime(2.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org2") + .repo("test-repo2") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org1") + .repo("test-repo3") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build())) + .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() + .name("Average") + .prLeadTime(1.0) + .pipelineLeadTime(1.0) + .totalDelayTime(2.0) + .build()) + .build(); + + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + + assertEquals(expect, result); + } + + @Test + void shouldReturnLeadTimeForChangesWhenSourceControlLeadTimeIsNull() { + List codeBaseList = List.of( + CodeBase.builder() + .organization("test-org1") + .branches(List.of("test-branch1")) + .repo("test-repo1") + .build(), + CodeBase.builder() + .organization("test-org2") + .branches(List.of("test-branch2")) + .repo("test-repo2") + .build(), + CodeBase.builder() + .organization("test-org3") + .branches(List.of("test-branch3")) + .repo("test-repo3") + .build()); + + SourceControlLeadTime sourceControlLeadTime = SourceControlLeadTime.builder() + .organization("test-org1") + .repo("test-repo1") + .build(); + + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(List.of()).build()); + fetchedData + .setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of(sourceControlLeadTime)).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(codeBaseList).build()) + .build(); + LeadTimeForChanges expect = LeadTimeForChanges.builder() + .leadTimeForChangesOfPipelines(List.of()) + .leadTimeForChangesOfSourceControls(List.of( + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org1") + .repo("test-repo1") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org2") + .repo("test-repo2") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org3") + .repo("test-repo3") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build())) + .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() + .name("Average") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build()) + .build(); + + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + + assertEquals(expect, result); + } + + @Test + void shouldReturnLeadTimeForChangesWhenSourceControlLeadTimeIsEmpty() { + List codeBaseList = List.of( + CodeBase.builder() + .organization("test-org1") + .branches(List.of("test-branch1")) + .repo("test-repo1") + .build(), + CodeBase.builder() + .organization("test-org2") + .branches(List.of("test-branch2")) + .repo("test-repo2") + .build(), + CodeBase.builder() + .organization("test-org3") + .branches(List.of("test-branch3")) + .repo("test-repo3") + .build()); + + SourceControlLeadTime sourceControlLeadTime = SourceControlLeadTime.builder() + .organization("test-org1") + .repo("test-repo1") + .leadTimes(List.of()) + .build(); + + FetchedData fetchedData = new FetchedData(); + fetchedData.setBuildKiteData(FetchedData.BuildKiteData.builder().pipelineLeadTimes(List.of()).build()); + fetchedData + .setRepoData(FetchedData.RepoData.builder().sourceControlLeadTimes(List.of(sourceControlLeadTime)).build()); + GenerateReportRequest request = GenerateReportRequest.builder() + .buildKiteSetting(BuildKiteSetting.builder().deploymentEnvList(List.of()).build()) + .codebaseSetting(CodebaseSetting.builder().codebases(codeBaseList).build()) + .build(); + LeadTimeForChanges expect = LeadTimeForChanges.builder() + .leadTimeForChangesOfPipelines(List.of()) + .leadTimeForChangesOfSourceControls(List.of( + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org1") + .repo("test-repo1") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org2") + .repo("test-repo2") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build(), + LeadTimeForChangesOfSourceControl.builder() + .organization("test-org3") + .repo("test-repo3") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build())) + .avgLeadTimeForChanges(AvgLeadTimeForChanges.builder() + .name("Average") + .prLeadTime(0.0) + .pipelineLeadTime(0.0) + .totalDelayTime(0.0) + .build()) + .build(); + + LeadTimeForChanges result = calculator.calculate(fetchedData, request); + assertEquals(expect, result); } 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 4b84c63fe0..fa4fff1c94 100644 --- a/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java +++ b/backend/src/test/java/heartbeat/service/source/github/GithubServiceTest.java @@ -10,16 +10,20 @@ import heartbeat.client.dto.codebase.github.OrganizationsInfoDTO; import heartbeat.client.dto.codebase.github.PageBranchesInfoDTO; import heartbeat.client.dto.codebase.github.PageOrganizationsInfoDTO; -import heartbeat.client.dto.codebase.github.PagePullRequestInfoDTO; +import heartbeat.client.dto.codebase.github.PagePullRequestInfo; import heartbeat.client.dto.codebase.github.PageReposInfoDTO; import heartbeat.client.dto.codebase.github.PipelineLeadTime; import heartbeat.client.dto.codebase.github.PullRequestInfo; -import heartbeat.client.dto.codebase.github.PullRequestInfoDTO; import heartbeat.client.dto.codebase.github.ReposInfoDTO; +import heartbeat.client.dto.codebase.github.SourceControlLeadTime; import heartbeat.client.dto.pipeline.buildkite.DeployInfo; import heartbeat.client.dto.pipeline.buildkite.DeployTimes; import heartbeat.controller.report.dto.request.CalendarTypeEnum; +import heartbeat.controller.report.dto.request.CodeBase; +import heartbeat.controller.report.dto.request.CodebaseSetting; import heartbeat.controller.report.dto.request.GenerateReportRequest; +import heartbeat.controller.report.dto.response.LeadTimeInfo; +import heartbeat.controller.report.dto.response.PipelineCSVInfo; import heartbeat.exception.BadRequestException; import heartbeat.exception.InternalServerErrorException; import heartbeat.exception.NotFoundException; @@ -27,6 +31,7 @@ import heartbeat.exception.UnauthorizedException; import heartbeat.service.pipeline.buildkite.CachePageService; import heartbeat.service.report.WorkDay; +import heartbeat.service.report.calculator.model.FetchedData; import heartbeat.service.report.model.WorkInfo; import heartbeat.service.source.github.model.PipelineInfoOfRepository; import org.junit.jupiter.api.AfterEach; @@ -113,6 +118,7 @@ public void setUp() { .mergeCommitSha("111") .url("https://api.github.com/repos/XXXX-fs/fs-platform-onboarding/pulls/1") .number(1) + .user(PullRequestInfo.PullRequestUser.builder().login("test-user").build()) .build(); deployInfo = DeployInfo.builder() .commitId("111") @@ -148,6 +154,8 @@ public void setUp() { .pipelineStep(PIPELINE_STEP) .leadTimes(List.of(LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(1658548980000L) @@ -306,10 +314,41 @@ void shouldReturnNullWhenMergeTimeIsNull() { assertNull(result); } + @Test + void shouldReturnNullWhenCommitterDateIsNull() { + GenerateReportRequest request = GenerateReportRequest.builder().build(); + LeadTime expect = LeadTime.builder() + .commitId("111") + .committer("test-user") + .pullNumber(1) + .prCreatedTime(1658548980000L) + .prMergedTime(1658549040000L) + .firstCommitTimeInPr(0L) + .jobStartTime(1658549040000L) + .jobFinishTime(1658549160000L) + .pipelineLeadTime(1658549100000L) + .pipelineCreateTime(1658549100000L) + .prLeadTime(0L) + .pipelineLeadTime(120000) + .firstCommitTime(1658549040000L) + .totalTime(120000L) + .isRevert(null) + .build(); + commitInfo = CommitInfo.builder() + .commit(Commit.builder().committer(Committer.builder().build()).build()) + .build(); + + LeadTime result = githubService.mapLeadTimeWithInfo(pullRequestInfo, deployInfo, commitInfo, request); + + assertEquals(expect, result); + } + @Test void shouldReturnLeadTimeWhenMergedTimeIsNotNull() { LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(1658548980000L) @@ -348,6 +387,8 @@ void CommitTimeInPrShouldBeZeroWhenCommitInfoIsNull() { LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -374,6 +415,8 @@ void CommitTimeInPrLeadTimeShouldBeZeroWhenCommitInfoIsNotNullGivenCommitIsRever LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -399,6 +442,8 @@ void CommitTimeInPrLeadTimeShouldBeZeroWhenCommitInfoIsInLowerCaseGivenCommitIsR GenerateReportRequest request = GenerateReportRequest.builder().build(); LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -424,6 +469,8 @@ void shouldReturnIsRevertIsNullWhenCommitInfoCommitIsNull() { GenerateReportRequest request = GenerateReportRequest.builder().build(); LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -449,6 +496,8 @@ void shouldReturnIsRevertIsNullWhenCommitInfoCommitMessageIsNull() { GenerateReportRequest request = GenerateReportRequest.builder().build(); LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -474,6 +523,8 @@ void shouldReturnFirstCommitTimeInPrZeroWhenCommitInfoIsNull() { GenerateReportRequest request = GenerateReportRequest.builder().build(); LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(0L) @@ -510,12 +561,15 @@ void shouldReturnPrLeadTimeIsZeroWhenWorkdayIsNegative() { .mergedAt("2022-07-23T04:04:00.000+00:00") .createdAt("2022-07-23T04:03:00.000+00:00") .mergeCommitSha("111") + .user(PullRequestInfo.PullRequestUser.builder().login("test-user").build()) .url("https://api.github.com/repos/XXXX-fs/fs-platform-onboarding/pulls/1") .number(1) .build(); LeadTime expect = LeadTime.builder() .commitId("111") + .committer("test-user") + .pullNumber(1) .prCreatedTime(1658548980000L) .prMergedTime(1658549040000L) .firstCommitTimeInPr(1658635440000L) @@ -1054,36 +1108,36 @@ void shouldReturnAllCrewsWhenPageMoreThan1() { String mockBranch = "branch"; long startTime = 1717171200000L; long endTime = 1719763199999L; - PullRequestInfoDTO pullRequestWhenMergeIsNull = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenMergeIsNull = PullRequestInfo.builder() .number(1) .createdAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test1").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test1").build()) .build(); - PullRequestInfoDTO pullRequestWhenCreateAndMergeTimeIsSuccess = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateAndMergeTimeIsSuccess = PullRequestInfo.builder() .number(2) .createdAt("2024-06-30T15:59:59Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test2").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test2").build()) .build(); - PullRequestInfoDTO pullRequestWhenCreateIsAfterEndTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateIsAfterEndTime = PullRequestInfo.builder() .number(3) .createdAt("2024-06-30T16:59:59Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test3").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test3").build()) .build(); - PullRequestInfoDTO pullRequestWhenMergeIsBeforeStartTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenMergeIsBeforeStartTime = PullRequestInfo.builder() .number(4) .createdAt("2024-06-30T15:59:59Z") .mergedAt("2024-05-31T15:00:00Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test4").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test4").build()) .build(); - PullRequestInfoDTO pullRequestWhenMergeIsAfterEndTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenMergeIsAfterEndTime = PullRequestInfo.builder() .number(5) .createdAt("2024-06-30T15:59:59Z") .mergedAt("2024-06-30T16:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test5").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test5").build()) .build(); - PagePullRequestInfoDTO pagePullRequestInfoDTO = PagePullRequestInfoDTO.builder() + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder() .totalPage(22) .pageInfo(List.of(pullRequestWhenMergeIsNull, pullRequestWhenCreateAndMergeTimeIsSuccess, pullRequestWhenCreateIsAfterEndTime, pullRequestWhenMergeIsBeforeStartTime, @@ -1091,7 +1145,7 @@ void shouldReturnAllCrewsWhenPageMoreThan1() { .build(); when(cachePageService.getGitHubPullRequest(eq("Bearer " + mockToken), eq(mockOrganization), eq(mockRepo), eq(mockBranch), anyInt(), eq(100))) - .thenReturn(pagePullRequestInfoDTO); + .thenReturn(pagePullRequestInfo); List allCrews = githubService.getAllCrews(mockToken, mockOrganization, mockRepo, mockBranch, startTime, endTime); @@ -1107,19 +1161,19 @@ void shouldReturnAllCrewsWhenPageIsEqualTo1() { String mockBranch = "branch"; long startTime = 1717171200000L; long endTime = 1719763199999L; - PullRequestInfoDTO pullRequestWhenCreateIsBeforeStartTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateIsBeforeStartTime = PullRequestInfo.builder() .number(1) .createdAt("2024-05-31T15:00:00Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test1").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test1").build()) .build(); - PagePullRequestInfoDTO pagePullRequestInfoDTO = PagePullRequestInfoDTO.builder() + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder() .totalPage(1) .pageInfo(List.of(pullRequestWhenCreateIsBeforeStartTime)) .build(); when(cachePageService.getGitHubPullRequest(eq("Bearer " + mockToken), eq(mockOrganization), eq(mockRepo), eq(mockBranch), anyInt(), eq(100))) - .thenReturn(pagePullRequestInfoDTO); + .thenReturn(pagePullRequestInfo); List allCrews = githubService.getAllCrews(mockToken, mockOrganization, mockRepo, mockBranch, startTime, endTime); @@ -1135,19 +1189,19 @@ void shouldReturnAllCrewsWhenPageIsMoreThan1AndDontGoToNextPage() { String mockBranch = "branch"; long startTime = 1717171200000L; long endTime = 1719763199999L; - PullRequestInfoDTO pullRequestWhenCreateIsBeforeStartTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateIsBeforeStartTime = PullRequestInfo.builder() .number(1) .createdAt("2024-05-31T15:00:00Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test1").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test1").build()) .build(); - PagePullRequestInfoDTO pagePullRequestInfoDTO = PagePullRequestInfoDTO.builder() + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder() .totalPage(2) .pageInfo(List.of(pullRequestWhenCreateIsBeforeStartTime)) .build(); when(cachePageService.getGitHubPullRequest(eq("Bearer " + mockToken), eq(mockOrganization), eq(mockRepo), eq(mockBranch), anyInt(), eq(100))) - .thenReturn(pagePullRequestInfoDTO); + .thenReturn(pagePullRequestInfo); List allCrews = githubService.getAllCrews(mockToken, mockOrganization, mockRepo, mockBranch, startTime, endTime); @@ -1163,32 +1217,32 @@ void shouldReturnAllCrewsWhenPageIsMoreThan1AndGoToNextPageAndDontGoToNextPageTw String mockBranch = "branch"; long startTime = 1717171200000L; long endTime = 1719763199999L; - PullRequestInfoDTO pullRequestWhenCreateIsAfterEndTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateIsAfterEndTime = PullRequestInfo.builder() .number(3) .createdAt("2024-06-30T16:59:59Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test3").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test3").build()) .build(); - PullRequestInfoDTO pullRequestWhenCreateIsBeforeStartTime = PullRequestInfoDTO.builder() + PullRequestInfo pullRequestWhenCreateIsBeforeStartTime = PullRequestInfo.builder() .number(1) .createdAt("2024-05-31T15:00:00Z") .mergedAt("2024-06-30T15:59:59Z") - .user(PullRequestInfoDTO.PullRequestUser.builder().login("test1").build()) + .user(PullRequestInfo.PullRequestUser.builder().login("test1").build()) .build(); - PagePullRequestInfoDTO pagePullRequestInfoDTO1 = PagePullRequestInfoDTO.builder() + PagePullRequestInfo pagePullRequestInfo1 = PagePullRequestInfo.builder() .totalPage(2) .pageInfo(List.of(pullRequestWhenCreateIsAfterEndTime)) .build(); - PagePullRequestInfoDTO pagePullRequestInfoDTO2 = PagePullRequestInfoDTO.builder() + PagePullRequestInfo pagePullRequestInfo2 = PagePullRequestInfo.builder() .totalPage(2) .pageInfo(List.of(pullRequestWhenCreateIsBeforeStartTime)) .build(); when(cachePageService.getGitHubPullRequest("Bearer " + mockToken, mockOrganization, mockRepo, mockBranch, 1, 100)) - .thenReturn(pagePullRequestInfoDTO1); + .thenReturn(pagePullRequestInfo1); when(cachePageService.getGitHubPullRequest("Bearer " + mockToken, mockOrganization, mockRepo, mockBranch, 2, 100)) - .thenReturn(pagePullRequestInfoDTO2); + .thenReturn(pagePullRequestInfo2); List allCrews = githubService.getAllCrews(mockToken, mockOrganization, mockRepo, mockBranch, startTime, endTime); @@ -1204,10 +1258,10 @@ void shouldReturnNoCrewsWhenPullRequestIsNullInTheFirstPage() { String mockBranch = "branch"; long startTime = 1717171200000L; long endTime = 1719763199999L; - PagePullRequestInfoDTO pagePullRequestInfoDTO = PagePullRequestInfoDTO.builder().totalPage(0).build(); + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder().totalPage(0).build(); when(cachePageService.getGitHubPullRequest("Bearer " + mockToken, mockOrganization, mockRepo, mockBranch, 1, 100)) - .thenReturn(pagePullRequestInfoDTO); + .thenReturn(pagePullRequestInfo); List allCrews = githubService.getAllCrews(mockToken, mockOrganization, mockRepo, mockBranch, startTime, endTime); @@ -1215,4 +1269,227 @@ void shouldReturnNoCrewsWhenPullRequestIsNullInTheFirstPage() { assertEquals(0, allCrews.size()); } + @Test + void shouldFetchPartialReportDataSuccessfullyWhenCrewIsNotEmpty() { + String mockToken = "mockToken"; + String mockOrganization = "mockOrg"; + String mockRepo = "mockRepo"; + GenerateReportRequest request = GenerateReportRequest.builder() + .timezone("Asia/Shanghai") + .calendarType(CalendarTypeEnum.CN) + .startTime("1717171200000") + .endTime("1719763199999") + .codebaseSetting(CodebaseSetting.builder() + .token(mockToken) + .crews(List.of("mockCrew1", "mockCrew2")) + .codebases(List.of(CodeBase.builder() + .organization(mockOrganization) + .repo(mockRepo) + .branches(List.of("mockBranch1")) + .build())) + .build()) + .build(); + List commitInfos = List.of(CommitInfo.builder() + .commit(Commit.builder() + .committer(Committer.builder().date("2024-05-31T17:00:00Z").email("1").name("1").build()) + .author(Author.builder().date("1").email("1").name("1").build()) + .message("mockMessage") + .build()) + .build()); + + pullRequestInfo = PullRequestInfo.builder() + .number(1) + .createdAt("2024-05-31T17:00:00Z") + .mergedAt("2024-06-30T15:59:59Z") + .user(PullRequestInfo.PullRequestUser.builder().login("mockCrew1").build()) + .build(); + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder() + .totalPage(1) + .pageInfo(List.of(pullRequestInfo)) + .build(); + + when(cachePageService.getGitHubPullRequest(eq("Bearer " + mockToken), eq(mockOrganization), eq(mockRepo), + anyString(), anyInt(), eq(100))) + .thenReturn(pagePullRequestInfo); + when(gitHubFeignClient.getPullRequestCommitInfo("mockOrg/mockRepo", "1", "Bearer mockToken")) + .thenReturn(commitInfos); + when(workDay.calculateWorkTimeAndHolidayBetween(any(Long.class), any(Long.class), any(CalendarTypeEnum.class), + any(ZoneId.class))) + .thenAnswer(invocation -> { + long firstParam = invocation.getArgument(0); + long secondParam = invocation.getArgument(1); + return WorkInfo.builder().workTime(secondParam - firstParam).build(); + }); + + FetchedData.RepoData repoData = githubService.fetchRepoData(request); + List sourceControlLeadTimes = repoData.getSourceControlLeadTimes(); + + assertEquals(1, sourceControlLeadTimes.size()); + + SourceControlLeadTime sourceControlLeadTime = sourceControlLeadTimes.get(0); + assertEquals("mockOrg", sourceControlLeadTime.getOrganization()); + assertEquals("mockRepo", sourceControlLeadTime.getRepo()); + + List leadTimes = sourceControlLeadTime.getLeadTimes(); + + assertEquals(1, leadTimes.size()); + + LeadTime leadTime = leadTimes.get(0); + assertEquals(1717174800000L, leadTime.getPrCreatedTime()); + assertEquals(1719763199000L, leadTime.getPrMergedTime()); + assertEquals(1717174800000L, leadTime.getFirstCommitTimeInPr()); + assertEquals(1719763199000L, leadTime.getFirstCommitTime()); + assertEquals(2588399000L, leadTime.getPrLeadTime()); + assertEquals(0L, leadTime.getPipelineLeadTime()); + assertEquals(2588399000L, leadTime.getTotalTime()); + assertEquals(0L, leadTime.getHolidays()); + assertEquals(Boolean.FALSE, leadTime.getIsRevert()); + assertNull(leadTime.getCommitId()); + assertNull(leadTime.getJobFinishTime()); + assertNull(leadTime.getJobStartTime()); + assertNull(leadTime.getNoPRCommitTime()); + assertNull(leadTime.getPipelineCreateTime()); + } + + @Test + void shouldFetchAllReportDataWhenCrewIsEmpty() { + String mockToken = "mockToken"; + String mockOrganization = "mockOrg"; + String mockRepo = "mockRepo"; + GenerateReportRequest request = GenerateReportRequest.builder() + .timezone("Asia/Shanghai") + .calendarType(CalendarTypeEnum.CN) + .startTime("1717171200000") + .endTime("1719763199999") + .codebaseSetting(CodebaseSetting.builder() + .token(mockToken) + .crews(List.of()) + .codebases(List.of(CodeBase.builder() + .organization(mockOrganization) + .repo(mockRepo) + .branches(List.of("mockBranch1")) + .build())) + .build()) + .build(); + List commitInfos = List.of(CommitInfo.builder() + .commit(Commit.builder() + .committer(Committer.builder().date("2024-05-31T17:00:00Z").email("1").name("1").build()) + .author(Author.builder().date("1").email("1").name("1").build()) + .message("mockMessage") + .build()) + .build()); + + pullRequestInfo = PullRequestInfo.builder() + .number(1) + .createdAt("2024-05-31T17:00:00Z") + .mergedAt("2024-06-30T15:59:59Z") + .user(PullRequestInfo.PullRequestUser.builder().login("mockCrew1").build()) + .build(); + PagePullRequestInfo pagePullRequestInfo = PagePullRequestInfo.builder() + .totalPage(1) + .pageInfo(List.of(pullRequestInfo)) + .build(); + + when(cachePageService.getGitHubPullRequest(eq("Bearer " + mockToken), eq(mockOrganization), eq(mockRepo), + anyString(), anyInt(), eq(100))) + .thenReturn(pagePullRequestInfo); + when(gitHubFeignClient.getPullRequestCommitInfo("mockOrg/mockRepo", "1", "Bearer mockToken")) + .thenReturn(commitInfos); + when(workDay.calculateWorkTimeAndHolidayBetween(any(Long.class), any(Long.class), any(CalendarTypeEnum.class), + any(ZoneId.class))) + .thenAnswer(invocation -> { + long firstParam = invocation.getArgument(0); + long secondParam = invocation.getArgument(1); + return WorkInfo.builder().workTime(secondParam - firstParam).build(); + }); + + FetchedData.RepoData repoData = githubService.fetchRepoData(request); + List sourceControlLeadTimes = repoData.getSourceControlLeadTimes(); + + assertEquals(1, sourceControlLeadTimes.size()); + + SourceControlLeadTime sourceControlLeadTime = sourceControlLeadTimes.get(0); + assertEquals("mockOrg", sourceControlLeadTime.getOrganization()); + assertEquals("mockRepo", sourceControlLeadTime.getRepo()); + + List leadTimes = sourceControlLeadTime.getLeadTimes(); + + assertEquals(1, leadTimes.size()); + + LeadTime leadTime = leadTimes.get(0); + assertEquals(1717174800000L, leadTime.getPrCreatedTime()); + assertEquals(1719763199000L, leadTime.getPrMergedTime()); + assertEquals(1717174800000L, leadTime.getFirstCommitTimeInPr()); + assertEquals(1719763199000L, leadTime.getFirstCommitTime()); + assertEquals(2588399000L, leadTime.getPrLeadTime()); + assertEquals(0L, leadTime.getPipelineLeadTime()); + assertEquals(2588399000L, leadTime.getTotalTime()); + assertEquals(0L, leadTime.getHolidays()); + assertEquals(Boolean.FALSE, leadTime.getIsRevert()); + assertNull(leadTime.getCommitId()); + assertNull(leadTime.getJobFinishTime()); + assertNull(leadTime.getJobStartTime()); + assertNull(leadTime.getNoPRCommitTime()); + assertNull(leadTime.getPipelineCreateTime()); + } + + @Test + void shouldReturnPipelineCSVInfoSuccessfully() { + LeadTime leadTime = LeadTime.builder() + .commitId("111") + .committer("test-user") + .pullNumber(1) + .prCreatedTime(1658548980000L) + .prMergedTime(1658549040000L) + .firstCommitTimeInPr(1658548980000L) + .jobStartTime(1658549040000L) + .jobFinishTime(1658549160000L) + .pipelineLeadTime(1658549100000L) + .pipelineCreateTime(1658549100000L) + .prLeadTime(60000L) + .pipelineLeadTime(120000) + .firstCommitTime(1658549040000L) + .totalTime(180000) + .isRevert(Boolean.FALSE) + .build(); + FetchedData.RepoData repoData = FetchedData.RepoData.builder() + .sourceControlLeadTimes(List.of( + SourceControlLeadTime.builder() + .organization("test-org1") + .repo("test-repo1") + .branch("test-branch1") + .leadTimes(List.of(leadTime)) + .build(), + SourceControlLeadTime.builder() + .organization("test-org1") + .repo("test-repo2") + .branch("test-branch1") + .leadTimes(List.of(leadTime)) + .build(), + SourceControlLeadTime.builder() + .organization("test-org2") + .repo("test-repo2") + .branch("test-branch1") + .leadTimes(List.of(leadTime)) + .build(), + SourceControlLeadTime.builder() + .organization("test-org2") + .repo("test-repo1") + .branch("test-branch1") + .leadTimes(List.of(leadTime)) + .build())) + .build(); + List codeBases = List.of(CodeBase.builder().organization("test-org1").repo("test-repo1").build()); + List expect = List.of(PipelineCSVInfo.builder() + .organizationName("test-org1") + .repoName("test-repo1") + .branchName("test-branch1") + .leadTimeInfo(new LeadTimeInfo(leadTime)) + .build()); + + List pipelineCSVInfos = githubService.generateCSVForSourceControl(repoData, codeBases); + + assertEquals(expect, pipelineCSVInfos); + } + } diff --git a/docs/src/content/docs/en/spikes/tech-spikes-github-api-for-pull-request-info-by-token.mdx b/docs/src/content/docs/en/spikes/tech-spikes-github-api-for-pull-request-info-by-token.mdx index 16809a5388..0daaf26fc7 100644 --- a/docs/src/content/docs/en/spikes/tech-spikes-github-api-for-pull-request-info-by-token.mdx +++ b/docs/src/content/docs/en/spikes/tech-spikes-github-api-for-pull-request-info-by-token.mdx @@ -212,7 +212,7 @@ backend --> frontend: return pr lead time ... "codebaseSetting": { ... - "pipelineCrews": ["zhou-yinyuan"], // new added + "crews": ["zhou-yinyuan"], // new added "codebases": [ // new added { "branches": ["branch name"], diff --git a/frontend/__tests__/components/Common/ReportGrid/ReportCard.test.tsx b/frontend/__tests__/components/Common/ReportGrid/ReportCard.test.tsx index e70704f3a2..49f4434358 100644 --- a/frontend/__tests__/components/Common/ReportGrid/ReportCard.test.tsx +++ b/frontend/__tests__/components/Common/ReportGrid/ReportCard.test.tsx @@ -38,4 +38,28 @@ describe('Report Card', () => { expect(screen.getByText(errorMessage)).toBeInTheDocument(); }); + + it('should show gray item when isExistSourceControl is true', () => { + const items = [ + { + value: 1, + subtitle: 'PR Lead Time', + }, + { + value: 2, + subtitle: 'Pipeline Lead Time', + }, + { + value: 3, + subtitle: 'Total Lead Time', + }, + ]; + + render(); + + const sectionItems = screen.getAllByLabelText('report card item'); + + expect(sectionItems.length).toBe(2); + expect(sectionItems[1].getAttribute('style')).toContain('color: gray;'); + }); }); diff --git a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx index 3c01847987..350dfce375 100644 --- a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx @@ -10,6 +10,7 @@ import { IUseGetSourceControlConfigurationCrewInterface } from '@src/hooks/useGe import { SourceControlConfiguration } from '@src/containers/MetricsStep/SouceControlConfiguration'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import { LIST_OPEN, LOADING, REMOVE_BUTTON } from '@test/fixtures'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; @@ -35,16 +36,37 @@ const mockInitRepoEffectResponse = { isGetRepo: true, isLoading: false, getSourceControlRepoInfo: jest.fn(), + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitBranchEffectResponse = { isLoading: false, getSourceControlBranchInfo: jest.fn(), isGetBranch: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitCrewEffectResponse = { isLoading: false, getSourceControlCrewInfo: jest.fn(), isGetAllCrews: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; let mockSourceControlSettings = mockInitSourceControlSettings; @@ -221,4 +243,18 @@ describe('SourceControlConfiguration', () => { expect(screen.getByText('Crew setting (optional)')).toBeInTheDocument(); }); + + it('should set error info when any request return error', () => { + mockOrganizationEffectResponse = { + ...mockOrganizationEffectResponse, + info: { + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }, + }; + setup(); + + expect(screen.getByLabelText('Error UI for pipeline settings')).toBeInTheDocument(); + }); }); diff --git a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx index 4b89988c61..4a3ea59c65 100644 --- a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx @@ -3,6 +3,7 @@ import { IUseGetSourceControlConfigurationBranchInterface } from '@src/hooks/use import { IUseGetSourceControlConfigurationRepoInterface } from '@src/hooks/useGetSourceControlConfigurationRepoEffect'; import { IUseGetSourceControlConfigurationCrewInterface } from '@src/hooks/useGetSourceControlConfigurationCrewEffect'; import { act, render, screen, within } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import { LIST_OPEN, LOADING } from '@test/fixtures'; @@ -12,16 +13,37 @@ const mockInitRepoEffectResponse = { isGetRepo: true, isLoading: false, getSourceControlRepoInfo: jest.fn(), + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitBranchEffectResponse = { isLoading: false, getSourceControlBranchInfo: jest.fn(), isGetBranch: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitCrewEffectResponse = { isLoading: false, getSourceControlCrewInfo: jest.fn(), isGetAllCrews: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; let mockRepoEffectResponse: IUseGetSourceControlConfigurationRepoInterface = mockInitRepoEffectResponse; @@ -34,6 +56,11 @@ let mockSelectSourceControlRepos = mockInitSelectSourceControlRepos; const mockInitSelectSourceControlBranches = ['mockBranchName', 'mockBranchName1']; let mockSelectSourceControlBranches = mockInitSelectSourceControlBranches; +const myDispatch = jest.fn(); +jest.mock('@src/hooks', () => ({ + ...jest.requireActual('@src/hooks'), + useAppDispatch: () => myDispatch, +})); jest.mock('@src/hooks/useGetSourceControlConfigurationRepoEffect', () => { return { useGetSourceControlConfigurationRepoEffect: () => mockRepoEffectResponse, @@ -50,6 +77,10 @@ jest.mock('@src/hooks/useGetSourceControlConfigurationCrewEffect', () => { }; }); +jest.mock('@src/context/notification/NotificationSlice', () => ({ + ...jest.requireActual('@src/context/notification/NotificationSlice'), + addNotification: jest.fn(), +})); jest.mock('@src/context/Metrics/metricsSlice', () => ({ ...jest.requireActual('@src/context/Metrics/metricsSlice'), selectSourceControlConfigurationSettings: jest.fn().mockImplementation(() => [ @@ -78,7 +109,11 @@ describe('SourceControlMetricSelection', () => { mockSelectSourceControlBranches = mockInitSelectSourceControlBranches; mockSelectSourceControlRepos = mockInitSelectSourceControlRepos; }); + afterEach(() => { + jest.clearAllMocks(); + }); const onUpdateSourceControl = jest.fn(); + const handleUpdateErrorInfo = jest.fn(); const setup = (isDuplicated: boolean = false) => { const sourceControlSetting = { id: 0, @@ -96,6 +131,7 @@ describe('SourceControlMetricSelection', () => { isDuplicated={isDuplicated} setLoadingCompletedNumber={jest.fn()} totalSourceControlNumber={1} + handleUpdateErrorInfo={handleUpdateErrorInfo} /> , ); @@ -188,4 +224,43 @@ describe('SourceControlMetricSelection', () => { expect(getSourceControlBranchInfoFunction).toHaveBeenCalledTimes(1); expect(getSourceControlCrewInfoFunction).toHaveBeenCalledTimes(2); }); + + it('should add partial failed 4xx notification when any failed status is PartialFailed4xx', async () => { + mockCrewEffectResponse = { + ...mockCrewEffectResponse, + stepFailedStatus: MetricsDataFailStatus.PartialFailed4xx, + }; + setup(); + + expect(myDispatch).toHaveBeenCalledTimes(1); + }); + + it('should add partial failed 4xx notification when any failed status is PartialFailedNoCards', async () => { + mockCrewEffectResponse = { + ...mockCrewEffectResponse, + stepFailedStatus: MetricsDataFailStatus.PartialFailedNoCards, + }; + setup(); + + expect(myDispatch).toHaveBeenCalledTimes(1); + }); + + it('should set error info when any request return error', () => { + mockRepoEffectResponse = { + ...mockRepoEffectResponse, + info: { + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }, + }; + setup(); + + expect(handleUpdateErrorInfo).toHaveBeenCalledTimes(1); + expect(handleUpdateErrorInfo).toBeCalledWith({ + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }); + }); }); diff --git a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx index fb8daad534..1b4f428059 100644 --- a/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx +++ b/frontend/__tests__/containers/ReportStep/DoraMetrics.test.tsx @@ -50,6 +50,7 @@ describe('Report Card', () => { onShowDetail={onShowDetail} doraReport={mockData} errorMessage={''} + isExistSourceControl={false} /> , ); diff --git a/frontend/__tests__/containers/ReportStep/ReportDetail/dora.test.tsx b/frontend/__tests__/containers/ReportStep/ReportDetail/dora.test.tsx index b7fc0d44c5..3d9063807f 100644 --- a/frontend/__tests__/containers/ReportStep/ReportDetail/dora.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportDetail/dora.test.tsx @@ -13,7 +13,7 @@ describe('DoraDetail', () => { it('should render a back link', () => { (reportMapper as jest.Mock).mockReturnValue({}); - render(); + render(); expect(screen.getByTestId('ArrowBackIcon')).toBeInTheDocument(); expect(screen.getByText('Back')).toBeInTheDocument(); @@ -24,7 +24,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ deploymentFrequencyList: [{ id: 0, name: 'name1', valueList: [{ value: 1 }] }], }); - render(); + render(); const deploymentFrequencyTable = screen.getByLabelText('Deployment Frequency'); expect(screen.getByText('Deployment Frequency')).toBeInTheDocument(); expect(deploymentFrequencyTable).toBeInTheDocument(); @@ -35,7 +35,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ deploymentFrequencyList: null, }); - render(); + render(); expect(screen.queryAllByText('Deployment Frequency').length).toEqual(0); }); }); @@ -45,7 +45,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ leadTimeForChangesList: [{ id: 0, name: 'name1', valuesList: [{ name: 'test1', values: [1] }] }], }); - render(); + render(); const leadTimeForChangesTable = screen.getByLabelText('Lead Time For Changes'); expect(screen.getByText('Lead Time For Changes')).toBeInTheDocument(); expect(leadTimeForChangesTable).toBeInTheDocument(); @@ -56,9 +56,33 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ leadTimeForChangesList: null, }); - render(); + render(); expect(screen.queryAllByText('Lead Time For Changes').length).toEqual(0); }); + + it('should show leadTimeForChangesList and color is gray when leadTimeForChangesList data is existing and exists source control', () => { + (reportMapper as jest.Mock).mockReturnValue({ + leadTimeForChangesList: [{ id: 0, name: 'name1', valueList: [{ name: 'Pipeline Lead Time', values: [1] }] }], + }); + render(); + + const leadTimeForChangesTable = screen.getByLabelText('Lead Time For Changes'); + + expect(screen.getByText('Lead Time For Changes')).toBeInTheDocument(); + expect(leadTimeForChangesTable).toBeInTheDocument(); + + const rows = within(leadTimeForChangesTable).queryAllByLabelText('tr'); + + expect(rows.length).toBe(2); + + const tds = within(rows[1]).getAllByLabelText('td'); + + expect(tds.length).toBe(2); + + tds.forEach((td) => { + expect(td.getAttribute('style')).toEqual('color: gray;'); + }); + }); }); describe('Pipeline Change Failure Rate', () => { @@ -66,7 +90,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ pipelineChangeFailureRateList: [{ id: 0, name: 'name1', valueList: [{ value: 1 }] }], }); - render(); + render(); const pipelineChangeFailureRateTable = screen.getByTestId('Pipeline Change Failure Rate'); expect(screen.getByText('Pipeline Change Failure Rate')).toBeInTheDocument(); expect(pipelineChangeFailureRateTable).toBeInTheDocument(); @@ -77,7 +101,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ devChangeFailureRateList: null, }); - render(); + render(); expect(screen.queryAllByText('Dev Change Failure Rate').length).toEqual(0); }); }); @@ -87,7 +111,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ pipelineMeanTimeToRecoveryList: [{ id: 0, name: 'name1', valueList: [{ value: 1 }] }], }); - render(); + render(); const pipelineMeanTimeToRecoveryTable = screen.getByTestId('Pipeline Mean Time To Recovery'); expect(screen.getByText('Pipeline Mean Time To Recovery')).toBeInTheDocument(); expect(pipelineMeanTimeToRecoveryTable).toBeInTheDocument(); @@ -98,7 +122,7 @@ describe('DoraDetail', () => { (reportMapper as jest.Mock).mockReturnValue({ devMeanTimeToRecoveryList: null, }); - render(); + render(); expect(screen.queryAllByText('Dev Mean Time To Recovery').length).toEqual(0); }); }); diff --git a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx index 6af03981df..6ff95d256b 100644 --- a/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx +++ b/frontend/__tests__/containers/ReportStep/ReportStep.test.tsx @@ -23,9 +23,11 @@ import { } from '../../fixtures'; import { addADeploymentFrequencySetting, + addOneSourceControlSetting, saveClassificationCharts, saveTargetFields, updateDeploymentFrequencySettings, + updateSourceControlConfigurationSettings, } from '@src/context/Metrics/metricsSlice'; import { DateRangeList, @@ -172,6 +174,11 @@ describe('Report Step', () => { store.dispatch( updateDeploymentFrequencySettings({ updateId: 1, label: 'organization', value: 'mock organization' }), ); + store.dispatch(addOneSourceControlSetting()); + store.dispatch( + updateSourceControlConfigurationSettings({ updateId: 1, label: 'organization', value: 'mock organization' }), + ); + store.dispatch(updateSourceControlConfigurationSettings({ updateId: 1, label: 'repo', value: 'mock repo' })); store.dispatch( saveClassificationCharts([ { key: 'issuetype', name: 'Issue Type', flag: true }, diff --git a/frontend/__tests__/fixtures.ts b/frontend/__tests__/fixtures.ts index c35316c46e..5468e6833a 100644 --- a/frontend/__tests__/fixtures.ts +++ b/frontend/__tests__/fixtures.ts @@ -220,6 +220,14 @@ export const MOCK_GENERATE_REPORT_REQUEST_PARAMS: ReportRequestDTO = { branches: [], }, ], + crews: ['mockCrew'], + codebases: [ + { + organization: 'mockOrganization', + repo: 'mockRepo', + branches: ['mockBranch1'], + }, + ], }, jiraBoardSetting: { token: 'mockToken', @@ -554,6 +562,7 @@ export const MOCK_REPORT_RESPONSE_WITH_AVERAGE_EXCEPTION: ReportResponseDTO = { totalDelayTime: 5289.95, }, ], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: 'other', prLeadTime: 3647.51, @@ -697,6 +706,7 @@ export const MOCK_REPORT_RESPONSE: ReportResponseDTO = { totalDelayTime: 5289.95, }, ], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: 'Average', prLeadTime: 3647.51, @@ -830,6 +840,7 @@ export const MOCK_REPORT_MOCK_PIPELINE_RESPONSE: ReportResponseDTO = { totalDelayTime: 5289.95, }, ], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: 'Average', prLeadTime: 3647.51, diff --git a/frontend/__tests__/hooks/reportMapper/leadTimeForChanges.test.tsx b/frontend/__tests__/hooks/reportMapper/leadTimeForChanges.test.tsx index 4d62f356d7..158ad22ce6 100644 --- a/frontend/__tests__/hooks/reportMapper/leadTimeForChanges.test.tsx +++ b/frontend/__tests__/hooks/reportMapper/leadTimeForChanges.test.tsx @@ -12,6 +12,7 @@ describe('lead time for changes data mapper', () => { totalDelayTime: 18313.579999999998, }, ], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: 'Average', prLeadTime: 22481.55, @@ -74,6 +75,7 @@ describe('lead time for changes data mapper', () => { totalDelayTime: 0, }, ], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: 'Average', prLeadTime: 0, @@ -106,4 +108,92 @@ describe('lead time for changes data mapper', () => { expect(mappedLeadTimeForChanges).toEqual(expectedLeadTimeForChangesValues); }); + + it('should maps response lead time for changes values when send source control data', () => { + const mockLeadTimeForChanges = { + leadTimeForChangesOfPipelines: [ + { + name: 'Heartbeat', + step: ':rocket: Run e2e', + prLeadTime: 22481.55, + pipelineLeadTime: 4167.97, + totalDelayTime: 18313.579999999998, + }, + ], + leadTimeForChangesOfSourceControls: [ + { + organization: 'au-heartbeat', + repo: 'heartbeat', + prLeadTime: 22481.55, + pipelineLeadTime: 4167.97, + totalDelayTime: 18313.579999999998, + }, + ], + avgLeadTimeForChanges: { + name: 'Average', + prLeadTime: 22481.55, + pipelineLeadTime: 4167.97, + totalDelayTime: 18313.579999999998, + }, + }; + const expectedLeadTimeForChangesValues = [ + { + id: 0, + name: 'Heartbeat/:rocket: Run e2e', + valueList: [ + { + name: 'PR Lead Time', + values: ['374.69'], + }, + { + name: 'Pipeline Lead Time', + values: ['69.47'], + }, + { + name: 'Total Lead Time', + values: ['305.23'], + }, + ], + }, + { + id: 1, + name: 'au-heartbeat/heartbeat', + valueList: [ + { + name: 'PR Lead Time', + values: ['374.69'], + }, + { + name: 'Pipeline Lead Time', + values: ['69.47'], + }, + { + name: 'Total Lead Time', + values: ['305.23'], + }, + ], + }, + { + id: 2, + name: 'Average', + valueList: [ + { + name: 'PR Lead Time', + values: ['374.69'], + }, + { + name: 'Pipeline Lead Time', + values: ['69.47'], + }, + { + name: 'Total Lead Time', + values: ['305.23'], + }, + ], + }, + ]; + const mappedLeadTimeForChanges = leadTimeForChangesMapper(mockLeadTimeForChanges); + + expect(mappedLeadTimeForChanges).toEqual(expectedLeadTimeForChangesValues); + }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx index f29375bf34..7750ea13e0 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx @@ -2,6 +2,7 @@ import { useGetSourceControlConfigurationBranchEffect } from '@src/hooks/useGetS import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { MOCK_GITHUB_GET_BRANCHES_RESPONSE } from '@test/fixtures'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import React, { ReactNode } from 'react'; import { Provider } from 'react-redux'; @@ -9,6 +10,10 @@ import { HttpStatusCode } from 'axios'; const mockDispatch = jest.fn(); const store = setupStore(); +jest.mock('@src/hooks', () => ({ + ...jest.requireActual('@src/hooks'), + useAppDispatch: () => jest.fn(), +})); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: () => mockDispatch, @@ -27,31 +32,40 @@ const Wrapper = ({ children }: { children: ReactNode }) => { const clientSpy = jest.fn(); const mockRepo = jest.fn().mockImplementation(() => { clientSpy(); - return { - code: HttpStatusCode.Ok, - data: MOCK_GITHUB_GET_BRANCHES_RESPONSE, - errorTittle: '', - errorMessage: '', - }; -}); - -beforeEach(() => { - sourceControlClient.getBranch = mockRepo; - clientSpy.mockClear(); + return new Promise(() => { + return { + code: HttpStatusCode.Ok, + data: MOCK_GITHUB_GET_BRANCHES_RESPONSE, + errorTittle: '', + errorMessage: '', + }; + }); }); describe('use get source control configuration branch info side effect', () => { + beforeEach(() => { + sourceControlClient.getBranch = mockRepo; + clientSpy.mockClear(); + }); + it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); expect(result.current.isLoading).toBeFalsy(); expect(result.current.isGetBranch).toBeFalsy(); }); - it('should return success data and loading state when client goes happy path', async () => { + it('should return success data and loading state when client return 200', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); const mockOrganization = 'mockOrg'; const mockRepo = 'mockRepo'; - + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + code: 200, + data: { + name: ['test-branch1', 'test-branch2'], + }, + }); + }); await act(async () => { result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); }); @@ -60,6 +74,64 @@ describe('use get source control configuration branch info side effect', () => { expect(result.current.isLoading).toBeFalsy(); expect(result.current.isGetBranch).toBeTruthy(); }); - expect(clientSpy).toBeCalled(); + + expect(sourceControlClient.getBranch).toBeCalled(); + }); + + it('should set error info when client dont return 200', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + code: 400, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.info).toEqual({ + code: 400, + }); + }); + + it('should set error step failed status to PartialFailed4xx when getting branch response is failed and client return 400', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.reject({ + reason: { + code: 400, + }, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); + + it('should set error step failed status to PartialFailedTimeout when getting branch response is failed and client dont return 400', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.reject({ + reason: { + code: 404, + }, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx index 6d5b64e68b..145b2bfec8 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx @@ -2,6 +2,7 @@ import { useGetSourceControlConfigurationCrewEffect } from '@src/hooks/useGetSou import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { act, renderHook, waitFor } from '@testing-library/react'; import { MOCK_GITHUB_GET_CREWS_RESPONSE } from '@test/fixtures'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { DateRange } from '@src/context/config/configSlice'; import { setupStore } from '@test/utils/setupStoreUtil'; import React, { ReactNode } from 'react'; @@ -36,12 +37,12 @@ const mockRepo = jest.fn().mockImplementation(() => { }; }); -beforeEach(() => { - sourceControlClient.getCrew = mockRepo; - clientSpy.mockClear(); -}); - describe('use get source control configuration crew info side effect', () => { + beforeEach(() => { + sourceControlClient.getCrew = mockRepo; + clientSpy.mockClear(); + }); + it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); expect(result.current.isLoading).toBeFalsy(); @@ -74,4 +75,68 @@ describe('use get source control configuration crew info side effect', () => { }); expect(clientSpy).toHaveBeenCalledTimes(2); }); + + it('should set error step failed status to PartialFailed4xx when one of getting repo response is failed and code is 400', async () => { + sourceControlClient.getCrew = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject({ + code: 400, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + const mockBranch = 'mockBranch'; + const dateRanges: DateRange[] = [ + { + startDate: '2024-07-31T00:00:00.000+08:00', + endDate: '2024-08-02T23:59:59.999+08:00', + }, + { + startDate: '2024-07-15T00:00:00.000+08:00', + endDate: '2024-07-28T23:59:59.999+08:00', + }, + ]; + + await act(async () => { + result.current.getSourceControlCrewInfo(mockOrganization, mockRepo, mockBranch, dateRanges); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); + + it('should set error step failed status to PartialFailedTimeout when one of getting repo responses is failed and code is not 400', async () => { + sourceControlClient.getCrew = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject('error'); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + const mockBranch = 'mockBranch'; + const dateRanges: DateRange[] = [ + { + startDate: '2024-07-31T00:00:00.000+08:00', + endDate: '2024-08-02T23:59:59.999+08:00', + }, + { + startDate: '2024-07-15T00:00:00.000+08:00', + endDate: '2024-07-28T23:59:59.999+08:00', + }, + ]; + + await act(async () => { + result.current.getSourceControlCrewInfo(mockOrganization, mockRepo, mockBranch, dateRanges); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); + }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx index c5d35dcb6b..2c63767782 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx @@ -1,6 +1,7 @@ import { useGetSourceControlConfigurationRepoEffect } from '@src/hooks/useGetSourceControlConfigurationRepoEffect'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { MOCK_GITHUB_GET_REPO_RESPONSE } from '@test/fixtures'; import { DateRange } from '@src/context/config/configSlice'; import { setupStore } from '@test/utils/setupStoreUtil'; @@ -36,12 +37,11 @@ const mockRepo = jest.fn().mockImplementation(() => { }; }); -beforeEach(() => { - sourceControlClient.getRepo = mockRepo; - clientSpy.mockClear(); -}); - describe('use get source control configuration repo info side effect', () => { + beforeEach(() => { + sourceControlClient.getRepo = mockRepo; + clientSpy.mockClear(); + }); it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); @@ -69,4 +69,64 @@ describe('use get source control configuration repo info side effect', () => { }); expect(clientSpy).toBeCalled(); }); + + it('should set error step failed status to PartialFailedTimeout when one of getting repo responses is failed and code is not 400', async () => { + sourceControlClient.getRepo = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject('error'); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockDateRanges: DateRange[] = [ + { + startDate: 'startTime', + endDate: 'endTime', + }, + { + startDate: 'startTime', + endDate: 'endTime', + }, + ]; + + await act(async () => { + result.current.getSourceControlRepoInfo(mockOrganization, mockDateRanges, 1); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); + }); + + it('should set error step failed status to PartialFailed4xx when one of getting repo response is failed and code is 400', async () => { + sourceControlClient.getRepo = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject({ + code: 400, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockDateRanges: DateRange[] = [ + { + startDate: 'startTime', + endDate: 'endTime', + }, + { + startDate: 'startTime', + endDate: 'endTime', + }, + ]; + + await act(async () => { + result.current.getSourceControlRepoInfo(mockOrganization, mockDateRanges, 1); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); }); diff --git a/frontend/e2e/fixtures/create-new/board-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/board-20240812-20240818.csv index 6fecc05e7b..3dabaf50dd 100644 --- a/frontend/e2e/fixtures/create-new/board-20240812-20240818.csv +++ b/frontend/e2e/fixtures/create-new/board-20240812-20240818.csv @@ -1,6 +1,7 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" -"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","14.76","0","0","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0","0" -"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.95","","","","","","","None","2.0","","","","0.98","42.00","0","0","0.89","0","0.06","0.09","0.91","0","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0","0" -"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","42.75","0","0","1.05","0","0","0.01","0.90","0","42.75","0.90","0.01","1.05","0","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","5.01","0","0","0","0","0","16.20","0","0","5.01","0",,,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" +"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","14.76","0","0","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0","0","0" +"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.95","","","","","","","None","2.0","","","","0.98","42.00","0","0","0.89","0","0.06","0.09","0.91","0","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0","0","0" +"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","42.75","0","0","1.05","0","0","0.01","0.90","0","42.75","0.90","0.01","1.05","0","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","7.93","1.94","0","0.01","1.11","0","16.20","1.11","0.01","7.93","0","1.94",,,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","15.99","0","0","2.94","0","0","0","0","0","15.99","0","0","2.94","0","0",,,,,, diff --git a/frontend/e2e/fixtures/create-new/board-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/board-20240819-20240825.csv index 56de95d30b..07f17b5e6b 100644 --- a/frontend/e2e/fixtures/create-new/board-20240819-20240825.csv +++ b/frontend/e2e/fixtures/create-new/board-20240819-20240825.csv @@ -1,4 +1,5 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" -"ADM-997","[FE] add a mark for new functions (release 1.3.0)","Story","Done","2024-08-19","1.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.98","","","","","","","None","1.0","","","","1.98","10.54","0","0","1.31","0","0","0.65","0.02","0","10.54","0.02","0.65","1.31","0","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","5.01","0","0","0","0","0","16.20","0","0","5.01","0",,,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" +"ADM-997","[FE] add a mark for new functions (release 1.3.0)","Story","Done","2024-08-19","1.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.98","","","","","","","None","1.0","","","","1.98","10.54","0","0","1.31","0","0","0.65","0.02","0","10.54","0.02","0.65","1.31","0","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","7.93","1.94","0","0.01","1.11","0","16.20","1.11","0.01","7.93","0","1.94",,,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","15.99","0","0","2.94","0","0","0","0","0","15.99","0","0","2.94","0","0",,,,,, diff --git a/frontend/e2e/fixtures/create-new/board-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/board-20240826-20240902.csv index 79fc012f69..80351bfcb4 100644 --- a/frontend/e2e/fixtures/create-new/board-20240826-20240902.csv +++ b/frontend/e2e/fixtures/create-new/board-20240826-20240902.csv @@ -1,5 +1,6 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" -"ADM-1001","[BE] spike source control configuration API","Spike","Done","2024-08-26","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","3.02","","","","","","","None","2.0","","","","1.51","5.86","0","0","2.99","0","0","0.03","0","0","5.86","0","0.03","2.99","0","0","0","0","0","0","0" -"ADM-998","[FE] support lead time for changes for single data source-config page","Story","Done","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","4.96","","","","","","","None","3.0","","","","1.65","11.00","0","0","2.00","0","0","0","2.96","0","11.00","2.96","0","2.00","0","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","5.01","0","0","0","0","0","16.20","0","0","5.01","0",,,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Waiting for testing","Rework: from Testing","Rework: from Done" +"ADM-1001","[BE] spike source control configuration API","Spike","Done","2024-08-26","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","3.02","","","","","","","None","2.0","","","","1.51","5.86","0","0","2.99","0","0","0.03","0","0","5.86","0","0.03","2.99","0","0","0","0","0","0","0","0" +"ADM-998","[FE] support lead time for changes for single data source-config page","Story","Done","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","4.96","","","","","","","None","3.0","","","","1.65","11.00","0","0","2.00","0","0","0","2.96","0","11.00","2.96","0","2.00","0","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","7.93","1.94","0","0.01","1.11","0","16.20","1.11","0.01","7.93","0","1.94",,,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","15.99","0","0","2.94","0","0","0","0","0","15.99","0","0","2.94","0","0",,,,,, diff --git a/frontend/e2e/fixtures/create-new/pipeline-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/pipeline-20240812-20240818.csv index 7d0ec5427f..920fa8a0ce 100644 --- a/frontend/e2e/fixtures/create-new/pipeline-20240812-20240818.csv +++ b/frontend/e2e/fixtures/create-new/pipeline-20240812-20240818.csv @@ -1,11 +1,11 @@ -"Organization","Pipeline Name","Pipeline Step","Valid","Build Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4185","zhou-yinyuan",,"2024-08-16T09:34:03Z","2024-08-16T09:34:35Z","2024-08-16T09:42:39Z",,"2024-08-16T10:05:48Z","2024-08-16T09:42:39Z","2024-08-16T10:06:16.456Z","0","0:32:13","0:8:36","0:23:37","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4183","zhou-yinyuan",,"2024-08-16T09:11:02Z","2024-08-16T09:11:32Z","2024-08-16T09:16:37Z",,"2024-08-16T09:40:33Z","2024-08-16T09:16:37Z","2024-08-16T09:41:01.451Z","0","0:29:59","0:5:35","0:24:24","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4181","zhou-yinyuan","heartbeat-user","2024-08-15T07:40:45Z","2024-08-15T07:41:18Z","2024-08-16T03:27:54Z",,"2024-08-16T03:50:27Z","2024-08-16T03:27:54Z","2024-08-16T03:50:58.880Z","0","20:10:13","19:47:9","0:23:4","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4175","guzhongren",,,,,,,,"2024-08-15T00:54:47.563Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4173","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T09:21:18Z","2024-08-14T09:29:12Z",,"2024-08-14T09:52:23Z","2024-08-14T09:29:12Z","2024-08-14T09:52:49.797Z","0","2:44:55","2:21:18","0:23:37","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4170","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T07:08:40Z","2024-08-14T07:13:45Z",,"2024-08-14T07:37:14Z","2024-08-14T07:13:45Z","2024-08-14T07:37:42.686Z","0","0:29:48","0:5:51","0:23:57","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4168","zhou-yinyuan",,"2024-08-14T02:18:08Z","2024-08-14T02:18:46Z","2024-08-14T02:25:41Z",,"2024-08-14T02:51:00Z","2024-08-14T02:25:41Z","2024-08-14T02:51:29.410Z","0","0:33:21","0:7:33","0:25:48","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4165","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-13T06:09:53Z","2024-08-13T07:00:49Z",,"2024-08-13T07:28:33Z","2024-08-13T07:00:49Z","2024-08-13T07:29:05.011Z","0","28:30:50","28:2:34","0:28:16","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4162","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-12T02:59:01Z","2024-08-13T05:56:53Z",,"2024-08-13T06:24:55Z","2024-08-13T05:56:53Z","2024-08-13T06:25:32.138Z","0","27:27:17","26:58:38","0:28:39","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4153","zhou-yinyuan",,"2024-08-09T03:54:06Z","2024-08-09T07:16:19Z","2024-08-12T01:08:39Z",,"2024-08-12T01:36:41Z","2024-08-12T01:08:39Z","2024-08-12T01:37:13.163Z","48","21:43:7","21:14:33","0:28:34","passed","main","false" +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4185","1579","zhou-yinyuan",,"2024-08-16T09:34:03Z","2024-08-16T09:34:35Z","2024-08-16T09:42:39Z",,"2024-08-16T10:05:48Z","2024-08-16T09:42:39Z","2024-08-16T10:06:16.456Z","0","0:32:13","0:8:36","0:23:37","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4183","1578","zhou-yinyuan",,"2024-08-16T09:11:02Z","2024-08-16T09:11:32Z","2024-08-16T09:16:37Z",,"2024-08-16T09:40:33Z","2024-08-16T09:16:37Z","2024-08-16T09:41:01.451Z","0","0:29:59","0:5:35","0:24:24","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4181","1577","zhou-yinyuan","heartbeat-user","2024-08-15T07:40:45Z","2024-08-15T07:41:18Z","2024-08-16T03:27:54Z",,"2024-08-16T03:50:27Z","2024-08-16T03:27:54Z","2024-08-16T03:50:58.880Z","0","20:10:13","19:47:9","0:23:4","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4175",,"guzhongren",,,,,,,,"2024-08-15T00:54:47.563Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4173","1576","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T09:21:18Z","2024-08-14T09:29:12Z",,"2024-08-14T09:52:23Z","2024-08-14T09:29:12Z","2024-08-14T09:52:49.797Z","0","2:44:55","2:21:18","0:23:37","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4170","1575","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T07:08:40Z","2024-08-14T07:13:45Z",,"2024-08-14T07:37:14Z","2024-08-14T07:13:45Z","2024-08-14T07:37:42.686Z","0","0:29:48","0:5:51","0:23:57","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4168","1574","zhou-yinyuan",,"2024-08-14T02:18:08Z","2024-08-14T02:18:46Z","2024-08-14T02:25:41Z",,"2024-08-14T02:51:00Z","2024-08-14T02:25:41Z","2024-08-14T02:51:29.410Z","0","0:33:21","0:7:33","0:25:48","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4165","1573","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-13T06:09:53Z","2024-08-13T07:00:49Z",,"2024-08-13T07:28:33Z","2024-08-13T07:00:49Z","2024-08-13T07:29:05.011Z","0","28:30:50","28:2:34","0:28:16","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4162","1572","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-12T02:59:01Z","2024-08-13T05:56:53Z",,"2024-08-13T06:24:55Z","2024-08-13T05:56:53Z","2024-08-13T06:25:32.138Z","0","27:27:17","26:58:38","0:28:39","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4153","1571","zhou-yinyuan",,"2024-08-09T03:54:06Z","2024-08-09T07:16:19Z","2024-08-12T01:08:39Z",,"2024-08-12T01:36:41Z","2024-08-12T01:08:39Z","2024-08-12T01:37:13.163Z","48","21:43:7","21:14:33","0:28:34","passed","main","false" diff --git a/frontend/e2e/fixtures/create-new/pipeline-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/pipeline-20240819-20240825.csv index fa8e8a8fd9..927d4a7228 100644 --- a/frontend/e2e/fixtures/create-new/pipeline-20240819-20240825.csv +++ b/frontend/e2e/fixtures/create-new/pipeline-20240819-20240825.csv @@ -1,7 +1,7 @@ -"Organization","Pipeline Name","Pipeline Step","Valid","Build Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" -"Heartbeat-backup","Heartbeat",":rocket: Deploy e2e","false","4224","zhou-yinyuan","heartbeat-user",,,,,,,"2024-08-25T00:50:29.642Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4221","Unknown","heartbeat-user",,,,,,,"2024-08-25T00:49:05.117Z","0",,,,"failed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4207","zhou-yinyuan",,,,,,,,"2024-08-21T09:48:48.575Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4201","zhou-yinyuan",,"2024-08-21T03:04:10Z","2024-08-21T03:04:36Z","2024-08-21T03:16:16Z",,"2024-08-21T03:39:02Z","2024-08-21T03:16:16Z","2024-08-21T03:39:31.606Z","0","0:35:21","0:12:6","0:23:15","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4197","zhou-yinyuan",,"2024-08-20T01:58:55Z","2024-08-20T01:59:41Z","2024-08-20T09:34:31Z",,"2024-08-20T10:02:47Z","2024-08-20T09:34:31Z","2024-08-20T10:03:29.671Z","0","8:4:34","7:35:36","0:28:58","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4188","renovate[bot]",,,,,,,,"2024-08-20T00:53:29.705Z","0",,,,"passed","main","" +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy e2e","false","4224",,"zhou-yinyuan","heartbeat-user",,,,,,,"2024-08-25T00:50:29.642Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4221",,"Unknown","heartbeat-user",,,,,,,"2024-08-25T00:49:05.117Z","0",,,,"failed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4207",,"zhou-yinyuan",,,,,,,,"2024-08-21T09:48:48.575Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4201","1583","zhou-yinyuan",,"2024-08-21T03:04:10Z","2024-08-21T03:04:36Z","2024-08-21T03:16:16Z",,"2024-08-21T03:39:02Z","2024-08-21T03:16:16Z","2024-08-21T03:39:31.606Z","0","0:35:21","0:12:6","0:23:15","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4197","1582","zhou-yinyuan",,"2024-08-20T01:58:55Z","2024-08-20T01:59:41Z","2024-08-20T09:34:31Z",,"2024-08-20T10:02:47Z","2024-08-20T09:34:31Z","2024-08-20T10:03:29.671Z","0","8:4:34","7:35:36","0:28:58","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4188",,"renovate[bot]",,,,,,,,"2024-08-20T00:53:29.705Z","0",,,,"passed","main","" diff --git a/frontend/e2e/fixtures/create-new/pipeline-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/pipeline-20240826-20240902.csv index 9a903507c0..ad175ab457 100644 --- a/frontend/e2e/fixtures/create-new/pipeline-20240826-20240902.csv +++ b/frontend/e2e/fixtures/create-new/pipeline-20240826-20240902.csv @@ -1,6 +1,6 @@ -"Organization","Pipeline Name","Pipeline Step","Valid","Build Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4259","renovate[bot]",,,,,,,,"2024-08-30T10:17:49.663Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4255","renovate[bot]",,,,,,,,"2024-08-30T09:10:56.484Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4249","guzhongren",,,,,,,,"2024-08-29T11:34:06.071Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","4240","Zhongren GU","heartbeat-user",,,,,,,"2024-08-27T15:40:47.556Z","0",,,,"passed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","4230","zhou-yinyuan",,"2024-08-26T01:56:02Z","2024-08-26T01:56:46Z","2024-08-26T02:02:59Z",,"2024-08-26T02:26:28Z","2024-08-26T02:02:59Z","2024-08-26T02:26:56.722Z","0","0:30:54","0:6:57","0:23:57","passed","main","false" +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4259",,"renovate[bot]",,,,,,,,"2024-08-30T10:17:49.663Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4255",,"renovate[bot]",,,,,,,,"2024-08-30T09:10:56.484Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4249",,"guzhongren",,,,,,,,"2024-08-29T11:34:06.071Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","4240",,"Zhongren GU","heartbeat-user",,,,,,,"2024-08-27T15:40:47.556Z","0",,,,"passed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","4230","1589","zhou-yinyuan",,"2024-08-26T01:56:02Z","2024-08-26T01:56:46Z","2024-08-26T02:02:59Z",,"2024-08-26T02:26:28Z","2024-08-26T02:02:59Z","2024-08-26T02:26:56.722Z","0","0:30:54","0:6:57","0:23:57","passed","main","false" diff --git a/frontend/e2e/fixtures/create-new/report-result.ts b/frontend/e2e/fixtures/create-new/report-result.ts index 69275fda96..aa1a128183 100644 --- a/frontend/e2e/fixtures/create-new/report-result.ts +++ b/frontend/e2e/fixtures/create-new/report-result.ts @@ -12,10 +12,10 @@ export interface IDoraMetricsResultItem { prLeadTime: string; pipelineLeadTime: string; totalLeadTime: string; - deploymentTimes: string; - deploymentFrequency: string; - failureRate: string; - pipelineMeanTimeToRecovery: string; + deploymentTimes?: string; + deploymentFrequency?: string; + failureRate?: string; + pipelineMeanTimeToRecovery?: string; } export interface IBoardMetricsDetailItem { @@ -803,6 +803,24 @@ export const DORA_METRICS_RESULT_MULTIPLE_RANGES: IDoraMetricsResultItem[] = [ }, ]; +export const DORA_METRICS_RESULT_FOR_SOURCE_CONTROL: IDoraMetricsResultItem[] = [ + { + prLeadTime: '16.73', + pipelineLeadTime: '0.00', + totalLeadTime: '16.73', + }, + { + prLeadTime: '17.69', + pipelineLeadTime: '0.00', + totalLeadTime: '17.69', + }, + { + prLeadTime: '9.70', + pipelineLeadTime: '0.00', + totalLeadTime: '9.70', + }, +]; + export const BOARD_METRICS_WITH_HOLIDAY_RESULT = { Velocity: '1', Throughput: '1', diff --git a/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240812-20240818.csv index 55e66d726e..59f2afb1f1 100644 --- a/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240812-20240818.csv +++ b/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240812-20240818.csv @@ -1,6 +1,7 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Waiting for deployment","Rework: from Done" -"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","16.72","","","","","","","None","3.0","","","","5.57","0","0","14.76","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0","0" -"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","43.95","","","","","","","None","2.0","","","","21.98","0","0","42.00","0.89","0","0.06","0","0.91","0.09","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0","0" -"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","44.71","","","","","","","None","3.0","","","","14.90","0","0","42.75","1.05","0","0","0","0.90","0.01","42.75","0.90","0.01","1.05","0","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","16.20","5.04","0","0","0","0","0","16.20","0","0","5.04","0",,,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Waiting for deployment","Rework: from Done" +"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","16.72","","","","","","","None","3.0","","","","5.57","0","0","14.76","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0","0","0" +"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","43.95","","","","","","","None","2.0","","","","21.98","0","0","42.00","0.89","0","0.06","0","0.91","0.09","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0","0","0" +"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","44.71","","","","","","","None","3.0","","","","14.90","0","0","42.75","1.05","0","0","0","0.90","0.01","42.75","0.90","0.01","1.05","0","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","16.20","7.93","1.94","0","0","1.11","0.01","16.20","1.11","0.01","7.93","0","1.94",,,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","15.99","2.94","0","0","0","0","0","15.99","0","0","2.94","0","0",,,,,, diff --git a/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240826-20240902.csv index 128acd13e0..0a2350623c 100644 --- a/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240826-20240902.csv +++ b/frontend/e2e/fixtures/create-new/with-design-and-wait-for-deployment-board-20240826-20240902.csv @@ -1,5 +1,6 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Waiting for deployment","Rework: from Done" -"ADM-1001","[BE] spike source control configuration API","Spike","Done","2024-08-26","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","8.88","","","","","","","None","2.0","","","","4.44","0","0","5.86","2.99","0","0","0","0","0.03","5.86","0","0.03","2.99","0","0","0","0","0","0","0" -"ADM-998","[FE] support lead time for changes for single data source-config page","Story","Done","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","15.96","","","","","","","None","3.0","","","","5.32","0","0","11.00","2.00","0","0","0","2.96","0","11.00","2.96","0","2.00","0","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","16.20","5.03","0","0","0","0","0","16.20","0","0","5.03","0",,,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Waiting for deployment","Rework: from Done" +"ADM-1001","[BE] spike source control configuration API","Spike","Done","2024-08-26","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","8.88","","","","","","","None","2.0","","","","4.44","0","0","5.86","2.99","0","0","0","0","0.03","5.86","0","0.03","2.99","0","0","0","0","0","0","0","0" +"ADM-998","[FE] support lead time for changes for single data source-config page","Story","Done","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 43","","15.96","","","","","","","None","3.0","","","","5.32","0","0","11.00","2.00","0","0","0","2.96","0","11.00","2.96","0","2.00","0","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","16.20","7.93","1.94","0","0","1.11","0.01","16.20","1.11","0.01","7.93","0","1.94",,,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","0","0","15.99","2.94","0","0","0","0","0","15.99","0","0","2.94","0","0",,,,,, diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240812-20240818.csv new file mode 100644 index 0000000000..2fd0ebd80a --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240812-20240818.csv @@ -0,0 +1,4 @@ +"Group","Metrics","Value" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","9.70" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","9.70" diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240819-20240825.csv new file mode 100644 index 0000000000..6cbc6254d7 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240819-20240825.csv @@ -0,0 +1,4 @@ +"Group","Metrics","Value" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","17.69" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","17.69" diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240826-20240902.csv new file mode 100644 index 0000000000..7a090a1528 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-metric-20240826-20240902.csv @@ -0,0 +1,4 @@ +"Group","Metrics","Value" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","16.73" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","16.73" diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240812-20240818.csv new file mode 100644 index 0000000000..e9192ec299 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240812-20240818.csv @@ -0,0 +1,9 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1579","zhou-yinyuan",,"2024-08-16T09:34:03Z","2024-08-16T09:34:35Z","2024-08-16T09:42:39Z",,,"2024-08-16T09:42:39Z",,"0","0:8:36","0:8:36","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1578","zhou-yinyuan",,"2024-08-16T09:11:02Z","2024-08-16T09:11:32Z","2024-08-16T09:16:37Z",,,"2024-08-16T09:16:37Z",,"0","0:5:35","0:5:35","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1577","zhou-yinyuan",,"2024-08-15T07:40:45Z","2024-08-15T07:41:18Z","2024-08-16T03:27:54Z",,,"2024-08-16T03:27:54Z",,"0","19:47:9","19:47:9","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1576","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T09:21:18Z","2024-08-14T09:29:12Z",,,"2024-08-14T09:29:12Z",,"0","2:21:18","2:21:18","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1575","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T07:08:40Z","2024-08-14T07:13:45Z",,,"2024-08-14T07:13:45Z",,"0","0:5:51","0:5:51","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1574","zhou-yinyuan",,"2024-08-14T02:18:08Z","2024-08-14T02:18:46Z","2024-08-14T02:25:41Z",,,"2024-08-14T02:25:41Z",,"0","0:7:33","0:7:33","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1573","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-13T06:09:53Z","2024-08-13T07:00:49Z",,,"2024-08-13T07:00:49Z",,"0","28:2:34","28:2:34","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1572","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-12T02:59:01Z","2024-08-13T05:56:53Z",,,"2024-08-13T05:56:53Z",,"0","26:58:38","26:58:38","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240819-20240825.csv new file mode 100644 index 0000000000..8e65a39cf9 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240819-20240825.csv @@ -0,0 +1,6 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1586","zhou-yinyuan",,"2024-08-22T08:29:03Z","2024-08-22T08:29:49Z","2024-08-24T23:49:29Z",,,"2024-08-24T23:49:29Z",,"0","63:20:26","63:20:26","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1585","zhou-yinyuan",,"2024-08-21T08:41:50Z","2024-08-22T00:43:37Z","2024-08-22T01:12:51Z",,,"2024-08-22T01:12:51Z",,"0","16:31:1","16:31:1","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1584","zhou-yinyuan",,"2024-08-21T08:41:50Z","2024-08-21T08:45:59Z","2024-08-21T09:29:19Z",,,"2024-08-21T09:29:19Z",,"0","0:47:29","0:47:29","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1583","zhou-yinyuan",,"2024-08-21T03:04:10Z","2024-08-21T03:04:36Z","2024-08-21T03:16:16Z",,,"2024-08-21T03:16:16Z",,"0","0:12:6","0:12:6","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1582","zhou-yinyuan",,"2024-08-20T01:58:55Z","2024-08-20T01:59:41Z","2024-08-20T09:34:31Z",,,"2024-08-20T09:34:31Z",,"0","7:35:36","7:35:36","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240826-20240902.csv new file mode 100644 index 0000000000..1f759838a7 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-only-source-control-lead-time-pipeline-20240826-20240902.csv @@ -0,0 +1,3 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1592","guzhongren",,"2024-08-28T01:53:24Z","2024-08-29T04:21:49Z","2024-08-29T11:14:25Z",,,"2024-08-29T11:14:25Z",,"0","33:21:1","33:21:1","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1589","zhou-yinyuan",,"2024-08-26T01:56:02Z","2024-08-26T01:56:46Z","2024-08-26T02:02:59Z",,,"2024-08-26T02:02:59Z",,"0","0:6:57","0:6:57","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240812-20240818.csv new file mode 100644 index 0000000000..631c62c904 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240812-20240818.csv @@ -0,0 +1,63 @@ +"Group","Metrics","Value" +"Velocity","Velocity(Story Point)","8.0" +"Velocity","Throughput(Cards Count)","3" +"Cycle time","Average cycle time(days/storyPoint)","0.73" +"Cycle time","Average cycle time(days/card)","1.96" +"Cycle time","Total development time / Total cycle time(%)","54.17" +"Cycle time","Total review time / Total cycle time(%)","1.02" +"Cycle time","Total waiting for testing time / Total cycle time(%)","1.70" +"Cycle time","Total testing time / Total cycle time(%)","43.10" +"Cycle time","Average development time(days/storyPoint)","0.40" +"Cycle time","Average development time(days/card)","1.06" +"Cycle time","Average review time(days/storyPoint)","0.01" +"Cycle time","Average review time(days/card)","0.02" +"Cycle time","Average waiting for testing time(days/storyPoint)","0.01" +"Cycle time","Average waiting for testing time(days/card)","0.03" +"Cycle time","Average testing time(days/storyPoint)","0.32" +"Cycle time","Average testing time(days/card)","0.84" +"Classifications","Issue Type / Story(Value/Cards count%)","100.00" +"Classifications","Issue Type / Story(Value/Story point%)","100.00" +"Classifications","Parent / ADM-322(Value/Cards count%)","66.67" +"Classifications","Parent / ADM-322(Value/Story point%)","62.50" +"Classifications","Parent / ADM-319(Value/Cards count%)","33.33" +"Classifications","Parent / ADM-319(Value/Story point%)","37.50" +"Classifications","Story testing-2 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-2 / None(Value/Story point%)","100.00" +"Classifications","Story testing-1 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-1 / None(Value/Story point%)","100.00" +"Classifications","Design / None(Value/Cards count%)","100.00" +"Classifications","Design / None(Value/Story point%)","100.00" +"Classifications","Vulnerability / None(Value/Cards count%)","100.00" +"Classifications","Vulnerability / None(Value/Story point%)","100.00" +"Classifications","Sprint / Sprint42(Value/Cards count%)","100.00" +"Classifications","Sprint / Sprint42(Value/Story point%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Cards count%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Story point%)","100.00" +"Classifications","Flagged / None(Value/Cards count%)","100.00" +"Classifications","Flagged / None(Value/Story point%)","100.00" +"Classifications","Fix versions / None(Value/Cards count%)","100.00" +"Classifications","Fix versions / None(Value/Story point%)","100.00" +"Classifications","Priority / Medium(Value/Cards count%)","100.00" +"Classifications","Priority / Medium(Value/Story point%)","100.00" +"Classifications","Partner / None(Value/Cards count%)","100.00" +"Classifications","Partner / None(Value/Story point%)","100.00" +"Classifications","Labels / None(Value/Cards count%)","100.00" +"Classifications","Labels / None(Value/Story point%)","100.00" +"Classifications","Time tracking / None(Value/Cards count%)","100.00" +"Classifications","Time tracking / None(Value/Story point%)","100.00" +"Classifications","Story point estimate / 2.0(Value/Cards count%)","33.33" +"Classifications","Story point estimate / 2.0(Value/Story point%)","25.00" +"Classifications","Story point estimate / 3.0(Value/Cards count%)","66.67" +"Classifications","Story point estimate / 3.0(Value/Story point%)","75.00" +"Classifications","QA / None(Value/Cards count%)","100.00" +"Classifications","QA / None(Value/Story point%)","100.00" +"Classifications","Feature/Operation / None(Value/Cards count%)","100.00" +"Classifications","Feature/Operation / None(Value/Story point%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Cards count%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Story point%)","100.00" +"Rework","Total rework times","0" +"Rework","Total rework cards","0" +"Rework","Rework cards ratio(Total rework cards/Throughput%)","0" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","9.70" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","9.70" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240819-20240825.csv new file mode 100644 index 0000000000..f35b870d0e --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240819-20240825.csv @@ -0,0 +1,59 @@ +"Group","Metrics","Value" +"Velocity","Velocity(Story Point)","1.0" +"Velocity","Throughput(Cards Count)","1" +"Cycle time","Average cycle time(days/storyPoint)","1.98" +"Cycle time","Average cycle time(days/card)","1.98" +"Cycle time","Total development time / Total cycle time(%)","66.16" +"Cycle time","Total review time / Total cycle time(%)","0" +"Cycle time","Total waiting for testing time / Total cycle time(%)","32.83" +"Cycle time","Total testing time / Total cycle time(%)","1.01" +"Cycle time","Average development time(days/storyPoint)","1.31" +"Cycle time","Average development time(days/card)","1.31" +"Cycle time","Average review time(days/storyPoint)","0" +"Cycle time","Average review time(days/card)","0" +"Cycle time","Average waiting for testing time(days/storyPoint)","0.65" +"Cycle time","Average waiting for testing time(days/card)","0.65" +"Cycle time","Average testing time(days/storyPoint)","0.02" +"Cycle time","Average testing time(days/card)","0.02" +"Classifications","Issue Type / Story(Value/Cards count%)","100.00" +"Classifications","Issue Type / Story(Value/Story point%)","100.00" +"Classifications","Parent / ADM-322(Value/Cards count%)","100.00" +"Classifications","Parent / ADM-322(Value/Story point%)","100.00" +"Classifications","Story testing-2 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-2 / None(Value/Story point%)","100.00" +"Classifications","Story testing-1 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-1 / None(Value/Story point%)","100.00" +"Classifications","Design / None(Value/Cards count%)","100.00" +"Classifications","Design / None(Value/Story point%)","100.00" +"Classifications","Vulnerability / None(Value/Cards count%)","100.00" +"Classifications","Vulnerability / None(Value/Story point%)","100.00" +"Classifications","Sprint / Sprint42(Value/Cards count%)","100.00" +"Classifications","Sprint / Sprint42(Value/Story point%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Cards count%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Story point%)","100.00" +"Classifications","Flagged / None(Value/Cards count%)","100.00" +"Classifications","Flagged / None(Value/Story point%)","100.00" +"Classifications","Fix versions / None(Value/Cards count%)","100.00" +"Classifications","Fix versions / None(Value/Story point%)","100.00" +"Classifications","Priority / Medium(Value/Cards count%)","100.00" +"Classifications","Priority / Medium(Value/Story point%)","100.00" +"Classifications","Partner / None(Value/Cards count%)","100.00" +"Classifications","Partner / None(Value/Story point%)","100.00" +"Classifications","Labels / None(Value/Cards count%)","100.00" +"Classifications","Labels / None(Value/Story point%)","100.00" +"Classifications","Time tracking / None(Value/Cards count%)","100.00" +"Classifications","Time tracking / None(Value/Story point%)","100.00" +"Classifications","Story point estimate / 1.0(Value/Cards count%)","100.00" +"Classifications","Story point estimate / 1.0(Value/Story point%)","100.00" +"Classifications","QA / None(Value/Cards count%)","100.00" +"Classifications","QA / None(Value/Story point%)","100.00" +"Classifications","Feature/Operation / None(Value/Cards count%)","100.00" +"Classifications","Feature/Operation / None(Value/Story point%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Cards count%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Story point%)","100.00" +"Rework","Total rework times","0" +"Rework","Total rework cards","0" +"Rework","Rework cards ratio(Total rework cards/Throughput%)","0" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","17.69" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","17.69" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240826-20240902.csv new file mode 100644 index 0000000000..6cbd72ba92 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-metric-20240826-20240902.csv @@ -0,0 +1,63 @@ +"Group","Metrics","Value" +"Velocity","Velocity(Story Point)","5.0" +"Velocity","Throughput(Cards Count)","2" +"Cycle time","Average cycle time(days/storyPoint)","1.6" +"Cycle time","Average cycle time(days/card)","3.99" +"Cycle time","Total development time / Total cycle time(%)","62.53" +"Cycle time","Total review time / Total cycle time(%)","0" +"Cycle time","Total waiting for testing time / Total cycle time(%)","0.38" +"Cycle time","Total testing time / Total cycle time(%)","37.09" +"Cycle time","Average development time(days/storyPoint)","1.00" +"Cycle time","Average development time(days/card)","2.50" +"Cycle time","Average review time(days/storyPoint)","0" +"Cycle time","Average review time(days/card)","0" +"Cycle time","Average waiting for testing time(days/storyPoint)","0.01" +"Cycle time","Average waiting for testing time(days/card)","0.02" +"Cycle time","Average testing time(days/storyPoint)","0.59" +"Cycle time","Average testing time(days/card)","1.48" +"Classifications","Issue Type / Spike(Value/Cards count%)","50.00" +"Classifications","Issue Type / Spike(Value/Story point%)","40.00" +"Classifications","Issue Type / Story(Value/Cards count%)","50.00" +"Classifications","Issue Type / Story(Value/Story point%)","60.00" +"Classifications","Parent / ADM-1002(Value/Cards count%)","100.00" +"Classifications","Parent / ADM-1002(Value/Story point%)","100.00" +"Classifications","Story testing-2 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-2 / None(Value/Story point%)","100.00" +"Classifications","Story testing-1 / None(Value/Cards count%)","100.00" +"Classifications","Story testing-1 / None(Value/Story point%)","100.00" +"Classifications","Design / None(Value/Cards count%)","100.00" +"Classifications","Design / None(Value/Story point%)","100.00" +"Classifications","Vulnerability / None(Value/Cards count%)","100.00" +"Classifications","Vulnerability / None(Value/Story point%)","100.00" +"Classifications","Sprint / Sprint 43(Value/Cards count%)","100.00" +"Classifications","Sprint / Sprint 43(Value/Story point%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Cards count%)","100.00" +"Classifications","Project / Auto Dora Metrics(Value/Story point%)","100.00" +"Classifications","Flagged / None(Value/Cards count%)","100.00" +"Classifications","Flagged / None(Value/Story point%)","100.00" +"Classifications","Fix versions / None(Value/Cards count%)","100.00" +"Classifications","Fix versions / None(Value/Story point%)","100.00" +"Classifications","Priority / High(Value/Cards count%)","100.00" +"Classifications","Priority / High(Value/Story point%)","100.00" +"Classifications","Partner / None(Value/Cards count%)","100.00" +"Classifications","Partner / None(Value/Story point%)","100.00" +"Classifications","Labels / None(Value/Cards count%)","100.00" +"Classifications","Labels / None(Value/Story point%)","100.00" +"Classifications","Time tracking / None(Value/Cards count%)","100.00" +"Classifications","Time tracking / None(Value/Story point%)","100.00" +"Classifications","Story point estimate / 2.0(Value/Cards count%)","50.00" +"Classifications","Story point estimate / 2.0(Value/Story point%)","40.00" +"Classifications","Story point estimate / 3.0(Value/Cards count%)","50.00" +"Classifications","Story point estimate / 3.0(Value/Story point%)","60.00" +"Classifications","QA / None(Value/Cards count%)","100.00" +"Classifications","QA / None(Value/Story point%)","100.00" +"Classifications","Feature/Operation / None(Value/Cards count%)","100.00" +"Classifications","Feature/Operation / None(Value/Story point%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Cards count%)","100.00" +"Classifications","Assignee / YinYuan Zhou(Value/Story point%)","100.00" +"Rework","Total rework times","0" +"Rework","Total rework cards","0" +"Rework","Rework cards ratio(Total rework cards/Throughput%)","0" +"Lead time for changes","au-heartbeat / Heartbeat / PR Lead Time","16.73" +"Lead time for changes","au-heartbeat / Heartbeat / Pipeline Lead Time","0" +"Lead time for changes","au-heartbeat / Heartbeat / Total Lead Time","16.73" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240812-20240818.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240812-20240818.csv new file mode 100644 index 0000000000..e9192ec299 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240812-20240818.csv @@ -0,0 +1,9 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1579","zhou-yinyuan",,"2024-08-16T09:34:03Z","2024-08-16T09:34:35Z","2024-08-16T09:42:39Z",,,"2024-08-16T09:42:39Z",,"0","0:8:36","0:8:36","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1578","zhou-yinyuan",,"2024-08-16T09:11:02Z","2024-08-16T09:11:32Z","2024-08-16T09:16:37Z",,,"2024-08-16T09:16:37Z",,"0","0:5:35","0:5:35","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1577","zhou-yinyuan",,"2024-08-15T07:40:45Z","2024-08-15T07:41:18Z","2024-08-16T03:27:54Z",,,"2024-08-16T03:27:54Z",,"0","19:47:9","19:47:9","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1576","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T09:21:18Z","2024-08-14T09:29:12Z",,,"2024-08-14T09:29:12Z",,"0","2:21:18","2:21:18","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1575","zhou-yinyuan",,"2024-08-14T07:07:54Z","2024-08-14T07:08:40Z","2024-08-14T07:13:45Z",,,"2024-08-14T07:13:45Z",,"0","0:5:51","0:5:51","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1574","zhou-yinyuan",,"2024-08-14T02:18:08Z","2024-08-14T02:18:46Z","2024-08-14T02:25:41Z",,,"2024-08-14T02:25:41Z",,"0","0:7:33","0:7:33","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1573","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-13T06:09:53Z","2024-08-13T07:00:49Z",,,"2024-08-13T07:00:49Z",,"0","28:2:34","28:2:34","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1572","zhou-yinyuan",,"2024-08-12T02:58:15Z","2024-08-12T02:59:01Z","2024-08-13T05:56:53Z",,,"2024-08-13T05:56:53Z",,"0","26:58:38","26:58:38","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240819-20240825.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240819-20240825.csv new file mode 100644 index 0000000000..8e65a39cf9 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240819-20240825.csv @@ -0,0 +1,6 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1586","zhou-yinyuan",,"2024-08-22T08:29:03Z","2024-08-22T08:29:49Z","2024-08-24T23:49:29Z",,,"2024-08-24T23:49:29Z",,"0","63:20:26","63:20:26","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1585","zhou-yinyuan",,"2024-08-21T08:41:50Z","2024-08-22T00:43:37Z","2024-08-22T01:12:51Z",,,"2024-08-22T01:12:51Z",,"0","16:31:1","16:31:1","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1584","zhou-yinyuan",,"2024-08-21T08:41:50Z","2024-08-21T08:45:59Z","2024-08-21T09:29:19Z",,,"2024-08-21T09:29:19Z",,"0","0:47:29","0:47:29","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1583","zhou-yinyuan",,"2024-08-21T03:04:10Z","2024-08-21T03:04:36Z","2024-08-21T03:16:16Z",,,"2024-08-21T03:16:16Z",,"0","0:12:6","0:12:6","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1582","zhou-yinyuan",,"2024-08-20T01:58:55Z","2024-08-20T01:59:41Z","2024-08-20T09:34:31Z",,,"2024-08-20T09:34:31Z",,"0","7:35:36","7:35:36","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240826-20240902.csv b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240826-20240902.csv new file mode 100644 index 0000000000..1f759838a7 --- /dev/null +++ b/frontend/e2e/fixtures/create-new/with-source-control-lead-time-pipeline-20240826-20240902.csv @@ -0,0 +1,3 @@ +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"au-heartbeat",,"Heartbeat",,,,"1592","guzhongren",,"2024-08-28T01:53:24Z","2024-08-29T04:21:49Z","2024-08-29T11:14:25Z",,,"2024-08-29T11:14:25Z",,"0","33:21:1","33:21:1","0:0:0",,"main","false" +"au-heartbeat",,"Heartbeat",,,,"1589","zhou-yinyuan",,"2024-08-26T01:56:02Z","2024-08-26T01:56:46Z","2024-08-26T02:02:59Z",,,"2024-08-26T02:02:59Z",,"0","0:6:57","0:6:57","0:0:0",,"main","false" diff --git a/frontend/e2e/fixtures/import-file/board-data-with-analysis-board-status.csv b/frontend/e2e/fixtures/import-file/board-data-with-analysis-board-status.csv index b3f02ee266..11b3593f46 100644 --- a/frontend/e2e/fixtures/import-file/board-data-with-analysis-board-status.csv +++ b/frontend/e2e/fixtures/import-file/board-data-with-analysis-board-status.csv @@ -1,6 +1,7 @@ -"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Done" -"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","14.76","0","0","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0" -"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.95","","","","","","","None","2.0","","","","0.98","42.00","0.09","0","0.89","0","0.06","0","0.91","0","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0" -"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","42.75","0.01","0","1.05","0","0","0","0.90","0","42.75","0.90","0.01","1.05","0","0","0","0","0","0" -,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Doing","2024-08-26","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0","0","5.08","0","0","0","0","0","16.20","0","0","5.08","0",,,,, +"Issue key","Summary","Issue Type","Status","Status Date","Story Points","assignee","Reporter","Project Key","Project Name","Priority","Parent Summary","Sprint","Labels","Cycle Time","Story testing-1","Design","Vulnerability","Flagged","Fix versions","Partner","Time tracking","Story point estimate","QA","Feature/Operation","Story testing-2","Cycle Time / Story Points","Todo Days","Analysis Days","Design Days","In Dev Days","Block Days","Review Days","Waiting For Testing Days","Testing Days","Waiting For Deployment Days","OriginCycleTime: TODO","OriginCycleTime: TESTING","OriginCycleTime: WAIT FOR TEST","OriginCycleTime: DOING","OriginCycleTime: REVIEW","OriginCycleTime: BLOCKED","Rework: total - In dev","Rework: from Block","Rework: from Review","Rework: from Testing","Rework: from Done" +"ADM-992","[FE] adds ‘story points’ dimension for 'classification' in report page","Story","Done","2024-08-14","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","Precise on Metrics","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","14.76","0","0","1.24","0","0","0","0.72","0","14.76","0.72","0","1.24","0","0","0","0","0","0","0" +"ADM-966","[FE&BE] export data when user mapped 'Design' & ‘Waiting for deployment' in board mapping","Story","Done","2024-08-13","2.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.95","","","","","","","None","2.0","","","","0.98","42.00","0.09","0","0.89","0","0.06","0","0.91","0","42.00","0.91","0.09","0.89","0.06","0","0","0","0","0","0" +"ADM-965","[FE&BE] add a heartbeat state 'Design' &'Waiting for deployment' in board mapping","Story","Done","2024-08-12","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","Medium","easy to use","Sprint42","","1.96","","","","","","","None","3.0","","","","0.65","42.75","0.01","0","1.05","0","0","0","0.90","0","42.75","0.90","0.01","1.05","0","0","0","0","0","0","0" +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +"ADM-999","[FE&BE] support lead time for changes for single data source- case1(metrics)","Story","Blocked","2024-09-06","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","16.20","0.01","0","7.93","1.95","0","0","1.11","0","16.20","1.11","0.01","7.93","0","1.95",,,,, +"ADM-1003","[FE&BE]support lead time for changes for single data source- case1(report)","Story","Doing","2024-09-05","3.0","YinYuan Zhou","Yufan Wang","ADM","Auto Dora Metrics","High","lead time for changes","Sprint 44","","0","","","","","","","None","3.0","","","","0","15.99","0","0","2.95","0","0","0","0","0","15.99","0","0","2.95","0","0",,,,, diff --git a/frontend/e2e/fixtures/import-file/chart-step-data.ts b/frontend/e2e/fixtures/import-file/chart-step-data.ts index e6256eafae..11db68512b 100644 --- a/frontend/e2e/fixtures/import-file/chart-step-data.ts +++ b/frontend/e2e/fixtures/import-file/chart-step-data.ts @@ -3,7 +3,7 @@ export const chartStepData = { addNewBranch: ['ADM-998'], errorDateRange: [ { - startDate: '2024-09-07T00:00:00.000+08:00', + startDate: '2099-09-07T00:00:00.000+08:00', endDate: '2024-04-08T23:59:59.999+08:00', }, ], diff --git a/frontend/e2e/fixtures/import-file/pipeline-with-holiday-data.csv b/frontend/e2e/fixtures/import-file/pipeline-with-holiday-data.csv index b886ad2fe2..add9548ee1 100644 --- a/frontend/e2e/fixtures/import-file/pipeline-with-holiday-data.csv +++ b/frontend/e2e/fixtures/import-file/pipeline-with-holiday-data.csv @@ -1,7 +1,7 @@ -"Organization","Pipeline Name","Pipeline Step","Valid","Build Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","3637","Mandy-Tang",,"2024-06-13T09:46:18Z","2024-06-14T01:57:16Z","2024-06-14T03:33:48Z",,"2024-06-14T06:07:02Z","2024-06-14T03:33:48Z","2024-06-14T06:07:35.879Z","0","20:21:17","17:47:30","2:33:47","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","3635","zhou-yinyuan",,"2024-06-13T08:37:51Z","2024-06-14T02:38:55Z","2024-06-14T02:57:41Z",,"2024-06-14T03:25:06Z","2024-06-14T02:57:41Z","2024-06-14T03:25:43.587Z","0","18:47:52","18:19:50","0:28:2","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Run e2e","false","3630","zhou-yinyuan",,,,,,,,"2024-06-14T02:39:02.628Z","0",,,,"failed","main","" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","3623","Mandy-Tang",,"2024-06-13T03:23:46Z","2024-06-13T03:49:42Z","2024-06-13T07:50:51Z",,"2024-06-13T08:12:35Z","2024-06-13T07:50:51Z","2024-06-13T08:13:08.492Z","0","4:49:22","4:27:5","0:22:17","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","3621","zhou-yinyuan",,"2024-06-13T03:20:02Z","2024-06-13T03:22:21Z","2024-06-13T03:49:10Z",,"2024-06-13T04:12:06Z","2024-06-13T03:49:10Z","2024-06-13T04:12:38.392Z","0","0:52:36","0:29:8","0:23:28","passed","main","false" -"Heartbeat-backup","Heartbeat",":rocket: Deploy prod","true","3616","zhou-yinyuan",,"2024-05-31T07:26:04Z","2024-06-07T03:49:08Z","2024-06-13T01:45:55Z",,"2024-06-13T02:07:09Z","2024-06-13T01:45:55Z","2024-06-13T02:07:51.479Z","120","186:41:47","186:19:51","0:21:56","passed","main","false" +"Organization","Pipeline Name","Repo Name","Pipeline Step","Valid","Build Number","Pull Number","Code Committer","Build Creator","First Code Committed Time In PR","PR Created Time","PR Merged Time","No PR Committed Time","Job Start Time","Pipeline Start Time","Pipeline Finish Time","Non-Workdays (Hours)","Total Lead Time (HH:mm:ss)","PR Lead Time (HH:mm:ss)","Pipeline Lead Time (HH:mm:ss)","Status","Branch","Revert" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","3637","1485","Mandy-Tang",,"2024-06-13T09:46:18Z","2024-06-14T01:57:16Z","2024-06-14T03:33:48Z",,"2024-06-14T06:07:02Z","2024-06-14T03:33:48Z","2024-06-14T06:07:35.879Z","0","20:21:17","17:47:30","2:33:47","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","3635","1486","zhou-yinyuan",,"2024-06-13T08:37:51Z","2024-06-14T02:38:55Z","2024-06-14T02:57:41Z",,"2024-06-14T03:25:06Z","2024-06-14T02:57:41Z","2024-06-14T03:25:43.587Z","0","18:47:52","18:19:50","0:28:2","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Run e2e","false","3630",,"zhou-yinyuan",,,,,,,,"2024-06-14T02:39:02.628Z","0",,,,"failed","main","" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","3623","1483","Mandy-Tang",,"2024-06-13T03:23:46Z","2024-06-13T03:49:42Z","2024-06-13T07:50:51Z",,"2024-06-13T08:12:35Z","2024-06-13T07:50:51Z","2024-06-13T08:13:08.492Z","0","4:49:22","4:27:5","0:22:17","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","3621","1482","zhou-yinyuan",,"2024-06-13T03:20:02Z","2024-06-13T03:22:21Z","2024-06-13T03:49:10Z",,"2024-06-13T04:12:06Z","2024-06-13T03:49:10Z","2024-06-13T04:12:38.392Z","0","0:52:36","0:29:8","0:23:28","passed","main","false" +"Heartbeat-backup","Heartbeat",,":rocket: Deploy prod","true","3616","1481","zhou-yinyuan",,"2024-05-31T07:26:04Z","2024-06-07T03:49:08Z","2024-06-13T01:45:55Z",,"2024-06-13T02:07:09Z","2024-06-13T01:45:55Z","2024-06-13T02:07:51.479Z","120","186:41:47","186:19:51","0:21:56","passed","main","false" diff --git a/frontend/e2e/fixtures/import-file/select-none-config.ts b/frontend/e2e/fixtures/import-file/select-none-config.ts index e7228e4dea..885753f365 100644 --- a/frontend/e2e/fixtures/import-file/select-none-config.ts +++ b/frontend/e2e/fixtures/import-file/select-none-config.ts @@ -106,5 +106,5 @@ export const SelectNoneConfig = { excludeStates: [], }, pipelineCrews: ['Yufan0226', 'Zhongren GU', 'guzhongren', 'renovate[bot]', 'zhou-yinyuan', 'Unknown'], - sourceControlCrews: ['zhou-yinyuan'], + sourceControlCrews: ['zhou-yinyuan', 'guzhongren'], }; diff --git a/frontend/e2e/pages/metrics/report-step.ts b/frontend/e2e/pages/metrics/report-step.ts index fc8bf7976b..3eb6b35d5d 100644 --- a/frontend/e2e/pages/metrics/report-step.ts +++ b/frontend/e2e/pages/metrics/report-step.ts @@ -273,13 +273,18 @@ export class ReportStep { } async checkDoraMetricsReportDetails(doraMetricsDetailData: IDoraMetricsResultItem) { - await expect(this.deploymentFrequencyRows.getByRole('cell').nth(0)).toContainText('Heartbeat/ Deploy prod'); - await expect(this.deploymentFrequencyRows.getByRole('cell').nth(1)).toContainText( - doraMetricsDetailData.deploymentFrequency, - ); - await expect(this.deploymentFrequencyRows.getByRole('cell').nth(2)).toContainText( - doraMetricsDetailData.deploymentTimes, - ); + if (doraMetricsDetailData.deploymentFrequency) { + await expect(this.deploymentFrequencyRows.getByRole('cell').nth(0)).toContainText('Heartbeat/ Deploy prod'); + await expect(this.deploymentFrequencyRows.getByRole('cell').nth(1)).toContainText( + doraMetricsDetailData.deploymentFrequency, + ); + } + + if (doraMetricsDetailData.deploymentTimes) { + await expect(this.deploymentFrequencyRows.getByRole('cell').nth(2)).toContainText( + doraMetricsDetailData.deploymentTimes, + ); + } await expect(this.leadTimeForChangesRows.nth(2)).toContainText( this.combineStrings(['PR Lead Time', doraMetricsDetailData.prLeadTime]), @@ -291,14 +296,21 @@ export class ReportStep { this.combineStrings(['Total Lead Time', doraMetricsDetailData.totalLeadTime]), ); - await expect(this.pipelineChangeFailureRateRows.getByRole('cell').nth(0)).toContainText('Heartbeat/ Deploy prod'); - await expect(this.pipelineChangeFailureRateRows.getByRole('cell').nth(1)).toContainText( - doraMetricsDetailData.failureRate.replace(' ', ''), - ); - await expect(this.pipelineMeanTimeToRecoveryRows.getByRole('cell').nth(0)).toContainText('Heartbeat/ Deploy prod'); - await expect(this.pipelineMeanTimeToRecoveryRows.getByRole('cell').nth(1)).toContainText( - doraMetricsDetailData.pipelineMeanTimeToRecovery, - ); + if (doraMetricsDetailData.failureRate) { + await expect(this.pipelineChangeFailureRateRows.getByRole('cell').nth(0)).toContainText('Heartbeat/ Deploy prod'); + await expect(this.pipelineChangeFailureRateRows.getByRole('cell').nth(1)).toContainText( + doraMetricsDetailData.failureRate.replace(' ', ''), + ); + } + + if (doraMetricsDetailData.pipelineMeanTimeToRecovery) { + await expect(this.pipelineMeanTimeToRecoveryRows.getByRole('cell').nth(0)).toContainText( + 'Heartbeat/ Deploy prod', + ); + await expect(this.pipelineMeanTimeToRecoveryRows.getByRole('cell').nth(1)).toContainText( + doraMetricsDetailData.pipelineMeanTimeToRecovery, + ); + } } async checkDoraMetricsReportDetailsForMultipleRanges(doraMetricsReportData: IDoraMetricsResultItem[]) { @@ -312,12 +324,14 @@ export class ReportStep { projectCreationType, doraMetricsReportData, fileNamePrefix, + showMoreIndex = 1, }: { projectCreationType: ProjectCreationType; doraMetricsReportData: IDoraMetricsResultItem[]; fileNamePrefix?: string; + showMoreIndex?: number; }) { - await this.showMoreLinks.nth(1).click(); + await this.showMoreLinks.nth(showMoreIndex).click(); if ( projectCreationType === ProjectCreationType.IMPORT_PROJECT_FROM_FILE || projectCreationType === ProjectCreationType.CREATE_A_NEW_PROJECT @@ -700,11 +714,17 @@ export class ReportStep { await expect(this.prLeadTime).toContainText(`${prLeadTime}PR Lead Time(Hours)`); await expect(this.pipelineLeadTime).toContainText(`${pipelineLeadTime}Pipeline Lead Time(Hours)`); await expect(this.totalLeadTime).toContainText(`${totalLeadTime}Total Lead Time(Hours)`); - await expect(this.deploymentFrequency).toContainText( - `${deploymentFrequency}Deployment Frequency(Times/Days)${deploymentTimes}Deployment Times(Times)`, - ); - await expect(this.failureRate).toContainText(failureRate); - await expect(this.pipelineMeanTimeToRecovery).toContainText(`${pipelineMeanTimeToRecovery}(Hours)`); + if (deploymentFrequency && deploymentTimes) { + await expect(this.deploymentFrequency).toContainText( + `${deploymentFrequency}Deployment Frequency(Times/Days)${deploymentTimes}Deployment Times(Times)`, + ); + } + if (failureRate) { + await expect(this.failureRate).toContainText(failureRate); + } + if (pipelineMeanTimeToRecovery) { + await expect(this.pipelineMeanTimeToRecovery).toContainText(`${pipelineMeanTimeToRecovery}(Hours)`); + } } async checkDoraMetricsForMultipleRanges(data: IDoraMetricsResultItem[]) { diff --git a/frontend/e2e/specs/major-path/create-a-new-project.spec.ts b/frontend/e2e/specs/major-path/create-a-new-project.spec.ts index 1f07d4fc1a..9a404f465c 100644 --- a/frontend/e2e/specs/major-path/create-a-new-project.spec.ts +++ b/frontend/e2e/specs/major-path/create-a-new-project.spec.ts @@ -7,6 +7,7 @@ import { DORA_METRICS_RESULT_MULTIPLE_RANGES, BOARD_METRICS_WITH_DESIGN_AND_WAITING_FOR_DEPLOYMENT_RESULT_MULTIPLE_RANGES, BOARD_METRICS_WITH_DESIGN_AND_WAITING_FOR_DEPLOYMENT_CYCLE_TIME, + DORA_METRICS_RESULT_FOR_SOURCE_CONTROL, } from '../../fixtures/create-new/report-result'; import { configWithDesignAndWaitingForDevelopmentStatus, @@ -210,7 +211,9 @@ test('Create a new project with design and waiting for deployment in the cycle t await reportStep.checkMetricDownloadDataForMultipleRanges(3, fileNamePrefix); }); -test('Create a new project with other pipeline setting', async ({ homePage, configStep, metricsStep }) => { +test('Create a new project with other pipeline setting', async ({ homePage, configStep, metricsStep, reportStep }) => { + const prefix = 'with-only-source-control-lead-time-'; + await homePage.goto(); await homePage.createANewProject(); await configStep.typeInProjectName(configStepData.projectName); @@ -227,5 +230,19 @@ test('Create a new project with other pipeline setting', async ({ homePage, conf await metricsStep.waitForShown(); await metricsStep.selectDefaultGivenSourceControlSetting(SelectNoneConfig.sourceControlConfigurationSettings); await metricsStep.selectAllPipelineCrews(metricsStep.sourceControlCrewSettingsLabel); + await metricsStep.goToReportPage(); + await reportStep.confirmGeneratedReport(); + await reportStep.goToReportListTab(); + await reportStep.checkOnlyLeadTimeForChangesPartVisible(); + await reportStep.checkExportMetricDataButtonClickable(); + await reportStep.checkExportPipelineDataButtonClickable(); + await reportStep.checkDoraMetricsForMultipleRanges(DORA_METRICS_RESULT_FOR_SOURCE_CONTROL); + await reportStep.checkDoraMetricsDetailsForMultipleRanges({ + doraMetricsReportData: DORA_METRICS_RESULT_FOR_SOURCE_CONTROL, + projectCreationType: ProjectCreationType.CREATE_A_NEW_PROJECT, + fileNamePrefix: prefix, + showMoreIndex: 0, + }); + await reportStep.checkMetricDownloadDataForMultipleRanges(3, prefix); }); diff --git a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts index 6eb2cbeba5..0571916e01 100644 --- a/frontend/e2e/specs/major-path/import-project-from-file.spec.ts +++ b/frontend/e2e/specs/major-path/import-project-from-file.spec.ts @@ -10,6 +10,7 @@ import { BOARD_CSV_COMPARED_LINES, DORA_METRICS_RESULT_MULTIPLE_RANGES, CYCLE_TIME_WITH_ANALYSIS_STATUS_PROJECT_BOARD_METRICS_RESULT, + DORA_METRICS_RESULT_FOR_SOURCE_CONTROL, } from '../../fixtures/create-new/report-result'; import { calculateWithHolidayConfigFile } from '../../fixtures/import-file/calculate-with-holiday-config-file'; import { cycleTimeByStatusFixture } from '../../fixtures/cycle-time-by-status/cycle-time-by-status-fixture'; @@ -300,10 +301,12 @@ test('Import project from file when select none in pipeline tool configuration', homePage, configStep, metricsStep, + reportStep, }) => { const hbStateData = SelectNoneConfig.cycleTime.jiraColumns.map( (jiraToHBSingleMap) => Object.values(jiraToHBSingleMap)[0], ); + const prefix = 'with-source-control-lead-time-'; await homePage.goto(); @@ -327,5 +330,27 @@ test('Import project from file when select none in pipeline tool configuration', await metricsStep.checkClassificationCharts(SelectNoneConfig.classificationCharts); await metricsStep.checkSourceControlConfigurationAreChanged(SelectNoneConfig.sourceControlConfigurationSettings); await metricsStep.selectGivenSourceControlCrews(SelectNoneConfig.sourceControlCrews); + await metricsStep.goToReportPage(); + await reportStep.confirmGeneratedReport(); + await reportStep.goToReportListTab(); + await reportStep.checkOnlyLeadTimeForChangesPartVisible(); + await reportStep.checkExportMetricDataButtonClickable(); + await reportStep.checkExportPipelineDataButtonClickable(); + await reportStep.checkBoardMetricsForMultipleRanges(BOARD_METRICS_RESULT_MULTIPLE_RANGES); + await reportStep.checkBoardMetricsDetailsForMultipleRanges({ + projectCreationType: ProjectCreationType.CREATE_A_NEW_PROJECT, + velocityData: BOARD_METRICS_VELOCITY_MULTIPLE_RANGES, + cycleTimeData: BOARD_METRICS_CYCLE_TIME_MULTIPLE_RANGES, + classificationData: BOARD_METRICS_CLASSIFICATION_MULTIPLE_RANGES, + reworkData: BOARD_METRICS_REWORK_MULTIPLE_RANGES, + csvCompareLines: BOARD_CSV_COMPARED_LINES, + }); + await reportStep.checkDoraMetricsForMultipleRanges(DORA_METRICS_RESULT_FOR_SOURCE_CONTROL); + await reportStep.checkDoraMetricsDetailsForMultipleRanges({ + doraMetricsReportData: DORA_METRICS_RESULT_FOR_SOURCE_CONTROL, + projectCreationType: ProjectCreationType.IMPORT_PROJECT_FROM_FILE, + fileNamePrefix: prefix, + }); + await reportStep.checkMetricDownloadDataForMultipleRanges(3, prefix); }); diff --git a/frontend/src/clients/report/ReportClient.ts b/frontend/src/clients/report/ReportClient.ts index 80a7d48e3f..9bf163f3d6 100644 --- a/frontend/src/clients/report/ReportClient.ts +++ b/frontend/src/clients/report/ReportClient.ts @@ -68,6 +68,7 @@ export class ReportClient extends HttpClient { }, leadTimeForChanges: { leadTimeForChangesOfPipelines: [], + leadTimeForChangesOfSourceControls: [], avgLeadTimeForChanges: { name: '', prLeadTime: 1, diff --git a/frontend/src/clients/report/dto/request.ts b/frontend/src/clients/report/dto/request.ts index ca3e26024e..057bbbcc1f 100644 --- a/frontend/src/clients/report/dto/request.ts +++ b/frontend/src/clients/report/dto/request.ts @@ -21,6 +21,12 @@ export interface ReportRequestDTO extends IBasicReportRequestDTO { codebaseSetting?: { type: string; token: string; + crews: string[]; + codebases: { + branches: string[]; + repo: string; + organization: string; + }[]; leadTime: { id: string; name: string; diff --git a/frontend/src/clients/report/dto/response.ts b/frontend/src/clients/report/dto/response.ts index 6f29af4507..1455fa7a85 100644 --- a/frontend/src/clients/report/dto/response.ts +++ b/frontend/src/clients/report/dto/response.ts @@ -78,7 +78,8 @@ export interface DeploymentFrequencyResponse { } export interface LeadTimeForChangesResponse { - leadTimeForChangesOfPipelines: Array; + leadTimeForChangesOfPipelines: LeadTimeOfPipeline[]; + leadTimeForChangesOfSourceControls: LeadTimeOfSourceControl[]; avgLeadTimeForChanges: AvgLeadTime; } @@ -121,6 +122,14 @@ export interface LeadTimeOfPipeline { totalDelayTime: number; } +export interface LeadTimeOfSourceControl { + organization: string; + repo: string; + prLeadTime: number; + pipelineLeadTime: number; + totalDelayTime: number; +} + export interface AvgLeadTime { name: string; step?: string; @@ -193,6 +202,7 @@ export interface ReportResponse { export interface ReportURLsResponse { metrics: string[]; pipelines: string[]; + sourceControls: string[]; reportURLs: string[]; classificationNames: string[]; } diff --git a/frontend/src/components/Common/ReportDetailTableContainsSubtitle/index.tsx b/frontend/src/components/Common/ReportDetailTableContainsSubtitle/index.tsx index e94ca35018..24010cdab0 100644 --- a/frontend/src/components/Common/ReportDetailTableContainsSubtitle/index.tsx +++ b/frontend/src/components/Common/ReportDetailTableContainsSubtitle/index.tsx @@ -16,6 +16,7 @@ import { Loading } from '@src/components/Loading'; import { styled } from '@mui/material/styles'; import { Optional } from '@src/utils/types'; import React, { Fragment } from 'react'; +import { theme } from '@src/theme'; import { isEmpty } from 'lodash'; interface ReportDetailTableContainsSubtitleProps { @@ -25,6 +26,7 @@ interface ReportDetailTableContainsSubtitleProps { listName: string; data: Optional; errorMessage?: string; + isGray?: boolean; } export const StyledLoadingWrapper = styled('div')({ @@ -40,6 +42,7 @@ export const ReportDetailTableContainsSubtitle = ({ listName, data, errorMessage, + isGray = false, }: ReportDetailTableContainsSubtitleProps) => { const emojiRow = (row: ReportDataForMultipleValueColumns) => { const { name } = row; @@ -59,13 +62,17 @@ export const ReportDetailTableContainsSubtitle = ({ return {name}; }; - const renderRowValueColumn = (values: string[]) => { + const renderRowValueColumn = (values: string[], isGray: boolean) => { return values.map((it, index) => ( {it} @@ -97,13 +104,18 @@ export const ReportDetailTableContainsSubtitle = ({ {row.valueList.map((valuesList) => ( {valuesList.name} - {renderRowValueColumn(valuesList.values)} + {renderRowValueColumn(valuesList.values, isGray && valuesList.name === 'Pipeline Lead Time')} ))} diff --git a/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx b/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx index cadfd4b3c2..adbac086dc 100644 --- a/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx +++ b/frontend/src/components/Common/ReportGrid/ReportCard/index.tsx @@ -19,9 +19,10 @@ interface ReportCardProps extends HTMLAttributes { items?: ReportCardItemProps[] | null; xs: number; errorMessage: string | undefined; + isExistSourceControl?: boolean; } -export const ReportCard = ({ title, items, xs, errorMessage }: ReportCardProps) => { +export const ReportCard = ({ title, items, xs, errorMessage, isExistSourceControl = false }: ReportCardProps) => { const [isShowDialog, setIsShowDialog] = useState(false); const defaultFlex = 1; const getReportItems = () => { @@ -64,6 +65,7 @@ export const ReportCard = ({ title, items, xs, errorMessage }: ReportCardProps) index < style.MAX_INDEX ? ( { value: number; isToFixed?: boolean; extraValue?: string; + isGray?: boolean; subtitle: string; showDividingLine?: boolean; } @@ -24,12 +26,19 @@ export const ReportCardItem = ({ style, value, isToFixed = true, + isGray, extraValue, subtitle, showDividingLine, }: ReportCardItemProps) => { return ( - + {showDividingLine && } @@ -39,7 +48,13 @@ export const ReportCardItem = ({ - {subtitle} + + {subtitle} + diff --git a/frontend/src/components/Common/ReportGrid/index.tsx b/frontend/src/components/Common/ReportGrid/index.tsx index e66a7126c4..d8bb8beb4b 100644 --- a/frontend/src/components/Common/ReportGrid/index.tsx +++ b/frontend/src/components/Common/ReportGrid/index.tsx @@ -9,6 +9,7 @@ export interface ReportGridProps { lastGrid?: boolean; reportDetails: ReportDetailProps[]; errorMessage?: string | undefined; + isExistSourceControl?: boolean; } export interface ReportDetailProps { @@ -16,7 +17,12 @@ export interface ReportDetailProps { items?: ReportCardItemProps[] | null; } -export const ReportGrid = ({ lastGrid, reportDetails, errorMessage }: ReportGridProps) => { +export const ReportGrid = ({ + lastGrid, + reportDetails, + errorMessage, + isExistSourceControl = false, +}: ReportGridProps) => { const getXS = (index: number) => { if (getDeviceSize() === 'md') { return GRID_CONFIG.FULL.XS; @@ -42,7 +48,13 @@ export const ReportGrid = ({ lastGrid, reportDetails, errorMessage }: ReportGrid const xs = getXS(index); return ( - + ); })} diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 0014b9cbda..2c0e37e087 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -384,6 +384,8 @@ export const DEV_MEAN_TIME_TO_RECOVERY_NAME = 'Dev mean time to recovery'; export const PIPELINE_STEP = 'Pipeline/step'; +export const REPO_NAME = 'Repo name'; + export const SUBTITLE = 'Subtitle'; export const AVERAGE_FIELD = 'Average'; @@ -429,6 +431,10 @@ export const MESSAGE = { 'Failed to get partial Pipeline configuration, please go back to the previous page and change your pipeline token with correct access permission, or click "Next" button to go to Report page.', PIPELINE_STEP_REQUEST_PARTIAL_FAILED_OTHERS: 'Failed to get partial Pipeline configuration, you can click "Next" button to go to Report page.', + SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_4XX: + 'Failed to get partial Source control configuration, please go back to the previous page and change your source control token with correct access permission, or click "Next" button to go to Report page.', + SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_OTHERS: + 'Failed to get partial Source control configuration, you can click "Next" button to go to Report page.', DORA_CHART_LOADING_FAILED: 'Dora metrics loading timeout, Please click "Retry"!', SHARE_REPORT_EXPIRED: 'The report has expired. Please go home page and generate it again.', }; diff --git a/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx b/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx index f74ae428c5..4c217fc466 100644 --- a/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx +++ b/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx @@ -5,10 +5,10 @@ import { WarningMessage, } from '@src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/style'; import { + selectDateRange, + selectSourceControlBranches, selectSourceControlOrganizations, selectSourceControlRepos, - selectSourceControlBranches, - selectDateRange, } from '@src/context/config/configSlice'; import { selectSourceControlConfigurationSettings, @@ -19,9 +19,14 @@ import { useGetSourceControlConfigurationRepoEffect } from '@src/hooks/useGetSou import { useGetSourceControlConfigurationCrewEffect } from '@src/hooks/useGetSourceControlConfigurationCrewEffect'; import { SourceControlBranch } from '@src/containers/MetricsStep/SouceControlConfiguration/SourceControlBranch'; import { SingleSelection } from '@src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection'; +import { ErrorInfoType } from '@src/containers/MetricsStep/SouceControlConfiguration'; +import { addNotification } from '@src/context/notification/NotificationSlice'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { useAppDispatch, useAppSelector } from '@src/hooks'; +import { MESSAGE } from '@src/constants/resources'; import { Loading } from '@src/components/Loading'; import { useEffect, useRef } from 'react'; +import { HttpStatusCode } from 'axios'; import { store } from '@src/store'; interface SourceControlMetricSelectionProps { @@ -33,6 +38,7 @@ interface SourceControlMetricSelectionProps { }; isShowRemoveButton: boolean; onRemoveSourceControl: (id: number) => void; + handleUpdateErrorInfo: (errorInfo: ErrorInfoType) => void; onUpdateSourceControl: (id: number, label: string, value: string | StringConstructor[] | string[]) => void; isDuplicated: boolean; setLoadingCompletedNumber: React.Dispatch>; @@ -47,6 +53,7 @@ export const SourceControlMetricSelection = ({ isDuplicated, setLoadingCompletedNumber, totalSourceControlNumber, + handleUpdateErrorInfo, }: SourceControlMetricSelectionProps) => { const { id, organization, repo } = sourceControlSetting; const isInitialMount = useRef(true); @@ -54,16 +61,22 @@ export const SourceControlMetricSelection = ({ isLoading: repoIsLoading, getSourceControlRepoInfo, isGetRepo, + info: repoInfo, + stepFailedStatus: getRepoFailedStatus, } = useGetSourceControlConfigurationRepoEffect(); const { isLoading: branchIsLoading, getSourceControlBranchInfo, isGetBranch, + info: branchInfo, + stepFailedStatus: getBranchFailedStatus, } = useGetSourceControlConfigurationBranchEffect(); const { isLoading: crewIsLoading, getSourceControlCrewInfo, isGetAllCrews, + info: crewInfo, + stepFailedStatus: getCrewFailedStatus, } = useGetSourceControlConfigurationCrewEffect(); const storeContext = store.getState(); const dispatch = useAppDispatch(); @@ -113,6 +126,49 @@ export const SourceControlMetricSelection = ({ } }, [isGetAllCrews, setLoadingCompletedNumber, totalSourceControlNumber]); + useEffect(() => { + const errorInfoList: ErrorInfoType[] = [repoInfo, branchInfo, crewInfo].filter( + (it) => it.code !== HttpStatusCode.Ok, + ); + const errorInfo = errorInfoList.length === 0 ? crewInfo : errorInfoList[0]; + handleUpdateErrorInfo(errorInfo); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [repoInfo, branchInfo, crewInfo]); + + useEffect(() => { + const popup = () => { + if ( + getRepoFailedStatus === MetricsDataFailStatus.PartialFailed4xx || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailed4xx || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailed4xx + ) { + dispatch( + addNotification({ + type: 'warning', + message: MESSAGE.SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_4XX, + }), + ); + } else if ( + getRepoFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getRepoFailedStatus === MetricsDataFailStatus.PartialFailedTimeout || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailedTimeout || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailedTimeout + ) { + dispatch( + addNotification({ + type: 'warning', + message: MESSAGE.SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_OTHERS, + }), + ); + } + }; + if (!isLoading) { + popup(); + } + }, [dispatch, getBranchFailedStatus, getCrewFailedStatus, getRepoFailedStatus, isLoading]); + const handleOnUpdateOrganization = (id: number, label: string, value: string | []): void => { onUpdateSourceControl(id, label, value); getSourceControlRepoInfo(value.toString(), dateRanges, id); diff --git a/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx b/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx index 6aa22b1018..b4dac9450b 100644 --- a/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx +++ b/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx @@ -4,6 +4,12 @@ import { selectSourceControlConfigurationSettings, updateSourceControlConfigurationSettings, } from '@src/context/Metrics/metricsSlice'; +import { + ISourceControlGetBranchResponseDTO, + ISourceControlGetCrewResponseDTO, + ISourceControlGetOrganizationResponseDTO, + ISourceControlGetRepoResponseDTO, +} from '@src/clients/sourceControl/dto/response'; import { useGetSourceControlConfigurationOrganizationEffect } from '@src/hooks/useGetSourceControlConfigurationOrganizationEffect'; import PresentationForErrorCases from '@src/components/Metrics/MetricsStep/DeploymentFrequencySettings/PresentationForErrorCases'; import { SourceControlMetricSelection } from '@src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection'; @@ -17,11 +23,17 @@ import { getErrorDetail } from '@src/context/meta/metaSlice'; import { useAppDispatch, useAppSelector } from '@src/hooks'; import { Crews } from '@src/containers/MetricsStep/Crews'; import { Loading } from '@src/components/Loading'; +import { useEffect, useState } from 'react'; import { HttpStatusCode } from 'axios'; import { store } from '@src/store'; -import { useState } from 'react'; import dayjs from 'dayjs'; +export type ErrorInfoType = + | ISourceControlGetOrganizationResponseDTO + | ISourceControlGetRepoResponseDTO + | ISourceControlGetBranchResponseDTO + | ISourceControlGetCrewResponseDTO; + export const SourceControlConfiguration = () => { const dispatch = useAppDispatch(); const storeContext = store.getState(); @@ -30,6 +42,7 @@ export const SourceControlConfiguration = () => { const sourceControlConfigurationSettings = useAppSelector(selectSourceControlConfigurationSettings); const { getDuplicatedSourceControlIds } = useMetricsStepValidationCheckContext(); const [loadingCompletedNumber, setLoadingCompletedNumber] = useState(0); + const [errorInfo, setErrorInfo] = useState(info); const dateRanges = useAppSelector(selectDateRange); const realSourceControlConfigurationSettings = isFirstFetch ? [] : sourceControlConfigurationSettings; const totalSourceControlNumber = realSourceControlConfigurationSettings.length; @@ -63,6 +76,16 @@ export const SourceControlConfiguration = () => { const handleUpdateSourceControl = (id: number, label: string, value: string | StringConstructor[] | string[]) => { dispatch(updateSourceControlConfigurationSettings({ updateId: id, label: label.toLowerCase(), value })); }; + const handleUpdateErrorInfo = (newErrorInfo: ErrorInfoType) => { + const errorInfoList: ErrorInfoType[] = [newErrorInfo, info].filter((it) => it.code !== HttpStatusCode.Ok); + const errorInfo = errorInfoList.length === 0 ? info : errorInfoList[0]; + setErrorInfo(errorInfo); + }; + + useEffect(() => { + handleUpdateErrorInfo(errorInfo); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [info]); const shouldShowCrews = loadingCompletedNumber !== 0 && @@ -72,8 +95,8 @@ export const SourceControlConfiguration = () => { return ( <> {isLoading && } - {info?.code !== HttpStatusCode.Ok ? ( - + {errorInfo?.code !== HttpStatusCode.Ok ? ( + ) : ( <> @@ -90,6 +113,7 @@ export const SourceControlConfiguration = () => { isDuplicated={getDuplicatedSourceControlIds(realSourceControlConfigurationSettings).includes( sourceControlConfigurationSetting.id, )} + handleUpdateErrorInfo={handleUpdateErrorInfo} totalSourceControlNumber={totalSourceControlNumber} setLoadingCompletedNumber={setLoadingCompletedNumber} /> diff --git a/frontend/src/containers/MetricsStepper/index.tsx b/frontend/src/containers/MetricsStepper/index.tsx index b9c805fdf2..9ae54bc074 100644 --- a/frontend/src/containers/MetricsStepper/index.tsx +++ b/frontend/src/containers/MetricsStepper/index.tsx @@ -10,11 +10,19 @@ import { } from '@src/containers/ConfigStep/Form/schema'; import { selectConfig, + selectDateRange, selectMetrics, selectPipelineList, selectPipelineTool, selectSourceControlBranches, + selectSourceControlCrews, } from '@src/context/config/configSlice'; +import { + ISavedMetricsSettingState, + ISourceControlConfig, + selectCycleTimeSettings, + selectMetricsContent, +} from '@src/context/Metrics/metricsSlice'; import { CycleTimeSettingsTypes, DONE, @@ -32,20 +40,15 @@ import { StyledStepLabel, StyledStepper, } from './style'; -import { - ISavedMetricsSettingState, - selectCycleTimeSettings, - selectMetricsContent, -} from '@src/context/Metrics/metricsSlice'; import { backStep, nextStep, selectStepNumber, updateTimeStamp } from '@src/context/stepper/StepperSlice'; import { useMetricsStepValidationCheckContext } from '@src/hooks/useMetricsStepValidationCheckContext'; import { convertCycleTimeSettings, exportToJsonFile, onlyEmptyAndDoneState } from '@src/utils/util'; import { useDefaultValues } from '@src/containers/ConfigStep/Form/useDefaultValues'; +import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { PageContentWrapper } from '@src/components/Common/PageContentWrapper'; import { COMMON_BUTTONS, METRICS_STEPS, STEPS } from '@src/constants/commons'; import { ConfirmDialog } from '@src/containers/MetricsStepper/ConfirmDialog'; import { useAppDispatch, useAppSelector } from '@src/hooks/useAppDispatch'; -import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { getFormMeta } from '@src/context/meta/metaSlice'; import SaveAltIcon from '@mui/icons-material/SaveAlt'; import { yupResolver } from '@hookform/resolvers/yup'; @@ -57,6 +60,7 @@ import isEmpty from 'lodash/isEmpty'; import { store } from '@src/store'; import every from 'lodash/every'; import omit from 'lodash/omit'; +import dayjs from 'dayjs'; const ConfigStep = lazy(() => import('@src/containers/ConfigStep')); const MetricsStep = lazy(() => import('@src/containers/MetricsStep')); @@ -64,12 +68,14 @@ const ReportStep = lazy(() => import('@src/containers/ReportStep')); /* istanbul ignore next */ const MetricsStepper = () => { + const storeContext = store.getState(); const navigate = useNavigate(); const dispatch = useAppDispatch(); const activeStep = useAppSelector(selectStepNumber); const [isDialogShowing, setIsDialogShowing] = useState(false); const requiredData = useAppSelector(selectMetrics); const config = useAppSelector(selectConfig); + const dateRanges = useAppSelector(selectDateRange); const metricsConfig = useAppSelector(selectMetricsContent); const cycleTimeSettings = useAppSelector(selectCycleTimeSettings); const [isDisableNextButton, setIsDisableNextButton] = useState(true); @@ -106,6 +112,24 @@ const MetricsStepper = () => { mode: 'onChange', }); + const getSourceControlCrews = useCallback( + (sourceControlConfigurationSetting: ISourceControlConfig) => { + return sourceControlConfigurationSetting.branches?.flatMap((branch) => + dateRanges.flatMap((dateRange) => + selectSourceControlCrews( + storeContext, + sourceControlConfigurationSetting.organization, + sourceControlConfigurationSetting.repo, + branch, + dayjs(dateRange.startDate).startOf('date').valueOf(), + dayjs(dateRange.endDate).startOf('date').valueOf(), + ), + ), + ); + }, + [dateRanges, storeContext], + ); + const { isValid: isBasicInfoValid } = basicInfoMethods.formState; const { isValid: isBoardConfigValid, isSubmitSuccessful: isBoardConfigSubmitSuccessful } = boardConfigMethods.formState; @@ -204,7 +228,6 @@ const MetricsStepper = () => { }, [pipelineList, formMeta.metrics.pipelines, getDuplicatedPipeLineIds, metricsConfig.deploymentFrequencySettings]); const isSourceControlConfigurationValid = useMemo(() => { - const storeContext = store.getState(); const sourceControlConfigurationSettings = metricsConfig.sourceControlConfigurationSettings; const selectedSourceControls = sourceControlConfigurationSettings.filter((sourceControl) => { @@ -212,8 +235,13 @@ const MetricsStepper = () => { return sourceControl.branches.every((it) => allBranches.includes(it)); }); + const sourceControlCrews = [ + ...new Set(sourceControlConfigurationSettings.flatMap((it) => getSourceControlCrews(it))), + ]; + return ( !isEmpty(selectedSourceControls) && + !isEmpty(sourceControlCrews) && sourceControlConfigurationSettings.every(({ organization }) => !isEmpty(organization)) && sourceControlConfigurationSettings.every(({ repo }) => !isEmpty(repo)) && sourceControlConfigurationSettings.every(({ branches }) => !isEmpty(branches)) && @@ -222,7 +250,12 @@ const MetricsStepper = () => { selectedSourceControls.every(({ branches }) => !isEmpty(branches)) && getDuplicatedSourceControlIds(sourceControlConfigurationSettings).length === 0 ); - }, [getDuplicatedSourceControlIds, metricsConfig.sourceControlConfigurationSettings]); + }, [ + metricsConfig.sourceControlConfigurationSettings, + getSourceControlCrews, + getDuplicatedSourceControlIds, + storeContext, + ]); useEffect(() => { if (activeStep === METRICS_STEPS.METRICS) { diff --git a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx index 75200ee09b..c9ec6393e6 100644 --- a/frontend/src/containers/ReportStep/DoraMetrics/index.tsx +++ b/frontend/src/containers/ReportStep/DoraMetrics/index.tsx @@ -25,9 +25,17 @@ interface DoraMetricsProps { doraReport: ReportResponseDTO | undefined; errorMessage: string; metrics: string[]; + isExistSourceControl: boolean; } -const DoraMetrics = ({ startToRequestDoraData, onShowDetail, doraReport, errorMessage, metrics }: DoraMetricsProps) => { +const DoraMetrics = ({ + startToRequestDoraData, + onShowDetail, + doraReport, + errorMessage, + metrics, + isExistSourceControl, +}: DoraMetricsProps) => { const shouldShowSourceControl = metrics.includes(RequiredData.LeadTimeForChanges); const sourceControlMetricsCompleted = metrics .filter((metric) => SOURCE_CONTROL_METRICS.includes(metric)) @@ -158,7 +166,11 @@ const DoraMetrics = ({ startToRequestDoraData, onShowDetail, doraReport, errorMe {shouldShowRetry() && {RETRY}} {shouldShowSourceControl && ( - + )} void; allDateRangeLoadingFinished: boolean; @@ -259,6 +260,7 @@ export const DoraMetricsChart = ({ selectedPipeline, onUpdatePipeline, allPipelines, + allSourceControls, allDateRangeLoadingFinished, }: DoraMetricsChartProps) => { const leadTimeForChange = useRef(null); @@ -268,7 +270,7 @@ export const DoraMetricsChart = ({ const mappedData = data.map((currentData) => { if (!currentData?.doraMetricsCompleted) { - return emptyDataMapperDoraChart(allPipelines, ''); + return emptyDataMapperDoraChart([...allPipelines, ...allSourceControls], ''); } else { return reportMapper(currentData); } @@ -322,7 +324,7 @@ export const DoraMetricsChart = ({ showChart(meanTimeToRecovery.current, meanTimeToRecoveryDataOption); }, [meanTimeToRecovery, meanTimeToRecoveryDataOption]); - const pipelineNameOptions = [DEFAULT_SELECTED_PIPELINE, ...allPipelines]; + const pipelineNameOptions = [DEFAULT_SELECTED_PIPELINE, ...allPipelines, ...allSourceControls]; return ( <> diff --git a/frontend/src/containers/ReportStep/ReportContent/index.tsx b/frontend/src/containers/ReportStep/ReportContent/index.tsx index acae5d2852..1a1dc38484 100644 --- a/frontend/src/containers/ReportStep/ReportContent/index.tsx +++ b/frontend/src/containers/ReportStep/ReportContent/index.tsx @@ -54,6 +54,7 @@ import { uniqueId } from 'lodash'; export interface ReportContentProps { metrics: string[]; allPipelines: string[]; + allSourceControls: string[]; classificationNames: string[]; dateRanges: DateRangeList; startToRequestData: () => void; @@ -82,6 +83,7 @@ const ReportContent = (props: ReportContentProps) => { const { metrics, allPipelines, + allSourceControls, dateRanges, startToRequestData, reportInfos, @@ -111,6 +113,7 @@ const ReportContent = (props: ReportContentProps) => { const [selectedDateRange, setSelectedDateRange] = useState(descendingDateRanges[0]); const [currentDataInfo, setCurrentDataInfo] = useState(initReportInfo()); + let isGray = true; const { closeReportInfosErrorStatus, @@ -279,6 +282,10 @@ const ReportContent = (props: ReportContentProps) => { currentDataInfo.generalError4Report, ]); + if (currentDataInfo.reportData?.leadTimeForChanges?.leadTimeForChangesOfPipelines?.length !== 0) { + isGray = false; + } + const showSummary = () => ( {shouldShowBoardMetrics && ( @@ -302,6 +309,7 @@ const ReportContent = (props: ReportContentProps) => { currentDataInfo.generalError4Dora.message || currentDataInfo.generalError4Report.message } + isExistSourceControl={isGray} /> )} @@ -351,6 +359,7 @@ const ReportContent = (props: ReportContentProps) => { dateRanges={allDateRanges} metrics={metrics} allPipelines={allPipelines} + allSourceControls={allSourceControls} selectedPipeline={selectedPipeline} onUpdatePipeline={setSelectedPipeline} allDateRangeLoadingFinished={allDateRangeLoadingFinished} @@ -377,7 +386,7 @@ const ReportContent = (props: ReportContentProps) => { /> ); const showDoraDetail = (data: ReportResponseDTO) => ( - + ); const handleBack = () => { diff --git a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx index e3eb407bbc..efbb94101e 100644 --- a/frontend/src/containers/ReportStep/ReportDetail/dora.tsx +++ b/frontend/src/containers/ReportStep/ReportDetail/dora.tsx @@ -2,8 +2,8 @@ import { ReportDataForMultipleValueColumns, ReportDataWithTwoColumns, } from '@src/hooks/reportMapper/reportUIDataStructure'; +import { MetricsTitle, PIPELINE_STEP, REPO_NAME, ReportSuffixUnits, SUBTITLE } from '@src/constants/resources'; import ReportDetailTableContainsSubtitle from '@src/components/Common/ReportDetailTableContainsSubtitle'; -import { MetricsTitle, PIPELINE_STEP, ReportSuffixUnits, SUBTITLE } from '@src/constants/resources'; import ReportForDeploymentFrequency from '@src/components/Common/ReportForDeploymentFrequency'; import { DetailContainer } from '@src/containers/ReportStep/ReportDetail/style'; import ReportForTwoColumns from '@src/components/Common/ReportForTwoColumns'; @@ -15,6 +15,7 @@ import React from 'react'; interface Property { data: ReportResponseDTO; + isExistSourceControl: boolean; onBack: () => void; isShowBack: boolean; } @@ -25,18 +26,23 @@ const showTwoColumnSection = (title: string, value: Optional) => value && ; -const showThreeColumnSection = (title: string, value: Optional) => +const showThreeColumnSection = ( + title: string, + isExistSourceControl: boolean, + value: Optional, +) => value && ( ); -export const DoraDetail = withGoBack(({ data }: Property) => { +export const DoraDetail = withGoBack(({ data, isExistSourceControl }: Property) => { const mappedData = reportMapper(data); return ( @@ -46,7 +52,7 @@ export const DoraDetail = withGoBack(({ data }: Property) => { [ReportSuffixUnits.DeploymentsPerDay, ReportSuffixUnits.DeploymentsTimes], mappedData.deploymentFrequencyList, )} - {showThreeColumnSection(MetricsTitle.LeadTimeForChanges, mappedData.leadTimeForChangesList)} + {showThreeColumnSection(MetricsTitle.LeadTimeForChanges, isExistSourceControl, mappedData.leadTimeForChangesList)} {showTwoColumnSection(MetricsTitle.PipelineChangeFailureRate, mappedData.pipelineChangeFailureRateList)} {showTwoColumnSection(MetricsTitle.PipelineMeanTimeToRecovery, mappedData.pipelineMeanTimeToRecoveryList)} diff --git a/frontend/src/containers/ReportStep/index.tsx b/frontend/src/containers/ReportStep/index.tsx index ea8476be81..af078bf566 100644 --- a/frontend/src/containers/ReportStep/index.tsx +++ b/frontend/src/containers/ReportStep/index.tsx @@ -23,7 +23,7 @@ import { } from '@src/context/stepper/StepperSlice'; import { IPipelineConfig, selectClassificationCharts, selectMetricsContent } from '@src/context/Metrics/metricsSlice'; import { ReportResponseDTO } from '@src/clients/report/dto/response'; -import { getTotalDateRangeLoadingStatus } from '../../utils/report'; +import { getTotalDateRangeLoadingStatus } from '@src/utils/report'; import { MESSAGE, RequiredData } from '@src/constants/resources'; import { useAppDispatch } from '@src/hooks/useAppDispatch'; import { MetricTypes } from '@src/constants/commons'; @@ -86,6 +86,8 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { pipelineCrews, deploymentFrequencySettings, leadTimeForChanges, + sourceControlCrews, + sourceControlConfigurationSettings, } = useAppSelector(selectMetricsContent); const classificationCharts = useAppSelector(selectClassificationCharts); @@ -152,6 +154,8 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { type: sourceControl.config.type, token: sourceControl.config.token, leadTime: getPipelineConfig(leadTimeForChanges), + crews: sourceControlCrews, + codebases: sourceControlConfigurationSettings, }, }; }; @@ -252,6 +256,7 @@ const ReportStep = ({ handleSave }: ReportStepProps) => { metrics={metrics} classificationNames={classificationNames} allPipelines={deploymentFrequencySettings.map((it) => `${it.pipelineName}/${it.step}`)} + allSourceControls={sourceControlConfigurationSettings.map((it) => `${it.organization}/${it.repo}`)} dateRanges={dateRanges} startToRequestData={() => startToRequestData(basicReportRequestBody)} reportInfos={reportInfos} diff --git a/frontend/src/containers/ShareReport/index.tsx b/frontend/src/containers/ShareReport/index.tsx index 59a0c3e1aa..73671cbb0b 100644 --- a/frontend/src/containers/ShareReport/index.tsx +++ b/frontend/src/containers/ShareReport/index.tsx @@ -20,6 +20,7 @@ const ShareReport = () => { metrics, isExpired, allPipelines, + allSourceControls, classificationNames, allDateRangeLoadingFinished, } = useShareReportEffect(); @@ -46,6 +47,7 @@ const ShareReport = () => { metrics={metrics} classificationNames={classificationNames} allPipelines={allPipelines} + allSourceControls={allSourceControls} dateRanges={dateRanges} reportInfos={reportInfos} startToRequestData={getData} diff --git a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts index 4b11cef931..5905d57b63 100644 --- a/frontend/src/hooks/reportMapper/leadTimeForChanges.ts +++ b/frontend/src/hooks/reportMapper/leadTimeForChanges.ts @@ -4,6 +4,7 @@ import { LeadTimeForChangesResponse } from '@src/clients/report/dto/response'; export const leadTimeForChangesMapper = ({ leadTimeForChangesOfPipelines, avgLeadTimeForChanges, + leadTimeForChangesOfSourceControls, }: LeadTimeForChangesResponse) => { const minutesPerHour = 60; const formatDuration = (duration: number) => { @@ -30,6 +31,21 @@ export const leadTimeForChangesMapper = ({ }, ); + mappedLeadTimeForChangesValue.push( + ...leadTimeForChangesOfSourceControls.map((item, index) => { + return { + id: index + mappedLeadTimeForChangesValue.length, + name: `${item.organization}/${item.repo}`, + valueList: Object.entries(item) + .slice(-3) + .map(([name, value]) => ({ + name: formatNameDisplay(name) as string, + values: [formatDuration(value)], + })), + }; + }), + ); + mappedLeadTimeForChangesValue.push({ id: mappedLeadTimeForChangesValue.length, name: avgLeadTimeForChanges.name, diff --git a/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx index c6a1f706a0..eb757eeeb0 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx @@ -3,8 +3,10 @@ import { updateSourceControlConfigurationSettingsFirstInto, } from '@src/context/Metrics/metricsSlice'; import { selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetBranchResponseDTO } from '@src/clients/sourceControl/dto/response'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { SourceControlTypes } from '@src/constants/resources'; import { HttpStatusCode } from 'axios'; import { useState } from 'react'; @@ -13,13 +15,23 @@ export interface IUseGetSourceControlConfigurationBranchInterface { readonly isLoading: boolean; readonly getSourceControlBranchInfo: (organization: string, repo: string, id: number) => Promise; readonly isGetBranch: boolean; + readonly info: ISourceControlGetBranchResponseDTO; + readonly stepFailedStatus: MetricsDataFailStatus; } export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceControlConfigurationBranchInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetBranch, setIsGetBranch] = useState(!shouldGetSourceControlConfig); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); + const [info, setInfo] = useState(defaultInfoStructure); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -36,31 +48,43 @@ export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceCon }; setIsLoading(true); try { - const response = await sourceControlClient.getBranch(params); - if (response.code === HttpStatusCode.Ok) { - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - { - name: 'organization', - value: organization, - }, - { - name: 'repo', - value: repo, - }, - ], - names: response.data?.name.map((it) => it), - }), - ); - dispatch( - updateSourceControlConfigurationSettingsFirstInto({ - ...response.data, - id, - type: 'branches', - }), - ); - } + sourceControlClient.getBranch(params).then( + (response) => { + if (response.code === HttpStatusCode.Ok) { + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + { + name: 'organization', + value: organization, + }, + { + name: 'repo', + value: repo, + }, + ], + names: response.data?.name.map((it) => it), + }), + ); + dispatch( + updateSourceControlConfigurationSettingsFirstInto({ + ...response.data, + id, + type: 'branches', + }), + ); + } else { + setInfo(response); + } + }, + (e) => { + if ((e as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + }, + ); } finally { setIsLoading(false); setIsGetBranch(true); @@ -71,5 +95,7 @@ export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceCon isLoading, getSourceControlBranchInfo, isGetBranch, + info, + stepFailedStatus, }; }; diff --git a/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx index bdb243616c..3ec2e07f86 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx @@ -1,27 +1,41 @@ import { DateRange, selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetCrewResponseDTO } from '@src/clients/sourceControl/dto/response'; import { selectShouldGetSourceControlConfig } from '@src/context/Metrics/metricsSlice'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { FULFILLED, SourceControlTypes } from '@src/constants/resources'; +import { FULFILLED, REJECTED, SourceControlTypes } from '@src/constants/resources'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; +import { HttpStatusCode } from 'axios'; import { useState } from 'react'; import dayjs from 'dayjs'; export interface IUseGetSourceControlConfigurationCrewInterface { readonly isLoading: boolean; readonly isGetAllCrews: boolean; + readonly info: ISourceControlGetCrewResponseDTO; readonly getSourceControlCrewInfo: ( organization: string, repo: string, branch: string, dateRanges: DateRange[], ) => Promise; + readonly stepFailedStatus: MetricsDataFailStatus; } + export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceControlConfigurationCrewInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetAllCrews, setIsGetAllCrews] = useState(!shouldGetSourceControlConfig); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); + const [info, setInfo] = useState(defaultInfoStructure); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -51,45 +65,63 @@ export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceContr }), ); + const hasRejected = allCrewsRes.some((crewInfo) => crewInfo.status === REJECTED); + const hasFulfilled = allCrewsRes.some((crewInfo) => crewInfo.status === FULFILLED); + if (!hasRejected) { + setStepFailedStatus(MetricsDataFailStatus.NotFailed); + } else if (hasRejected && hasFulfilled) { + const rejectedStep = allCrewsRes.find((stepInfo) => stepInfo.status === REJECTED); + if ((rejectedStep as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + } + allCrewsRes.forEach((response, index) => { if (response.status === FULFILLED) { - const startTime = dayjs(dateRanges[index].startDate).startOf('date').valueOf(); - const endTime = dayjs(dateRanges[index].endDate).startOf('date').valueOf(); - const parents = [ - { - name: 'organization', - value: organization, - }, - { - name: 'repo', - value: repo, - }, - { - name: 'branch', - value: branch, - }, - ]; - const savedTime = `${startTime}-${endTime}`; - dispatch( - updateSourceControlVerifiedResponse({ - parents: parents, - names: [savedTime], - }), - ); - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - ...parents, - { - name: 'time', - value: savedTime, - }, - ], - names: response.value.data?.crews.map((it) => it), - }), - ); + if (response.value.code !== HttpStatusCode.Ok) { + setInfo(response.value); + } else { + const startTime = dayjs(dateRanges[index].startDate).startOf('date').valueOf(); + const endTime = dayjs(dateRanges[index].endDate).startOf('date').valueOf(); + const parents = [ + { + name: 'organization', + value: organization, + }, + { + name: 'repo', + value: repo, + }, + { + name: 'branch', + value: branch, + }, + ]; + const savedTime = `${startTime}-${endTime}`; + dispatch( + updateSourceControlVerifiedResponse({ + parents: parents, + names: [savedTime], + }), + ); + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + ...parents, + { + name: 'time', + value: savedTime, + }, + ], + names: response.value.data?.crews.map((it) => it), + }), + ); + } } }); + setIsLoading(false); setIsGetAllCrews(true); }; @@ -98,5 +130,7 @@ export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceContr isLoading, getSourceControlCrewInfo, isGetAllCrews, + info, + stepFailedStatus, }; }; diff --git a/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx index 19e77bb7f7..092a43c9af 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx @@ -3,9 +3,12 @@ import { updateSourceControlConfigurationSettingsFirstInto, } from '@src/context/Metrics/metricsSlice'; import { DateRange, selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetRepoResponseDTO } from '@src/clients/sourceControl/dto/response'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { FULFILLED, SourceControlTypes } from '@src/constants/resources'; +import { FULFILLED, REJECTED, SourceControlTypes } from '@src/constants/resources'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; +import { HttpStatusCode } from 'axios'; import { useState } from 'react'; import dayjs from 'dayjs'; @@ -13,13 +16,24 @@ export interface IUseGetSourceControlConfigurationRepoInterface { readonly isLoading: boolean; readonly getSourceControlRepoInfo: (value: string, dateRanges: DateRange[], id: number) => Promise; readonly isGetRepo: boolean; + readonly info: ISourceControlGetRepoResponseDTO; + readonly stepFailedStatus: MetricsDataFailStatus; } + export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceControlConfigurationRepoInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetRepo, setIsGetRepo] = useState(!shouldGetSourceControlConfig); + const [info, setInfo] = useState(defaultInfoStructure); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -40,26 +54,45 @@ export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceContr return sourceControlClient.getRepo(params); }), ); + + const hasRejected = allRepoRes.some((repoInfo) => repoInfo.status === REJECTED); + const hasFulfilled = allRepoRes.some((repoInfo) => repoInfo.status === FULFILLED); + + if (!hasRejected) { + setStepFailedStatus(MetricsDataFailStatus.NotFailed); + } else if (hasRejected && hasFulfilled) { + const rejectedStep = allRepoRes.find((repoInfo) => repoInfo.status === REJECTED); + if ((rejectedStep as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + } + allRepoRes.forEach((response) => { if (response.status === FULFILLED) { - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - { - name: 'organization', - value: organization, - }, - ], - names: response.value.data?.name.map((it) => it), - }), - ); - dispatch( - updateSourceControlConfigurationSettingsFirstInto({ - ...response.value.data, - id, - type: 'repo', - }), - ); + if (response.value.code !== HttpStatusCode.Ok) { + setInfo(response.value); + } else { + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + { + name: 'organization', + value: organization, + }, + ], + names: response.value.data?.name.map((it) => it), + }), + ); + dispatch( + updateSourceControlConfigurationSettingsFirstInto({ + ...response.value.data, + id, + type: 'repo', + }), + ); + } } }); setIsLoading(false); @@ -70,5 +103,7 @@ export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceContr isLoading, getSourceControlRepoInfo, isGetRepo, + info, + stepFailedStatus, }; }; diff --git a/frontend/src/hooks/useShareReportEffect.ts b/frontend/src/hooks/useShareReportEffect.ts index 13c1ccf7e7..abf760cb29 100644 --- a/frontend/src/hooks/useShareReportEffect.ts +++ b/frontend/src/hooks/useShareReportEffect.ts @@ -22,6 +22,7 @@ export const useShareReportEffect = () => { const [metrics, setMetrics] = useState([]); const [classificationNames, setClassificationNames] = useState([]); const [allPipelines, setAllPipelines] = useState([]); + const [allSourceControls, setAllSourceControls] = useState([]); const [isExpired, setIsExpired] = useState(false); const reportPageTimeRangeLoadingStatus = useAppSelector(selectReportPageFailedTimeRangeInfos); @@ -43,6 +44,7 @@ export const useShareReportEffect = () => { setMetrics(reportURLsRes.data.metrics); setAllPipelines(reportURLsRes.data.pipelines); + setAllSourceControls(reportURLsRes.data.sourceControls); setClassificationNames(reportURLsRes.data.classificationNames); setDateRanges(dateRanges); setReportInfos(reportInfos); @@ -125,6 +127,7 @@ export const useShareReportEffect = () => { isExpired, getData, allPipelines, + allSourceControls, allDateRangeLoadingFinished, }; };