Add Data Product Lineage (#20400)

* Add Data Product Lineage

* Migrate Data Products

* add null check for entity

* add data product layer

* Add index specific conditions

* add tests

* update nlp icon

* Add missing conditions

---------

Co-authored-by: karanh37 <karanh37@gmail.com>
This commit is contained in:
Mohit Yadav 2025-03-26 10:02:00 +05:30 committed by GitHub
parent 3245b2d98e
commit 7e731648ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 415 additions and 211 deletions

View File

@ -50,6 +50,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import javax.json.JsonPatch; import javax.json.JsonPatch;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import lombok.Getter; import lombok.Getter;
@ -196,6 +197,7 @@ public class LineageRepository {
addServiceLineage(fromEntity, toEntity, lineageDetails, childRelationExists); addServiceLineage(fromEntity, toEntity, lineageDetails, childRelationExists);
addDomainLineage(fromEntity, toEntity, lineageDetails, childRelationExists); addDomainLineage(fromEntity, toEntity, lineageDetails, childRelationExists);
addDataProductsLineage(fromEntity, toEntity, lineageDetails, childRelationExists);
} }
private void addServiceLineage( private void addServiceLineage(
@ -203,44 +205,17 @@ public class LineageRepository {
EntityInterface toEntity, EntityInterface toEntity,
LineageDetails entityLineageDetails, LineageDetails entityLineageDetails,
boolean childRelationExists) { boolean childRelationExists) {
boolean addService = if (!shouldAddServiceLineage(fromEntity, toEntity)) {
Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_SERVICE) return;
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_SERVICE); }
// Add Service Level Lineage // Add Service Level Lineage
if (addService && fromEntity.getService() != null && toEntity.getService() != null) { EntityReference fromService = fromEntity.getService();
EntityReference fromService = fromEntity.getService(); EntityReference toService = toEntity.getService();
EntityReference toService = toEntity.getService(); if (Boolean.FALSE.equals(fromService.getId().equals(toService.getId()))) {
if (Boolean.FALSE.equals(fromService.getId().equals(toService.getId()))) { LineageDetails serviceLineageDetails =
CollectionDAO.EntityRelationshipObject serviceRelation = getOrCreateLineageDetails(
dao.relationshipDAO() fromService.getId(), toService.getId(), entityLineageDetails, childRelationExists);
.getRecord(fromService.getId(), toService.getId(), Relationship.UPSTREAM.ordinal()); insertLineage(fromService, toService, serviceLineageDetails);
LineageDetails serviceLineageDetails;
if (serviceRelation != null) {
serviceLineageDetails =
JsonUtils.readValue(serviceRelation.getJson(), LineageDetails.class);
if (!childRelationExists) {
serviceLineageDetails.withAssetEdges(serviceLineageDetails.getAssetEdges() + 1);
}
} else {
serviceLineageDetails =
new LineageDetails()
.withCreatedAt(entityLineageDetails.getCreatedAt())
.withCreatedBy(entityLineageDetails.getCreatedBy())
.withUpdatedAt(entityLineageDetails.getUpdatedAt())
.withUpdatedBy(entityLineageDetails.getUpdatedBy())
.withSource(LineageDetails.Source.CHILD_ASSETS)
.withAssetEdges(1);
}
dao.relationshipDAO()
.insert(
fromService.getId(),
toService.getId(),
fromService.getType(),
toService.getType(),
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(serviceLineageDetails));
addLineageToSearch(fromService, toService, serviceLineageDetails);
}
} }
} }
@ -249,47 +224,106 @@ public class LineageRepository {
EntityInterface toEntity, EntityInterface toEntity,
LineageDetails entityLineageDetails, LineageDetails entityLineageDetails,
boolean childRelationExists) { boolean childRelationExists) {
boolean addDomain =
Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_DOMAIN) if (!shouldAddDomainsLineage(fromEntity, toEntity)) {
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_DOMAIN); return;
// Add Service Level Lineage }
if (addDomain && fromEntity.getDomain() != null && toEntity.getDomain() != null) {
EntityReference fromDomain = fromEntity.getDomain(); EntityReference fromDomain = fromEntity.getDomain();
EntityReference toDomain = toEntity.getDomain(); EntityReference toDomain = toEntity.getDomain();
if (Boolean.FALSE.equals(fromDomain.getId().equals(toDomain.getId()))) { if (Boolean.FALSE.equals(fromDomain.getId().equals(toDomain.getId()))) {
CollectionDAO.EntityRelationshipObject serviceRelation = LineageDetails domainLineageDetails =
dao.relationshipDAO() getOrCreateLineageDetails(
.getRecord(fromDomain.getId(), toDomain.getId(), Relationship.UPSTREAM.ordinal()); fromDomain.getId(), toDomain.getId(), entityLineageDetails, childRelationExists);
LineageDetails domainLineageDetails; insertLineage(fromDomain, toDomain, domainLineageDetails);
if (serviceRelation != null) { }
domainLineageDetails = }
JsonUtils.readValue(serviceRelation.getJson(), LineageDetails.class);
if (!childRelationExists) { private void addDataProductsLineage(
domainLineageDetails.withAssetEdges(domainLineageDetails.getAssetEdges() + 1); EntityInterface fromEntity,
} EntityInterface toEntity,
} else { LineageDetails entityLineageDetails,
domainLineageDetails = boolean childRelationExists) {
new LineageDetails()
.withCreatedAt(entityLineageDetails.getCreatedAt()) if (!shouldAddDataProductLineage(fromEntity, toEntity)) {
.withCreatedBy(entityLineageDetails.getCreatedBy()) return;
.withUpdatedAt(entityLineageDetails.getUpdatedAt()) }
.withUpdatedBy(entityLineageDetails.getUpdatedBy())
.withSource(LineageDetails.Source.CHILD_ASSETS) for (EntityReference fromEntityRef : fromEntity.getDataProducts()) {
.withAssetEdges(1); for (EntityReference toEntityRef : toEntity.getDataProducts()) {
if (!fromEntityRef.getId().equals(toEntityRef.getId())) {
LineageDetails dataProductsLineageDetails =
getOrCreateLineageDetails(
fromEntityRef.getId(),
toEntityRef.getId(),
entityLineageDetails,
childRelationExists);
insertLineage(fromEntityRef, toEntityRef, dataProductsLineageDetails);
} }
dao.relationshipDAO()
.insert(
fromDomain.getId(),
toDomain.getId(),
fromDomain.getType(),
toDomain.getType(),
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(domainLineageDetails));
addLineageToSearch(fromDomain, toDomain, domainLineageDetails);
} }
} }
} }
private boolean shouldAddDataProductLineage(
EntityInterface fromEntity, EntityInterface toEntity) {
return Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_DATA_PRODUCTS)
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_DATA_PRODUCTS)
&& !nullOrEmpty(fromEntity.getDataProducts())
&& !nullOrEmpty(toEntity.getDataProducts());
}
private boolean shouldAddDomainsLineage(EntityInterface fromEntity, EntityInterface toEntity) {
return Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_DOMAIN)
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_DOMAIN)
&& fromEntity.getDomain() != null
&& toEntity.getDomain() != null;
}
private boolean shouldAddServiceLineage(EntityInterface fromEntity, EntityInterface toEntity) {
return Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_SERVICE)
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_SERVICE)
&& fromEntity.getService() != null
&& toEntity.getService() != null;
}
private LineageDetails getOrCreateLineageDetails(
UUID fromId, UUID toId, LineageDetails entityLineageDetails, boolean childRelationExists) {
CollectionDAO.EntityRelationshipObject existingRelation =
dao.relationshipDAO().getRecord(fromId, toId, Relationship.UPSTREAM.ordinal());
if (existingRelation != null) {
LineageDetails lineageDetails =
JsonUtils.readValue(existingRelation.getJson(), LineageDetails.class);
if (!childRelationExists) {
lineageDetails.withAssetEdges(lineageDetails.getAssetEdges() + 1);
}
return lineageDetails;
}
return new LineageDetails()
.withCreatedAt(entityLineageDetails.getCreatedAt())
.withCreatedBy(entityLineageDetails.getCreatedBy())
.withUpdatedAt(entityLineageDetails.getUpdatedAt())
.withUpdatedBy(entityLineageDetails.getUpdatedBy())
.withSource(LineageDetails.Source.CHILD_ASSETS)
.withAssetEdges(1);
}
private void insertLineage(
EntityReference from, EntityReference to, LineageDetails lineageDetails) {
dao.relationshipDAO()
.insert(
from.getId(),
to.getId(),
from.getType(),
to.getType(),
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(lineageDetails));
addLineageToSearch(from, to, lineageDetails);
}
private String getExtendedLineageFields(boolean service, boolean domain, boolean dataProducts) { private String getExtendedLineageFields(boolean service, boolean domain, boolean dataProducts) {
StringBuilder fieldsBuilder = new StringBuilder(); StringBuilder fieldsBuilder = new StringBuilder();
@ -535,6 +569,12 @@ public class LineageRepository {
JsonNode fromEntity = entityMap.getOrDefault(fromEntityId, null); JsonNode fromEntity = entityMap.getOrDefault(fromEntityId, null);
JsonNode toEntity = entityMap.getOrDefault(toEntityId, null); JsonNode toEntity = entityMap.getOrDefault(toEntityId, null);
if (fromEntity == null || toEntity == null) {
LOG.error(
"Entity not found for IDs: fromEntityId={}, toEntityId={}", fromEntityId, toEntityId);
return;
}
Map<String, String> baseRow = new HashMap<>(); Map<String, String> baseRow = new HashMap<>();
baseRow.put("fromEntityFQN", getText(fromEntity, FIELD_FULLY_QUALIFIED_NAME)); baseRow.put("fromEntityFQN", getText(fromEntity, FIELD_FULLY_QUALIFIED_NAME));
baseRow.put("fromServiceName", getText(fromEntity.path(FIELD_SERVICE), FIELD_NAME)); baseRow.put("fromServiceName", getText(fromEntity.path(FIELD_SERVICE), FIELD_NAME));
@ -906,99 +946,88 @@ public class LineageRepository {
} }
private void cleanUpExtendedLineage(EntityReference from, EntityReference to) { private void cleanUpExtendedLineage(EntityReference from, EntityReference to) {
boolean addService = boolean addService = hasField(from, FIELD_SERVICE) && hasField(to, FIELD_SERVICE);
Entity.entityHasField(from.getType(), FIELD_SERVICE) boolean addDomain = hasField(from, FIELD_DOMAIN) && hasField(to, FIELD_DOMAIN);
&& Entity.entityHasField(to.getType(), FIELD_SERVICE);
boolean addDomain =
Entity.entityHasField(from.getType(), FIELD_DOMAIN)
&& Entity.entityHasField(to.getType(), FIELD_DOMAIN);
boolean addDataProduct = boolean addDataProduct =
Entity.entityHasField(from.getType(), FIELD_DATA_PRODUCTS) hasField(from, FIELD_DATA_PRODUCTS) && hasField(to, FIELD_DATA_PRODUCTS);
&& Entity.entityHasField(to.getType(), FIELD_DATA_PRODUCTS);
String fields = getExtendedLineageFields(addService, addDomain, addDataProduct); String fields = getExtendedLineageFields(addService, addDomain, addDataProduct);
EntityInterface fromEntity = EntityInterface fromEntity =
Entity.getEntity(from.getType(), from.getId(), fields, Include.ALL); Entity.getEntity(from.getType(), from.getId(), fields, Include.ALL);
EntityInterface toEntity = Entity.getEntity(to.getType(), to.getId(), fields, Include.ALL); EntityInterface toEntity = Entity.getEntity(to.getType(), to.getId(), fields, Include.ALL);
cleanUpServiceLineage(fromEntity, toEntity); cleanUpLineage(fromEntity, toEntity, FIELD_SERVICE, EntityInterface::getService);
cleanUpDomainLineage(fromEntity, toEntity); cleanUpLineage(fromEntity, toEntity, FIELD_DOMAIN, EntityInterface::getDomain);
cleanUpLineageForDataProducts(
fromEntity, toEntity, FIELD_DATA_PRODUCTS, EntityInterface::getDataProducts);
} }
private void cleanUpServiceLineage(EntityInterface fromEntity, EntityInterface toEntity) { private boolean hasField(EntityReference entity, String field) {
boolean hasServiceField = return Entity.entityHasField(entity.getType(), field);
Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_SERVICE) }
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_SERVICE);
if (hasServiceField && fromEntity.getService() != null && toEntity.getService() != null) { private void cleanUpLineage(
EntityReference fromService = fromEntity.getService(); EntityInterface fromEntity,
EntityReference toService = toEntity.getService(); EntityInterface toEntity,
CollectionDAO.EntityRelationshipObject serviceRelation = String field,
dao.relationshipDAO() Function<EntityInterface, EntityReference> getter) {
.getRecord(fromService.getId(), toService.getId(), Relationship.UPSTREAM.ordinal()); boolean hasField =
LineageDetails serviceLineageDetails; hasField(fromEntity.getEntityReference(), field)
if (serviceRelation != null) { && hasField(toEntity.getEntityReference(), field);
serviceLineageDetails = if (!hasField) return;
JsonUtils.readValue(serviceRelation.getJson(), LineageDetails.class);
if (serviceLineageDetails.getAssetEdges() - 1 < 1) { EntityReference fromRef = getter.apply(fromEntity);
dao.relationshipDAO() EntityReference toRef = getter.apply(toEntity);
.delete( processExtendedLineageCleanup(fromRef, toRef);
fromService.getId(), }
fromService.getType(),
toService.getId(), private void cleanUpLineageForDataProducts(
toService.getType(), EntityInterface fromEntity,
Relationship.UPSTREAM.ordinal()); EntityInterface toEntity,
deleteLineageFromSearch(fromService, toService, serviceLineageDetails); String field,
} else { Function<EntityInterface, List<EntityReference>> getter) {
serviceLineageDetails.withAssetEdges(serviceLineageDetails.getAssetEdges() - 1); boolean hasField =
dao.relationshipDAO() hasField(fromEntity.getEntityReference(), field)
.insert( && hasField(toEntity.getEntityReference(), field);
fromService.getId(), if (!hasField) return;
toService.getId(),
fromService.getType(), for (EntityReference fromRef : getter.apply(fromEntity)) {
toService.getType(), for (EntityReference toRef : getter.apply(toEntity)) {
Relationship.UPSTREAM.ordinal(), processExtendedLineageCleanup(fromRef, toRef);
JsonUtils.pojoToJson(serviceLineageDetails));
addLineageToSearch(fromService, toService, serviceLineageDetails);
}
} }
} }
} }
private void cleanUpDomainLineage(EntityInterface fromEntity, EntityInterface toEntity) { private void processExtendedLineageCleanup(EntityReference fromRef, EntityReference toRef) {
boolean hasDomainField = if (fromRef == null || toRef == null) return;
Entity.entityHasField(fromEntity.getEntityReference().getType(), FIELD_DOMAIN)
&& Entity.entityHasField(toEntity.getEntityReference().getType(), FIELD_DOMAIN); CollectionDAO.EntityRelationshipObject relation =
if (hasDomainField && fromEntity.getDomain() != null && toEntity.getDomain() != null) { dao.relationshipDAO()
EntityReference fromDomain = fromEntity.getDomain(); .getRecord(fromRef.getId(), toRef.getId(), Relationship.UPSTREAM.ordinal());
EntityReference toDomain = toEntity.getDomain();
CollectionDAO.EntityRelationshipObject domainRelation = if (relation == null) return;
dao.relationshipDAO()
.getRecord(fromDomain.getId(), toDomain.getId(), Relationship.UPSTREAM.ordinal()); LineageDetails lineageDetails = JsonUtils.readValue(relation.getJson(), LineageDetails.class);
LineageDetails domainLineageDetails; if (lineageDetails.getAssetEdges() - 1 < 1) {
if (domainRelation != null) { dao.relationshipDAO()
domainLineageDetails = JsonUtils.readValue(domainRelation.getJson(), LineageDetails.class); .delete(
if (domainLineageDetails.getAssetEdges() - 1 < 1) { fromRef.getId(),
dao.relationshipDAO() fromRef.getType(),
.delete( toRef.getId(),
fromDomain.getId(), toRef.getType(),
fromDomain.getType(), Relationship.UPSTREAM.ordinal());
toDomain.getId(), deleteLineageFromSearch(fromRef, toRef, lineageDetails);
toDomain.getType(), } else {
Relationship.UPSTREAM.ordinal()); lineageDetails.withAssetEdges(lineageDetails.getAssetEdges() - 1);
deleteLineageFromSearch(fromDomain, toDomain, domainLineageDetails); dao.relationshipDAO()
} else { .insert(
domainLineageDetails.withAssetEdges(domainLineageDetails.getAssetEdges() - 1); fromRef.getId(),
dao.relationshipDAO() toRef.getId(),
.insert( fromRef.getType(),
fromDomain.getId(), toRef.getType(),
toDomain.getId(), Relationship.UPSTREAM.ordinal(),
fromDomain.getType(), JsonUtils.pojoToJson(lineageDetails));
toDomain.getType(), addLineageToSearch(fromRef, toRef, lineageDetails);
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(domainLineageDetails));
addLineageToSearch(fromDomain, toDomain, domainLineageDetails);
}
}
} }
} }

