Skip to content

Commit

Permalink
ADM-813[frontend/backend]: add Advance field in metrics page (#1075)
Browse files Browse the repository at this point in the history
* ADM-797: [backend]feat: add overrideFields to override storyPointKey and flagged key and add relevant tests

* ADM-813: [frontend] feat: add advanced config

* ADM-813: [frontend] feat: display advancedSettings in metrics page

* ADM-813: [frontend] feat: save advancedSettings into config file

* ADM-813: [frontend] feat: could modify advancedSettings in metric page

* ADM-813: [frontend] refactor: export getAdvancedStettings

* ADM-813: [frontend] feat: select whether to add advancedSettings using a checkbox

* ADM-813: [frontend] feat: remove link href

* ADM-813: [frontend] feat: modify getBoardReportRequestBody

* ADM-813[frontend]refactor: change advance to advanced

* ADM-813: [frontend] feat: modify getBoardReportRequestBody

* ADM-813[frontend]refactor: modify test ReportRequestDTO

* ADM-813: [frontend] fix: remove advancedSettings in BoardReportRequestBody

* ADM-813[frontend]test: add test for Advance

* ADM-813[frontend]fix: fix test for save config file

* ADM-813[frontend]test: add test for have data in start

* ADM-813: [frontend] feat: modify test for metricsSlice

* ADM-813[frontend]test: change metricsImportedData advancedSettings to importedAdvancedSettings

* ADM-797:[backend]feat: let the add flag as block logic function work again and modify test

* ADM-797: [backend]refactor: reduce complexity

* ADM-797: [backend]refactor: use equalsIgnorecase instead of toUpperCase

* ADM-813:[frontend]feat: do not store config if the input are all empty

* ADM-813:[frontend]feat: add test for Advance

* ADM-813: [backend]refactor: format code

---------

Co-authored-by: Shiqi Yuan <shiqi.yuan@thoughtworks.com>
Co-authored-by: GuangbinMa <guangbin.ma@thoughtworks.com>
Co-authored-by: junbo.dai <junbo.dai@thoughtworks.com>
  • Loading branch information
4 people authored Feb 28, 2024
1 parent 2522b97 commit af7a7ea
Show file tree
Hide file tree
Showing 18 changed files with 445 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class StoryPointsAndCycleTimeRequest {

private List<TargetField> targetFields;

private List<TargetField> overrideFields;

private boolean treatFlagCardAsBlock;

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ public class JiraBoardSetting {

private List<TargetField> targetFields;

private List<TargetField> overrideFields;

}
155 changes: 94 additions & 61 deletions backend/src/main/java/heartbeat/service/board/jira/JiraService.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Type;
import java.net.URI;
Expand Down Expand Up @@ -233,7 +234,7 @@ public CardCollection getStoryPointsAndCycleTimeForDoneCards(StoryPointsAndCycle
.build();

JiraCardWithFields jiraCardWithFields = getAllDoneCards(boardType, baseUrl, request.getStatus(),
boardRequestParam);
boardRequestParam, request.getOverrideFields());
List<JiraCard> allDoneCards = jiraCardWithFields.getJiraCards();

for (RequestJiraBoardColumnSetting boardColumn : boardColumns) {
Expand Down Expand Up @@ -359,10 +360,10 @@ private List<String> getUsers(BoardType boardType, URI baseUrl, BoardRequestPara
}

private JiraCardWithFields getAllDoneCards(BoardType boardType, URI baseUrl, List<String> doneColumns,
BoardRequestParam boardRequestParam) {
BoardRequestParam boardRequestParam, List<TargetField> overrideFields) {
String jql = parseJiraJql(boardType, doneColumns, boardRequestParam);

return getCardList(baseUrl, boardRequestParam, jql, "done", QUERY_COUNT);
return getCardList(baseUrl, boardRequestParam, jql, "done", overrideFields, QUERY_COUNT);
}

private JiraCardWithFields getAllCards(BoardType boardType, URI baseUrl, BoardRequestParam boardRequestParam) {
Expand All @@ -374,10 +375,11 @@ private JiraCardWithFields getAllCards(BoardType boardType, URI baseUrl, BoardRe
else {
throw new BadRequestException("boardType param is not correct");
}
return getCardList(baseUrl, boardRequestParam, jql, "all", QUERY_COUNT);
return getCardList(baseUrl, boardRequestParam, jql, "all", null, QUERY_COUNT);
}

private AllCardsResponseDTO formatAllCards(String allCardResponse, List<TargetField> targetFields) {
private AllCardsResponseDTO formatAllCards(String allCardResponse, List<TargetField> targetFields,
List<TargetField> overrideFields) {
Gson gson = new Gson();
AllCardsResponseDTO allCardsResponseDTO = gson.fromJson(allCardResponse, AllCardsResponseDTO.class);
List<JiraCard> jiraCards = allCardsResponseDTO.getIssues();
Expand All @@ -388,7 +390,7 @@ private AllCardsResponseDTO formatAllCards(String allCardResponse, List<TargetFi
Map<String, Sprint> sprintMap = new HashMap<>();
Map<String, String> resultMap = targetFields.stream()
.collect(Collectors.toMap(TargetField::getKey, TargetField::getName));
CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields);
CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields, overrideFields);
for (JsonElement element : elements) {
JsonObject jsonElement = element.getAsJsonObject().get("fields").getAsJsonObject();
JsonElement storyPoints = jsonElement.getAsJsonObject().get(cardCustomFieldKey.getStoryPoints());
Expand All @@ -406,45 +408,7 @@ private AllCardsResponseDTO formatAllCards(String allCardResponse, List<TargetFi
jiraCards.get(index).getFields().setStoryPoints(storyPointList.get(index));
}
}
Map<String, JsonElement> customFieldMap = new HashMap<>();
for (Map.Entry<String, String> entry : resultMap.entrySet()) {
String customFieldKey = entry.getKey();
String customFieldValue = entry.getValue();
if (jsonElement.has(customFieldKey)) {
JsonElement fieldValue = jsonElement.get(customFieldKey);
if (customFieldValue.equals("Sprint") && !fieldValue.isJsonNull() && fieldValue.isJsonArray()) {
JsonArray jsonArray = fieldValue.getAsJsonArray();
if (!jsonArray.isJsonNull() && !jsonArray.isEmpty()) {
Type listType = new TypeToken<List<Sprint>>() {
}.getType();
List<Sprint> sprints = gson.fromJson(jsonArray, listType);
sprints.sort(Comparator.comparing(Sprint::getCompleteDate,
Comparator.nullsLast(Comparator.comparing(ZonedDateTime::parse))));
sprintMap.put(element.getAsJsonObject().get("key").getAsString(),
sprints.get(sprints.size() - 1));
}
}
else if (customFieldValue.equals("Story point estimate") && !fieldValue.isJsonNull()
&& fieldValue.isJsonPrimitive()) {
JsonPrimitive jsonPrimitive = fieldValue.getAsJsonPrimitive();
if (jsonPrimitive.isNumber()) {
Number numberValue = jsonPrimitive.getAsNumber();
double doubleValue = numberValue.doubleValue();
fieldValue = new JsonPrimitive(doubleValue);
}
}
else if (customFieldValue.equals("Flagged") && !fieldValue.isJsonNull()
&& fieldValue.isJsonArray()) {
JsonArray jsonArray = fieldValue.getAsJsonArray();
if (!jsonArray.isJsonNull() && !jsonArray.isEmpty()) {
JsonElement targetField = jsonArray.get(jsonArray.size() - 1);
fieldValue = targetField.getAsJsonObject().get("value");
}
}
customFieldMap.put(customFieldKey, fieldValue);
}
}
customFieldMapList.add(customFieldMap);
customFieldMapList.add(getCustomfieldMap(gson, sprintMap, resultMap, element, jsonElement));
}
for (int index = 0; index < customFieldMapList.size(); index++) {
jiraCards.get(index).getFields().setCustomFields(customFieldMapList.get(index));
Expand All @@ -457,6 +421,48 @@ else if (customFieldValue.equals("Flagged") && !fieldValue.isJsonNull()
return allCardsResponseDTO;
}

private static Map<String, JsonElement> getCustomfieldMap(Gson gson, Map<String, Sprint> sprintMap,
Map<String, String> resultMap, JsonElement element, JsonObject jsonElement) {
Map<String, JsonElement> customFieldMap = new HashMap<>();
for (Map.Entry<String, String> entry : resultMap.entrySet()) {
String customFieldKey = entry.getKey();
String customFieldValue = entry.getValue();
if (jsonElement.has(customFieldKey)) {
JsonElement fieldValue = jsonElement.get(customFieldKey);
if (customFieldValue.equals("Sprint") && !fieldValue.isJsonNull() && fieldValue.isJsonArray()) {
JsonArray jsonArray = fieldValue.getAsJsonArray();
if (!jsonArray.isJsonNull() && !jsonArray.isEmpty()) {
Type listType = new TypeToken<List<Sprint>>() {
}.getType();
List<Sprint> sprints = gson.fromJson(jsonArray, listType);
sprints.sort(Comparator.comparing(Sprint::getCompleteDate,
Comparator.nullsLast(Comparator.comparing(ZonedDateTime::parse))));
sprintMap.put(element.getAsJsonObject().get("key").getAsString(),
sprints.get(sprints.size() - 1));
}
}
else if (customFieldValue.equals("Story point estimate") && !fieldValue.isJsonNull()
&& fieldValue.isJsonPrimitive()) {
JsonPrimitive jsonPrimitive = fieldValue.getAsJsonPrimitive();
if (jsonPrimitive.isNumber()) {
Number numberValue = jsonPrimitive.getAsNumber();
double doubleValue = numberValue.doubleValue();
fieldValue = new JsonPrimitive(doubleValue);
}
}
else if (customFieldValue.equals("Flagged") && !fieldValue.isJsonNull() && fieldValue.isJsonArray()) {
JsonArray jsonArray = fieldValue.getAsJsonArray();
if (!jsonArray.isJsonNull() && !jsonArray.isEmpty()) {
JsonElement targetField = jsonArray.get(jsonArray.size() - 1);
fieldValue = targetField.getAsJsonObject().get("value");
}
}
customFieldMap.put(customFieldKey, fieldValue);
}
}
return customFieldMap;
}

private String parseJiraJql(BoardType boardType, List<String> doneColumns, BoardRequestParam boardRequestParam) {
if (boardType == BoardType.JIRA) {
return String.format("status in ('%s') AND status changed during (%s, %s)", String.join("','", doneColumns),
Expand Down Expand Up @@ -533,7 +539,7 @@ private List<JiraCardDTO> getRealDoneCards(StoryPointsAndCycleTimeRequest reques
List<RequestJiraBoardColumnSetting> boardColumns, List<String> users, URI baseUrl,
List<JiraCard> allDoneCards, List<TargetField> targetFields, String filterMethod) {

CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields);
CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields, request.getOverrideFields());
String keyFlagged = cardCustomFieldKey.getFlagged();
List<JiraCardDTO> realDoneCards = new ArrayList<>();
List<JiraCard> jiraCards = new ArrayList<>();
Expand Down Expand Up @@ -723,7 +729,7 @@ private CardCycleTime calculateCardCycleTime(String cardId, List<CycleTimeInfo>
double total = 0;
for (CycleTimeInfo cycleTimeInfo : cycleTimeInfos) {
String swimLane = cycleTimeInfo.getColumn();
if (swimLane.equals("FLAG")) {
if (swimLane.equalsIgnoreCase(CardStepsEnum.BLOCK.getValue())) {
boardMap.put(swimLane, CardStepsEnum.BLOCK);
}
if (boardMap.containsKey(swimLane)) {
Expand Down Expand Up @@ -758,7 +764,7 @@ private CardCycleTime calculateCardCycleTime(String cardId, List<CycleTimeInfo>
return CardCycleTime.builder().name(cardId).steps(stepsDay).total(total).build();
}

private CardCustomFieldKey covertCustomFieldKey(List<TargetField> model) {
private CardCustomFieldKey covertCustomFieldKey(List<TargetField> model, List<TargetField> overrideFields) {
CardCustomFieldKey cardCustomFieldKey = CardCustomFieldKey.builder().build();
for (TargetField value : model) {
String lowercaseName = value.getName().toLowerCase();
Expand All @@ -770,6 +776,29 @@ private CardCustomFieldKey covertCustomFieldKey(List<TargetField> model) {
}
}
}
if (!CollectionUtils.isEmpty(overrideFields)) {

String storyPointsKey = overrideFields.stream()
.filter(targetField -> ("story points").equalsIgnoreCase(targetField.getName()))
.map(TargetField::getKey)
.filter(key -> !key.isEmpty())
.findFirst()
.orElse("");

String flaggedKey = overrideFields.stream()
.filter(targetField -> ("flagged").equalsIgnoreCase(targetField.getName()))
.map(TargetField::getKey)
.filter(key -> !key.isEmpty())
.findFirst()
.orElse("");

if (!storyPointsKey.isEmpty()) {
cardCustomFieldKey.setStoryPoints(storyPointsKey);
}
if (!flaggedKey.isEmpty()) {
cardCustomFieldKey.setFlagged(flaggedKey);
}
}
Map<String, String> envMap = systemUtil.getEnvMap();
if (Objects.nonNull(envMap.get(STORY_POINT_KEY))) {
cardCustomFieldKey.setStoryPoints(envMap.get(STORY_POINT_KEY));
Expand All @@ -790,10 +819,11 @@ public CardCollection getStoryPointsAndCycleTimeForNonDoneCards(StoryPointsAndCy
.build();

JiraCardWithFields jiraCardWithFields = getAllNonDoneCardsForActiveSprint(baseUrl, request.getStatus(),
boardRequestParam);
boardRequestParam, request.getOverrideFields());

if (jiraCardWithFields.getJiraCards().isEmpty()) {
jiraCardWithFields = getAllNonDoneCardsForKanBan(baseUrl, request.getStatus(), boardRequestParam);
jiraCardWithFields = getAllNonDoneCardsForKanBan(baseUrl, request.getStatus(), boardRequestParam,
request.getOverrideFields());
}

List<JiraCardDTO> matchedNonCards = getMatchedNonDoneCards(request, boardColumns, users, baseUrl,
Expand All @@ -814,7 +844,7 @@ private List<JiraCardDTO> getMatchedNonDoneCards(StoryPointsAndCycleTimeRequest
List<JiraCard> allNonDoneCards, List<TargetField> targetFields) {

List<JiraCardDTO> matchedCards = new ArrayList<>();
CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields);
CardCustomFieldKey cardCustomFieldKey = covertCustomFieldKey(targetFields, request.getOverrideFields());
String keyFlagged = cardCustomFieldKey.getFlagged();

allNonDoneCards.forEach(card -> {
Expand Down Expand Up @@ -869,7 +899,7 @@ private JiraCardDTO buildJiraCardDTO(JiraCard card, CycleTimeInfoDTO cycleTimeIn
}

private JiraCardWithFields getAllNonDoneCardsForActiveSprint(URI baseUrl, List<String> status,
BoardRequestParam boardRequestParam) {
BoardRequestParam boardRequestParam, List<TargetField> overrideFields) {
String jql;
if (status.isEmpty()) {
jql = "sprint in openSprints() ORDER BY updated DESC";
Expand All @@ -879,23 +909,25 @@ private JiraCardWithFields getAllNonDoneCardsForActiveSprint(URI baseUrl, List<S
+ "') ORDER BY updated DESC";
}

return getCardList(baseUrl, boardRequestParam, jql, NONE_DONE_CARD_TAG, NONE_DONE_MAX_QUERY_COUNT);
return getCardList(baseUrl, boardRequestParam, jql, NONE_DONE_CARD_TAG, overrideFields,
NONE_DONE_MAX_QUERY_COUNT);
}

private JiraCardWithFields getAllNonDoneCardsForKanBan(URI baseUrl, List<String> status,
BoardRequestParam boardRequestParam) {
BoardRequestParam boardRequestParam, List<TargetField> overrideFields) {
String jql;
if (status.isEmpty()) {
jql = "ORDER BY updated DESC";
}
else {
jql = "status not in ('" + String.join("','", status) + "') ORDER BY updated DESC";
}
return getCardList(baseUrl, boardRequestParam, jql, NONE_DONE_CARD_TAG, NONE_DONE_MAX_QUERY_COUNT);
return getCardList(baseUrl, boardRequestParam, jql, NONE_DONE_CARD_TAG, overrideFields,
NONE_DONE_MAX_QUERY_COUNT);
}

private JiraCardWithFields getCardList(URI baseUrl, BoardRequestParam boardRequestParam, String jql,
String cardType, int queryCount) {
String cardType, List<TargetField> overrideFields, int queryCount) {
log.info("Start to get first-page xxx card information form kanban, _param {}", cardType);
String allCardResponse = jiraFeignClient.getJiraCards(baseUrl, boardRequestParam.getBoardId(), queryCount, 0,
jql, boardRequestParam.getToken());
Expand All @@ -905,7 +937,7 @@ private JiraCardWithFields getCardList(URI baseUrl, BoardRequestParam boardReque
log.info("Successfully get first-page xxx card information form kanban, _param {}", cardType);

List<TargetField> targetField = getTargetField(baseUrl, boardRequestParam);
AllCardsResponseDTO allCardsResponseDTO = formatAllCards(allCardResponse, targetField);
AllCardsResponseDTO allCardsResponseDTO = formatAllCards(allCardResponse, targetField, overrideFields);

List<JiraCard> cards = new ArrayList<>(new LinkedHashSet<>(allCardsResponseDTO.getIssues()));
int pages = (int) Math.ceil(Double.parseDouble(allCardsResponseDTO.getTotal()) / QUERY_COUNT);
Expand All @@ -916,10 +948,11 @@ private JiraCardWithFields getCardList(URI baseUrl, BoardRequestParam boardReque
log.info("Start to get more xxx card information form kanban, _param {}", cardType);
List<Integer> range = IntStream.rangeClosed(1, pages - 1).boxed().toList();
List<CompletableFuture<AllCardsResponseDTO>> futures = range.stream()
.map(startFrom -> CompletableFuture.supplyAsync(
() -> (formatAllCards(jiraFeignClient.getJiraCards(baseUrl, boardRequestParam.getBoardId(),
QUERY_COUNT, startFrom * QUERY_COUNT, jql, boardRequestParam.getToken()), targetField)),
customTaskExecutor))
.map(startFrom -> CompletableFuture
.supplyAsync(() -> (formatAllCards(
jiraFeignClient.getJiraCards(baseUrl, boardRequestParam.getBoardId(), QUERY_COUNT,
startFrom * QUERY_COUNT, jql, boardRequestParam.getToken()),
targetField, overrideFields)), customTaskExecutor))
.toList();
log.info("Successfully get more xxx card information form kanban, _param {}", cardType);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ private static StoryPointsAndCycleTimeRequest buildStoryPointsAndCycleTimeReques
.startTime(startTime)
.endTime(endTime)
.targetFields(jiraBoardSetting.getTargetFields())
.overrideFields(jiraBoardSetting.getOverrideFields())
.treatFlagCardAsBlock(jiraBoardSetting.getTreatFlagCardAsBlock())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,24 @@ public static FieldResponseDTO.FieldResponseDTOBuilder ALL_FIELD_RESPONSE_BUILDE
return FieldResponseDTO.builder().projects(List.of(new Project(List.of(new Issuetype(issueFieldMap)))));
}

public static FieldResponseDTO.FieldResponseDTOBuilder ALL_FIELD_RESPONSE_BUILDER_HAS_STORY_POINT() {
IssueField timetrackingIssueField = new IssueField("timetracking", "Time tracking");
IssueField summaryIssueField = new IssueField("summary", "Summary");
IssueField descriptionIssueField = new IssueField("description", "Description");
IssueField priorityIssueField = new IssueField("priority", "Priority");
IssueField flaggedIssueField = new IssueField("customfield_10021", "Flagged");
IssueField storyPointIssueField = new IssueField("customfield_10006", "story points");
HashMap<String, IssueField> issueFieldMap = new HashMap<>();
issueFieldMap.put("timetracking", timetrackingIssueField);
issueFieldMap.put("summary", summaryIssueField);
issueFieldMap.put("description", descriptionIssueField);
issueFieldMap.put("priority", priorityIssueField);
issueFieldMap.put("customfield_10021", flaggedIssueField);
issueFieldMap.put("customfield_10006", storyPointIssueField);

return FieldResponseDTO.builder().projects(List.of(new Project(List.of(new Issuetype(issueFieldMap)))));
}

public static FieldResponseDTO.FieldResponseDTOBuilder INCLUDE_UNREASONABLE_FIELD_RESPONSE_BUILDER() {
IssueField timetrackingIssueField = new IssueField("timetracking", "Time tracking");
IssueField priorityIssueField = new IssueField("priority", "Priority");
Expand Down Expand Up @@ -570,7 +588,7 @@ public static List<CycleTimeInfo> CYCLE_TIME_INFO_LIST() {
CycleTimeInfo.builder().column("REVIEW").day(4.0).build(),
CycleTimeInfo.builder().column("ANALYSIS").day(9.0).build(),
CycleTimeInfo.builder().column(UNKNOWN).day(5.0).build(),
CycleTimeInfo.builder().column(FLAG).day(6.0).build());
CycleTimeInfo.builder().column("BLOCK").day(6.0).build());
}

public static JiraBoardSetting.JiraBoardSettingBuilder JIRA_BOARD_SETTING_WITH_HISTORICAL_ASSIGNEE_FILTER_METHOD() {
Expand Down
Loading

0 comments on commit af7a7ea

Please sign in to comment.