mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-09 15:56:33 +00:00
* Backend support for domain hierarchy listing * use linkedHashmap to maintain order of results in hierarchy * Revert changes to generated files to match main * show all domains for non admin user * change select domain layout * fix multiple save * fix playwright * cleanup * fix domain tests * fix domain tests * fix icon styling * show icons on navbar domain list * update tests --------- Co-authored-by: karanh37 <karanh37@gmail.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
This commit is contained in:
parent
41b1ec081d
commit
5b63c36ef1
@ -18,10 +18,15 @@ import static org.openmetadata.schema.type.Include.ALL;
|
||||
import static org.openmetadata.service.Entity.DOMAIN;
|
||||
import static org.openmetadata.service.Entity.FIELD_ASSETS;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jdbi.v3.sqlobject.transaction.Transaction;
|
||||
import org.openmetadata.schema.EntityInterface;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy;
|
||||
import org.openmetadata.schema.entity.domains.Domain;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
@ -30,8 +35,11 @@ import org.openmetadata.schema.type.api.BulkAssets;
|
||||
import org.openmetadata.schema.type.api.BulkOperationResult;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.domains.DomainResource;
|
||||
import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.EntityUtil.Fields;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.ResultList;
|
||||
|
||||
@Slf4j
|
||||
public class DomainRepository extends EntityRepository<Domain> {
|
||||
@ -140,6 +148,47 @@ public class DomainRepository extends EntityRepository<Domain> {
|
||||
: null;
|
||||
}
|
||||
|
||||
public List<EntityHierarchy> buildHierarchy(String fieldsParam, int limit) {
|
||||
fieldsParam = EntityUtil.addField(fieldsParam, Entity.FIELD_PARENT);
|
||||
Fields fields = getFields(fieldsParam);
|
||||
ResultList<Domain> resultList = listAfter(null, fields, new ListFilter(null), limit, null);
|
||||
List<Domain> domains = resultList.getData();
|
||||
|
||||
/*
|
||||
Maintaining hierarchy in terms of EntityHierarchy to get all other fields of Domain like style,
|
||||
which would have been restricted if built using hierarchy of Domain, as Domain.getChildren() returns List<EntityReference>
|
||||
and EntityReference does not support additional properties
|
||||
*/
|
||||
List<EntityHierarchy> rootDomains = new ArrayList<>();
|
||||
|
||||
Map<UUID, EntityHierarchy> entityHierarchyMap =
|
||||
domains.stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Domain::getId,
|
||||
domain -> {
|
||||
EntityHierarchy entityHierarchy =
|
||||
JsonUtils.readValue(JsonUtils.pojoToJson(domain), EntityHierarchy.class);
|
||||
entityHierarchy.setChildren(new ArrayList<>());
|
||||
return entityHierarchy;
|
||||
}));
|
||||
|
||||
for (Domain domain : domains) {
|
||||
EntityHierarchy entityHierarchy = entityHierarchyMap.get(domain.getId());
|
||||
|
||||
if (domain.getParent() != null) {
|
||||
EntityHierarchy parentHierarchy = entityHierarchyMap.get(domain.getParent().getId());
|
||||
if (parentHierarchy != null) {
|
||||
parentHierarchy.getChildren().add(entityHierarchy);
|
||||
}
|
||||
} else {
|
||||
rootDomains.add(entityHierarchy);
|
||||
}
|
||||
}
|
||||
|
||||
return rootDomains;
|
||||
}
|
||||
|
||||
public class DomainUpdater extends EntityUpdater {
|
||||
public DomainUpdater(Domain original, Domain updated, Operation operation) {
|
||||
super(original, updated, operation);
|
||||
|
@ -45,6 +45,7 @@ import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.schema.api.domains.CreateDomain;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy;
|
||||
import org.openmetadata.schema.entity.domains.Domain;
|
||||
import org.openmetadata.schema.type.ChangeEvent;
|
||||
import org.openmetadata.schema.type.EntityHistory;
|
||||
@ -57,6 +58,7 @@ import org.openmetadata.service.limits.Limits;
|
||||
import org.openmetadata.service.resources.Collection;
|
||||
import org.openmetadata.service.resources.EntityResource;
|
||||
import org.openmetadata.service.security.Authorizer;
|
||||
import org.openmetadata.service.util.EntityHierarchyList;
|
||||
import org.openmetadata.service.util.ResultList;
|
||||
|
||||
@Slf4j
|
||||
@ -433,4 +435,32 @@ public class DomainResource extends EntityResource<Domain, DomainRepository> {
|
||||
String name) {
|
||||
return deleteByName(uriInfo, securityContext, name, true, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/hierarchy")
|
||||
@Operation(
|
||||
operationId = "listDomainsHierarchy",
|
||||
summary = "List domains in hierarchical order",
|
||||
description = "Get a list of Domains in hierarchical order.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of Domains in hierarchical order",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = EntityHierarchyList.class)))
|
||||
})
|
||||
public ResultList<EntityHierarchy> listHierarchy(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(
|
||||
description = "Fields requested in the returned resource",
|
||||
schema = @Schema(type = "string", example = FIELDS))
|
||||
@QueryParam("fields")
|
||||
String fieldsParam,
|
||||
@DefaultValue("10") @Min(0) @Max(1000000) @QueryParam("limit") int limitParam) {
|
||||
|
||||
return new EntityHierarchyList(repository.buildHierarchy(fieldsParam, limitParam));
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.json.JsonObject;
|
||||
import javax.net.ssl.SSLContext;
|
||||
@ -470,50 +471,10 @@ public class ElasticSearchClient implements SearchClient {
|
||||
.getIndexMapping(GLOSSARY_TERM)
|
||||
.getIndexName(clusterAlias))) {
|
||||
searchSourceBuilder.query(QueryBuilders.boolQuery().must(searchSourceBuilder.query()));
|
||||
|
||||
if (request.isGetHierarchy()) {
|
||||
QueryBuilder baseQuery =
|
||||
QueryBuilders.boolQuery()
|
||||
.should(searchSourceBuilder.query())
|
||||
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()))
|
||||
.should(
|
||||
QueryBuilders.matchPhraseQuery(
|
||||
"glossary.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"))
|
||||
.minimumShouldMatch(1);
|
||||
searchSourceBuilder.query(baseQuery);
|
||||
|
||||
SearchResponse searchResponse =
|
||||
client.search(
|
||||
new es.org.elasticsearch.action.search.SearchRequest(request.getIndex())
|
||||
.source(searchSourceBuilder),
|
||||
RequestOptions.DEFAULT);
|
||||
|
||||
// Extract parent terms from aggregation
|
||||
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();
|
||||
Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");
|
||||
|
||||
// Build es query to get parent terms for the user input query , to build correct hierarchy
|
||||
if (!parentTerms.getBuckets().isEmpty() && !request.getQuery().equals("*")) {
|
||||
parentTerms.getBuckets().stream()
|
||||
.map(Terms.Bucket::getKeyAsString)
|
||||
.forEach(
|
||||
parentTerm ->
|
||||
parentTermQueryBuilder.should(
|
||||
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));
|
||||
|
||||
searchSourceBuilder.query(
|
||||
parentTermQueryBuilder
|
||||
.minimumShouldMatch(1)
|
||||
.must(QueryBuilders.matchQuery("status", "Approved")));
|
||||
}
|
||||
searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
|
||||
}
|
||||
}
|
||||
|
||||
buildHierarchyQuery(request, searchSourceBuilder, client);
|
||||
|
||||
/* for performance reasons ElasticSearch doesn't provide accurate hits
|
||||
if we enable trackTotalHits parameter it will try to match every result, count and return hits
|
||||
however in most cases for search results an approximate value is good enough.
|
||||
@ -581,15 +542,89 @@ public class ElasticSearchClient implements SearchClient {
|
||||
return getResponse(NOT_FOUND, "Document not found.");
|
||||
}
|
||||
|
||||
private void buildHierarchyQuery(
|
||||
SearchRequest request, SearchSourceBuilder searchSourceBuilder, RestHighLevelClient client)
|
||||
throws IOException {
|
||||
|
||||
if (!request.isGetHierarchy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String indexName = request.getIndex();
|
||||
String glossaryTermIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
|
||||
String domainIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);
|
||||
|
||||
BoolQueryBuilder baseQuery =
|
||||
QueryBuilders.boolQuery()
|
||||
.should(searchSourceBuilder.query())
|
||||
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()));
|
||||
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
baseQuery
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"));
|
||||
} else if (indexName.equalsIgnoreCase(domainIndex)) {
|
||||
baseQuery
|
||||
.should(QueryBuilders.matchPhraseQuery("parent.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("parent.displayName", request.getQuery()));
|
||||
}
|
||||
|
||||
baseQuery.minimumShouldMatch(1);
|
||||
searchSourceBuilder.query(baseQuery);
|
||||
|
||||
SearchResponse searchResponse =
|
||||
client.search(
|
||||
new es.org.elasticsearch.action.search.SearchRequest(request.getIndex())
|
||||
.source(searchSourceBuilder),
|
||||
RequestOptions.DEFAULT);
|
||||
|
||||
Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");
|
||||
|
||||
// Build es query to get parent terms for the user input query , to build correct hierarchy
|
||||
// In case of default search , no need to get parent terms they are already present in the
|
||||
// response
|
||||
if (parentTerms != null
|
||||
&& !parentTerms.getBuckets().isEmpty()
|
||||
&& !request.getQuery().equals("*")) {
|
||||
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();
|
||||
|
||||
parentTerms.getBuckets().stream()
|
||||
.map(Terms.Bucket::getKeyAsString)
|
||||
.forEach(
|
||||
parentTerm ->
|
||||
parentTermQueryBuilder.should(
|
||||
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
parentTermQueryBuilder
|
||||
.minimumShouldMatch(1)
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"));
|
||||
} else {
|
||||
parentTermQueryBuilder.minimumShouldMatch(1);
|
||||
}
|
||||
searchSourceBuilder.query(parentTermQueryBuilder);
|
||||
}
|
||||
|
||||
searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
|
||||
}
|
||||
|
||||
public List<?> buildSearchHierarchy(SearchRequest request, SearchResponse searchResponse) {
|
||||
List<?> response = new ArrayList<>();
|
||||
if (request
|
||||
.getIndex()
|
||||
.equalsIgnoreCase(
|
||||
Entity.getSearchRepository()
|
||||
.getIndexMapping(GLOSSARY_TERM)
|
||||
.getIndexName(clusterAlias))) {
|
||||
|
||||
String indexName = request.getIndex();
|
||||
String glossaryTermIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
|
||||
String domainIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);
|
||||
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
response = buildGlossaryTermSearchHierarchy(searchResponse);
|
||||
} else if (indexName.equalsIgnoreCase(domainIndex)) {
|
||||
response = buildDomainSearchHierarchy(searchResponse);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@ -645,6 +680,38 @@ public class ElasticSearchClient implements SearchClient {
|
||||
return new ArrayList<>(rootTerms.values());
|
||||
}
|
||||
|
||||
public List<EntityHierarchy> buildDomainSearchHierarchy(SearchResponse searchResponse) {
|
||||
Map<String, EntityHierarchy> entityHierarchyMap =
|
||||
Arrays.stream(searchResponse.getHits().getHits())
|
||||
.map(hit -> JsonUtils.readValue(hit.getSourceAsString(), EntityHierarchy.class))
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
EntityHierarchy::getFullyQualifiedName,
|
||||
entity -> {
|
||||
entity.setChildren(new ArrayList<>());
|
||||
return entity;
|
||||
},
|
||||
(existing, replacement) -> existing,
|
||||
LinkedHashMap::new));
|
||||
|
||||
List<EntityHierarchy> rootDomains = new ArrayList<>();
|
||||
|
||||
entityHierarchyMap
|
||||
.values()
|
||||
.forEach(
|
||||
entity -> {
|
||||
String parentFqn = getParentFQN(entity.getFullyQualifiedName());
|
||||
EntityHierarchy parentEntity = entityHierarchyMap.get(parentFqn);
|
||||
if (parentEntity != null) {
|
||||
parentEntity.getChildren().add(entity);
|
||||
} else {
|
||||
rootDomains.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
return rootDomains;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResultListMapper listWithOffset(
|
||||
String filter,
|
||||
@ -1819,7 +1886,10 @@ public class ElasticSearchClient implements SearchClient {
|
||||
buildSearchQueryBuilder(query, DomainIndex.getFields());
|
||||
FunctionScoreQueryBuilder queryBuilder = boostScore(queryStringBuilder);
|
||||
HighlightBuilder hb = buildHighlights(new ArrayList<>());
|
||||
return searchBuilder(queryBuilder, hb, from, size);
|
||||
SearchSourceBuilder searchSourceBuilder = searchBuilder(queryBuilder, hb, from, size);
|
||||
searchSourceBuilder.aggregation(
|
||||
AggregationBuilders.terms("fqnParts_agg").field("fqnParts").size(1000));
|
||||
return addAggregation(searchSourceBuilder);
|
||||
}
|
||||
|
||||
private static SearchSourceBuilder buildCostAnalysisReportDataSearch(
|
||||
|
@ -47,6 +47,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.json.JsonObject;
|
||||
import javax.net.ssl.SSLContext;
|
||||
@ -463,53 +464,10 @@ public class OpenSearchClient implements SearchClient {
|
||||
.getIndexMapping(GLOSSARY_TERM)
|
||||
.getIndexName(clusterAlias))) {
|
||||
searchSourceBuilder.query(QueryBuilders.boolQuery().must(searchSourceBuilder.query()));
|
||||
|
||||
if (request.isGetHierarchy()) {
|
||||
/*
|
||||
Search for user input terms in name, fullyQualifiedName, displayName and glossary.fullyQualifiedName, glossary.displayName
|
||||
*/
|
||||
QueryBuilder baseQuery =
|
||||
QueryBuilders.boolQuery()
|
||||
.should(searchSourceBuilder.query())
|
||||
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()))
|
||||
.should(
|
||||
QueryBuilders.matchPhraseQuery(
|
||||
"glossary.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"))
|
||||
.minimumShouldMatch(1);
|
||||
searchSourceBuilder.query(baseQuery);
|
||||
|
||||
SearchResponse searchResponse =
|
||||
client.search(
|
||||
new os.org.opensearch.action.search.SearchRequest(request.getIndex())
|
||||
.source(searchSourceBuilder),
|
||||
RequestOptions.DEFAULT);
|
||||
|
||||
// Extract parent terms from aggregation
|
||||
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();
|
||||
Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");
|
||||
|
||||
// Build es query to get parent terms for the user input query , to build correct hierarchy
|
||||
if (!parentTerms.getBuckets().isEmpty() && !request.getQuery().equals("*")) {
|
||||
parentTerms.getBuckets().stream()
|
||||
.map(Terms.Bucket::getKeyAsString)
|
||||
.forEach(
|
||||
parentTerm ->
|
||||
parentTermQueryBuilder.should(
|
||||
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));
|
||||
|
||||
searchSourceBuilder.query(
|
||||
parentTermQueryBuilder
|
||||
.minimumShouldMatch(1)
|
||||
.must(QueryBuilders.matchQuery("status", "Approved")));
|
||||
}
|
||||
searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
|
||||
}
|
||||
}
|
||||
|
||||
buildHierarchyQuery(request, searchSourceBuilder, client);
|
||||
|
||||
/* for performance reasons OpenSearch doesn't provide accurate hits
|
||||
if we enable trackTotalHits parameter it will try to match every result, count and return hits
|
||||
however in most cases for search results an approximate value is good enough.
|
||||
@ -572,15 +530,88 @@ public class OpenSearchClient implements SearchClient {
|
||||
return getResponse(NOT_FOUND, "Document not found.");
|
||||
}
|
||||
|
||||
private void buildHierarchyQuery(
|
||||
SearchRequest request, SearchSourceBuilder searchSourceBuilder, RestHighLevelClient client)
|
||||
throws IOException {
|
||||
|
||||
if (!request.isGetHierarchy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String indexName = request.getIndex();
|
||||
String glossaryTermIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
|
||||
String domainIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);
|
||||
|
||||
BoolQueryBuilder baseQuery =
|
||||
QueryBuilders.boolQuery()
|
||||
.should(searchSourceBuilder.query())
|
||||
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()));
|
||||
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
baseQuery
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"));
|
||||
} else if (indexName.equalsIgnoreCase(domainIndex)) {
|
||||
baseQuery
|
||||
.should(QueryBuilders.matchPhraseQuery("parent.fullyQualifiedName", request.getQuery()))
|
||||
.should(QueryBuilders.matchPhraseQuery("parent.displayName", request.getQuery()));
|
||||
}
|
||||
|
||||
baseQuery.minimumShouldMatch(1);
|
||||
searchSourceBuilder.query(baseQuery);
|
||||
|
||||
SearchResponse searchResponse =
|
||||
client.search(
|
||||
new os.org.opensearch.action.search.SearchRequest(request.getIndex())
|
||||
.source(searchSourceBuilder),
|
||||
RequestOptions.DEFAULT);
|
||||
|
||||
Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");
|
||||
|
||||
// Build es query to get parent terms for the user input query , to build correct hierarchy
|
||||
// In case of default search , no need to get parent terms they are already present in the
|
||||
// response
|
||||
if (parentTerms != null
|
||||
&& !parentTerms.getBuckets().isEmpty()
|
||||
&& !request.getQuery().equals("*")) {
|
||||
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();
|
||||
|
||||
parentTerms.getBuckets().stream()
|
||||
.map(Terms.Bucket::getKeyAsString)
|
||||
.forEach(
|
||||
parentTerm ->
|
||||
parentTermQueryBuilder.should(
|
||||
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
parentTermQueryBuilder
|
||||
.minimumShouldMatch(1)
|
||||
.must(QueryBuilders.matchQuery("status", "Approved"));
|
||||
} else {
|
||||
parentTermQueryBuilder.minimumShouldMatch(1);
|
||||
}
|
||||
searchSourceBuilder.query(parentTermQueryBuilder);
|
||||
}
|
||||
|
||||
searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
|
||||
}
|
||||
|
||||
public List<?> buildSearchHierarchy(SearchRequest request, SearchResponse searchResponse) {
|
||||
List<?> response = new ArrayList<>();
|
||||
if (request
|
||||
.getIndex()
|
||||
.equalsIgnoreCase(
|
||||
Entity.getSearchRepository()
|
||||
.getIndexMapping(GLOSSARY_TERM)
|
||||
.getIndexName(clusterAlias))) {
|
||||
String indexName = request.getIndex();
|
||||
String glossaryTermIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
|
||||
String domainIndex =
|
||||
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);
|
||||
|
||||
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
|
||||
response = buildGlossaryTermSearchHierarchy(searchResponse);
|
||||
} else if (indexName.equalsIgnoreCase(domainIndex)) {
|
||||
response = buildDomainSearchHierarchy(searchResponse);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@ -636,6 +667,38 @@ public class OpenSearchClient implements SearchClient {
|
||||
return new ArrayList<>(rootTerms.values());
|
||||
}
|
||||
|
||||
public List<EntityHierarchy> buildDomainSearchHierarchy(SearchResponse searchResponse) {
|
||||
Map<String, EntityHierarchy> entityHierarchyMap =
|
||||
Arrays.stream(searchResponse.getHits().getHits())
|
||||
.map(hit -> JsonUtils.readValue(hit.getSourceAsString(), EntityHierarchy.class))
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
EntityHierarchy::getFullyQualifiedName,
|
||||
entity -> {
|
||||
entity.setChildren(new ArrayList<>());
|
||||
return entity;
|
||||
},
|
||||
(existing, replacement) -> existing,
|
||||
LinkedHashMap::new));
|
||||
|
||||
List<EntityHierarchy> rootDomains = new ArrayList<>();
|
||||
|
||||
entityHierarchyMap
|
||||
.values()
|
||||
.forEach(
|
||||
entity -> {
|
||||
String parentFqn = getParentFQN(entity.getFullyQualifiedName());
|
||||
EntityHierarchy parentEntity = entityHierarchyMap.get(parentFqn);
|
||||
if (parentEntity != null) {
|
||||
parentEntity.getChildren().add(entity);
|
||||
} else {
|
||||
rootDomains.add(entity);
|
||||
}
|
||||
});
|
||||
|
||||
return rootDomains;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResultListMapper listWithOffset(
|
||||
String filter,
|
||||
|
@ -0,0 +1,13 @@
|
||||
package org.openmetadata.service.util;
|
||||
|
||||
import java.util.List;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy;
|
||||
|
||||
public class EntityHierarchyList extends ResultList<EntityHierarchy> {
|
||||
@SuppressWarnings("unused")
|
||||
public EntityHierarchyList() {}
|
||||
|
||||
public EntityHierarchyList(List<EntityHierarchy> data) {
|
||||
super(data, null, null, data.size());
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ package org.openmetadata.service.resources.domains;
|
||||
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
import static org.openmetadata.service.Entity.TABLE;
|
||||
import static org.openmetadata.service.security.SecurityUtil.authHeaders;
|
||||
@ -17,15 +19,18 @@ import static org.openmetadata.service.util.TestUtils.assertListNull;
|
||||
import static org.openmetadata.service.util.TestUtils.assertResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.openmetadata.schema.api.domains.CreateDomain;
|
||||
import org.openmetadata.schema.api.domains.CreateDomain.DomainType;
|
||||
import org.openmetadata.schema.entity.data.EntityHierarchy;
|
||||
import org.openmetadata.schema.entity.domains.Domain;
|
||||
import org.openmetadata.schema.entity.type.Style;
|
||||
import org.openmetadata.schema.type.ChangeDescription;
|
||||
@ -35,7 +40,9 @@ import org.openmetadata.service.exception.EntityNotFoundException;
|
||||
import org.openmetadata.service.jdbi3.TableRepository;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
import org.openmetadata.service.resources.domains.DomainResource.DomainList;
|
||||
import org.openmetadata.service.util.EntityHierarchyList;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.TestUtils;
|
||||
|
||||
public class DomainResourceTest extends EntityResourceTest<Domain, CreateDomain> {
|
||||
public DomainResourceTest() {
|
||||
@ -143,6 +150,214 @@ public class DomainResourceTest extends EntityResourceTest<Domain, CreateDomain>
|
||||
String.format("user instance for %s not found", expertReference.getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_buildDomainNestedHierarchyFromSearch() throws HttpResponseException {
|
||||
CreateDomain createRootDomain =
|
||||
createRequest("rootDomain")
|
||||
.withDisplayName("Global Headquarters")
|
||||
.withDescription("Root Domain")
|
||||
.withDomainType(DomainType.AGGREGATE)
|
||||
.withStyle(null)
|
||||
.withExperts(null);
|
||||
Domain rootDomain = createEntity(createRootDomain, ADMIN_AUTH_HEADERS);
|
||||
|
||||
CreateDomain createSecondLevelDomain =
|
||||
createRequest("secondLevelDomain")
|
||||
.withDisplayName("Operations Hub")
|
||||
.withDescription("Second Level Domain")
|
||||
.withDomainType(DomainType.AGGREGATE)
|
||||
.withStyle(null)
|
||||
.withExperts(null)
|
||||
.withParent(rootDomain.getFullyQualifiedName());
|
||||
Domain secondLevelDomain = createEntity(createSecondLevelDomain, ADMIN_AUTH_HEADERS);
|
||||
|
||||
CreateDomain createThirdLevelDomain =
|
||||
createRequest("thirdLevelDomain")
|
||||
.withDisplayName("Innovation Center")
|
||||
.withDescription("Third Level Domain")
|
||||
.withDomainType(DomainType.AGGREGATE)
|
||||
.withStyle(null)
|
||||
.withExperts(null)
|
||||
.withParent(secondLevelDomain.getFullyQualifiedName());
|
||||
Domain thirdLevelDomain = createEntity(createThirdLevelDomain, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Search for the displayName of third-level child domain and verify the hierarchy
|
||||
String response = getResponseFormSearchWithHierarchy("domain_search_index", "*innovation*");
|
||||
List<EntityHierarchy> domains = JsonUtils.readObjects(response, EntityHierarchy.class);
|
||||
|
||||
boolean isChild =
|
||||
domains.stream()
|
||||
.filter(domain -> "rootDomain".equals(domain.getName()))
|
||||
.findFirst()
|
||||
.map(
|
||||
root ->
|
||||
root.getChildren().stream()
|
||||
.filter(domain -> "secondLevelDomain".equals(domain.getName()))
|
||||
.flatMap(secondLevel -> secondLevel.getChildren().stream())
|
||||
.anyMatch(thirdLevel -> "thirdLevelDomain".equals(thirdLevel.getName())))
|
||||
.orElse(false);
|
||||
|
||||
assertTrue(
|
||||
isChild,
|
||||
"thirdLevelDomain should be a child of secondLevelDomain, which should be a child of rootDomain");
|
||||
|
||||
// Search for the fqn of third-level child domain and verify the hierarchy
|
||||
response = getResponseFormSearchWithHierarchy("domain_search_index", "*third*");
|
||||
domains = JsonUtils.readObjects(response, EntityHierarchy.class);
|
||||
|
||||
isChild =
|
||||
domains.stream()
|
||||
.filter(domain -> "rootDomain".equals(domain.getName()))
|
||||
.findFirst()
|
||||
.map(
|
||||
root ->
|
||||
root.getChildren().stream()
|
||||
.filter(domain -> "secondLevelDomain".equals(domain.getName()))
|
||||
.flatMap(secondLevel -> secondLevel.getChildren().stream())
|
||||
.anyMatch(thirdLevel -> "thirdLevelDomain".equals(thirdLevel.getName())))
|
||||
.orElse(false);
|
||||
|
||||
assertTrue(
|
||||
isChild,
|
||||
"thirdLevelDomain should be a child of secondLevelDomain, which should be a child of rootDomain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void get_hierarchicalListOfDomain(TestInfo test) throws HttpResponseException {
|
||||
Domain rootDomain = createEntity(createRequest("A_ROOT_DOMAIN"), ADMIN_AUTH_HEADERS);
|
||||
Domain subDomain1 =
|
||||
createEntity(
|
||||
createRequest("A_subDomain1").withParent(rootDomain.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
Domain subDomain2 =
|
||||
createEntity(
|
||||
createRequest("A_subDomain2").withParent(rootDomain.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
Domain subDomain3 =
|
||||
createEntity(
|
||||
createRequest("A_subDomain3").withParent(rootDomain.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Ensure parent has all the newly created children
|
||||
rootDomain = getEntity(rootDomain.getId(), "children,parent", ADMIN_AUTH_HEADERS);
|
||||
assertEntityReferences(
|
||||
new ArrayList<>(
|
||||
List.of(
|
||||
subDomain1.getEntityReference(),
|
||||
subDomain2.getEntityReference(),
|
||||
subDomain3.getEntityReference())),
|
||||
rootDomain.getChildren());
|
||||
|
||||
Domain subSubDomain1 =
|
||||
createEntity(
|
||||
createRequest("A_subSubDomain11").withParent(subDomain1.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
Domain subSubDomain2 =
|
||||
createEntity(
|
||||
createRequest("A_subSubDomain12").withParent(subDomain1.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
Domain subSubDomain3 =
|
||||
createEntity(
|
||||
createRequest("A_subSubDomain13").withParent(subDomain1.getFullyQualifiedName()),
|
||||
ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Ensure parent has all the newly created children
|
||||
subDomain1 = getEntity(subDomain1.getId(), "children,parent", ADMIN_AUTH_HEADERS);
|
||||
assertEntityReferences(
|
||||
new ArrayList<>(
|
||||
List.of(
|
||||
subSubDomain1.getEntityReference(),
|
||||
subSubDomain2.getEntityReference(),
|
||||
subSubDomain3.getEntityReference())),
|
||||
subDomain1.getChildren());
|
||||
assertParent(subSubDomain1, subDomain1.getEntityReference());
|
||||
assertParent(subSubDomain2, subDomain1.getEntityReference());
|
||||
assertParent(subSubDomain3, subDomain1.getEntityReference());
|
||||
|
||||
// Create another root domain without hierarchy
|
||||
Domain secondRootDomain = createEntity(createRequest("B_ROOT_DOMAIN"), ADMIN_AUTH_HEADERS);
|
||||
|
||||
List<EntityHierarchy> hierarchyList = getDomainsHierarchy(ADMIN_AUTH_HEADERS).getData();
|
||||
|
||||
UUID rootDomainId = rootDomain.getId();
|
||||
UUID subDomain1Id = subDomain1.getId();
|
||||
UUID subDomain2Id = subDomain2.getId();
|
||||
UUID subDomain3Id = subDomain3.getId();
|
||||
UUID subSubDomain1Id = subSubDomain1.getId();
|
||||
UUID subSubDomain2Id = subSubDomain2.getId();
|
||||
UUID subSubDomain3Id = subSubDomain3.getId();
|
||||
UUID secondRootDomainId = secondRootDomain.getId();
|
||||
|
||||
EntityHierarchy rootHierarchy =
|
||||
hierarchyList.stream().filter(h -> h.getId().equals(rootDomainId)).findAny().orElse(null);
|
||||
assertNotNull(rootHierarchy);
|
||||
assertEquals(3, rootHierarchy.getChildren().size());
|
||||
|
||||
List<EntityHierarchy> rootChildren = rootHierarchy.getChildren();
|
||||
assertEquals(3, rootChildren.size());
|
||||
assertTrue(rootChildren.stream().anyMatch(h -> h.getId().equals(subDomain1Id)));
|
||||
assertTrue(rootChildren.stream().anyMatch(h -> h.getId().equals(subDomain2Id)));
|
||||
assertTrue(rootChildren.stream().anyMatch(h -> h.getId().equals(subDomain3Id)));
|
||||
|
||||
EntityHierarchy subDomain1Hierarchy =
|
||||
rootChildren.stream().filter(h -> h.getId().equals(subDomain1Id)).findAny().orElse(null);
|
||||
assertNotNull(subDomain1Hierarchy);
|
||||
assertEquals(3, subDomain1Hierarchy.getChildren().size());
|
||||
|
||||
List<EntityHierarchy> subDomain1Children = subDomain1Hierarchy.getChildren();
|
||||
assertTrue(subDomain1Children.stream().anyMatch(h -> h.getId().equals(subSubDomain1Id)));
|
||||
assertTrue(subDomain1Children.stream().anyMatch(h -> h.getId().equals(subSubDomain2Id)));
|
||||
assertTrue(subDomain1Children.stream().anyMatch(h -> h.getId().equals(subSubDomain3Id)));
|
||||
|
||||
EntityHierarchy subSubDomain1Hierarchy =
|
||||
subDomain1Children.stream()
|
||||
.filter(h -> h.getId().equals(subSubDomain1Id))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
assertNotNull(subSubDomain1Hierarchy);
|
||||
assertEquals(0, subSubDomain1Hierarchy.getChildren().size());
|
||||
|
||||
EntityHierarchy subSubDomain2Hierarchy =
|
||||
subDomain1Children.stream()
|
||||
.filter(h -> h.getId().equals(subSubDomain2Id))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
assertNotNull(subSubDomain2Hierarchy);
|
||||
assertEquals(0, subSubDomain2Hierarchy.getChildren().size());
|
||||
|
||||
EntityHierarchy subSubDomain3Hierarchy =
|
||||
subDomain1Children.stream()
|
||||
.filter(h -> h.getId().equals(subSubDomain3Id))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
assertNotNull(subSubDomain3Hierarchy);
|
||||
assertEquals(0, subSubDomain3Hierarchy.getChildren().size());
|
||||
|
||||
// Verify the new root domain without hierarchy
|
||||
EntityHierarchy secondRootDomainHierarchy =
|
||||
hierarchyList.stream()
|
||||
.filter(h -> h.getId().equals(secondRootDomainId))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
assertNotNull(secondRootDomainHierarchy);
|
||||
assertEquals(0, secondRootDomainHierarchy.getChildren().size());
|
||||
}
|
||||
|
||||
private void assertParent(Domain domain, EntityReference expectedParent)
|
||||
throws HttpResponseException {
|
||||
assertEquals(expectedParent, domain.getParent());
|
||||
// Ensure the parent has the given domain as a child
|
||||
Domain parent = getEntity(expectedParent.getId(), "children", ADMIN_AUTH_HEADERS);
|
||||
assertEntityReferencesContain(parent.getChildren(), domain.getEntityReference());
|
||||
}
|
||||
|
||||
private EntityHierarchyList getDomainsHierarchy(Map<String, String> authHeaders)
|
||||
throws HttpResponseException {
|
||||
WebTarget target = getResource("domains/hierarchy");
|
||||
target = target.queryParam("limit", 25);
|
||||
return TestUtils.get(target, EntityHierarchyList.class, authHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateDomain createRequest(String name) {
|
||||
return new CreateDomain()
|
||||
|
@ -141,9 +141,13 @@ test.describe('Domains', () => {
|
||||
const dataProduct1 = new DataProduct(domain);
|
||||
const dataProduct2 = new DataProduct(domain);
|
||||
await domain.create(apiContext);
|
||||
await sidebarClick(page, SidebarItem.DOMAIN);
|
||||
await page.reload();
|
||||
await addAssetsToDomain(page, domain, assets);
|
||||
|
||||
await test.step('Add assets to domain', async () => {
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.DOMAIN);
|
||||
await addAssetsToDomain(page, domain, assets);
|
||||
});
|
||||
|
||||
await test.step('Create DataProducts', async () => {
|
||||
await selectDomain(page, domain.data);
|
||||
@ -582,7 +586,7 @@ test.describe('Domains Rbac', () => {
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const qParam = urlParams.get('q');
|
||||
|
||||
expect(qParam).toContain(`domain.fullyQualifiedName:`);
|
||||
expect(qParam).toEqual('');
|
||||
});
|
||||
|
||||
for (const asset of domainAssset2) {
|
||||
|
@ -115,7 +115,7 @@ entities.forEach((EntityClass) => {
|
||||
},
|
||||
false
|
||||
);
|
||||
await removeDomain(page);
|
||||
await removeDomain(page, EntityDataClass.domain1.responseData);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -348,6 +348,6 @@ export class DatabaseClass extends EntityClass {
|
||||
await assignDomain(page, domain1);
|
||||
await this.verifyDomainPropagation(page, domain1);
|
||||
await updateDomain(page, domain2);
|
||||
await removeDomain(page);
|
||||
await removeDomain(page, domain2);
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ export class EntityClass {
|
||||
) {
|
||||
await assignDomain(page, domain1);
|
||||
await updateDomain(page, domain2);
|
||||
await removeDomain(page);
|
||||
await removeDomain(page, domain2);
|
||||
}
|
||||
|
||||
async owner(
|
||||
|
@ -152,7 +152,7 @@ export const visitOwnProfilePage = async (page: Page) => {
|
||||
|
||||
export const assignDomain = async (
|
||||
page: Page,
|
||||
domain: { name: string; displayName: string }
|
||||
domain: { name: string; displayName: string; fullyQualifiedName?: string }
|
||||
) => {
|
||||
await page.getByTestId('add-domain').click();
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
@ -160,11 +160,13 @@ export const assignDomain = async (
|
||||
`/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*`
|
||||
);
|
||||
await page
|
||||
.getByTestId('selectable-list')
|
||||
.getByTestId('domain-selectable-tree')
|
||||
.getByTestId('searchbar')
|
||||
.fill(domain.name);
|
||||
await searchDomain;
|
||||
await page.getByRole('listitem', { name: domain.displayName }).click();
|
||||
|
||||
await page.getByTestId(`tag-${domain.fullyQualifiedName}`).click();
|
||||
await page.getByTestId('saveAssociatedTag').click();
|
||||
|
||||
await expect(page.getByTestId('domain-link')).toContainText(
|
||||
domain.displayName
|
||||
@ -173,33 +175,42 @@ export const assignDomain = async (
|
||||
|
||||
export const updateDomain = async (
|
||||
page: Page,
|
||||
domain: { name: string; displayName: string }
|
||||
domain: { name: string; displayName: string; fullyQualifiedName?: string }
|
||||
) => {
|
||||
await page.getByTestId('add-domain').click();
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
await page.getByTestId('selectable-list').getByTestId('searchbar').clear();
|
||||
|
||||
await page
|
||||
.getByTestId('domain-selectable-tree')
|
||||
.getByTestId('searchbar')
|
||||
.clear();
|
||||
|
||||
const searchDomain = page.waitForResponse(
|
||||
`/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*`
|
||||
);
|
||||
await page
|
||||
.getByTestId('selectable-list')
|
||||
.getByTestId('domain-selectable-tree')
|
||||
.getByTestId('searchbar')
|
||||
.fill(domain.name);
|
||||
await searchDomain;
|
||||
await page.getByRole('listitem', { name: domain.displayName }).click();
|
||||
|
||||
await page.getByTestId(`tag-${domain.fullyQualifiedName}`).click();
|
||||
await page.getByTestId('saveAssociatedTag').click();
|
||||
|
||||
await expect(page.getByTestId('domain-link')).toContainText(
|
||||
domain.displayName
|
||||
);
|
||||
};
|
||||
|
||||
export const removeDomain = async (page: Page) => {
|
||||
export const removeDomain = async (
|
||||
page: Page,
|
||||
domain: { name: string; displayName: string; fullyQualifiedName?: string }
|
||||
) => {
|
||||
await page.getByTestId('add-domain').click();
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
|
||||
await expect(page.getByTestId('remove-owner').locator('path')).toBeVisible();
|
||||
|
||||
await page.getByTestId('remove-owner').locator('svg').click();
|
||||
await page.getByTestId(`tag-${domain.fullyQualifiedName}`).click();
|
||||
await page.getByTestId('saveAssociatedTag').click();
|
||||
|
||||
await expect(page.getByTestId('no-domain-text')).toContainText('No Domain');
|
||||
};
|
||||
|
@ -0,0 +1,13 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0089 7.46018C10.0089 8.72267 9.70565 9.85293 9.22814 10.6596C8.74866 11.4698 8.11725 11.9204 7.46019 11.9204C6.80312 11.9204 6.17171 11.4698 5.69223 10.6596C5.21472 9.85293 4.91151 8.72302 4.91151 7.46018C4.91151 6.19768 5.21472 5.06742 5.69223 4.26074C6.17171 3.45057 6.80312 3 7.46019 3C8.11725 3 8.74866 3.45057 9.22814 4.26074C9.70532 5.06707 10.0089 6.19768 10.0089 7.46018Z" stroke="#292929" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<path d="M3.39484 9.23071C4.84275 9.75337 8.53057 10.4844 11.7025 9.23071" stroke="#292929" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<path d="M11.7025 5.76921C10.2546 5.24656 6.56679 4.51553 3.39484 5.76921" stroke="#292929" stroke-opacity="0.35" stroke-width="0.6"/>
|
||||
<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="#292929" stroke-width="0.6"/>
|
||||
<path d="M3 7.20771H11.9204M7.46018 11.9204V3" stroke="#292929" stroke-opacity="0.4" stroke-width="0.6"/>
|
||||
<path d="M15.6726 14.9735C15.6726 15.8151 15.4704 16.5686 15.1521 17.1064C14.8324 17.6465 14.4115 17.9469 13.9735 17.9469C13.5354 17.9469 13.1145 17.6465 12.7948 17.1064C12.4765 16.5686 12.2743 15.8153 12.2743 14.9735C12.2743 14.1318 12.4765 13.3783 12.7948 12.8405C13.1145 12.3004 13.5354 12 13.9735 12C14.4115 12 14.8324 12.3004 15.1521 12.8405C15.4702 13.378 15.6726 14.1318 15.6726 14.9735Z" stroke="#292929" stroke-opacity="0.4" stroke-width="0.5"/>
|
||||
<path d="M11.2632 16.1538C12.2285 16.5022 14.6871 16.9896 16.8017 16.1538" stroke="#292929" stroke-opacity="0.4" stroke-width="0.5"/>
|
||||
<path d="M16.8017 13.8462C15.8364 13.4977 13.3779 13.0104 11.2632 13.8462" stroke="#292929" stroke-opacity="0.35" stroke-width="0.5"/>
|
||||
<path d="M14 18C15.6569 18 17 16.6569 17 15C17 13.3431 15.6569 12 14 12C12.3431 12 11 13.3431 11 15C11 16.6569 12.3431 18 14 18Z" stroke="#292929" stroke-width="0.5"/>
|
||||
<path d="M11 14.8051H16.9469M13.9735 17.9469V12" stroke="#292929" stroke-opacity="0.4" stroke-width="0.5"/>
|
||||
<path d="M8 13V14C8 14.5523 8.44772 15 9 15H10" stroke="black" stroke-width="0.5" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -40,6 +40,7 @@ import { useHistory, useParams } from 'react-router-dom';
|
||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-delete.svg';
|
||||
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
|
||||
import { ReactComponent as SubDomainIcon } from '../../../assets/svg/ic-subdomain.svg';
|
||||
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
|
||||
import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg';
|
||||
import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg';
|
||||
@ -638,6 +639,41 @@ const DomainDetailsPage = ({
|
||||
fetchSubDomains();
|
||||
}, [domainFqn]);
|
||||
|
||||
const iconData = useMemo(() => {
|
||||
if (domain.style?.iconURL) {
|
||||
return (
|
||||
<img
|
||||
alt="domain-icon"
|
||||
className="align-middle"
|
||||
data-testid="icon"
|
||||
height={36}
|
||||
src={domain.style.iconURL}
|
||||
width={32}
|
||||
/>
|
||||
);
|
||||
} else if (isSubDomain) {
|
||||
return (
|
||||
<SubDomainIcon
|
||||
className="align-middle"
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={36}
|
||||
name="folder"
|
||||
width={32}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DomainIcon
|
||||
className="align-middle"
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={36}
|
||||
name="folder"
|
||||
width={32}
|
||||
/>
|
||||
);
|
||||
}, [domain, isSubDomain]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row
|
||||
@ -649,26 +685,7 @@ const DomainDetailsPage = ({
|
||||
breadcrumb={breadcrumbs}
|
||||
entityData={{ ...domain, displayName, name }}
|
||||
entityType={EntityType.DOMAIN}
|
||||
icon={
|
||||
domain.style?.iconURL ? (
|
||||
<img
|
||||
alt="domain-icon"
|
||||
className="align-middle"
|
||||
data-testid="icon"
|
||||
height={36}
|
||||
src={domain.style.iconURL}
|
||||
width={32}
|
||||
/>
|
||||
) : (
|
||||
<DomainIcon
|
||||
className="align-middle"
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={36}
|
||||
name="folder"
|
||||
width={32}
|
||||
/>
|
||||
)
|
||||
}
|
||||
icon={iconData}
|
||||
serviceName=""
|
||||
titleColor={domain.style?.color}
|
||||
/>
|
||||
|
@ -82,7 +82,7 @@ import Fqn from '../../../utils/Fqn';
|
||||
import {
|
||||
buildTree,
|
||||
findExpandableKeysForArray,
|
||||
findGlossaryTermByFqn,
|
||||
findItemByFqn,
|
||||
glossaryTermTableColumnsWidth,
|
||||
StatusClass,
|
||||
} from '../../../utils/GlossaryUtils';
|
||||
@ -672,10 +672,7 @@ const GlossaryTermTab = ({
|
||||
);
|
||||
const terms = cloneDeep(glossaryTerms) ?? [];
|
||||
|
||||
const item = findGlossaryTermByFqn(
|
||||
terms,
|
||||
record.fullyQualifiedName ?? ''
|
||||
);
|
||||
const item = findItemByFqn(terms, record.fullyQualifiedName ?? '');
|
||||
|
||||
(item as ModifiedGlossary).children = data;
|
||||
|
||||
|
@ -43,7 +43,7 @@ import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import {
|
||||
convertGlossaryTermsToTreeOptions,
|
||||
filterTreeNodeOptions,
|
||||
findGlossaryTermByFqn,
|
||||
findItemByFqn,
|
||||
} from '../../../utils/GlossaryUtils';
|
||||
import {
|
||||
escapeESReservedCharacters,
|
||||
@ -200,7 +200,7 @@ const TreeAsyncSelectList: FC<Omit<AsyncSelectListProps, 'fetchOptions'>> = ({
|
||||
if (lastSelectedMap.has(value)) {
|
||||
return lastSelectedMap.get(value) as SelectOption;
|
||||
}
|
||||
const initialData = findGlossaryTermByFqn(
|
||||
const initialData = findItemByFqn(
|
||||
[
|
||||
...glossaries,
|
||||
...(isNull(searchOptions) ? [] : searchOptions),
|
||||
|
@ -15,26 +15,12 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
|
||||
import {
|
||||
DE_ACTIVE_COLOR,
|
||||
PAGE_SIZE_MEDIUM,
|
||||
} from '../../../constants/constants';
|
||||
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
|
||||
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import { getDomainList } from '../../../rest/domainAPI';
|
||||
import { searchData } from '../../../rest/miscAPI';
|
||||
import { formatDomainsResponse } from '../../../utils/APIUtils';
|
||||
import { Transi18next } from '../../../utils/CommonUtils';
|
||||
import {
|
||||
getEntityName,
|
||||
getEntityReferenceListFromEntities,
|
||||
} from '../../../utils/EntityUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import Fqn from '../../../utils/Fqn';
|
||||
import { getDomainPath } from '../../../utils/RouterUtils';
|
||||
import { SelectableList } from '../SelectableList/SelectableList.component';
|
||||
import DomainSelectablTree from '../DomainSelectableTree/DomainSelectableTree';
|
||||
import './domain-select-dropdown.less';
|
||||
import { DomainSelectableListProps } from './DomainSelectableList.interface';
|
||||
|
||||
@ -74,57 +60,18 @@ const DomainSelectableList = ({
|
||||
multiple = false,
|
||||
}: DomainSelectableListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useApplicationStore();
|
||||
const [popupVisible, setPopupVisible] = useState(false);
|
||||
|
||||
const selectedDomainsList = useMemo(() => {
|
||||
if (selectedDomain) {
|
||||
return Array.isArray(selectedDomain) ? selectedDomain : [selectedDomain];
|
||||
return Array.isArray(selectedDomain)
|
||||
? selectedDomain.map((item) => item.fullyQualifiedName)
|
||||
: [selectedDomain.fullyQualifiedName];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [selectedDomain]);
|
||||
|
||||
const fetchOptions = async (searchText: string, after?: string) => {
|
||||
if (searchText) {
|
||||
try {
|
||||
const res = await searchData(
|
||||
searchText,
|
||||
1,
|
||||
PAGE_SIZE_MEDIUM,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
SearchIndex.DOMAIN
|
||||
);
|
||||
|
||||
const data = getEntityReferenceListFromEntities(
|
||||
formatDomainsResponse(res.data.hits.hits),
|
||||
EntityType.DOMAIN
|
||||
);
|
||||
|
||||
return { data, paging: { total: res.data.hits.total.value } };
|
||||
} catch (error) {
|
||||
return { data: [], paging: { total: 0 } };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { data, paging } = await getDomainList({
|
||||
limit: PAGE_SIZE_MEDIUM,
|
||||
after: after ?? undefined,
|
||||
});
|
||||
const filterData = getEntityReferenceListFromEntities(
|
||||
data,
|
||||
EntityType.DOMAIN
|
||||
);
|
||||
|
||||
return { data: filterData, paging };
|
||||
} catch (error) {
|
||||
return { data: [], paging: { total: 0 } };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (domains: EntityReference[]) => {
|
||||
if (multiple) {
|
||||
@ -132,6 +79,7 @@ const DomainSelectableList = ({
|
||||
} else {
|
||||
await onUpdate(domains[0]);
|
||||
}
|
||||
|
||||
setPopupVisible(false);
|
||||
},
|
||||
[onUpdate, multiple]
|
||||
@ -146,39 +94,16 @@ const DomainSelectableList = ({
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={
|
||||
<SelectableList
|
||||
customTagRenderer={DomainListItemRenderer}
|
||||
emptyPlaceholderText={
|
||||
<Transi18next
|
||||
i18nKey="message.no-domain-available"
|
||||
renderElement={
|
||||
<a
|
||||
href={getDomainPath()}
|
||||
rel="noreferrer"
|
||||
style={{ color: theme.primaryColor }}
|
||||
target="_blank"
|
||||
/>
|
||||
}
|
||||
values={{
|
||||
link: t('label.domain-plural'),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
fetchOptions={fetchOptions}
|
||||
multiSelect={multiple}
|
||||
removeIconTooltipLabel={t('label.remove-entity', {
|
||||
entity: t('label.domain-lowercase'),
|
||||
})}
|
||||
searchPlaceholder={t('label.search-for-type', {
|
||||
type: t('label.domain'),
|
||||
})}
|
||||
selectedItems={selectedDomainsList}
|
||||
<DomainSelectablTree
|
||||
isMultiple={multiple}
|
||||
value={selectedDomainsList as string[]}
|
||||
visible={popupVisible}
|
||||
onCancel={() => setPopupVisible(false)}
|
||||
onUpdate={handleUpdate}
|
||||
onSubmit={handleUpdate}
|
||||
/>
|
||||
}
|
||||
open={popupVisible}
|
||||
overlayClassName="domain-select-popover"
|
||||
overlayClassName="domain-select-popover w-400"
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
trigger="click"
|
||||
|
@ -15,7 +15,11 @@
|
||||
|
||||
.domain-select-popover {
|
||||
min-width: 275px;
|
||||
padding-top: 0;
|
||||
|
||||
&.ant-popover {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DefaultOptionType } from 'antd/lib/select';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
|
||||
export interface DomainSelectableTreeProps {
|
||||
value?: string[]; // array of fqn
|
||||
onSubmit: (option: EntityReference[]) => Promise<void>;
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
isMultiple?: boolean;
|
||||
}
|
||||
|
||||
export type TreeListItem = Omit<DefaultOptionType, 'label'>;
|
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, Empty, Space, Tree } from 'antd';
|
||||
import Search from 'antd/lib/input/Search';
|
||||
import { AxiosError } from 'axios';
|
||||
import { debounce } from 'lodash';
|
||||
import React, {
|
||||
FC,
|
||||
Key,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconDown } from '../../../assets/svg/ic-arrow-down.svg';
|
||||
import { ReactComponent as IconRight } from '../../../assets/svg/ic-arrow-right.svg';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { Domain } from '../../../generated/entity/domains/domain';
|
||||
import { EntityReference } from '../../../generated/tests/testCase';
|
||||
import { listDomainHierarchy, searchDomains } from '../../../rest/domainAPI';
|
||||
import { convertDomainsToTreeOptions } from '../../../utils/DomainUtils';
|
||||
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
|
||||
import { findItemByFqn } from '../../../utils/GlossaryUtils';
|
||||
import {
|
||||
escapeESReservedCharacters,
|
||||
getEncodedFqn,
|
||||
} from '../../../utils/StringsUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import Loader from '../Loader/Loader';
|
||||
import './domain-selectable.less';
|
||||
import {
|
||||
DomainSelectableTreeProps,
|
||||
TreeListItem,
|
||||
} from './DomainSelectableTree.interface';
|
||||
|
||||
const DomainSelectablTree: FC<DomainSelectableTreeProps> = ({
|
||||
onSubmit,
|
||||
value,
|
||||
visible,
|
||||
onCancel,
|
||||
isMultiple = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [treeData, setTreeData] = useState<TreeListItem[]>([]);
|
||||
const [domains, setDomains] = useState<Domain[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||
const [selectedDomains, setSelectedDomains] = useState<Key[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSubmitLoading(true);
|
||||
if (isMultiple) {
|
||||
const selectedData = [];
|
||||
for (const item of selectedDomains) {
|
||||
selectedData.push(findItemByFqn(domains, item as string, false));
|
||||
}
|
||||
const domains1 = selectedData.map((item) =>
|
||||
getEntityReferenceFromEntity(item as EntityReference, EntityType.DOMAIN)
|
||||
);
|
||||
await onSubmit(domains1);
|
||||
} else {
|
||||
let retn: EntityReference[] = [];
|
||||
if (selectedDomains.length > 0) {
|
||||
const initialData = findItemByFqn(
|
||||
domains,
|
||||
selectedDomains[0] as string,
|
||||
false
|
||||
);
|
||||
const domain = getEntityReferenceFromEntity(
|
||||
initialData as EntityReference,
|
||||
EntityType.DOMAIN
|
||||
);
|
||||
retn = [domain];
|
||||
}
|
||||
await onSubmit(retn);
|
||||
}
|
||||
|
||||
setIsSubmitLoading(false);
|
||||
};
|
||||
|
||||
const fetchAPI = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await listDomainHierarchy({ limit: 100 });
|
||||
setTreeData(convertDomainsToTreeOptions(data.data, 0, isMultiple));
|
||||
setDomains(data.data);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSelect = (selectedKeys: React.Key[]) => {
|
||||
if (!isMultiple) {
|
||||
setSelectedDomains(selectedKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const onCheck = (
|
||||
checked: Key[] | { checked: Key[]; halfChecked: Key[] }
|
||||
): void => {
|
||||
if (Array.isArray(checked)) {
|
||||
setSelectedDomains(checked);
|
||||
} else {
|
||||
setSelectedDomains(checked.checked);
|
||||
}
|
||||
};
|
||||
|
||||
const onSearch = debounce(async (value: string) => {
|
||||
setSearchTerm(value);
|
||||
if (value) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const encodedValue = getEncodedFqn(escapeESReservedCharacters(value));
|
||||
const results: Domain[] = await searchDomains(encodedValue);
|
||||
const updatedTreeData = convertDomainsToTreeOptions(
|
||||
results,
|
||||
0,
|
||||
isMultiple
|
||||
);
|
||||
setTreeData(updatedTreeData);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
fetchAPI();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const switcherIcon = useCallback(({ expanded }) => {
|
||||
return expanded ? <IconDown /> : <IconRight />;
|
||||
}, []);
|
||||
|
||||
const treeContent = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
} else if (treeData.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
description={t('label.no-entity-available', {
|
||||
entity: t('label.domain'),
|
||||
})}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tree
|
||||
blockNode
|
||||
checkStrictly
|
||||
defaultExpandAll
|
||||
showLine
|
||||
autoExpandParent={Boolean(searchTerm)}
|
||||
checkable={isMultiple}
|
||||
className="domain-selectable-tree"
|
||||
defaultCheckedKeys={isMultiple ? value : []}
|
||||
defaultExpandedKeys={value}
|
||||
defaultSelectedKeys={isMultiple ? [] : value}
|
||||
multiple={isMultiple}
|
||||
switcherIcon={switcherIcon}
|
||||
treeData={treeData}
|
||||
onCheck={onCheck}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [isLoading, treeData, value, onSelect, isMultiple, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setSearchTerm('');
|
||||
fetchAPI();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div className="p-sm" data-testid="domain-selectable-tree">
|
||||
<Search
|
||||
data-testid="searchbar"
|
||||
placeholder="Search"
|
||||
style={{ marginBottom: 8 }}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
{treeContent}
|
||||
|
||||
<Space className="p-sm p-b-xss p-l-xs custom-dropdown-render" size={8}>
|
||||
<Button
|
||||
className="update-btn"
|
||||
data-testid="saveAssociatedTag"
|
||||
// disabled={isEmpty(glossaries)}
|
||||
htmlType="submit"
|
||||
loading={isSubmitLoading}
|
||||
size="small"
|
||||
type="default"
|
||||
onClick={() => handleSave()}>
|
||||
{t('label.update')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancelAssociatedTag"
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={onCancel}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainSelectablTree;
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@import (reference) url('../../../styles/variables.less');
|
||||
|
||||
.domain-selectable-tree {
|
||||
overflow: auto;
|
||||
height: 270px;
|
||||
|
||||
.ant-tree-treenode {
|
||||
.ant-tree-node-content-wrapper {
|
||||
&.ant-tree-node-selected {
|
||||
background-color: @radio-button-checked-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ export const useDomainStore = create<DomainStore>()(
|
||||
activeDomain: DEFAULT_DOMAIN_VALUE, // Set default value here
|
||||
activeDomainEntityRef: undefined,
|
||||
domainOptions: [],
|
||||
updateDomains: (data: Domain[], selectDefault = true) => {
|
||||
updateDomains: (data: Domain[]) => {
|
||||
const currentUser = useApplicationStore.getState().currentUser;
|
||||
const { isAdmin = false, domains = [] } = currentUser ?? {};
|
||||
const userDomainsObj = isAdmin ? [] : domains;
|
||||
@ -51,19 +51,9 @@ export const useDomainStore = create<DomainStore>()(
|
||||
set({
|
||||
domains: filteredDomains,
|
||||
domainOptions: getDomainOptions(
|
||||
isAdmin ? filteredDomains : userDomainsObj,
|
||||
isAdmin
|
||||
isAdmin ? filteredDomains : userDomainsObj
|
||||
),
|
||||
});
|
||||
|
||||
if (
|
||||
selectDefault &&
|
||||
!isAdmin &&
|
||||
userDomainsObj.length > 0 &&
|
||||
get().activeDomain === DEFAULT_DOMAIN_VALUE
|
||||
) {
|
||||
get().updateActiveDomain(userDomainsObj[0].fullyQualifiedName ?? '');
|
||||
}
|
||||
},
|
||||
updateActiveDomain: (activeDomainKey: string) => {
|
||||
const currentUser = useApplicationStore.getState().currentUser;
|
||||
|
@ -14,6 +14,8 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { PagingResponse } from 'Models';
|
||||
import { PAGE_SIZE_MEDIUM } from '../constants/constants';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { CreateDomain } from '../generated/api/domains/createDomain';
|
||||
import { Domain, EntityReference } from '../generated/entity/domains/domain';
|
||||
import { EntityHistory } from '../generated/type/entityHistory';
|
||||
@ -105,3 +107,31 @@ export const removeAssetsFromDomain = async (
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const listDomainHierarchy = async (params?: ListParams) => {
|
||||
const response = await APIClient.get<PagingResponse<Domain[]>>(
|
||||
`${BASE_URL}/hierarchy`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const searchDomains = async (search: string, page = 1) => {
|
||||
const apiUrl = `/search/query?q=*${search ?? ''}*`;
|
||||
|
||||
const { data } = await APIClient.get(apiUrl, {
|
||||
params: {
|
||||
index: SearchIndex.DOMAIN,
|
||||
from: (page - 1) * PAGE_SIZE_MEDIUM,
|
||||
size: PAGE_SIZE_MEDIUM,
|
||||
deleted: false,
|
||||
track_total_hits: true,
|
||||
getHierarchy: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
@ -14,12 +14,16 @@ import { Divider, Space, Typography } from 'antd';
|
||||
import { ItemType } from 'antd/lib/menu/hooks/useItems';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import { get, isEmpty, isUndefined } from 'lodash';
|
||||
import React, { Fragment, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ReactComponent as DomainIcon } from '../assets/svg/ic-domain.svg';
|
||||
import { ReactComponent as SubDomainIcon } from '../assets/svg/ic-subdomain.svg';
|
||||
import { TreeListItem } from '../components/common/DomainSelectableTree/DomainSelectableTree.interface';
|
||||
import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component';
|
||||
import {
|
||||
DEFAULT_DOMAIN_VALUE,
|
||||
DE_ACTIVE_COLOR,
|
||||
NO_DATA_PLACEHOLDER,
|
||||
} from '../constants/constants';
|
||||
import { DOMAIN_TYPE_DATA } from '../constants/Domain.constants';
|
||||
@ -140,23 +144,33 @@ export const domainTypeTooltipDataRender = () => (
|
||||
</Space>
|
||||
);
|
||||
|
||||
export const getDomainOptions = (
|
||||
domains: Domain[] | EntityReference[],
|
||||
isAdmin = true
|
||||
) => {
|
||||
const domainOptions: ItemType[] =
|
||||
isAdmin || domains.length === 0
|
||||
? [
|
||||
{
|
||||
label: t('label.all-domain-plural'),
|
||||
key: DEFAULT_DOMAIN_VALUE,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
export const getDomainOptions = (domains: Domain[] | EntityReference[]) => {
|
||||
const domainOptions: ItemType[] = [
|
||||
{
|
||||
label: t('label.all-domain-plural'),
|
||||
key: DEFAULT_DOMAIN_VALUE,
|
||||
},
|
||||
];
|
||||
|
||||
domains.forEach((domain) => {
|
||||
domainOptions.push({
|
||||
label: getEntityName(domain),
|
||||
key: domain.fullyQualifiedName ?? '',
|
||||
icon: get(domain, 'parent') ? (
|
||||
<SubDomainIcon
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={20}
|
||||
name="subdomain"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<DomainIcon
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={20}
|
||||
name="domain"
|
||||
width={20}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
@ -204,3 +218,54 @@ export const getDomainFieldFromEntityType = (
|
||||
return 'domain';
|
||||
}
|
||||
};
|
||||
|
||||
export const convertDomainsToTreeOptions = (
|
||||
options: EntityReference[] | Domain[] = [],
|
||||
level = 0,
|
||||
multiple = false
|
||||
): TreeListItem[] => {
|
||||
const treeData = options.map((option) => {
|
||||
const hasChildren = 'children' in option && !isEmpty(option?.children);
|
||||
|
||||
return {
|
||||
id: option.id,
|
||||
value: option.fullyQualifiedName,
|
||||
name: option.name,
|
||||
label: option.name,
|
||||
key: option.fullyQualifiedName,
|
||||
title: (
|
||||
<div className="d-flex items-center gap-1">
|
||||
{level === 0 ? (
|
||||
<DomainIcon
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={20}
|
||||
name="domain"
|
||||
width={20}
|
||||
/>
|
||||
) : (
|
||||
<SubDomainIcon
|
||||
color={DE_ACTIVE_COLOR}
|
||||
height={20}
|
||||
name="subdomain"
|
||||
width={20}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Typography.Text ellipsis>{getEntityName(option)}</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
'data-testid': `tag-${option.fullyQualifiedName}`,
|
||||
isLeaf: !hasChildren,
|
||||
selectable: !multiple,
|
||||
children: hasChildren
|
||||
? convertDomainsToTreeOptions(
|
||||
(option as unknown as Domain)?.children as EntityReference[],
|
||||
level + 1,
|
||||
multiple
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return treeData;
|
||||
};
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
Status,
|
||||
TermReference,
|
||||
} from '../generated/entity/data/glossaryTerm';
|
||||
import { Domain } from '../generated/entity/domains/domain';
|
||||
import { calculatePercentageFromValue } from './CommonUtils';
|
||||
import { getEntityName } from './EntityUtils';
|
||||
import { VersionStatus } from './EntityVersionUtils.interface';
|
||||
@ -191,23 +192,28 @@ export const updateGlossaryTermByFqn = (
|
||||
// This function finds and gives you the glossary term you're looking for.
|
||||
// You can then use this term or update its information in the Glossary or Term with it's reference created
|
||||
// Reference will only be created if withReference is true
|
||||
export const findGlossaryTermByFqn = (
|
||||
list: ModifiedGlossaryTerm[],
|
||||
export const findItemByFqn = (
|
||||
list: ModifiedGlossaryTerm[] | Domain[],
|
||||
fullyQualifiedName: string,
|
||||
withReference = true
|
||||
): GlossaryTerm | Glossary | ModifiedGlossary | null => {
|
||||
for (const item of list) {
|
||||
if ((item.fullyQualifiedName ?? item.value) === fullyQualifiedName) {
|
||||
if (
|
||||
(item.fullyQualifiedName ?? (item as ModifiedGlossaryTerm).value) ===
|
||||
fullyQualifiedName
|
||||
) {
|
||||
return withReference
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
fullyQualifiedName: item.fullyQualifiedName ?? item.data?.tagFQN,
|
||||
...(item.data ?? {}),
|
||||
fullyQualifiedName:
|
||||
item.fullyQualifiedName ??
|
||||
(item as ModifiedGlossaryTerm).data?.tagFQN,
|
||||
...((item as ModifiedGlossaryTerm).data ?? {}),
|
||||
};
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findGlossaryTermByFqn(
|
||||
const found = findItemByFqn(
|
||||
item.children as ModifiedGlossaryTerm[],
|
||||
fullyQualifiedName
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user