View File

@ -3,6 +3,7 @@ package org.openmetadata.service.migration.mysql.v170;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.createServiceCharts; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.createServiceCharts;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNonNullColumn; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNonNullColumn;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNullColumn; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNullColumn;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDataProductsLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDomainLineage; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDomainLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationServiceLineage; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationServiceLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.updateDataInsightsApplication; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.updateDataInsightsApplication;
@ -32,6 +33,7 @@ public class Migration extends MigrationProcessImpl {
runLineageMigrationForNonNullColumn(handle); runLineageMigrationForNonNullColumn(handle);
runMigrationServiceLineage(handle); runMigrationServiceLineage(handle);
runMigrationForDomainLineage(handle); runMigrationForDomainLineage(handle);
runMigrationForDataProductsLineage(handle);
// DI // DI
createServiceCharts(); createServiceCharts();

View File

@ -3,6 +3,7 @@ package org.openmetadata.service.migration.postgres.v170;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.createServiceCharts; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.createServiceCharts;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNonNullColumn; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNonNullColumn;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNullColumn; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runLineageMigrationForNullColumn;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDataProductsLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDomainLineage; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationForDomainLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationServiceLineage; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.runMigrationServiceLineage;
import static org.openmetadata.service.migration.utils.v170.MigrationUtil.updateDataInsightsApplication; import static org.openmetadata.service.migration.utils.v170.MigrationUtil.updateDataInsightsApplication;
@ -32,6 +33,7 @@ public class Migration extends MigrationProcessImpl {
runLineageMigrationForNonNullColumn(handle); runLineageMigrationForNonNullColumn(handle);
runMigrationServiceLineage(handle); runMigrationServiceLineage(handle);
runMigrationForDomainLineage(handle); runMigrationForDomainLineage(handle);
runMigrationForDataProductsLineage(handle);
// DI // DI
createServiceCharts(); createServiceCharts();

View File

@ -19,12 +19,14 @@ import org.openmetadata.schema.ServiceEntityInterface;
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart; import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
import org.openmetadata.schema.dataInsight.custom.LineChart; import org.openmetadata.schema.dataInsight.custom.LineChart;
import org.openmetadata.schema.dataInsight.custom.LineChartMetric; import org.openmetadata.schema.dataInsight.custom.LineChartMetric;
import org.openmetadata.schema.entity.domains.DataProduct;
import org.openmetadata.schema.entity.domains.Domain; import org.openmetadata.schema.entity.domains.Domain;
import org.openmetadata.schema.entity.policies.Policy; import org.openmetadata.schema.entity.policies.Policy;
import org.openmetadata.schema.entity.policies.accessControl.Rule; import org.openmetadata.schema.entity.policies.accessControl.Rule;
import org.openmetadata.schema.governance.workflows.WorkflowConfiguration; import org.openmetadata.schema.governance.workflows.WorkflowConfiguration;
import org.openmetadata.schema.governance.workflows.WorkflowDefinition; import org.openmetadata.schema.governance.workflows.WorkflowDefinition;
import org.openmetadata.schema.governance.workflows.elements.WorkflowNodeDefinitionInterface; import org.openmetadata.schema.governance.workflows.elements.WorkflowNodeDefinitionInterface;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.LineageDetails; import org.openmetadata.schema.type.LineageDetails;
import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.MetadataOperation;
@ -36,6 +38,7 @@ import org.openmetadata.service.governance.workflows.flowable.MainWorkflow;
import org.openmetadata.service.jdbi3.AppMarketPlaceRepository; import org.openmetadata.service.jdbi3.AppMarketPlaceRepository;
import org.openmetadata.service.jdbi3.AppRepository; import org.openmetadata.service.jdbi3.AppRepository;
import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository; import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository;
import org.openmetadata.service.jdbi3.DataProductRepository;
import org.openmetadata.service.jdbi3.DomainRepository; import org.openmetadata.service.jdbi3.DomainRepository;
import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.ListFilter;
import org.openmetadata.service.jdbi3.PolicyRepository; import org.openmetadata.service.jdbi3.PolicyRepository;
@ -63,7 +66,7 @@ public class MigrationUtil {
private MigrationUtil() {} private MigrationUtil() {}
public static final String DOMAIN_LINEAGE = public static final String DOMAIN_AND_PRODUCTS_LINEAGE =
"select count(*) from entity_relationship where fromId in (select toId from entity_relationship where fromId = '%s' and relation = 10) AND toId in (select toId from entity_relationship where fromId = '%s' and relation = 10) and relation = 13"; "select count(*) from entity_relationship where fromId in (select toId from entity_relationship where fromId = '%s' and relation = 10) AND toId in (select toId from entity_relationship where fromId = '%s' and relation = 10) and relation = 13";
public static final String SERVICE_ENTITY_MIGRATION = public static final String SERVICE_ENTITY_MIGRATION =
@ -383,36 +386,11 @@ public class MigrationUtil {
public static void runMigrationForDomainLineage(Handle handle) { public static void runMigrationForDomainLineage(Handle handle) {
try { try {
LOG.info("MIGRATION 1.7.0 - STARTING MIGRATION FOR DOMAIN LINEAGE");
List<Domain> allDomains = getAllDomains(); List<Domain> allDomains = getAllDomains();
for (Domain fromDomain : allDomains) { for (Domain fromDomain : allDomains) {
for (Domain toDomain : allDomains) { for (Domain toDomain : allDomains) {
if (fromDomain.getId().equals(toDomain.getId())) { insertDomainAndDataProductLineage(
continue; handle, fromDomain.getEntityReference(), toDomain.getEntityReference());
}
String sql =
String.format(
DOMAIN_LINEAGE, fromDomain.getId().toString(), toDomain.getId().toString());
int count = handle.createQuery(sql).mapTo(Integer.class).one();
if (count > 0) {
LineageDetails domainLineageDetails =
new LineageDetails()
.withCreatedAt(System.currentTimeMillis())
.withUpdatedAt(System.currentTimeMillis())
.withCreatedBy(ADMIN_USER_NAME)
.withUpdatedBy(ADMIN_USER_NAME)
.withSource(LineageDetails.Source.CHILD_ASSETS)
.withAssetEdges(count);
Entity.getCollectionDAO()
.relationshipDAO()
.insert(
fromDomain.getId(),
toDomain.getId(),
fromDomain.getEntityReference().getType(),
toDomain.getEntityReference().getType(),
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(domainLineageDetails));
}
} }
} }
@ -423,6 +401,57 @@ public class MigrationUtil {
} }
} }
public static void runMigrationForDataProductsLineage(Handle handle) {
try {
List<DataProduct> allDataProducts = getAllDataProducts();
for (DataProduct fromDataProduct : allDataProducts) {
for (DataProduct toDataProduct : allDataProducts) {
insertDomainAndDataProductLineage(
handle, fromDataProduct.getEntityReference(), toDataProduct.getEntityReference());
}
}
} catch (Exception ex) {
LOG.error(
"Error while updating null json rows with createdAt, createdBy, updatedAt and updatedBy for lineage.",
ex);
}
}
private static void insertDomainAndDataProductLineage(
Handle handle, EntityReference fromRef, EntityReference toRef) {
LOG.info(
"MIGRATION 1.7.0 - STARTING MIGRATION FOR DOMAIN/DATA_PRODUCT LINEAGE, FROM: {} TO: {}",
fromRef.getFullyQualifiedName(),
toRef.getFullyQualifiedName());
if (fromRef.getId().equals(toRef.getId())) {
return;
}
String sql =
String.format(
DOMAIN_AND_PRODUCTS_LINEAGE, fromRef.getId().toString(), toRef.getId().toString());
int count = handle.createQuery(sql).mapTo(Integer.class).one();
if (count > 0) {
LineageDetails domainLineageDetails =
new LineageDetails()
.withCreatedAt(System.currentTimeMillis())
.withUpdatedAt(System.currentTimeMillis())
.withCreatedBy(ADMIN_USER_NAME)
.withUpdatedBy(ADMIN_USER_NAME)
.withSource(LineageDetails.Source.CHILD_ASSETS)
.withAssetEdges(count);
Entity.getCollectionDAO()
.relationshipDAO()
.insert(
fromRef.getId(),
toRef.getId(),
fromRef.getType(),
toRef.getType(),
Relationship.UPSTREAM.ordinal(),
JsonUtils.pojoToJson(domainLineageDetails));
}
}
public static void runMigrationServiceLineage(Handle handle) { public static void runMigrationServiceLineage(Handle handle) {
try { try {
List<ServiceEntityInterface> allServices = getAllServicesForLineage(); List<ServiceEntityInterface> allServices = getAllServicesForLineage();
@ -441,7 +470,10 @@ public class MigrationUtil {
private static void insertServiceLineageDetails( private static void insertServiceLineageDetails(
Handle handle, ServiceEntityInterface fromService, ServiceEntityInterface toService) { Handle handle, ServiceEntityInterface fromService, ServiceEntityInterface toService) {
try { try {
LOG.info("MIGRATION 1.7.0 - STARTING MIGRATION FOR SERVICES LINEAGE"); LOG.info(
"MIGRATION 1.7.0 - STARTING MIGRATION FOR SERVICES LINEAGE , FROM: {} TO: {}",
fromService.getFullyQualifiedName(),
toService.getFullyQualifiedName());
if (fromService.getId().equals(toService.getId()) if (fromService.getId().equals(toService.getId())
&& fromService && fromService
@ -510,6 +542,12 @@ public class MigrationUtil {
return repository.listAll(repository.getFields("id"), new ListFilter(Include.ALL)); return repository.listAll(repository.getFields("id"), new ListFilter(Include.ALL));
} }
private static List<DataProduct> getAllDataProducts() {
DataProductRepository repository =
(DataProductRepository) Entity.getEntityRepository(Entity.DATA_PRODUCT);
return repository.listAll(repository.getFields("id"), new ListFilter(Include.ALL));
}
public static void updateLineageBotPolicy() { public static void updateLineageBotPolicy() {
PolicyRepository policyRepository = PolicyRepository policyRepository =
(PolicyRepository) Entity.getEntityRepository(Entity.POLICY); (PolicyRepository) Entity.getEntityRepository(Entity.POLICY);

View File

@ -261,7 +261,7 @@ public class LineageResource {
@Parameter(description = "view (service or domain)") @Parameter(description = "view (service or domain)")
@QueryParam("view") @QueryParam("view")
@Pattern( @Pattern(
regexp = "service|domain|all", regexp = "service|domain|dataProduct|all",
message = "Invalid type. Allowed values: service, domain.") message = "Invalid type. Allowed values: service, domain.")
String view, String view,
@Parameter( @Parameter(
@ -273,6 +273,12 @@ public class LineageResource {
@QueryParam("includeDeleted") @QueryParam("includeDeleted")
boolean deleted) boolean deleted)
throws IOException { throws IOException {
if (Entity.getSearchRepository().getIndexMapping(view) != null) {
view =
Entity.getSearchRepository()
.getIndexMapping(view)
.getIndexName(Entity.getSearchRepository().getClusterAlias());
}
return Entity.getSearchRepository().searchPlatformLineage(view, queryFilter, deleted); return Entity.getSearchRepository().searchPlatformLineage(view, queryFilter, deleted);
} }

View File

@ -28,6 +28,7 @@ public record DataProductIndex(DataProduct dataProduct) implements SearchIndex {
ParseTags parseTags = new ParseTags(Entity.getEntityTags(Entity.DATA_PRODUCT, dataProduct)); ParseTags parseTags = new ParseTags(Entity.getEntityTags(Entity.DATA_PRODUCT, dataProduct));
doc.put("tags", parseTags.getTags()); doc.put("tags", parseTags.getTags());
doc.putAll(commonAttributes); doc.putAll(commonAttributes);
doc.put("upstreamLineage", SearchIndex.getLineageData(dataProduct.getEntityReference()));
return doc; return doc;
} }

View File

@ -44,7 +44,6 @@ export const SIDEBAR_LIST_ITEMS = {
[SidebarItem.GLOSSARY]: [SidebarItem.GOVERNANCE, SidebarItem.GLOSSARY], [SidebarItem.GLOSSARY]: [SidebarItem.GOVERNANCE, SidebarItem.GLOSSARY],
[SidebarItem.TAGS]: [SidebarItem.GOVERNANCE, SidebarItem.TAGS], [SidebarItem.TAGS]: [SidebarItem.GOVERNANCE, SidebarItem.TAGS],
[SidebarItem.METRICS]: [SidebarItem.GOVERNANCE, SidebarItem.METRICS], [SidebarItem.METRICS]: [SidebarItem.GOVERNANCE, SidebarItem.METRICS],
[SidebarItem.LINEAGE]: [SidebarItem.GOVERNANCE, SidebarItem.LINEAGE],
// Profile Dropdown // Profile Dropdown
'user-name': ['dropdown-profile', 'user-name'], 'user-name': ['dropdown-profile', 'user-name'],

View File

@ -0,0 +1,50 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import test, { expect } from '@playwright/test';
import { SidebarItem } from '../../constant/sidebar';
import { redirectToHomePage } from '../../utils/common';
import { sidebarClick } from '../../utils/sidebar';
test.use({
storageState: 'playwright/.auth/admin.json',
});
test('Verify Platform Lineage View', async ({ page }) => {
await redirectToHomePage(page);
const lineageRes = page.waitForResponse(
'/api/v1/lineage/getPlatformLineage?view=service*'
);
await sidebarClick(page, SidebarItem.LINEAGE);
await lineageRes;
await expect(page.getByTestId('lineage-export')).not.toBeVisible();
await page.getByTestId('lineage-layer-btn').click();
await page.waitForSelector(
'[data-testid="lineage-layer-domain-btn"]:not(.active)'
);
const domainRes = page.waitForResponse(
'/api/v1/lineage/getPlatformLineage?view=domain*'
);
await page.getByTestId('lineage-layer-domain-btn').click();
await domainRes;
await page.getByTestId('lineage-layer-btn').click();
const dataProductRes = page.waitForResponse(
'/api/v1/lineage/getPlatformLineage?view=dataProduct*'
);
await page.getByTestId('lineage-layer-data-product-btn').click();
await dataProductRes;
});

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -354,7 +354,7 @@ const Suggestions = ({
className="m-b-md w-100 text-left d-flex items-center p-0" className="m-b-md w-100 text-left d-flex items-center p-0"
data-testid="nlp-suggestions-button" data-testid="nlp-suggestions-button"
icon={ icon={
<div className="nlp-button active w-6 h-6 flex-center m-r-md"> <div className="nlp-button w-6 h-6 flex-center m-r-md">
<IconSuggestionsBlue /> <IconSuggestionsBlue />
</div> </div>
} }

View File

@ -135,19 +135,21 @@ const LineageControlButtons: FC<LineageControlButtonsProps> = ({
/> />
)} )}
<Button {entityType && (
className="lineage-button" <Button
data-testid="lineage-export" className="lineage-button"
disabled={isEditMode} data-testid="lineage-export"
icon={ disabled={isEditMode}
<span className="anticon"> icon={
<ExportIcon height={18} width={18} /> <span className="anticon">
</span> <ExportIcon height={18} width={18} />
} </span>
title={t('label.export-entity', { entity: t('label.lineage') })} }
type="text" title={t('label.export-entity', { entity: t('label.lineage') })}
onClick={onExportClick} type="text"
/> onClick={onExportClick}
/>
)}
{handleFullScreenViewClick && ( {handleFullScreenViewClick && (
<Button <Button

View File

@ -16,6 +16,7 @@ import classNames from 'classnames';
import { t } from 'i18next'; import { t } from 'i18next';
import React from 'react'; import React from 'react';
import { ReactComponent as DataQualityIcon } from '../../../../assets/svg/ic-data-contract.svg'; import { ReactComponent as DataQualityIcon } from '../../../../assets/svg/ic-data-contract.svg';
import { ReactComponent as DataProductIcon } from '../../../../assets/svg/ic-data-product.svg';
import { ReactComponent as DomainIcon } from '../../../../assets/svg/ic-domain.svg'; import { ReactComponent as DomainIcon } from '../../../../assets/svg/ic-domain.svg';
import { ReactComponent as Layers } from '../../../../assets/svg/ic-layers.svg'; import { ReactComponent as Layers } from '../../../../assets/svg/ic-layers.svg';
import { ReactComponent as ServiceView } from '../../../../assets/svg/services.svg'; import { ReactComponent as ServiceView } from '../../../../assets/svg/services.svg';
@ -23,6 +24,7 @@ import { SERVICE_TYPES } from '../../../../constants/Services.constant';
import { useLineageProvider } from '../../../../context/LineageProvider/LineageProvider'; import { useLineageProvider } from '../../../../context/LineageProvider/LineageProvider';
import { LineagePlatformView } from '../../../../context/LineageProvider/LineageProvider.interface'; import { LineagePlatformView } from '../../../../context/LineageProvider/LineageProvider.interface';
import { EntityType } from '../../../../enums/entity.enum'; import { EntityType } from '../../../../enums/entity.enum';
import { Table } from '../../../../generated/entity/data/table';
import { LineageLayer } from '../../../../generated/settings/settings'; import { LineageLayer } from '../../../../generated/settings/settings';
import searchClassBase from '../../../../utils/SearchClassBase'; import searchClassBase from '../../../../utils/SearchClassBase';
import { AssetsUnion } from '../../../DataAssets/AssetsSelectionModal/AssetSelectionModal.interface'; import { AssetsUnion } from '../../../DataAssets/AssetsSelectionModal/AssetSelectionModal.interface';
@ -48,7 +50,7 @@ const LayerButton: React.FC<LayerButtonProps> = React.memo(
) )
); );
const LineageLayers = ({ entityType }: LineageLayersProps) => { const LineageLayers = ({ entityType, entity }: LineageLayersProps) => {
const { const {
activeLayer, activeLayer,
onUpdateLayerView, onUpdateLayerView,
@ -116,7 +118,9 @@ const LineageLayers = ({ entityType }: LineageLayersProps) => {
)} )}
{(isPlatformLineage || {(isPlatformLineage ||
(entityType && entityType !== EntityType.DOMAIN)) && ( (entityType &&
entityType !== EntityType.DOMAIN &&
entity?.domain)) && (
<LayerButton <LayerButton
icon={<DomainIcon />} icon={<DomainIcon />}
isActive={platformView === LineagePlatformView.Domain} isActive={platformView === LineagePlatformView.Domain}
@ -125,6 +129,21 @@ const LineageLayers = ({ entityType }: LineageLayersProps) => {
onClick={() => handlePlatformViewChange(LineagePlatformView.Domain)} onClick={() => handlePlatformViewChange(LineagePlatformView.Domain)}
/> />
)} )}
{(isPlatformLineage ||
(entityType &&
entityType !== EntityType.DOMAIN &&
((entity as Table)?.dataProducts ?? [])?.length > 0)) && (
<LayerButton
icon={<DataProductIcon />}
isActive={platformView === LineagePlatformView.DataProduct}
label={t('label.data-product')}
testId="lineage-layer-data-product-btn"
onClick={() =>
handlePlatformViewChange(LineagePlatformView.DataProduct)
}
/>
)}
</ButtonGroup> </ButtonGroup>
), ),
[ [

View File

@ -25,6 +25,7 @@ import React, {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { ReactComponent as IconCloseCircleOutlined } from '../../assets/svg/close-circle-outlined.svg'; import { ReactComponent as IconCloseCircleOutlined } from '../../assets/svg/close-circle-outlined.svg';
import { ReactComponent as IconSuggestionsActive } from '../../assets/svg/ic-suggestions-active.svg';
import { ReactComponent as IconSuggestionsBlue } from '../../assets/svg/ic-suggestions-blue.svg'; import { ReactComponent as IconSuggestionsBlue } from '../../assets/svg/ic-suggestions-blue.svg';
import { ReactComponent as IconSearch } from '../../assets/svg/search.svg'; import { ReactComponent as IconSearch } from '../../assets/svg/search.svg';
import { TOUR_SEARCH_TERM } from '../../constants/constants'; import { TOUR_SEARCH_TERM } from '../../constants/constants';
@ -185,7 +186,13 @@ export const GlobalSearchBar = () => {
active: isNLPActive, active: isNLPActive,
})} })}
data-testid="nlp-suggestions-button" data-testid="nlp-suggestions-button"
icon={<Icon component={IconSuggestionsBlue} />} icon={
<Icon
component={
isNLPActive ? IconSuggestionsActive : IconSuggestionsBlue
}
/>
}
type="text" type="text"
onClick={() => setNLPActive(!isNLPActive)} onClick={() => setNLPActive(!isNLPActive)}
/> />

View File

@ -13,6 +13,8 @@
@import (reference) '../../styles/variables.less'; @import (reference) '../../styles/variables.less';
@nlp-border-color: #b9e6fe;
.search-container { .search-container {
border: 1px solid #eaecf5; border: 1px solid #eaecf5;
border-radius: 12px; border-radius: 12px;
@ -20,8 +22,9 @@
padding: 6px 20px; padding: 6px 20px;
.nlp-button { .nlp-button {
border: 0.5px solid @border-color !important; border: 0.5px solid @nlp-border-color !important;
border-radius: 8px; border-radius: 8px;
background-color: @blue-11 !important;
svg { svg {
width: 14px; width: 14px;
@ -29,8 +32,17 @@
} }
&.active { &.active {
background-color: @blue-11 !important; padding: 0;
border: 0.5px solid #b9e6fe !important; display: flex;
align-items: center;
justify-content: center;
border: none !important;
svg {
width: 24px;
height: 24px;
fill: none;
}
} }
} }
} }

View File

@ -662,7 +662,7 @@ const AssetsTabs = forwardRef(
activeEntity && activeEntity &&
permissions.Create && permissions.Create &&
data.length > 0 && ( data.length > 0 && (
<div className="w-full d-flex justify-between items-center"> <div className="w-full d-flex justify-between items-center m-b-sm">
<Checkbox <Checkbox
className="assets-checkbox p-x-sm" className="assets-checkbox p-x-sm"
onChange={(e) => onSelectAll(e.target.checked)}> onChange={(e) => onSelectAll(e.target.checked)}>

View File

@ -196,7 +196,7 @@ const Lineage = ({
<MiniMap pannable zoomable position="bottom-right" /> <MiniMap pannable zoomable position="bottom-right" />
<Panel position="bottom-left"> <Panel position="bottom-left">
<LineageLayers entityType={entityType} /> <LineageLayers entity={entity} entityType={entityType} />
</Panel> </Panel>
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>

View File

@ -47,6 +47,7 @@ export enum LineagePlatformView {
None = 'None', None = 'None',
Service = 'Service', Service = 'Service',
Domain = 'Domain', Domain = 'Domain',
DataProduct = 'DataProduct',
} }
export interface LineageContextType { export interface LineageContextType {

View File

@ -65,6 +65,7 @@ import { EntityLineageNodeType, EntityType } from '../../enums/entity.enum';
import { AddLineage } from '../../generated/api/lineage/addLineage'; import { AddLineage } from '../../generated/api/lineage/addLineage';
import { LineageDirection } from '../../generated/api/lineage/lineageDirection'; import { LineageDirection } from '../../generated/api/lineage/lineageDirection';
import { LineageSettings } from '../../generated/configuration/lineageSettings'; import { LineageSettings } from '../../generated/configuration/lineageSettings';
import { Table } from '../../generated/entity/data/table';
import { LineageLayer } from '../../generated/settings/settings'; import { LineageLayer } from '../../generated/settings/settings';
import { import {
ColumnLineage, ColumnLineage,
@ -94,6 +95,7 @@ import {
getConnectedNodesEdges, getConnectedNodesEdges,
getEdgeDataFromEdge, getEdgeDataFromEdge,
getELKLayoutedElements, getELKLayoutedElements,
getEntityTypeFromPlatformView,
getLineageEdge, getLineageEdge,
getLineageEdgeForAPI, getLineageEdgeForAPI,
getLoadingStatusValue, getLoadingStatusValue,
@ -346,7 +348,7 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
); );
const fetchPlatformLineage = useCallback( const fetchPlatformLineage = useCallback(
async (view: 'service' | 'domain', config?: LineageConfig) => { async (view: string, config?: LineageConfig) => {
try { try {
setLoading(true); setLoading(true);
setInit(false); setInit(false);
@ -1329,17 +1331,26 @@ const LineageProvider = ({ children }: LineageProviderProps) => {
entity?.domain.type, entity?.domain.type,
lineageConfig lineageConfig
); );
} else if (
platformView === LineagePlatformView.DataProduct &&
((entity as Table)?.dataProducts ?? [])?.length > 0
) {
fetchLineageData(
(entity as Table)?.dataProducts?.[0]?.fullyQualifiedName ?? '',
(entity as Table)?.dataProducts?.[0]?.type ?? '',
lineageConfig
);
} else if (platformView === LineagePlatformView.None) { } else if (platformView === LineagePlatformView.None) {
fetchLineageData(decodedFqn, entityType, lineageConfig); fetchLineageData(decodedFqn, entityType, lineageConfig);
} else if (isPlatformLineage) { } else if (isPlatformLineage) {
fetchPlatformLineage( fetchPlatformLineage(
platformView === LineagePlatformView.Domain ? 'domain' : 'service', getEntityTypeFromPlatformView(platformView),
lineageConfig lineageConfig
); );
} }
} else if (isPlatformLineage) { } else if (isPlatformLineage) {
fetchPlatformLineage( fetchPlatformLineage(
platformView === LineagePlatformView.Domain ? 'domain' : 'service', getEntityTypeFromPlatformView(platformView),
lineageConfig lineageConfig
); );
} }

View File

@ -94,7 +94,7 @@ export const getPlatformLineage = async ({
}: { }: {
config?: LineageConfig; config?: LineageConfig;
queryFilter?: string; queryFilter?: string;
view: 'service' | 'domain'; view: string;
}) => { }) => {
const { upstreamDepth = 1, downstreamDepth = 1 } = config ?? {}; const { upstreamDepth = 1, downstreamDepth = 1 } = config ?? {};
const API_PATH = `lineage/getPlatformLineage`; const API_PATH = `lineage/getPlatformLineage`;

View File

@ -168,7 +168,7 @@
@text-highlighter: #ffc34e40; @text-highlighter: #ffc34e40;
@team-avatar-bg: #0950c51a; @team-avatar-bg: #0950c51a;
@om-navbar-height: ~'var(--ant-navbar-height)'; @om-navbar-height: ~'var(--ant-navbar-height)';
@sidebar-width: 60px; @sidebar-width: 84px;
@alert-text-color: @text-color-tertiary; @alert-text-color: @text-color-tertiary;
@alert-info-bg: @blue-14; @alert-info-bg: @blue-14;
@alert-success-bg: #f6fef9; @alert-success-bg: #f6fef9;

View File

@ -63,6 +63,7 @@ import {
ZOOM_TRANSITION_DURATION, ZOOM_TRANSITION_DURATION,
ZOOM_VALUE, ZOOM_VALUE,
} from '../constants/Lineage.constants'; } from '../constants/Lineage.constants';
import { LineagePlatformView } from '../context/LineageProvider/LineageProvider.interface';
import { import {
EntityLineageDirection, EntityLineageDirection,
EntityLineageNodeType, EntityLineageNodeType,
@ -1748,3 +1749,16 @@ export const getLineageEntityExclusionFilter = () => {
}, },
}; };
}; };
export const getEntityTypeFromPlatformView = (
platformView: LineagePlatformView
): string => {
switch (platformView) {
case LineagePlatformView.DataProduct:
return EntityType.DATA_PRODUCT;
case LineagePlatformView.Domain:
return EntityType.DOMAIN;
default:
return 'service';
}
};