Fix #15735: Glossary,Term, Classification,Tag rename operation fixes (#16874)

* 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:
sonika-shah 2024-07-16 17:20:36 +05:30 committed by GitHub
parent 3bcfdfe014
commit 692c21f2f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 586 additions and 196 deletions

View File

@ -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());
}

View File

@ -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);

View File

@ -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));

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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

View File

@ -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 =

View File

@ -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<>();
}
}

View File

@ -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);

View File

@ -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);

View File

@ -164,4 +164,8 @@ public class ReindexingUtil {
return entities;
}
public static String escapeDoubleQuotes(String str) {
return str.replace("\"", "\\\"");
}
}

View File

@ -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);

View File

@ -138,4 +138,9 @@ export class GlossaryTerm {
return await response.json();
}
rename(newTermName: string, newTermFqn: string) {
this.responseData.name = newTermName;
this.responseData.fullyQualifiedName = newTermFqn;
}
}

View File

@ -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);
};

View File

@ -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>
)}

View File

@ -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;
}
}

View File

@ -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>
)}

View File

@ -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

View File

@ -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"

View File

@ -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;
}