Fix #18332: Do not consolidate changes for Parent Entities (#18333)

* Fix #18332: Do not consolidate changes for Parent Entities

* Fix #18332: Do not consolidate changes for Parent Entities

* added no data placeholder when no tags there

* fix failing playwright test due to consolidation changes and added the missing services

* Fix tests

* Fix parent entity change notifications

* Fix tests

* Fix parent entity change notifications

* Fix tests

* Fix tests

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
Sriharsha Chintalapani 2024-10-31 11:48:46 -07:00 committed by GitHub
parent 2539ffa8de
commit 680ace99b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 331 additions and 163 deletions

View File

@ -243,6 +243,7 @@ public final class Entity {
public static final String DOCUMENT = "document";
// ServiceType - Service Entity name map
static final Map<ServiceType, String> SERVICE_TYPE_ENTITY_MAP = new EnumMap<>(ServiceType.class);
public static final List<String> PARENT_ENTITY_TYPES = new ArrayList<>();
static {
SERVICE_TYPE_ENTITY_MAP.put(ServiceType.DATABASE, DATABASE_SERVICE);
@ -254,6 +255,25 @@ public final class Entity {
SERVICE_TYPE_ENTITY_MAP.put(ServiceType.STORAGE, STORAGE_SERVICE);
SERVICE_TYPE_ENTITY_MAP.put(ServiceType.SEARCH, SEARCH_SERVICE);
SERVICE_TYPE_ENTITY_MAP.put(ServiceType.API, API_SERVICE);
PARENT_ENTITY_TYPES.addAll(
listOf(
DATABASE_SERVICE,
DASHBOARD_SERVICE,
MESSAGING_SERVICE,
MLMODEL_SERVICE,
PIPELINE_SERVICE,
API_SERVICE,
API_COLLCECTION,
STORAGE_SERVICE,
METADATA_SERVICE,
SEARCH_SERVICE,
DATABASE,
DATABASE_SCHEMA,
CLASSIFICATION,
GLOSSARY,
DOMAIN,
TEST_SUITE,
TEAM));
}
private Entity() {}

View File

@ -39,6 +39,7 @@ public class APICollectionRepository extends EntityRepository<APICollection> {
"",
"");
supportsSearch = true;
parent = true;
}
@Override

View File

@ -16,5 +16,6 @@ public class APIServiceRepository extends ServiceEntityRepository<ApiService, Ap
"",
ServiceType.API);
supportsSearch = true;
parent = true;
}
}

View File

@ -54,6 +54,7 @@ public class ClassificationRepository extends EntityRepository<Classification> {
quoteFqn = true;
supportsSearch = true;
renameAllowed = true;
parent = true;
}
@Override

View File

@ -33,5 +33,6 @@ public class DashboardServiceRepository
"",
ServiceType.DASHBOARD);
supportsSearch = true;
parent = true;
}
}

View File

@ -69,6 +69,7 @@ public class DatabaseRepository extends EntityRepository<Database> {
"",
"");
supportsSearch = true;
parent = true;
fieldFetchers.put("name", this::fetchAndSetService);
}

View File

@ -74,6 +74,7 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
"",
"");
supportsSearch = true;
parent = true;
}
@Override

View File

@ -60,6 +60,7 @@ public class DatabaseServiceRepository
"",
ServiceType.DATABASE);
supportsSearch = true;
parent = true;
}
@Override

View File

@ -46,6 +46,7 @@ public class DomainRepository extends EntityRepository<Domain> {
UPDATE_FIELDS,
UPDATE_FIELDS);
supportsSearch = true;
parent = true;
}
@Override

View File

@ -260,6 +260,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
@Getter protected final Fields putFields;
protected boolean supportsSearch = false;
@Getter protected boolean parent = false;
protected final Map<String, BiConsumer<List<T>, Fields>> fieldFetchers = new HashMap<>();
protected EntityRepository(
@ -3276,7 +3277,9 @@ public abstract class EntityRepository<T extends EntityInterface> {
private boolean consolidateChanges(T original, T updated, Operation operation) {
// If user is the same and the new update is with in the user session timeout
return original.getVersion() > 0.1 // First update on an entity that
return !parent // Parent entity shouldn't consolidate changes, as we need ChangeDescription to
// propagate to children
&& original.getVersion() > 0.1 // First update on an entity that
&& operation == Operation.PATCH
&& !Boolean.TRUE.equals(original.getDeleted()) // Entity is not soft deleted
&& !operation.isDelete() // Operation must be an update
@ -3285,6 +3288,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
.equals(updated.getUpdatedBy()) // Must be updated by the same user
&& updated.getUpdatedAt() - original.getUpdatedAt()
<= sessionTimeoutMillis; // With in session timeout
// changes to children
}
private T getPreviousVersion(T original) {

View File

@ -89,6 +89,7 @@ public class GlossaryRepository extends EntityRepository<Glossary> {
quoteFqn = true;
supportsSearch = true;
renameAllowed = true;
parent = true;
}
@Override

View File

@ -34,5 +34,6 @@ public class MessagingServiceRepository
UPDATE_FIELDS,
ServiceType.MESSAGING);
supportsSearch = true;
parent = true;
}
}

