mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 03:29:03 +00:00
* Fix #15735: Glossary,Term, Classification,Tag rename operation fixes * add playwright for rename * make description fields mandatory even if it contains whitespaces * fix review comments * hide edit button for system apps * fix glossary owner reviewer spacing * fix glossary term permission issue * rename function to entityRelationshipReindex * fix merge errors --------- Co-authored-by: karanh37 <karanh37@gmail.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
This commit is contained in:
parent
3bcfdfe014
commit
692c21f2f3
@ -15,16 +15,20 @@ package org.openmetadata.service.jdbi3;
|
||||
|
||||
import static org.openmetadata.service.Entity.CLASSIFICATION;
|
||||
import static org.openmetadata.service.Entity.TAG;
|
||||
import static org.openmetadata.service.search.SearchClient.TAG_SEARCH_INDEX;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.sqlobject.transaction.Transaction;
|
||||
import org.openmetadata.schema.entity.classification.Classification;
|
||||
import org.openmetadata.schema.entity.classification.Tag;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.ProviderType;
|
||||
import org.openmetadata.schema.type.Relationship;
|
||||
@ -35,6 +39,7 @@ import org.openmetadata.service.exception.CatalogExceptionMessage;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord;
|
||||
import org.openmetadata.service.resources.tags.ClassificationResource;
|
||||
import org.openmetadata.service.util.EntityUtil.Fields;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
|
||||
@Slf4j
|
||||
public class ClassificationRepository extends EntityRepository<Classification> {
|
||||
@ -112,6 +117,38 @@ public class ClassificationRepository extends EntityRepository<Classification> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entityRelationshipReindex(Classification original, Classification updated) {
|
||||
super.entityRelationshipReindex(original, updated);
|
||||
|
||||
// Update search on name , fullyQualifiedName and displayName change
|
||||
if (!Objects.equals(original.getFullyQualifiedName(), updated.getFullyQualifiedName())
|
||||
|| !Objects.equals(original.getDisplayName(), updated.getDisplayName())) {
|
||||
List<Tag> tagsWithUpdatedClassification = getAllTagsByClassification(updated);
|
||||
List<EntityReference> tagsWithOriginalClassification =
|
||||
searchRepository.getEntitiesContainingFQNFromES(
|
||||
original.getFullyQualifiedName(),
|
||||
tagsWithUpdatedClassification.size(),
|
||||
TAG_SEARCH_INDEX);
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("classification.name", original.getEntityReference());
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("classification.fullyQualifiedName", original.getEntityReference());
|
||||
for (EntityReference tag : tagsWithOriginalClassification) {
|
||||
searchRepository.getSearchClient().reindexAcrossIndices("tags.tagFQN", tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Tag> getAllTagsByClassification(Classification classification) {
|
||||
// Get all the tags under the specified classification
|
||||
List<String> jsons =
|
||||
daoCollection.tagDAO().getTagsStartingWithPrefix(classification.getFullyQualifiedName());
|
||||
return JsonUtils.readObjects(jsons, Tag.class);
|
||||
}
|
||||
|
||||
public class ClassificationUpdater extends EntityUpdater {
|
||||
public ClassificationUpdater(
|
||||
Classification original, Classification updated, Operation operation) {
|
||||
@ -133,16 +170,17 @@ public class ClassificationRepository extends EntityRepository<Classification> {
|
||||
throw new IllegalArgumentException(
|
||||
CatalogExceptionMessage.systemEntityRenameNotAllowed(original.getName(), entityType));
|
||||
}
|
||||
// Classification name changed - update tag names starting from classification and all the
|
||||
// children tags
|
||||
LOG.info(
|
||||
"Classification name changed from {} to {}", original.getName(), updated.getName());
|
||||
// on Classification name change - update tag's name under classification
|
||||
setFullyQualifiedName(updated);
|
||||
daoCollection.tagDAO().updateFqn(original.getName(), updated.getName());
|
||||
daoCollection
|
||||
.tagDAO()
|
||||
.updateFqn(original.getFullyQualifiedName(), updated.getFullyQualifiedName());
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.updateTagPrefix(
|
||||
TagSource.CLASSIFICATION.ordinal(), original.getName(), updated.getName());
|
||||
TagSource.CLASSIFICATION.ordinal(),
|
||||
original.getFullyQualifiedName(),
|
||||
updated.getFullyQualifiedName());
|
||||
recordChange("name", original.getName(), updated.getName());
|
||||
invalidateClassification(original.getId());
|
||||
}
|
||||
|
||||
@ -1501,6 +1501,19 @@ public interface CollectionDAO {
|
||||
@SqlQuery("select id from thread_entity where entityId = :entityId")
|
||||
List<String> findByEntityId(@Bind("entityId") String entityId);
|
||||
|
||||
@ConnectionAwareSqlUpdate(
|
||||
value =
|
||||
"UPDATE thread_entity SET json = JSON_SET(json, '$.about', :newEntityLink)\n"
|
||||
+ "WHERE entityId = :entityId",
|
||||
connectionType = MYSQL)
|
||||
@ConnectionAwareSqlUpdate(
|
||||
value =
|
||||
"UPDATE thread_entity SET json = jsonb_set(json, '{about}', to_jsonb(:newEntityLink::text), false)\n"
|
||||
+ "WHERE entityId = :entityId",
|
||||
connectionType = POSTGRES)
|
||||
void updateByEntityId(
|
||||
@Bind("newEntityLink") String newEntityLink, @Bind("entityId") String entityId);
|
||||
|
||||
class OwnerCountFieldMapper implements RowMapper<List<String>> {
|
||||
@Override
|
||||
public List<String> map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
@ -1651,6 +1664,41 @@ public interface CollectionDAO {
|
||||
@Bind("toType") String toType,
|
||||
@Bind("relation") int relation);
|
||||
|
||||
default void renameByToFQN(String oldToFQN, String newToFQN) {
|
||||
renameByToFQNInternal(
|
||||
oldToFQN,
|
||||
FullyQualifiedName.buildHash(oldToFQN),
|
||||
newToFQN,
|
||||
FullyQualifiedName.buildHash(newToFQN)); // First rename targetFQN from oldFQN to newFQN
|
||||
renameByToFQNPrefix(oldToFQN, newToFQN);
|
||||
// Rename all the targetFQN prefixes starting with the oldFQN to newFQN
|
||||
}
|
||||
|
||||
@SqlUpdate(
|
||||
"Update field_relationship set toFQN = :newToFQN , toFQNHash = :newToFQNHash "
|
||||
+ "where fromtype = 'THREAD' AND relation='3' AND toFQN = :oldToFQN and toFQNHash =:oldToFQNHash ;")
|
||||
void renameByToFQNInternal(
|
||||
@Bind("oldToFQN") String oldToFQN,
|
||||
@Bind("oldToFQNHash") String oldToFQNHash,
|
||||
@Bind("newToFQN") String newToFQN,
|
||||
@Bind("newToFQNHash") String newToFQNHash);
|
||||
|
||||
default void renameByToFQNPrefix(String oldToFQNPrefix, String newToFQNPrefix) {
|
||||
String update =
|
||||
String.format(
|
||||
"UPDATE field_relationship SET toFQN = REPLACE(toFQN, '%s.', '%s.') , toFQNHash = REPLACE(toFQNHash, '%s.', '%s.') where fromtype = 'THREAD' AND relation='3' AND toFQN like '%s.%%' and toFQNHash like '%s.%%' ",
|
||||
escapeApostrophe(oldToFQNPrefix),
|
||||
escapeApostrophe(newToFQNPrefix),
|
||||
FullyQualifiedName.buildHash(oldToFQNPrefix),
|
||||
FullyQualifiedName.buildHash(newToFQNPrefix),
|
||||
escapeApostrophe(oldToFQNPrefix),
|
||||
FullyQualifiedName.buildHash(oldToFQNPrefix));
|
||||
renameByToFQNPrefixInternal(update);
|
||||
}
|
||||
|
||||
@SqlUpdate("<update>")
|
||||
void renameByToFQNPrefixInternal(@Define("update") String update);
|
||||
|
||||
class FromFieldMapper implements RowMapper<Triple<String, String, String>> {
|
||||
@Override
|
||||
public Triple<String, String, String> map(ResultSet rs, StatementContext ctx)
|
||||
@ -1987,15 +2035,8 @@ public interface CollectionDAO {
|
||||
return listAfter(getTableName(), filter.getQueryParams(), condition, limit, after);
|
||||
}
|
||||
|
||||
@SqlQuery("select fqnhash FROM glossary_term_entity where fqnhash LIKE CONCAT(:fqnhash, '.%')")
|
||||
List<String> getNestedChildrenByFQN(@BindFQN("fqnhash") String fqnhash);
|
||||
|
||||
default List<String> getAllTerms(String fqnPrefix) {
|
||||
return getAllTermsInternal((FullyQualifiedName.quoteName(fqnPrefix)));
|
||||
}
|
||||
|
||||
@SqlQuery("select json FROM glossary_term_entity where fqnhash LIKE CONCAT(:fqnhash, '.%')")
|
||||
List<String> getAllTermsInternal(@BindFQN("fqnhash") String fqnhash);
|
||||
@SqlQuery("select json FROM glossary_term_entity where fqnhash LIKE CONCAT(:fqnhash, '.%')")
|
||||
List<String> getNestedTerms(@BindFQN("fqnhash") String fqnhash);
|
||||
}
|
||||
|
||||
interface IngestionPipelineDAO extends EntityDAO<IngestionPipeline> {
|
||||
@ -2601,6 +2642,9 @@ public interface CollectionDAO {
|
||||
return listAfter(
|
||||
getTableName(), filter.getQueryParams(), mySqlCondition, postgresCondition, limit, after);
|
||||
}
|
||||
|
||||
@SqlQuery("select json FROM tag where fqnhash LIKE CONCAT(:fqnhash, '.%')")
|
||||
List<String> getTagsStartingWithPrefix(@BindFQN("fqnhash") String fqnhash);
|
||||
}
|
||||
|
||||
@RegisterRowMapper(TagLabelMapper.class)
|
||||
@ -2778,10 +2822,6 @@ public interface CollectionDAO {
|
||||
|
||||
default void renameByTargetFQNHash(
|
||||
int source, String oldTargetFQNHash, String newTargetFQNHash) {
|
||||
renameByTargetFQNHashInternal(
|
||||
source,
|
||||
(oldTargetFQNHash),
|
||||
newTargetFQNHash); // First rename targetFQN from oldFQN to newFQN
|
||||
updateTargetFQNHashPrefix(
|
||||
source,
|
||||
oldTargetFQNHash,
|
||||
@ -2797,14 +2837,6 @@ public interface CollectionDAO {
|
||||
@Bind("newFQN") String newFQN,
|
||||
@BindFQN("newFQNHash") String newFQNHash);
|
||||
|
||||
/** Rename the targetFQN */
|
||||
@SqlUpdate(
|
||||
"Update tag_usage set targetFQNHash = :newTargetFQNHash WHERE source = :source AND targetFQNHash = :oldTargetFQNHash")
|
||||
void renameByTargetFQNHashInternal(
|
||||
@Bind("source") int source,
|
||||
@BindFQN("oldTargetFQNHash") String oldTargetFQNHash,
|
||||
@BindFQN("newTargetFQNHash") String newTargetFQNHash);
|
||||
|
||||
@SqlUpdate("<update>")
|
||||
void updateTagPrefixInternal(@Define("update") String update);
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.jdbi.BindFQN;
|
||||
import org.openmetadata.service.util.jdbi.BindUUID;
|
||||
import org.openmetadata.service.workflows.searchIndex.ReindexingUtil;
|
||||
|
||||
public interface EntityDAO<T extends EntityInterface> {
|
||||
org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(EntityDAO.class);
|
||||
@ -111,8 +112,8 @@ public interface EntityDAO<T extends EntityInterface> {
|
||||
+ ", fqnHash = REPLACE(fqnHash, '%s.', '%s.') "
|
||||
+ "WHERE fqnHash LIKE '%s.%%'",
|
||||
getTableName(),
|
||||
escapeApostrophe(oldPrefix),
|
||||
escapeApostrophe(newPrefix),
|
||||
ReindexingUtil.escapeDoubleQuotes(escapeApostrophe(oldPrefix)),
|
||||
ReindexingUtil.escapeDoubleQuotes(escapeApostrophe(newPrefix)),
|
||||
FullyQualifiedName.buildHash(oldPrefix),
|
||||
FullyQualifiedName.buildHash(newPrefix),
|
||||
FullyQualifiedName.buildHash(oldPrefix));
|
||||
|
||||
@ -412,6 +412,15 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
updated.setChangeDescription(original.getChangeDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* This function updates the Elasticsearch indexes wherever the specific entity is present.
|
||||
* It is typically invoked when there are changes in the entity that might affect its indexing in Elasticsearch.
|
||||
* The function ensures that the indexes are kept up-to-date with the latest state of the entity across all relevant Elasticsearch indexes.
|
||||
*/
|
||||
protected void entityRelationshipReindex(T original, T updated) {
|
||||
// Logic override by the child class to update the indexes
|
||||
}
|
||||
|
||||
/** Set fullyQualifiedName of an entity */
|
||||
public void setFullyQualifiedName(T entity) {
|
||||
entity.setFullyQualifiedName(quoteName(entity.getName()));
|
||||
@ -875,6 +884,9 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
// Update the attributes and relationships of an entity
|
||||
EntityUpdater entityUpdater = getUpdater(original, updated, Operation.PATCH);
|
||||
entityUpdater.update();
|
||||
|
||||
entityRelationshipReindex(original, updated);
|
||||
|
||||
EventType change = ENTITY_NO_CHANGE;
|
||||
if (entityUpdater.fieldsChanged()) {
|
||||
change = EventType.ENTITY_UPDATED;
|
||||
|
||||
@ -25,6 +25,8 @@ import static org.openmetadata.csv.CsvUtil.addOwner;
|
||||
import static org.openmetadata.csv.CsvUtil.addTagLabels;
|
||||
import static org.openmetadata.service.Entity.GLOSSARY;
|
||||
import static org.openmetadata.service.Entity.GLOSSARY_TERM;
|
||||
import static org.openmetadata.service.search.SearchClient.GLOSSARY_TERM_SEARCH_INDEX;
|
||||
import static org.openmetadata.service.util.EntityUtil.compareTagLabel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@ -33,6 +35,7 @@ import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@ -62,6 +65,7 @@ import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.exception.CatalogExceptionMessage;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord;
|
||||
import org.openmetadata.service.resources.feeds.MessageParser;
|
||||
import org.openmetadata.service.resources.glossary.GlossaryResource;
|
||||
import org.openmetadata.service.util.EntityUtil.Fields;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
@ -72,8 +76,6 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
private static final String UPDATE_FIELDS = "";
|
||||
private static final String PATCH_FIELDS = "";
|
||||
|
||||
FeedRepository feedRepository = Entity.getFeedRepository();
|
||||
|
||||
public GlossaryRepository() {
|
||||
super(
|
||||
GlossaryResource.COLLECTION_PATH,
|
||||
@ -84,6 +86,7 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
UPDATE_FIELDS);
|
||||
quoteFqn = true;
|
||||
supportsSearch = true;
|
||||
renameAllowed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -135,6 +138,15 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
return new GlossaryUpdater(original, updated, operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entityRelationshipReindex(Glossary original, Glossary updated) {
|
||||
super.entityRelationshipReindex(original, updated);
|
||||
if (!Objects.equals(original.getFullyQualifiedName(), updated.getFullyQualifiedName())
|
||||
|| !Objects.equals(original.getDisplayName(), updated.getDisplayName())) {
|
||||
updateAssetIndexes(original, updated);
|
||||
}
|
||||
}
|
||||
|
||||
/** Export glossary as CSV */
|
||||
@Override
|
||||
public String exportToCsv(String name, String user) throws IOException {
|
||||
@ -275,7 +287,7 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAssetIndexesOnGlossaryUpdate(Glossary original, Glossary updated) {
|
||||
private void updateAssetIndexes(Glossary original, Glossary updated) {
|
||||
// Update ES indexes of entity tagged with the glossary term and its children terms to reflect
|
||||
// its latest value.
|
||||
GlossaryTermRepository repository =
|
||||
@ -285,14 +297,9 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.getTargetFQNHashForTagPrefix(updated.getFullyQualifiedName()));
|
||||
List<GlossaryTerm> childTerms = getAllTerms(updated);
|
||||
|
||||
List<EntityReference> childTerms =
|
||||
findTo(
|
||||
updated.getId(),
|
||||
GLOSSARY,
|
||||
Relationship.CONTAINS,
|
||||
GLOSSARY_TERM); // get new value of children terms from DB
|
||||
for (EntityReference child : childTerms) {
|
||||
for (GlossaryTerm child : childTerms) {
|
||||
targetFQNHashesFromDb.addAll( // for each child term find the targetFQNHashes of assets
|
||||
daoCollection.tagUsageDAO().getTargetFQNHashForTag(child.getFullyQualifiedName()));
|
||||
}
|
||||
@ -301,32 +308,54 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
Map<String, EntityReference> targetFQNFromES =
|
||||
repository.getGlossaryUsageFromES(
|
||||
original.getFullyQualifiedName(), targetFQNHashesFromDb.size());
|
||||
Map<String, EntityReference> childrenTerms =
|
||||
repository.getGlossaryTermsContainingFQNFromES(
|
||||
List<EntityReference> childrenTerms =
|
||||
searchRepository.getEntitiesContainingFQNFromES(
|
||||
original.getFullyQualifiedName(),
|
||||
getTermCount(updated)); // get old value of children term from ES
|
||||
|
||||
for (EntityReference child : childrenTerms.values()) {
|
||||
getTermCount(updated),
|
||||
GLOSSARY_TERM_SEARCH_INDEX); // get old value of children term from ES
|
||||
for (EntityReference child : childrenTerms) {
|
||||
targetFQNFromES.putAll( // List of entity references tagged with the children term
|
||||
repository.getGlossaryUsageFromES(
|
||||
child.getFullyQualifiedName(), targetFQNHashesFromDb.size()));
|
||||
searchRepository.updateEntity(child); // update es index of child term
|
||||
searchRepository.getSearchClient().reindexAcrossIndices("tags.tagFQN", child);
|
||||
}
|
||||
|
||||
if (targetFQNFromES.size() == targetFQNHashesFromDb.size()) {
|
||||
for (String fqnHash : targetFQNHashesFromDb) {
|
||||
EntityReference refDetails = targetFQNFromES.get(fqnHash);
|
||||
searchRepository.updateEntity(original); // update es index of child term
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("fullyQualifiedName", original.getEntityReference());
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("glossary.name", original.getEntityReference());
|
||||
}
|
||||
|
||||
if (refDetails != null) {
|
||||
searchRepository.updateEntity(refDetails); // update ES index of assets
|
||||
}
|
||||
}
|
||||
private void updateEntityLinksOnGlossaryRename(Glossary original, Glossary updated) {
|
||||
// update field relationships for feed
|
||||
daoCollection
|
||||
.fieldRelationshipDAO()
|
||||
.renameByToFQN(original.getFullyQualifiedName(), updated.getFullyQualifiedName());
|
||||
|
||||
MessageParser.EntityLink about =
|
||||
new MessageParser.EntityLink(GLOSSARY_TERM, original.getFullyQualifiedName());
|
||||
|
||||
MessageParser.EntityLink newAbout =
|
||||
new MessageParser.EntityLink(entityType, updated.getFullyQualifiedName());
|
||||
|
||||
daoCollection.feedDAO().updateByEntityId(newAbout.getLinkString(), original.getId().toString());
|
||||
|
||||
List<GlossaryTerm> childTerms = getAllTerms(updated);
|
||||
|
||||
for (GlossaryTerm child : childTerms) {
|
||||
newAbout = new MessageParser.EntityLink(GLOSSARY_TERM, child.getFullyQualifiedName());
|
||||
daoCollection.feedDAO().updateByEntityId(newAbout.getLinkString(), child.getId().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private List<GlossaryTerm> getAllTerms(Glossary glossary) {
|
||||
// Get all the hierarchically nested terms of the glossary
|
||||
List<String> jsons = daoCollection.glossaryTermDAO().getAllTerms(glossary.getName());
|
||||
List<String> jsons =
|
||||
daoCollection.glossaryTermDAO().getNestedTerms(glossary.getFullyQualifiedName());
|
||||
return JsonUtils.readObjects(jsons, GlossaryTerm.class);
|
||||
}
|
||||
|
||||
@ -354,22 +383,30 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
|
||||
// Glossary name changed - update tag names starting from glossary and all the children tags
|
||||
LOG.info("Glossary name changed from {} to {}", original.getName(), updated.getName());
|
||||
setFullyQualifiedName(updated);
|
||||
daoCollection.glossaryTermDAO().updateFqn(original.getName(), updated.getName());
|
||||
daoCollection
|
||||
.glossaryTermDAO()
|
||||
.updateFqn(original.getFullyQualifiedName(), updated.getFullyQualifiedName());
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.updateTagPrefix(TagSource.GLOSSARY.ordinal(), original.getName(), updated.getName());
|
||||
.updateTagPrefix(
|
||||
TagSource.GLOSSARY.ordinal(),
|
||||
original.getFullyQualifiedName(),
|
||||
updated.getFullyQualifiedName());
|
||||
recordChange("name", original.getName(), updated.getName());
|
||||
invalidateGlossary(original.getId());
|
||||
|
||||
// update tags
|
||||
// update Tags Of Glossary On Rename
|
||||
daoCollection.tagUsageDAO().deleteTagsByTarget(original.getFullyQualifiedName());
|
||||
List<TagLabel> updatedTags = updated.getTags();
|
||||
updatedTags.sort(compareTagLabel);
|
||||
applyTags(updatedTags, updated.getFullyQualifiedName());
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.renameByTargetFQNHash(
|
||||
TagSource.CLASSIFICATION.ordinal(),
|
||||
original.getFullyQualifiedName(),
|
||||
updated.getFullyQualifiedName());
|
||||
|
||||
updateAssetIndexesOnGlossaryUpdate(original, updated);
|
||||
updateEntityLinksOnGlossaryRename(original, updated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import static org.openmetadata.service.resources.tags.TagLabelUtil.checkMutually
|
||||
import static org.openmetadata.service.resources.tags.TagLabelUtil.checkMutuallyExclusiveForParentAndSubField;
|
||||
import static org.openmetadata.service.resources.tags.TagLabelUtil.getUniqueTags;
|
||||
import static org.openmetadata.service.search.SearchClient.GLOBAL_SEARCH_ALIAS;
|
||||
import static org.openmetadata.service.search.SearchClient.GLOSSARY_TERM_SEARCH_INDEX;
|
||||
import static org.openmetadata.service.util.EntityUtil.compareEntityReferenceById;
|
||||
import static org.openmetadata.service.util.EntityUtil.compareTagLabel;
|
||||
import static org.openmetadata.service.util.EntityUtil.entityReferenceMatch;
|
||||
@ -97,6 +98,7 @@ import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.RestUtil;
|
||||
import org.openmetadata.service.util.WebsocketNotificationHandler;
|
||||
import org.openmetadata.service.workflows.searchIndex.ReindexingUtil;
|
||||
|
||||
@Slf4j
|
||||
public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
@ -105,8 +107,6 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
private static final String UPDATE_FIELDS = "references,relatedTerms,synonyms";
|
||||
private static final String PATCH_FIELDS = "references,relatedTerms,synonyms";
|
||||
|
||||
private static GlossaryTerm valueBeforeUpdate = new GlossaryTerm();
|
||||
|
||||
FeedRepository feedRepository = Entity.getFeedRepository();
|
||||
|
||||
public GlossaryTermRepository() {
|
||||
@ -237,6 +237,18 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
updated.withChildren(original.getChildren());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entityRelationshipReindex(GlossaryTerm original, GlossaryTerm updated) {
|
||||
super.entityRelationshipReindex(original, updated);
|
||||
|
||||
// Update search indexes of assets and entity itself on name , fullyQualifiedName and
|
||||
// displayName change
|
||||
if (!Objects.equals(original.getFullyQualifiedName(), updated.getFullyQualifiedName())
|
||||
|| !Objects.equals(original.getDisplayName(), updated.getDisplayName())) {
|
||||
updateAssetIndexes(original, updated);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFullyQualifiedName(GlossaryTerm entity) {
|
||||
// Validate parent
|
||||
@ -426,7 +438,9 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
String key = "_source";
|
||||
SearchRequest searchRequest =
|
||||
new SearchRequest.ElasticSearchRequestBuilder(
|
||||
String.format("** AND (tags.tagFQN:\"%s\")", glossaryFqn),
|
||||
String.format(
|
||||
"** AND (tags.tagFQN:\"%s\")",
|
||||
ReindexingUtil.escapeDoubleQuotes(glossaryFqn)),
|
||||
size,
|
||||
Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS))
|
||||
.from(0)
|
||||
@ -468,63 +482,6 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
protected Map<String, EntityReference> getGlossaryTermsContainingFQNFromES(
|
||||
String termFQN, int size) {
|
||||
try {
|
||||
String queryFilter =
|
||||
String.format(
|
||||
"{\"query\":{\"bool\":{\"must\":[{\"wildcard\":{\"fullyQualifiedName\":\"%s.*\"}}]}}}",
|
||||
termFQN);
|
||||
|
||||
SearchRequest searchRequest =
|
||||
new SearchRequest.ElasticSearchRequestBuilder(
|
||||
"*", size, Entity.getSearchRepository().getIndexOrAliasName(GLOBAL_SEARCH_ALIAS))
|
||||
.from(0)
|
||||
.queryFilter(queryFilter)
|
||||
.fetchSource(true)
|
||||
.trackTotalHits(false)
|
||||
.sortFieldParam("_score")
|
||||
.deleted(false)
|
||||
.sortOrder("desc")
|
||||
.includeSourceFields(new ArrayList<>())
|
||||
.build();
|
||||
|
||||
// Execute the search and parse the response
|
||||
Response response = searchRepository.search(searchRequest);
|
||||
String json = (String) response.getEntity();
|
||||
Set<EntityReference> fqns = new TreeSet<>(compareEntityReferenceById);
|
||||
|
||||
// Extract hits from the response JSON and create entity references
|
||||
for (Iterator<JsonNode> it =
|
||||
((ArrayNode) JsonUtils.extractValue(json, "hits", "hits")).elements();
|
||||
it.hasNext(); ) {
|
||||
JsonNode jsonNode = it.next();
|
||||
String id = JsonUtils.extractValue(jsonNode, "_source", "id");
|
||||
String fqn = JsonUtils.extractValue(jsonNode, "_source", "fullyQualifiedName");
|
||||
String type = JsonUtils.extractValue(jsonNode, "_source", "entityType");
|
||||
if (!CommonUtil.nullOrEmpty(fqn) && !CommonUtil.nullOrEmpty(type)) {
|
||||
fqns.add(
|
||||
new EntityReference()
|
||||
.withId(UUID.fromString(id))
|
||||
.withFullyQualifiedName(fqn)
|
||||
.withType(type));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the results into a map by the hash of the FQN
|
||||
return fqns.stream()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
entityReference ->
|
||||
FullyQualifiedName.buildHash(entityReference.getFullyQualifiedName()),
|
||||
entityReference -> entityReference));
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error while fetching glossary terms with prefix from ES", ex);
|
||||
}
|
||||
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
public BulkOperationResult bulkRemoveGlossaryToAssets(
|
||||
UUID glossaryTermId, AddGlossaryToAssetsRequest request) {
|
||||
GlossaryTerm term = this.get(null, glossaryTermId, getFields("id,tags"));
|
||||
@ -572,7 +529,6 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
@Override
|
||||
public GlossaryTermUpdater getUpdater(
|
||||
GlossaryTerm original, GlossaryTerm updated, Operation operation) {
|
||||
valueBeforeUpdate = original;
|
||||
return new GlossaryTermUpdater(original, updated, operation);
|
||||
}
|
||||
|
||||
@ -595,10 +551,6 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
closeApprovalTask(updated, "Rejected the glossary term");
|
||||
}
|
||||
}
|
||||
if (!nullOrEmpty(valueBeforeUpdate)
|
||||
&& !valueBeforeUpdate.getFullyQualifiedName().equals(updated.getFullyQualifiedName())) {
|
||||
updateAssetIndexesOnGlossaryTermUpdate(valueBeforeUpdate, updated);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -757,36 +709,34 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAssetIndexesOnGlossaryTermUpdate(GlossaryTerm original, GlossaryTerm updated) {
|
||||
private void updateAssetIndexes(GlossaryTerm original, GlossaryTerm updated) {
|
||||
// Update ES indexes of entity tagged with the glossary term and its children terms to reflect
|
||||
// its latest value.
|
||||
Set<String> targetFQNHashesFromDb =
|
||||
new HashSet<>(
|
||||
daoCollection.tagUsageDAO().getTargetFQNHashForTag(updated.getFullyQualifiedName()));
|
||||
|
||||
List<String> childTerms =
|
||||
daoCollection
|
||||
.glossaryTermDAO()
|
||||
.getNestedChildrenByFQN(
|
||||
updated.getFullyQualifiedName()); // get new value of children terms from DB
|
||||
for (String child : childTerms) {
|
||||
List<GlossaryTerm> childTerms =
|
||||
getNestedTerms(updated); // get old value of children term from database
|
||||
for (GlossaryTerm child : childTerms) {
|
||||
targetFQNHashesFromDb.addAll( // for each child term find the targetFQNHashes of assets
|
||||
daoCollection.tagUsageDAO().getTargetFQNHashForTag(child));
|
||||
daoCollection.tagUsageDAO().getTargetFQNHashForTag(child.getFullyQualifiedName()));
|
||||
}
|
||||
|
||||
// List of entity references tagged with the glossary term
|
||||
Map<String, EntityReference> targetFQNFromES =
|
||||
getGlossaryUsageFromES(original.getFullyQualifiedName(), targetFQNHashesFromDb.size());
|
||||
Map<String, EntityReference> childrenTerms =
|
||||
getGlossaryTermsContainingFQNFromES(
|
||||
List<EntityReference> childrenTerms =
|
||||
searchRepository.getEntitiesContainingFQNFromES(
|
||||
original.getFullyQualifiedName(),
|
||||
childTerms.size()); // get old value of children term from ES
|
||||
childTerms.size(),
|
||||
GLOSSARY_TERM_SEARCH_INDEX); // get old value of children term from ES
|
||||
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("tags.tagFQN", original.getEntityReference());
|
||||
|
||||
for (EntityReference child : childrenTerms.values()) {
|
||||
for (EntityReference child : childrenTerms) {
|
||||
targetFQNFromES.putAll( // List of entity references tagged with the children term
|
||||
getGlossaryUsageFromES(child.getFullyQualifiedName(), targetFQNHashesFromDb.size()));
|
||||
searchRepository.updateEntity(child); // update es index of child term
|
||||
@ -794,10 +744,28 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEntityLinks(GlossaryTerm original, GlossaryTerm updated) {
|
||||
daoCollection
|
||||
.fieldRelationshipDAO()
|
||||
.renameByToFQN(original.getFullyQualifiedName(), updated.getFullyQualifiedName());
|
||||
EntityLink about = new EntityLink(GLOSSARY_TERM, original.getFullyQualifiedName());
|
||||
|
||||
EntityLink newAbout = new EntityLink(GLOSSARY_TERM, updated.getFullyQualifiedName());
|
||||
|
||||
daoCollection.feedDAO().updateByEntityId(newAbout.getLinkString(), original.getId().toString());
|
||||
List<EntityReference> childTerms =
|
||||
findTo(updated.getId(), GLOSSARY_TERM, Relationship.CONTAINS, GLOSSARY_TERM);
|
||||
|
||||
for (EntityReference child : childTerms) {
|
||||
newAbout = new EntityLink(entityType, child.getFullyQualifiedName());
|
||||
daoCollection.feedDAO().updateByEntityId(newAbout.getLinkString(), child.getId().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private List<GlossaryTerm> getNestedTerms(GlossaryTerm glossaryTerm) {
|
||||
// Get all the hierarchically nested child terms of the glossary term
|
||||
List<String> jsons =
|
||||
daoCollection.glossaryTermDAO().getAllTermsInternal(glossaryTerm.getFullyQualifiedName());
|
||||
daoCollection.glossaryTermDAO().getNestedTerms(glossaryTerm.getFullyQualifiedName());
|
||||
return JsonUtils.readObjects(jsons, GlossaryTerm.class);
|
||||
}
|
||||
|
||||
@ -993,13 +961,20 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
updated.getFullyQualifiedName());
|
||||
recordChange("name", original.getName(), updated.getName());
|
||||
invalidateTerm(original.getId());
|
||||
|
||||
// update tags
|
||||
daoCollection.tagUsageDAO().deleteTagsByTarget(original.getFullyQualifiedName());
|
||||
List<TagLabel> updatedTags = updated.getTags();
|
||||
updatedTags.sort(compareTagLabel);
|
||||
applyTags(updatedTags, updated.getFullyQualifiedName());
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.renameByTargetFQNHash(
|
||||
TagSource.CLASSIFICATION.ordinal(),
|
||||
original.getFullyQualifiedName(),
|
||||
updated.getFullyQualifiedName());
|
||||
|
||||
updateEntityLinks(original, updated);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1028,6 +1003,13 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
updated.getFullyQualifiedName());
|
||||
|
||||
// update tags
|
||||
daoCollection.tagUsageDAO().deleteTagsByTarget(original.getFullyQualifiedName());
|
||||
List<TagLabel> updatedTags = updated.getTags();
|
||||
updatedTags.sort(compareTagLabel);
|
||||
applyTags(updatedTags, updated.getFullyQualifiedName());
|
||||
|
||||
// do same for children terms
|
||||
// update Tags Of Glossary On Rename
|
||||
daoCollection
|
||||
.tagUsageDAO()
|
||||
.renameByTargetFQNHash(
|
||||
@ -1035,6 +1017,8 @@ public class GlossaryTermRepository extends EntityRepository<GlossaryTerm> {
|
||||
original.getFullyQualifiedName(),
|
||||
updated.getFullyQualifiedName());
|
||||
|
||||
updateEntityLinks(original, updated);
|
||||
|
||||
if (glossaryChanged) {
|
||||
updateGlossaryRelationship(original, updated);
|
||||
recordChange(
|
||||
|
||||
@ -103,6 +103,17 @@ public class TagRepository extends EntityRepository<Tag> {
|
||||
return new TagUpdater(original, updated, operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entityRelationshipReindex(Tag original, Tag updated) {
|
||||
super.entityRelationshipReindex(original, updated);
|
||||
if (!Objects.equals(original.getFullyQualifiedName(), updated.getFullyQualifiedName())
|
||||
|| !Objects.equals(original.getDisplayName(), updated.getDisplayName())) {
|
||||
searchRepository
|
||||
.getSearchClient()
|
||||
.reindexAcrossIndices("tags.tagFQN", original.getEntityReference());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postDelete(Tag entity) {
|
||||
// Cleanup all the tag labels using this tag
|
||||
|
||||
@ -35,6 +35,8 @@ public interface SearchClient {
|
||||
|
||||
String DELETE = "delete";
|
||||
String GLOBAL_SEARCH_ALIAS = "all";
|
||||
String GLOSSARY_TERM_SEARCH_INDEX = "glossary_term_search_index";
|
||||
String TAG_SEARCH_INDEX = "tag_search_index";
|
||||
String DEFAULT_UPDATE_SCRIPT = "for (k in params.keySet()) { ctx._source.put(k, params.get(k)) }";
|
||||
String REMOVE_DOMAINS_CHILDREN_SCRIPT = "ctx._source.remove('domain')";
|
||||
String PROPAGATE_ENTITY_REFERENCE_FIELD_SCRIPT =
|
||||
|
||||
@ -22,20 +22,25 @@ import static org.openmetadata.service.search.SearchClient.SOFT_DELETE_RESTORE_S
|
||||
import static org.openmetadata.service.search.SearchClient.UPDATE_ADDED_DELETE_GLOSSARY_TAGS;
|
||||
import static org.openmetadata.service.search.SearchClient.UPDATE_PROPAGATED_ENTITY_REFERENCE_FIELD_SCRIPT;
|
||||
import static org.openmetadata.service.search.models.IndexMapping.indexNameSeparator;
|
||||
import static org.openmetadata.service.util.EntityUtil.compareEntityReferenceById;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import javax.json.JsonObject;
|
||||
import javax.ws.rs.core.Response;
|
||||
@ -45,6 +50,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openmetadata.common.utils.CommonUtil;
|
||||
import org.openmetadata.schema.EntityInterface;
|
||||
import org.openmetadata.schema.EntityTimeSeriesInterface;
|
||||
import org.openmetadata.schema.analytics.ReportData;
|
||||
@ -64,6 +70,7 @@ import org.openmetadata.service.search.indexes.SearchIndex;
|
||||
import org.openmetadata.service.search.models.IndexMapping;
|
||||
import org.openmetadata.service.search.opensearch.OpenSearchClient;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.workflows.searchIndex.ReindexingUtil;
|
||||
|
||||
@Slf4j
|
||||
public class SearchRepository {
|
||||
@ -786,4 +793,54 @@ public class SearchRepository {
|
||||
return searchClient.listDataInsightChartResult(
|
||||
startTs, endTs, tier, team, dataInsightChartName, size, from, queryFilter, dataReportIndex);
|
||||
}
|
||||
|
||||
public List<EntityReference> getEntitiesContainingFQNFromES(
|
||||
String entityFQN, int size, String indexName) {
|
||||
try {
|
||||
String queryFilter =
|
||||
String.format(
|
||||
"{\"query\":{\"bool\":{\"must\":[{\"wildcard\":{\"fullyQualifiedName\":\"%s.*\"}}]}}}",
|
||||
ReindexingUtil.escapeDoubleQuotes(entityFQN));
|
||||
|
||||
SearchRequest searchRequest =
|
||||
new SearchRequest.ElasticSearchRequestBuilder(
|
||||
"*", size, Entity.getSearchRepository().getIndexOrAliasName(indexName))
|
||||
.from(0)
|
||||
.queryFilter(queryFilter)
|
||||
.fetchSource(true)
|
||||
.trackTotalHits(false)
|
||||
.sortFieldParam("_score")
|
||||
.deleted(false)
|
||||
.sortOrder("desc")
|
||||
.includeSourceFields(new ArrayList<>())
|
||||
.build();
|
||||
|
||||
// Execute the search and parse the response
|
||||
Response response = search(searchRequest);
|
||||
String json = (String) response.getEntity();
|
||||
Set<EntityReference> fqns = new TreeSet<>(compareEntityReferenceById);
|
||||
|
||||
// Extract hits from the response JSON and create entity references
|
||||
for (Iterator<JsonNode> it =
|
||||
((ArrayNode) JsonUtils.extractValue(json, "hits", "hits")).elements();
|
||||
it.hasNext(); ) {
|
||||
JsonNode jsonNode = it.next();
|
||||
String id = JsonUtils.extractValue(jsonNode, "_source", "id");
|
||||
String fqn = JsonUtils.extractValue(jsonNode, "_source", "fullyQualifiedName");
|
||||
String type = JsonUtils.extractValue(jsonNode, "_source", "entityType");
|
||||
if (!CommonUtil.nullOrEmpty(fqn) && !CommonUtil.nullOrEmpty(type)) {
|
||||
fqns.add(
|
||||
new EntityReference()
|
||||
.withId(UUID.fromString(id))
|
||||
.withFullyQualifiedName(fqn)
|
||||
.withType(type));
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(fqns);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error while getting entities from ES for validation", ex);
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1537,7 +1537,9 @@ public class ElasticSearchClient implements SearchClient {
|
||||
while (hasMoreResults) {
|
||||
List<EntityReference> entities =
|
||||
ReindexingUtil.findReferenceInElasticSearchAcrossAllIndexes(
|
||||
matchingKey, sourceRef.getFullyQualifiedName(), from);
|
||||
matchingKey,
|
||||
ReindexingUtil.escapeDoubleQuotes(sourceRef.getFullyQualifiedName()),
|
||||
from);
|
||||
|
||||
// Async Re-index the entities which matched
|
||||
processEntitiesForReindex(entities);
|
||||
|
||||
@ -1527,7 +1527,9 @@ public class OpenSearchClient implements SearchClient {
|
||||
while (hasMoreResults) {
|
||||
List<EntityReference> entities =
|
||||
ReindexingUtil.findReferenceInElasticSearchAcrossAllIndexes(
|
||||
matchingKey, sourceRef.getFullyQualifiedName(), from);
|
||||
matchingKey,
|
||||
ReindexingUtil.escapeDoubleQuotes(sourceRef.getFullyQualifiedName()),
|
||||
from);
|
||||
|
||||
// Async Re-index the entities which matched
|
||||
processEntitiesForReindex(entities);
|
||||
|
||||
@ -164,4 +164,8 @@ public class ReindexingUtil {
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static String escapeDoubleQuotes(String str) {
|
||||
return str.replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { SidebarItem } from '../../constant/sidebar';
|
||||
import { DashboardClass } from '../../support/entity/DashboardClass';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import { TopicClass } from '../../support/entity/TopicClass';
|
||||
import { Glossary } from '../../support/glossary/Glossary';
|
||||
import { GlossaryTerm } from '../../support/glossary/GlossaryTerm';
|
||||
import { TeamClass } from '../../support/team/TeamClass';
|
||||
@ -21,18 +23,23 @@ import {
|
||||
performAdminLogin,
|
||||
performUserLogin,
|
||||
redirectToHomePage,
|
||||
uuid,
|
||||
toastNotification,
|
||||
} from '../../utils/common';
|
||||
import {
|
||||
addAssetToGlossaryTerm,
|
||||
approveGlossaryTermTask,
|
||||
createGlossary,
|
||||
createGlossaryTerms,
|
||||
goToAssetsTab,
|
||||
renameGlossaryTerm,
|
||||
selectActiveGlossary,
|
||||
validateGlossaryTerm,
|
||||
verifyGlossaryDetails,
|
||||
verifyGlossaryTermAssets,
|
||||
} from '../../utils/glossary';
|
||||
import { sidebarClick } from '../../utils/sidebar';
|
||||
import { getRandomLastName } from '../../utils/user';
|
||||
|
||||
const user1 = new UserClass();
|
||||
const user2 = new UserClass();
|
||||
@ -368,17 +375,76 @@ test.describe('Glossary tests', () => {
|
||||
expect(assetContainerText).toContain(dashboardEntity.charts.name);
|
||||
});
|
||||
} finally {
|
||||
await glossary1.delete(apiContext);
|
||||
await glossary2.delete(apiContext);
|
||||
await glossaryTerm1.delete(apiContext);
|
||||
await glossaryTerm2.delete(apiContext);
|
||||
await glossaryTerm3.delete(apiContext);
|
||||
await glossaryTerm4.delete(apiContext);
|
||||
await glossary1.delete(apiContext);
|
||||
await glossary2.delete(apiContext);
|
||||
await dashboardEntity.delete(apiContext);
|
||||
await afterAction();
|
||||
}
|
||||
});
|
||||
|
||||
test('Rename Glossary Term and verify assets', async ({ browser }) => {
|
||||
const { page, afterAction, apiContext } = await performAdminLogin(browser);
|
||||
const table = new TableClass();
|
||||
const topic = new TopicClass();
|
||||
const dashboard = new DashboardClass();
|
||||
|
||||
await table.create(apiContext);
|
||||
await topic.create(apiContext);
|
||||
await dashboard.create(apiContext);
|
||||
|
||||
const glossary1 = new Glossary();
|
||||
const glossaryTerm1 = new GlossaryTerm(glossary1);
|
||||
glossary1.data.terms = [glossaryTerm1];
|
||||
await glossary1.create(apiContext);
|
||||
await glossaryTerm1.create(apiContext);
|
||||
|
||||
const assets = [table, topic, dashboard];
|
||||
|
||||
try {
|
||||
await test.step('Rename Glossary Term', async () => {
|
||||
const newName = `PW.${uuid()}%${getRandomLastName()}`;
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
await goToAssetsTab(page, glossaryTerm1.data.displayName);
|
||||
await addAssetToGlossaryTerm(page, assets);
|
||||
await renameGlossaryTerm(page, glossaryTerm1, newName);
|
||||
await verifyGlossaryTermAssets(
|
||||
page,
|
||||
glossary1.data,
|
||||
glossaryTerm1.data,
|
||||
assets.length
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Rename the same entity again', async () => {
|
||||
const newName = `PW Space.${uuid()}%${getRandomLastName()}`;
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary1.data.displayName);
|
||||
await goToAssetsTab(page, glossaryTerm1.data.displayName);
|
||||
await renameGlossaryTerm(page, glossaryTerm1, newName);
|
||||
await verifyGlossaryTermAssets(
|
||||
page,
|
||||
glossary1.data,
|
||||
glossaryTerm1.data,
|
||||
assets.length
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await table.delete(apiContext);
|
||||
await topic.delete(apiContext);
|
||||
await dashboard.delete(apiContext);
|
||||
await glossaryTerm1.delete(apiContext);
|
||||
await glossary1.delete(apiContext);
|
||||
await afterAction();
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||
await user1.delete(apiContext);
|
||||
|
||||
@ -138,4 +138,9 @@ export class GlossaryTerm {
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
rename(newTermName: string, newTermFqn: string) {
|
||||
this.responseData.name = newTermName;
|
||||
this.responseData.fullyQualifiedName = newTermFqn;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, Page } from '@playwright/test';
|
||||
import { get } from 'lodash';
|
||||
import { SidebarItem } from '../constant/sidebar';
|
||||
import { GLOSSARY_TERM_PATCH_PAYLOAD } from '../constant/version';
|
||||
import { DashboardClass } from '../support/entity/DashboardClass';
|
||||
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
|
||||
import { TableClass } from '../support/entity/TableClass';
|
||||
import { TopicClass } from '../support/entity/TopicClass';
|
||||
import { Glossary, GlossaryData } from '../support/glossary/Glossary';
|
||||
import {
|
||||
GlossaryTerm,
|
||||
@ -602,3 +606,104 @@ export const createGlossaryTerms = async (
|
||||
await createGlossaryTerm(page, term.data, termStatus, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkAssetsCount = async (page: Page, assetsCount: number) => {
|
||||
await expect(
|
||||
page.locator('[data-testid="assets"] [data-testid="filter-count"]')
|
||||
).toHaveText(assetsCount.toString());
|
||||
};
|
||||
|
||||
export const addAssetToGlossaryTerm = async (
|
||||
page: Page,
|
||||
assets: (TableClass | TopicClass | DashboardClass)[],
|
||||
hasExistingAssets = false
|
||||
) => {
|
||||
if (!hasExistingAssets) {
|
||||
await page.waitForSelector(
|
||||
'text=Adding a new Asset is easy, just give it a spin!'
|
||||
);
|
||||
}
|
||||
|
||||
await page.click('[data-testid="glossary-term-add-button-menu"]');
|
||||
await page.getByRole('menuitem', { name: 'Assets' }).click();
|
||||
|
||||
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-testid="asset-selection-modal"] .ant-modal-title')
|
||||
).toContainText('Add Assets');
|
||||
|
||||
for (const asset of assets) {
|
||||
const entityFqn = get(asset, 'entityResponseData.fullyQualifiedName');
|
||||
const entityName = get(asset, 'entityResponseData.name');
|
||||
const searchRes = page.waitForResponse('/api/v1/search/query*');
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'[data-testid="asset-selection-modal"] [data-testid="searchbar"]'
|
||||
)
|
||||
.fill(entityName);
|
||||
|
||||
await searchRes;
|
||||
await page.click(
|
||||
`[data-testid="table-data-card_${entityFqn}"] input[type="checkbox"]`
|
||||
);
|
||||
}
|
||||
|
||||
await page.click('[data-testid="save-btn"]');
|
||||
await checkAssetsCount(page, assets.length);
|
||||
};
|
||||
|
||||
export const updateNameForGlossaryTerm = async (
|
||||
page: Page,
|
||||
name: string,
|
||||
endPoint: string
|
||||
) => {
|
||||
await page.click('[data-testid="manage-button"]');
|
||||
await page.click('[data-testid="rename-button"]');
|
||||
|
||||
await expect(page.locator('#name')).toBeVisible();
|
||||
|
||||
await page.fill('#name', name);
|
||||
const updateNameResponsePromise = page.waitForResponse(
|
||||
`/api/v1/${endPoint}/*`
|
||||
);
|
||||
await page.click('[data-testid="save-button"]');
|
||||
const updateNameResponse = await updateNameResponsePromise;
|
||||
const data = await updateNameResponse.json();
|
||||
|
||||
await expect(page.locator('[data-testid="entity-header-name"]')).toHaveText(
|
||||
name
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const verifyGlossaryTermAssets = async (
|
||||
page: Page,
|
||||
glossary: GlossaryData,
|
||||
glossaryTermData: GlossaryTermData,
|
||||
assetsLength: number
|
||||
) => {
|
||||
await page.click('[data-testid="overview"]');
|
||||
await redirectToHomePage(page);
|
||||
await sidebarClick(page, SidebarItem.GLOSSARY);
|
||||
await selectActiveGlossary(page, glossary.displayName);
|
||||
await goToAssetsTab(
|
||||
page,
|
||||
glossaryTermData.displayName,
|
||||
assetsLength.toString()
|
||||
);
|
||||
};
|
||||
|
||||
export const renameGlossaryTerm = async (
|
||||
page: Page,
|
||||
glossaryTerm: GlossaryTerm,
|
||||
glossaryNewName: string
|
||||
) => {
|
||||
const data = await updateNameForGlossaryTerm(
|
||||
page,
|
||||
glossaryNewName,
|
||||
EntityTypeEndpoint.GlossaryTerm
|
||||
);
|
||||
await glossaryTerm.rename(data.name, data.fullyQualifiedName);
|
||||
};
|
||||
|
||||
@ -26,16 +26,14 @@ import {
|
||||
FormItemLayout,
|
||||
HelperTextType,
|
||||
} from '../../../interface/FormUtils.interface';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { generateFormFields, getField } from '../../../utils/formUtils';
|
||||
|
||||
import { NAME_FIELD_RULES } from '../../../constants/Form.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
|
||||
import ResizablePanels from '../../common/ResizablePanels/ResizablePanels';
|
||||
import TitleBreadcrumb from '../../common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
import { UserTag } from '../../common/UserTag/UserTag.component';
|
||||
import { UserTagSize } from '../../common/UserTag/UserTag.interface';
|
||||
import './add-glossary.less';
|
||||
import { AddGlossaryProps } from './AddGlossary.interface';
|
||||
|
||||
const AddGlossary = ({
|
||||
@ -136,6 +134,15 @@ const AddGlossary = ({
|
||||
height: 'auto',
|
||||
readonly: !allowAccess,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
whitespace: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.description'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
@ -236,36 +243,20 @@ const AddGlossary = ({
|
||||
<div className="add-glossary" data-testid="add-glossary">
|
||||
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||
{generateFormFields(formFields)}
|
||||
<div className="m-t-xss">
|
||||
<div className="m-y-xs">
|
||||
{getField(ownerField)}
|
||||
{selectedOwner && (
|
||||
<div className="m-y-xs" data-testid="owner-container">
|
||||
<UserTag
|
||||
id={selectedOwner.name ?? selectedOwner.id}
|
||||
isTeam={selectedOwner.type === EntityType.TEAM}
|
||||
name={getEntityName(selectedOwner)}
|
||||
size={UserTagSize.small}
|
||||
/>
|
||||
<OwnerLabel pills owner={selectedOwner} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="m-t-xss">
|
||||
<div className="m-y-xs">
|
||||
{getField(reviewersField)}
|
||||
{Boolean(reviewersList.length) && (
|
||||
<Space
|
||||
wrap
|
||||
className="m-y-xs"
|
||||
data-testid="reviewers-container"
|
||||
size={[8, 8]}>
|
||||
{reviewersList.map((d, index) => (
|
||||
<UserTag
|
||||
avatarType="outlined"
|
||||
id={d.name ?? d.id}
|
||||
isTeam={d.type === EntityType.TEAM}
|
||||
key={index}
|
||||
name={getEntityName(d)}
|
||||
size={UserTagSize.small}
|
||||
/>
|
||||
<Space wrap data-testid="reviewers-container" size={[8, 8]}>
|
||||
{reviewersList.map((d) => (
|
||||
<OwnerLabel pills key={d.id} owner={d} />
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
.add-glossary {
|
||||
.form-item-horizontal {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
@ -25,15 +25,12 @@ import {
|
||||
FormItemLayout,
|
||||
HelperTextType,
|
||||
} from '../../../interface/FormUtils.interface';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { generateFormFields, getField } from '../../../utils/formUtils';
|
||||
import { fetchGlossaryList } from '../../../utils/TagsUtils';
|
||||
|
||||
import { NAME_FIELD_RULES } from '../../../constants/Form.constants';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import { UserTeam } from '../../common/AssigneeList/AssigneeList.interface';
|
||||
import { UserTag } from '../../common/UserTag/UserTag.component';
|
||||
import { UserTagSize } from '../../common/UserTag/UserTag.interface';
|
||||
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
|
||||
import { AddGlossaryTermFormProps } from './AddGlossaryTermForm.interface';
|
||||
|
||||
const AddGlossaryTermForm = ({
|
||||
@ -195,6 +192,15 @@ const AddGlossaryTermForm = ({
|
||||
initialValue: '',
|
||||
height: 'auto',
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
whitespace: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.description'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
@ -432,14 +438,8 @@ const AddGlossaryTermForm = ({
|
||||
<div className="m-t-xss">
|
||||
{getField(ownerField)}
|
||||
{owner && (
|
||||
<div className="m-y-sm" data-testid="owner-container">
|
||||
<UserTag
|
||||
avatarType="outlined"
|
||||
id={owner.name ?? owner.id}
|
||||
isTeam={owner.type === UserTeam.Team}
|
||||
name={getEntityName(owner)}
|
||||
size={UserTagSize.small}
|
||||
/>
|
||||
<div className="m-b-sm" data-testid="owner-container">
|
||||
<OwnerLabel pills owner={owner} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -448,14 +448,7 @@ const AddGlossaryTermForm = ({
|
||||
{Boolean(reviewersList.length) && (
|
||||
<Space wrap data-testid="reviewers-container" size={[8, 8]}>
|
||||
{reviewersList.map((d) => (
|
||||
<UserTag
|
||||
avatarType="outlined"
|
||||
id={d.name ?? d.id}
|
||||
isTeam={d.type === UserTeam.Team}
|
||||
key={d.id}
|
||||
name={getEntityName(d)}
|
||||
size={UserTagSize.small}
|
||||
/>
|
||||
<OwnerLabel pills key={d.id} owner={d} />
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
@ -38,6 +38,8 @@ import EntityDeleteModal from '../../../components/Modals/EntityDeleteModal/Enti
|
||||
import EntityNameModal from '../../../components/Modals/EntityNameModal/EntityNameModal.component';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { DE_ACTIVE_COLOR } from '../../../constants/constants';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { EntityAction, EntityType } from '../../../enums/entity.enum';
|
||||
import { Glossary } from '../../../generated/entity/data/glossary';
|
||||
import {
|
||||
@ -45,6 +47,7 @@ import {
|
||||
GlossaryTerm,
|
||||
Status,
|
||||
} from '../../../generated/entity/data/glossaryTerm';
|
||||
import { Operation } from '../../../generated/entity/policies/policy';
|
||||
import { Style } from '../../../generated/type/tagLabel';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import { useFqn } from '../../../hooks/useFqn';
|
||||
@ -57,6 +60,7 @@ import {
|
||||
import { getEntityDeleteMessage } from '../../../utils/CommonUtils';
|
||||
import { getEntityVoteStatus } from '../../../utils/EntityUtils';
|
||||
import Fqn from '../../../utils/Fqn';
|
||||
import { checkPermission } from '../../../utils/PermissionsUtils';
|
||||
import {
|
||||
getGlossaryPath,
|
||||
getGlossaryPathWithAction,
|
||||
@ -104,6 +108,17 @@ const GlossaryHeader = ({
|
||||
const [isStyleEditing, setIsStyleEditing] = useState(false);
|
||||
const [openChangeParentHierarchyModal, setOpenChangeParentHierarchyModal] =
|
||||
useState(false);
|
||||
const { permissions: globalPermissions } = usePermissionProvider();
|
||||
|
||||
const createGlossaryTermPermission = useMemo(
|
||||
() =>
|
||||
checkPermission(
|
||||
Operation.Create,
|
||||
ResourceEntity.GLOSSARY_TERM,
|
||||
globalPermissions
|
||||
),
|
||||
[globalPermissions]
|
||||
);
|
||||
|
||||
// To fetch the latest glossary data
|
||||
// necessary to handle back click functionality to work properly in version page
|
||||
@ -436,7 +451,7 @@ const GlossaryHeader = ({
|
||||
}, [isGlossary, selectedData]);
|
||||
|
||||
const createButtons = useMemo(() => {
|
||||
if (permissions.Create) {
|
||||
if (permissions.Create || createGlossaryTermPermission) {
|
||||
return isGlossary ? (
|
||||
<Button
|
||||
className="m-l-xs"
|
||||
@ -471,7 +486,13 @@ const GlossaryHeader = ({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [isGlossary, permissions, addButtonContent, glossaryTermStatus]);
|
||||
}, [
|
||||
isGlossary,
|
||||
permissions,
|
||||
createGlossaryTermPermission,
|
||||
addButtonContent,
|
||||
glossaryTermStatus,
|
||||
]);
|
||||
|
||||
/**
|
||||
* To create breadcrumb from the fqn
|
||||
|
||||
@ -203,13 +203,15 @@ const AppSchedule = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
data-testid="edit-button"
|
||||
disabled={appData.deleted}
|
||||
type="primary"
|
||||
onClick={() => setShowModal(true)}>
|
||||
{t('label.edit')}
|
||||
</Button>
|
||||
{!appData.system && (
|
||||
<Button
|
||||
data-testid="edit-button"
|
||||
disabled={appData.deleted}
|
||||
type="primary"
|
||||
onClick={() => setShowModal(true)}>
|
||||
{t('label.edit')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
data-testid="run-now-button"
|
||||
|
||||
@ -145,9 +145,17 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.edit-glossary-modal .ant-form-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.edit-glossary-modal {
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.form-item-horizontal {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-glossary-modal .ant-form-item {
|
||||
&.m-b-xss {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user