fix - 17995 Avoid duplicate teams in team listing hierarchy (#19844)

* fix - 17995 Avoid duplicate teams in team listing hierarchy

* added check in playwright to see if the team is not repeated

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
sonika-shah 2025-02-20 16:44:22 +05:30 committed by GitHub
parent aefc36b596
commit b341d5e1cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 112 additions and 80 deletions

View File

@ -37,7 +37,6 @@ import static org.openmetadata.service.exception.CatalogExceptionMessage.CREATE_
import static org.openmetadata.service.exception.CatalogExceptionMessage.DELETE_ORGANIZATION;
import static org.openmetadata.service.exception.CatalogExceptionMessage.INVALID_GROUP_TEAM_CHILDREN_UPDATE;
import static org.openmetadata.service.exception.CatalogExceptionMessage.INVALID_GROUP_TEAM_UPDATE;
import static org.openmetadata.service.exception.CatalogExceptionMessage.TEAM_HIERARCHY;
import static org.openmetadata.service.exception.CatalogExceptionMessage.UNEXPECTED_PARENT;
import static org.openmetadata.service.exception.CatalogExceptionMessage.invalidChild;
import static org.openmetadata.service.exception.CatalogExceptionMessage.invalidParent;
@ -47,6 +46,7 @@ import static org.openmetadata.service.util.EntityUtil.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -340,51 +340,8 @@ public class TeamRepository extends EntityRepository<Team> {
.withDescription(team.getDescription());
}
private TeamHierarchy deepCopy(TeamHierarchy team) {
TeamHierarchy newTeam =
new TeamHierarchy()
.withId(team.getId())
.withTeamType(team.getTeamType())
.withName(team.getName())
.withDisplayName(team.getDisplayName())
.withHref(team.getHref())
.withFullyQualifiedName(team.getFullyQualifiedName())
.withIsJoinable(team.getIsJoinable());
if (team.getChildren() != null) {
List<TeamHierarchy> children = new ArrayList<>();
for (TeamHierarchy n : team.getChildren()) {
children.add(deepCopy(n));
}
newTeam.withChildren(children);
}
return newTeam;
}
private TeamHierarchy mergeTrees(TeamHierarchy team1, TeamHierarchy team2) {
List<TeamHierarchy> team1Children = team1.getChildren();
List<TeamHierarchy> team2Children = team2.getChildren();
if (team1Children != null && team2Children != null) {
List<TeamHierarchy> toMerge = new ArrayList<>(team1Children);
toMerge.retainAll(team2Children);
for (TeamHierarchy n : toMerge) mergeTrees(n, team2Children.get(team2Children.indexOf(n)));
}
if (team2Children != null) {
List<TeamHierarchy> toAdd = new ArrayList<>(team2Children);
if (team1Children != null) {
toAdd.removeAll(team1Children);
} else {
team1.setChildren(new ArrayList<>());
}
for (TeamHierarchy n : toAdd) team1.getChildren().add(deepCopy(n));
}
return team1;
}
public List<TeamHierarchy> listHierarchy(ListFilter filter, int limit, Boolean isJoinable) {
Fields fields = getFields(PARENTS_FIELD);
Map<UUID, TeamHierarchy> map = new HashMap<>();
ResultList<Team> resultList = listAfter(null, fields, filter, limit, null);
List<Team> allTeams = resultList.getData();
List<Team> joinableTeams =
@ -392,42 +349,61 @@ public class TeamRepository extends EntityRepository<Team> {
.filter(Boolean.TRUE.equals(isJoinable) ? Team::getIsJoinable : t -> true)
.filter(t -> !t.getName().equals(ORGANIZATION_NAME))
.toList();
// build hierarchy of joinable teams
joinableTeams.forEach(
team -> {
Team currentTeam = team;
TeamHierarchy currentHierarchy = getTeamHierarchy(team);
while (currentTeam != null
&& !currentTeam.getParents().isEmpty()
&& !currentTeam.getParents().get(0).getName().equals(ORGANIZATION_NAME)) {
EntityReference parentRef = currentTeam.getParents().get(0);
Team parent =
allTeams.stream()
.filter(t -> t.getId().equals(parentRef.getId()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(TEAM_HIERARCHY));
currentHierarchy =
getTeamHierarchy(parent).withChildren(new ArrayList<>(List.of(currentHierarchy)));
if (map.containsKey(parent.getId())) {
TeamHierarchy parentTeam = map.get(parent.getId());
currentHierarchy = mergeTrees(parentTeam, currentHierarchy);
currentTeam =
allTeams.stream()
.filter(t -> t.getId().equals(parent.getId()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(TEAM_HIERARCHY));
} else {
currentTeam = parent;
}
}
UUID currentId = currentHierarchy.getId();
if (!map.containsKey(currentId)) {
map.put(currentId, currentHierarchy);
} else {
map.put(currentId, mergeTrees(map.get(currentId), currentHierarchy));
}
});
return new ArrayList<>(map.values());
Map<UUID, TeamHierarchy> hierarchyMap = new HashMap<>();
for (Team team : joinableTeams) {
if (!hierarchyMap.containsKey(team.getId())) {
hierarchyMap.put(team.getId(), getTeamHierarchy(team));
}
}
for (Team team : joinableTeams) {
for (EntityReference parentRef : team.getParents()) {
if (parentRef.getName().equals(ORGANIZATION_NAME)) {
continue;
}
Team parentTeam =
allTeams.stream()
.filter(t -> t.getId().equals(parentRef.getId()))
.findFirst()
.orElse(null);
if (parentTeam == null) {
continue;
}
hierarchyMap.putIfAbsent(parentTeam.getId(), getTeamHierarchy(parentTeam));
TeamHierarchy parentNode = hierarchyMap.get(parentTeam.getId());
TeamHierarchy childNode = hierarchyMap.get(team.getId());
if (parentNode.getChildren() == null) {
parentNode.setChildren(new ArrayList<>());
}
boolean childAlreadyAdded =
parentNode.getChildren().stream()
.anyMatch(child -> child.getId().equals(childNode.getId()));
if (!childAlreadyAdded) {
parentNode.getChildren().add(childNode);
}
}
}
Set<UUID> childIds = new HashSet<>();
for (TeamHierarchy node : hierarchyMap.values()) {
if (node.getChildren() != null) {
for (TeamHierarchy child : node.getChildren()) {
childIds.add(child.getId());
}
}
}
List<TeamHierarchy> topLevelNodes =
hierarchyMap.values().stream()
.filter(node -> !childIds.contains(node.getId()))
.sorted(Comparator.comparing(TeamHierarchy::getName))
.collect(Collectors.toList());
return topLevelNodes;
}
private List<EntityReference> getUsers(Team team) {

View File

@ -64,9 +64,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -608,6 +610,47 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
patchEntityAndCheck(bu2, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
void test_hierarchyNoDuplicateForGroupInDept() throws HttpResponseException {
// Team hierarchy: BU -> Division -> Department -> Group
Team bu = createWithParents("buTest-341f887e", BUSINESS_UNIT, ORG_TEAM.getEntityReference());
Team div = createWithParents("divTest-010f23ef", DIVISION, bu.getEntityReference());
Team dept = createWithParents("deptTest-0574ff5c", DEPARTMENT, div.getEntityReference());
Team group = createWithParents("groupTest-148facc0", GROUP, dept.getEntityReference());
List<TeamHierarchy> hierarchyList = getTeamsHierarchy(false, ADMIN_AUTH_HEADERS);
TeamHierarchy buHierarchy =
hierarchyList.stream().filter(t -> t.getId().equals(bu.getId())).findFirst().orElse(null);
assertNotNull(buHierarchy, "BU node should be present in the hierarchy");
TeamHierarchy divHierarchy =
buHierarchy.getChildren().stream()
.filter(t -> t.getId().equals(div.getId()))
.findFirst()
.orElse(null);
assertNotNull(divHierarchy, "Division node should be present under BU");
TeamHierarchy deptHierarchy =
divHierarchy.getChildren().stream()
.filter(t -> t.getId().equals(dept.getId()))
.findFirst()
.orElse(null);
assertNotNull(deptHierarchy, "Department node should be present under Division");
TeamHierarchy groupHierarchy =
deptHierarchy.getChildren().stream()
.filter(t -> t.getId().equals(group.getId()))
.findFirst()
.orElse(null);
assertNotNull(groupHierarchy, "Group node should be present under Department");
// Verify that within each parent's children list, no node is duplicated
for (TeamHierarchy topLevel : hierarchyList) {
verifyNoDuplicateChildrenTeam(topLevel);
}
}
@Test
void patch_isJoinable_200(TestInfo test) throws IOException {
CreateTeam create =
@ -1354,4 +1397,16 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
defaultRoles,
policies);
}
void verifyNoDuplicateChildrenTeam(TeamHierarchy parent) {
if (parent.getChildren() != null) {
Set<UUID> seenChildIds = new HashSet<>();
for (TeamHierarchy child : parent.getChildren()) {
assertTrue(
seenChildIds.add(child.getId()),
"Duplicate child " + child.getId() + " found under parent " + parent.getId());
verifyNoDuplicateChildrenTeam(child);
}
}
}
}

View File

@ -91,6 +91,7 @@ test.describe('Add Nested Teams and Test TeamsSelectable', () => {
const dropdown = page.locator('.ant-tree-select-dropdown');
await expect(dropdown).toContainText(teamName);
await expect(dropdown.getByText(teamName)).toHaveCount(1);
}
for (const teamName of teamNames) {