View File

@ -19,5 +19,6 @@ public class MetadataServiceRepository
UPDATE_FIELDS,
ServiceType.METADATA);
supportsSearch = true;
parent = true;
}
}

View File

@ -34,5 +34,6 @@ public class MlModelServiceRepository
UPDATE_FIELDS,
ServiceType.ML_MODEL);
supportsSearch = true;
parent = true;
}
}

View File

@ -33,5 +33,6 @@ public class PipelineServiceRepository
"",
ServiceType.PIPELINE);
supportsSearch = true;
parent = true;
}
}

View File

@ -17,5 +17,6 @@ public class SearchServiceRepository
"",
ServiceType.SEARCH);
supportsSearch = true;
parent = true;
}
}

View File

@ -17,5 +17,6 @@ public class StorageServiceRepository
"",
ServiceType.STORAGE);
supportsSearch = true;
parent = true;
}
}

View File

@ -102,6 +102,7 @@ public class TeamRepository extends EntityRepository<Team> {
TEAM_UPDATE_FIELDS);
this.quoteFqn = true;
supportsSearch = true;
parent = true;
}
@Override

View File

@ -104,6 +104,7 @@ public class TestSuiteRepository extends EntityRepository<TestSuite> {
UPDATE_FIELDS);
quoteFqn = false;
supportsSearch = true;
parent = true;
}
@Override

View File

@ -449,7 +449,7 @@ public class SearchRepository {
Map<String, Object> fieldData = new HashMap<>();
if (changeDescription != null) {
for (FieldChange field : changeDescription.getFieldsAdded()) {
for (FieldChange field : changeDescription.getFieldsDeleted()) {
if (inheritableFields.contains(field.getName())) {
try {
if (field.getName().equals(FIELD_OWNERS)) {
@ -458,24 +458,21 @@ public class SearchRepository {
for (EntityReference inheritedOwner : inheritedOwners) {
inheritedOwner.setInherited(true);
}
fieldData.put("updatedOwners", inheritedOwners);
scriptTxt.append(ADD_OWNERS_SCRIPT);
fieldData.put("deletedOwners", inheritedOwners);
scriptTxt.append(REMOVE_OWNERS_SCRIPT);
} else {
EntityReference entityReference =
JsonUtils.readValue(field.getNewValue().toString(), EntityReference.class);
JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class);
scriptTxt.append(
String.format(
PROPAGATE_ENTITY_REFERENCE_FIELD_SCRIPT,
field.getName(),
field.getName(),
REMOVE_PROPAGATED_ENTITY_REFERENCE_FIELD_SCRIPT,
field.getName(),
field.getName(),
field.getName()));
fieldData.put(field.getName(), entityReference);
fieldData.put(field.getName(), JsonUtils.getMap(entityReference));
}
} catch (UnhandledServerException e) {
scriptTxt.append(
String.format(PROPAGATE_FIELD_SCRIPT, field.getName(), field.getNewValue()));
scriptTxt.append(String.format(REMOVE_PROPAGATED_FIELD_SCRIPT, field.getName()));
}
}
}
@ -507,7 +504,7 @@ public class SearchRepository {
}
}
}
for (FieldChange field : changeDescription.getFieldsDeleted()) {
for (FieldChange field : changeDescription.getFieldsAdded()) {
if (inheritableFields.contains(field.getName())) {
try {
if (field.getName().equals(FIELD_OWNERS)) {
@ -516,21 +513,24 @@ public class SearchRepository {
for (EntityReference inheritedOwner : inheritedOwners) {
inheritedOwner.setInherited(true);
}
fieldData.put("deletedOwners", inheritedOwners);
scriptTxt.append(REMOVE_OWNERS_SCRIPT);
fieldData.put("updatedOwners", inheritedOwners);
scriptTxt.append(ADD_OWNERS_SCRIPT);
} else {
EntityReference entityReference =
JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class);
JsonUtils.readValue(field.getNewValue().toString(), EntityReference.class);
scriptTxt.append(
String.format(
REMOVE_PROPAGATED_ENTITY_REFERENCE_FIELD_SCRIPT,
PROPAGATE_ENTITY_REFERENCE_FIELD_SCRIPT,
field.getName(),
field.getName(),
field.getName(),
field.getName(),
field.getName()));
fieldData.put(field.getName(), JsonUtils.getMap(entityReference));
fieldData.put(field.getName(), entityReference);
}
} catch (UnhandledServerException e) {
scriptTxt.append(String.format(REMOVE_PROPAGATED_FIELD_SCRIPT, field.getName()));
scriptTxt.append(
String.format(PROPAGATE_FIELD_SCRIPT, field.getName(), field.getNewValue()));
}
}
}

View File

@ -1549,13 +1549,24 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
checkOwnerOwns(TEAM11_REF, entity.getId(), true);
// V0-2 (consolidated) - Change owner from TEAM_OWNER1 to USER_OWNER1 using PATCH request
json = JsonUtils.pojoToJson(entity);
entity.setOwners(List.of(USER1_REF));
change = getChangeDescription(entity, CHANGE_CONSOLIDATED);
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
checkOwnerOwns(USER1_REF, entity.getId(), true);
checkOwnerOwns(TEAM11_REF, entity.getId(), false);
if (!isParent()) {
json = JsonUtils.pojoToJson(entity);
entity.setOwners(List.of(USER1_REF));
change = getChangeDescription(entity, getChangeType());
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
checkOwnerOwns(USER1_REF, entity.getId(), true);
checkOwnerOwns(TEAM11_REF, entity.getId(), false);
} else {
json = JsonUtils.pojoToJson(entity);
entity.setOwners(List.of(USER1_REF));
change = getChangeDescription(entity, getChangeType());
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
fieldDeleted(change, FIELD_OWNERS, List.of(TEAM11_REF));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
checkOwnerOwns(USER1_REF, entity.getId(), true);
checkOwnerOwns(TEAM11_REF, entity.getId(), false);
}
// V0.2 (no change) - Set the owner to the existing owner. No ownership change must be recorded.
json = JsonUtils.pojoToJson(entity);
@ -1565,11 +1576,20 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
// V0.1 (revert) - Remove ownership (USER_OWNER1) using PATCH. We are back to original state no
// owner and no change
json = JsonUtils.pojoToJson(entity);
entity.setOwners(null);
change = getChangeDescription(entity, REVERT);
patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, REVERT, change);
checkOwnerOwns(USER1_REF, entity.getId(), false);
if (!isParent()) {
json = JsonUtils.pojoToJson(entity);
entity.setOwners(null);
change = getChangeDescription(entity, REVERT);
patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, REVERT, change);
checkOwnerOwns(USER1_REF, entity.getId(), false);
} else {
json = JsonUtils.pojoToJson(entity);
entity.setOwners(null);
change = getChangeDescription(entity, MINOR_UPDATE);
fieldDeleted(change, FIELD_OWNERS, List.of(USER1_REF));
patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
checkOwnerOwns(USER1_REF, entity.getId(), false);
}
// set random type as entity. Check if the ownership validate.
T newEntity = entity;
@ -1873,37 +1893,71 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
//
// Replace description, add tags tier, owner - Changes are consolidated
//
origJson = JsonUtils.pojoToJson(entity);
entity.setDescription("description1");
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, CHANGE_CONSOLIDATED);
fieldUpdated(change, "description", "", "description1");
fieldAdded(change, "displayName", "displayName1");
if (supportsOwners) {
entity.setOwners(List.of(USER1_REF));
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
if (!isParent()) {
origJson = JsonUtils.pojoToJson(entity);
entity.setDescription("description1");
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, CHANGE_CONSOLIDATED);
fieldUpdated(change, "description", "", "description1");
fieldAdded(change, "displayName", "displayName1");
if (supportsOwners) {
entity.setOwners(List.of(USER1_REF));
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
}
if (supportsTags) {
entity.getTags().add(TIER1_TAG_LABEL);
fieldAdded(
change,
FIELD_TAGS,
List.of(USER_ADDRESS_TAG_LABEL, GLOSSARY2_TERM1_LABEL, TIER1_TAG_LABEL));
}
entity = patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, getChangeType(), change);
//
// Remove description, tier, owner - Changes are reverted going to 0.1 version of the entity
//
origJson = JsonUtils.pojoToJson(entity);
change = getChangeDescription(entity, REVERT);
entity.setDescription("");
entity.setDisplayName(null);
entity.setOwners(null);
entity.setTags(null);
patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, REVERT, change);
} else {
origJson = JsonUtils.pojoToJson(entity);
entity.setDescription("description1");
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, MINOR_UPDATE);
fieldUpdated(change, "description", "description", "description1");
fieldUpdated(change, "displayName", "displayName", "displayName1");
if (supportsOwners) {
entity.setOwners(List.of(USER1_REF));
fieldAdded(change, FIELD_OWNERS, List.of(USER1_REF));
fieldDeleted(change, FIELD_OWNERS, List.of(TEAM11_REF));
}
if (supportsTags) {
entity.getTags().add(TIER1_TAG_LABEL);
fieldAdded(change, FIELD_TAGS, List.of(TIER1_TAG_LABEL));
}
entity = patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, getChangeType(), change);
origJson = JsonUtils.pojoToJson(entity);
change = getChangeDescription(entity, MINOR_UPDATE);
entity.setDescription("");
entity.setDisplayName(null);
entity.setOwners(null);
entity.setTags(null);
fieldUpdated(change, "description", "description1", "");
fieldDeleted(change, "displayName", "displayName1");
if (supportsOwners) {
fieldDeleted(change, FIELD_OWNERS, List.of(USER1_REF));
}
if (supportsTags) {
fieldDeleted(change, FIELD_TAGS, List.of(TIER1_TAG_LABEL));
}
patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
if (supportsTags) {
entity.getTags().add(TIER1_TAG_LABEL);
fieldAdded(
change,
FIELD_TAGS,
List.of(USER_ADDRESS_TAG_LABEL, GLOSSARY2_TERM1_LABEL, TIER1_TAG_LABEL));
}
entity = patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
//
// Remove description, tier, owner - Changes are reverted going to 0.1 version of the entity
//
origJson = JsonUtils.pojoToJson(entity);
change = getChangeDescription(entity, REVERT);
entity.setDescription("");
entity.setDisplayName(null);
entity.setOwners(null);
entity.setTags(null);
patchEntityAndCheck(entity, origJson, ADMIN_AUTH_HEADERS, REVERT, change);
}
@Test
@ -1942,32 +1996,61 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
// Update description with a new description and the version changes as admin - the changes are
// consolidated
json = JsonUtils.pojoToJson(entity);
entity.setDescription("description2");
change = getChangeDescription(entity, CHANGE_CONSOLIDATED); // New version remains the same
fieldUpdated(change, "description", "description", "description2");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
if (!isParent()) {
json = JsonUtils.pojoToJson(entity);
entity.setDescription("description2");
change = getChangeDescription(entity, getChangeType()); // Version changes
fieldUpdated(change, "description", "description", "description2");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
// Update displayName with a new displayName - but as USER1
// Since the previous change is done by a different user, changes ** are not ** consolidated
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName");
change = getChangeDescription(entity, CHANGE_CONSOLIDATED); // Version changes
fieldUpdated(change, "description", "description", "description2");
fieldAdded(change, "displayName", "displayName");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
// Update displayName with a new displayName - but as USER1
// Since the previous change is done by a different user, changes ** are not ** consolidated
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName");
change = getChangeDescription(entity, CHANGE_CONSOLIDATED); // Version changes
fieldUpdated(change, "description", "description", "description2");
fieldAdded(change, "displayName", "displayName");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
// Update displayName to a new displayName.
// In this test, the user who previously made a change makes the change after session timeout.
// The changes are not consolidated.
EntityUpdater.setSessionTimeout(1); // Reduce the session timeout for this test
java.lang.Thread.sleep(2);
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, MINOR_UPDATE); // Version changes
fieldUpdated(change, "displayName", "displayName", "displayName1");
patchEntityAndCheck(entity, json, authHeaders(USER1.getName()), MINOR_UPDATE, change);
EntityUpdater.setSessionTimeout(10 * 60 * 10000); // Reset the session timeout back
// Update displayName to a new displayName.
// In this test, the user who previously made a change makes the change after session timeout.
// The changes are not consolidated.
EntityUpdater.setSessionTimeout(1); // Reduce the session timeout for this test
java.lang.Thread.sleep(2);
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, MINOR_UPDATE); // Version changes
fieldUpdated(change, "displayName", "displayName", "displayName1");
patchEntityAndCheck(entity, json, authHeaders(USER1.getName()), MINOR_UPDATE, change);
EntityUpdater.setSessionTimeout(10 * 60 * 10000); // Reset the session timeout back
} else {
json = JsonUtils.pojoToJson(entity);
entity.setDescription("description2");
change = getChangeDescription(entity, getChangeType()); // Version changes
fieldUpdated(change, "description", "description1", "description2");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
// Update displayName with a new displayName - but as USER1
// Since the previous change is done by a different user, changes ** are not ** consolidated
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName");
change = getChangeDescription(entity, MINOR_UPDATE); // Version changes
fieldAdded(change, "displayName", "displayName");
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Update displayName to a new displayName.
// In this test, the user who previously made a change makes the change after session timeout.
// The changes are not consolidated.
EntityUpdater.setSessionTimeout(1); // Reduce the session timeout for this test
java.lang.Thread.sleep(2);
json = JsonUtils.pojoToJson(entity);
entity.setDisplayName("displayName1");
change = getChangeDescription(entity, MINOR_UPDATE); // Version changes
fieldUpdated(change, "displayName", "displayName", "displayName1");
patchEntityAndCheck(entity, json, authHeaders(USER1.getName()), MINOR_UPDATE, change);
EntityUpdater.setSessionTimeout(10 * 60 * 10000); // Reset the session timeout back
}
}
@Test
@ -2058,17 +2141,21 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
JsonNode stringBValue = mapper.convertValue("stringB", JsonNode.class);
jsonNode.set("stringB", stringBValue);
entity.setExtension(jsonNode);
change =
getChangeDescription(
entity, CHANGE_CONSOLIDATED); // Patch operation update is consolidated in a session
fieldUpdated(
change,
EntityUtil.getExtensionField("intA"),
mapper.convertValue(1, JsonNode.class),
intAValue);
fieldAdded(change, "extension", List.of(JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
change = getChangeDescription(entity, getChangeType()); // PATCH operation update is not
if (!isParent()) {
fieldUpdated(
change,
EntityUtil.getExtensionField("intA"),
mapper.convertValue(1, JsonNode.class),
intAValue);
fieldAdded(change, "extension", List.of(JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
} else {
fieldAdded(change, "extension", List.of(JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
}
// PUT and remove field intA from the entity extension - *** for BOT this should be ignored ***
JsonNode oldNode = JsonUtils.valueToTree(entity.getExtension());
@ -2088,21 +2175,33 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
assertEquals(
JsonUtils.valueToTree(create.getExtension()), JsonUtils.valueToTree(entity.getExtension()));
// PATCH and remove field stringB from the entity extension
json = JsonUtils.pojoToJson(entity);
jsonNode.remove("stringB");
entity.setExtension(jsonNode);
change =
getChangeDescription(
entity, CHANGE_CONSOLIDATED); // PATCH operation update is consolidated into a session
fieldDeleted(
change,
"extension",
List.of(
JsonUtils.getObjectNode("intA", intAValue),
JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
if (!isParent()) {
// PATCH and remove field stringB from the entity extension
json = JsonUtils.pojoToJson(entity);
jsonNode.remove("stringB");
entity.setExtension(jsonNode);
change =
getChangeDescription(
entity, CHANGE_CONSOLIDATED); // PATCH operation update is consolidated into a session
fieldDeleted(
change,
"extension",
List.of(
JsonUtils.getObjectNode("intA", intAValue),
JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
} else {
json = JsonUtils.pojoToJson(entity);
jsonNode.remove("stringB");
entity.setExtension(jsonNode);
change =
getChangeDescription(
entity, MINOR_UPDATE); // PATCH operation update is consolidated into a session
fieldDeleted(change, "extension", List.of(JsonUtils.getObjectNode("stringB", stringBValue)));
entity = patchEntityAndCheck(entity, json, ADMIN_AUTH_HEADERS, getChangeType(), change);
assertEquals(JsonUtils.valueToTree(jsonNode), JsonUtils.valueToTree(entity.getExtension()));
}
// Now set the entity custom property to an invalid value
jsonNode.set(
@ -4115,4 +4214,16 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
}
}
}
public UpdateType getChangeType() {
if (isParent()) {
return MINOR_UPDATE;
} else {
return CHANGE_CONSOLIDATED;
}
}
public boolean isParent() {
return PARENT_ENTITY_TYPES.contains(entityType);
}
}

View File

@ -10,9 +10,7 @@ import static org.openmetadata.service.util.EntityUtil.fieldAdded;
import static org.openmetadata.service.util.EntityUtil.fieldDeleted;
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED;
import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE;
import static org.openmetadata.service.util.TestUtils.UpdateType.REVERT;
import static org.openmetadata.service.util.TestUtils.assertEntityReferenceNames;
import static org.openmetadata.service.util.TestUtils.assertListNotNull;
import static org.openmetadata.service.util.TestUtils.assertListNull;
@ -71,21 +69,17 @@ public class DomainResourceTest extends EntityResourceTest<Domain, CreateDomain>
fieldDeleted(change, "experts", listOf(USER2.getEntityReference()));
domain = updateAndCheckEntity(create, Status.OK, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Add User2 back as expert using PATCH
// Version 0. 2 - Changes from this PATCH is consolidated with the previous change resulting in
// no change
String json = JsonUtils.pojoToJson(domain);
domain.withExperts(List.of(USER1.getEntityReference(), USER2.getEntityReference()));
change = getChangeDescription(domain, REVERT);
domain = patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, REVERT, change);
change = getChangeDescription(domain, MINOR_UPDATE);
fieldAdded(change, "experts", listOf(USER2.getEntityReference()));
domain = patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Remove User2 as expert using PATCH
// Version 0.1 - Changes from this PATCH is consolidated with the previous two changes resulting
// in deletion of USER2
json = JsonUtils.pojoToJson(domain);
change = getChangeDescription(domain, REVERT);
change = getChangeDescription(domain, MINOR_UPDATE);
fieldDeleted(change, "experts", listOf(USER2.getEntityReference()));
domain.withExperts(List.of(USER1.getEntityReference()));
patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, REVERT, change);
patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
@ -104,9 +98,9 @@ public class DomainResourceTest extends EntityResourceTest<Domain, CreateDomain>
// Changes from this PATCH is consolidated with the previous changes
String json = JsonUtils.pojoToJson(domain);
domain.withDomainType(DomainType.CONSUMER_ALIGNED);
change = getChangeDescription(domain, CHANGE_CONSOLIDATED);
fieldUpdated(change, "domainType", DomainType.AGGREGATE, DomainType.CONSUMER_ALIGNED);
patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
change = getChangeDescription(domain, MINOR_UPDATE);
fieldUpdated(change, "domainType", DomainType.SOURCE_ALIGNED, DomainType.CONSUMER_ALIGNED);
patchEntityAndCheck(domain, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test

View File

@ -37,11 +37,11 @@ import static org.openmetadata.schema.type.ProviderType.SYSTEM;
import static org.openmetadata.schema.type.TaskType.RequestDescription;
import static org.openmetadata.service.security.SecurityUtil.authHeaders;
import static org.openmetadata.service.util.EntityUtil.fieldAdded;
import static org.openmetadata.service.util.EntityUtil.fieldDeleted;
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
import static org.openmetadata.service.util.EntityUtil.getFqn;
import static org.openmetadata.service.util.EntityUtil.toTagLabels;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED;
import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE;
import static org.openmetadata.service.util.TestUtils.assertListNull;
import static org.openmetadata.service.util.TestUtils.assertResponse;
@ -168,11 +168,9 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
glossary.withReviewers(List.of(USER1_REF, USER2_REF));
change =
getChangeDescription(
glossary,
CHANGE_CONSOLIDATED); // PATCH operation update is consolidated in a user session
fieldAdded(change, "reviewers", List.of(USER1_REF, USER2_REF));
glossary =
patchEntityAndCheck(glossary, origJson, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
glossary, MINOR_UPDATE); // PATCH operation update is consolidated in a user session
fieldAdded(change, "reviewers", List.of(USER2_REF));
glossary = patchEntityAndCheck(glossary, origJson, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Create a glossary term and assign USER2 as a reviewer
GlossaryTermResourceTest glossaryTermResourceTest = new GlossaryTermResourceTest();
@ -220,10 +218,9 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
glossary.withReviewers(List.of(USER2_REF));
change =
getChangeDescription(
glossary,
CHANGE_CONSOLIDATED); // PATCH operation update is consolidated in a user session
fieldAdded(change, "reviewers", List.of(USER2_REF));
patchEntityAndCheck(glossary, origJson, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
glossary, MINOR_UPDATE); // PATCH operation update is consolidated in a user session
fieldDeleted(change, "reviewers", List.of(USER1_REF));
patchEntityAndCheck(glossary, origJson, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Verify that USER1_REF is removed from the reviewers for the terms inside the glossary
GLOSSARY_TERM1 =

View File

@ -52,10 +52,8 @@ import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.TEST_AUTH_HEADERS;
import static org.openmetadata.service.util.TestUtils.TEST_USER_NAME;
import static org.openmetadata.service.util.TestUtils.USER_WITH_CREATE_HEADERS;
import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED;
import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE;
import static org.openmetadata.service.util.TestUtils.UpdateType.NO_CHANGE;
import static org.openmetadata.service.util.TestUtils.UpdateType.REVERT;
import static org.openmetadata.service.util.TestUtils.assertListNotNull;
import static org.openmetadata.service.util.TestUtils.assertResponse;
import static org.openmetadata.service.util.TestUtils.validateEntityReferences;
@ -600,11 +598,12 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
bu2 = updateAndCheckEntity(create, OK, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Change bu2 parent from Organization to bu1 using PATCH operation.
// Change from this PATCH is combined with the previous PUT resulting in no change
String json = JsonUtils.pojoToJson(bu2);
change = getChangeDescription(bu2, REVERT);
change = getChangeDescription(bu2, MINOR_UPDATE);
bu2.setParents(List.of(bu1.getEntityReference()));
patchEntityAndCheck(bu2, json, ADMIN_AUTH_HEADERS, REVERT, change);
fieldAdded(change, "parents", List.of(bu1.getEntityReference()));
fieldDeleted(change, "parents", List.of(ORG_TEAM.getEntityReference()));
patchEntityAndCheck(bu2, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
@ -626,11 +625,11 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
fieldUpdated(change, "isJoinable", false, true);
team = patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// set isJoinable to false - change from this PATCH and the previous are consolidated resulting
// in no change
json = JsonUtils.pojoToJson(team);
team.setIsJoinable(false);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, NO_CHANGE, null);
change = getChangeDescription(team, MINOR_UPDATE);
fieldUpdated(change, "isJoinable", true, false);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
@ -672,10 +671,9 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
int removeDefaultRoleIndex = new Random().nextInt(roles.size());
EntityReference deletedRole = team.getDefaultRoles().get(removeDefaultRoleIndex);
team.getDefaultRoles().remove(removeDefaultRoleIndex);
change = getChangeDescription(team, CHANGE_CONSOLIDATED);
fieldDeleted(change, "users", CommonUtil.listOf(deletedUser));
change = getChangeDescription(team, MINOR_UPDATE);
fieldDeleted(change, "defaultRoles", CommonUtil.listOf(deletedRole));
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
@ -725,8 +723,10 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
// resulting in no change
json = JsonUtils.pojoToJson(team);
team.withPolicies(null);
change = getChangeDescription(team, REVERT);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, REVERT, change);
change = getChangeDescription(team, MINOR_UPDATE);
fieldDeleted(
change, "policies", List.of(POLICY1.getEntityReference(), POLICY2.getEntityReference()));
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test
@ -768,13 +768,11 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
fieldUpdated(change, "profile", PROFILE, profile1);
team = patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
// Remove profile from the team - Change from this PATCH and previous are consolidated to no
// change
json = JsonUtils.pojoToJson(team);
team.withProfile(null);
change = getChangeDescription(team, CHANGE_CONSOLIDATED);
fieldDeleted(change, "profile", PROFILE);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
change = getChangeDescription(team, MINOR_UPDATE);
fieldDeleted(change, "profile", profile1);
patchEntityAndCheck(team, json, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
}
@Test

View File

@ -89,7 +89,7 @@ test('Glossary', async ({ page }) => {
await page.reload();
const versionPageResponse = page.waitForResponse(
`/api/v1/glossaries/${glossary.responseData.id}/versions/0.2`
`/api/v1/glossaries/${glossary.responseData.id}/versions/0.3`
);
await page.click('[data-testid="version-button"]');
await versionPageResponse;

View File

@ -173,7 +173,9 @@ test('Classification Page', async ({ page }) => {
)
).not.toBeVisible();
await expect(page.getByTestId('saveAssociatedTag')).not.toBeVisible();
await expect(page.getByText('No Tags are available')).toBeVisible();
await expect(page.getByTestId('saveAssociatedTag')).toBeDisabled();
// Re-enable the disabled Classification
await classification.visitPage(page);
@ -207,6 +209,7 @@ test('Classification Page', async ({ page }) => {
});
await test.step('Create classification with validation checks', async () => {
await classification.visitPage(page);
await page.click('[data-testid="add-classification"]');
await page.waitForSelector('.ant-modal-content', {
state: 'visible',

View File

@ -80,12 +80,12 @@ test('Classification version page', async ({ page }) => {
await patchClassificationResponse;
// Verify disabled state
await page.click('[data-testid="version-button"]:has-text("0.2")');
await page.click('[data-testid="version-button"]:has-text("0.3")');
await expect(page.locator('[data-testid="disabled"]')).toBeVisible();
// Toggle back to enabled
await page.click('[data-testid="version-button"]:has-text("0.2")');
await page.click('[data-testid="version-button"]:has-text("0.3")');
await page.click('[data-testid="manage-button"]');
const patchClassificationResponse2 = page.waitForResponse(
`/api/v1/classifications/${classification.responseData?.id}`
@ -94,7 +94,7 @@ test('Classification version page', async ({ page }) => {
await patchClassificationResponse2;
// Verify enabled state
await page.click('[data-testid="version-button"]:has-text("0.2")');
await page.click('[data-testid="version-button"]:has-text("0.4")');
await expect(
page.locator(`[data-testid="classification-${classification.data.name}"]`)

View File

@ -153,7 +153,7 @@ entities.forEach((EntityClass) => {
type: 'Users',
});
const versionDetailResponse = page.waitForResponse(`**/versions/0.2`);
const versionDetailResponse = page.waitForResponse(`**/versions/0.3`);
await page.locator('[data-testid="version-button"]').click();
await versionDetailResponse;
@ -169,7 +169,7 @@ entities.forEach((EntityClass) => {
await assignTier(page, 'Tier1', entity.endpoint);
const versionDetailResponse = page.waitForResponse(`**/versions/0.2`);
const versionDetailResponse = page.waitForResponse(`**/versions/0.4`);
await page.locator('[data-testid="version-button"]').click();
await versionDetailResponse;
@ -210,7 +210,7 @@ entities.forEach((EntityClass) => {
await expect(deletedBadge).toHaveText('Deleted');
const versionDetailResponse = page.waitForResponse(`**/versions/0.3`);
const versionDetailResponse = page.waitForResponse(`**/versions/0.5`);
await page.locator('[data-testid="version-button"]').click();
await versionDetailResponse;

View File

@ -136,7 +136,7 @@ export const addTagToTableColumn = async (
await expect(
page.locator(
`[data-testid="classification-tags-${columnNumber}"] [data-testid="tags-container"] [data-testid="icon"]`
`[data-testid="classification-tags-${columnNumber}"] [data-testid="tags-container"] [data-testid="tag-${tagFqn}"]`
)
).toBeVisible();
};

View File

@ -13,6 +13,7 @@
import { CloseOutlined } from '@ant-design/icons';
import {
Button,
Empty,
Form,
Select,
SelectProps,
@ -187,6 +188,7 @@ const AsyncSelectList: FC<AsyncSelectListProps & SelectProps> = ({
<Button
className="update-btn"
data-testid="saveAssociatedTag"
disabled={isEmpty(tagOptions)}
htmlType="submit"
loading={isSubmitLoading}
size="small"
@ -302,7 +304,18 @@ const AsyncSelectList: FC<AsyncSelectListProps & SelectProps> = ({
dropdownRender={dropdownRender}
filterOption={false}
mode={mode}
notFoundContent={isLoading ? <Loader size="small" /> : null}
notFoundContent={
isLoading ? (
<Loader size="small" />
) : (
<Empty
description={t('label.no-entity-available', {
entity: t('label.tag-plural'),
})}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)
}
optionLabelProp="label"
style={{ width: '100%' }}
tagRender={customTagRender}

View File

@ -773,6 +773,7 @@
"no-description": "Keine Beschreibung",
"no-diff-available": "Keine Unterschiede verfügbar",
"no-entity": "Keine {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "Keine passenden Datenanlagen gefunden",
"no-of-test": " Nr. der Tests",

View File

@ -773,6 +773,7 @@
"no-description": "No description",
"no-diff-available": "No diff available",
"no-entity": "No {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "No matching data assets found",
"no-of-test": " No. of Test",

View File

@ -773,6 +773,7 @@
"no-description": "Sin descripción",
"no-diff-available": "Sin diferencia disponible",
"no-entity": "No hay {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "No se encontraron activos de datos coincidentes",
"no-of-test": "No. de prueba",

View File

@ -773,6 +773,7 @@
"no-description": "Aucune description",
"no-diff-available": "Aucune différence disponible",
"no-entity": "Pas de {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "Aucun actif de données trouvé",
"no-of-test": " No. de Tests",

View File

@ -773,6 +773,7 @@
"no-description": "-",
"no-diff-available": "אין הבדל זמין",
"no-entity": "אין {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "לא נמצאו נכסי נתונים תואמים",
"no-of-test": "מספר הבדיקות",

View File

@ -773,6 +773,7 @@
"no-description": "説明がありません",
"no-diff-available": "差分を見ることはできません",
"no-entity": "No {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "マッチするデータアセットはありません",
"no-of-test": " テスト番号",

View File

@ -773,6 +773,7 @@
"no-description": "Geen beschrijving",
"no-diff-available": "Geen verschil beschikbaar",
"no-entity": "Geen {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "Geen overeenkomende data-assets gevonden",
"no-of-test": " Aantal tests",

View File

@ -773,6 +773,7 @@
"no-description": "بدون توضیح",
"no-diff-available": "هیچ تفاوتی موجود نیست",
"no-entity": "هیچ {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "هیچ {{entity}} انتخاب نشده",
"no-matching-data-asset": "هیچ دارایی داده‌ی منطبق یافت نشد",
"no-of-test": "تعداد تست",

View File

@ -773,6 +773,7 @@
"no-description": "Sem descrição",
"no-diff-available": "Nenhuma diferença disponível",
"no-entity": "Nenhum(a) {{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "Nenhum ativo de dados correspondente encontrado",
"no-of-test": " Nº de Teste",

View File

@ -773,6 +773,7 @@
"no-description": "Нет описания",
"no-diff-available": "Нет различий",
"no-entity": "{{entity}} отсутствует",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "Подходящие объекты данных не найдены",
"no-of-test": "№ теста",

View File

@ -773,6 +773,7 @@
"no-description": "无描述",
"no-diff-available": "没有可用的差异",
"no-entity": "没有{{entity}}",
"no-entity-available": "No {{entity}} are available",
"no-entity-selected": "No {{entity}} Selected",
"no-matching-data-asset": "未找到匹配的数据资产",
"no-of-test": "测试数量",