diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ChartRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ChartRepository.java index e3330c90fe2..6efeb8000a9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ChartRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ChartRepository.java @@ -13,6 +13,11 @@ package org.openmetadata.service.jdbi3; +import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.sqlobject.transaction.Transaction; @@ -21,21 +26,27 @@ import org.openmetadata.schema.entity.data.Chart; import org.openmetadata.schema.entity.services.DashboardService; import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; +import org.openmetadata.schema.type.Relationship; import org.openmetadata.service.Entity; import org.openmetadata.service.resources.charts.ChartResource; +import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.FullyQualifiedName; @Slf4j public class ChartRepository extends EntityRepository { + + private static final String CHART_UPDATE_FIELDS = "dashboards"; + private static final String CHART_PATCH_FIELDS = "dashboards"; + public ChartRepository() { super( ChartResource.COLLECTION_PATH, Entity.CHART, Chart.class, Entity.getCollectionDAO().chartDAO(), - "", - ""); + CHART_PATCH_FIELDS, + CHART_UPDATE_FIELDS); supportsSearch = true; } @@ -50,26 +61,35 @@ public class ChartRepository extends EntityRepository { DashboardService dashboardService = Entity.getEntity(chart.getService(), "", Include.ALL); chart.setService(dashboardService.getEntityReference()); chart.setServiceType(dashboardService.getServiceType()); + chart.setDashboards(EntityUtil.getEntityReferences(chart.getDashboards(), Include.NON_DELETED)); } @Override public void storeEntity(Chart chart, boolean update) { // Relationships and fields such as tags are not stored as part of json EntityReference service = chart.getService(); - chart.withService(null); + List dashboards = chart.getDashboards(); + chart.withService(null).withDashboards(null); store(chart, update); - chart.withService(service); + chart.withService(service).withDashboards(dashboards); } @Override @SneakyThrows public void storeRelationships(Chart chart) { addServiceRelationship(chart, chart.getService()); + // Add relationship from dashboard to chart + for (EntityReference dashboard : listOrEmpty(chart.getDashboards())) { + addRelationship( + dashboard.getId(), chart.getId(), Entity.DASHBOARD, Entity.CHART, Relationship.HAS); + } } @Override public void setFields(Chart chart, Fields fields) { chart.withService(getContainer(chart.getId())); + chart.setDashboards( + fields.contains("dashboards") ? getRelatedEntities(chart, Entity.DASHBOARD) : null); } @Override @@ -94,6 +114,12 @@ public class ChartRepository extends EntityRepository { return Entity.getEntity(entity.getService(), fields, Include.ALL); } + private List getRelatedEntities(Chart chart, String entityType) { + return chart == null + ? Collections.emptyList() + : findFrom(chart.getId(), Entity.CHART, Relationship.HAS, entityType); + } + public class ChartUpdater extends ColumnEntityUpdater { public ChartUpdater(Chart chart, Chart updated, Operation operation) { super(chart, updated, operation); @@ -105,6 +131,32 @@ public class ChartRepository extends EntityRepository { recordChange("chartType", original.getChartType(), updated.getChartType()); recordChange("sourceUrl", original.getSourceUrl(), updated.getSourceUrl()); recordChange("sourceHash", original.getSourceHash(), updated.getSourceHash()); + update( + Entity.DASHBOARD, + "dashboards", + listOrEmpty(updated.getDashboards()), + listOrEmpty(original.getDashboards())); + } + + private void update( + String entityType, + String field, + List updEntities, + List oriEntities) { + + // Remove all entity type associated with this dashboard + deleteTo(updated.getId(), Entity.CHART, Relationship.HAS, entityType); + + // Add relationship from dashboard to chart type + for (EntityReference entity : updEntities) { + addRelationship( + entity.getId(), updated.getId(), entityType, Entity.CHART, Relationship.HAS); + } + + List added = new ArrayList<>(); + List deleted = new ArrayList<>(); + recordListChange( + field, oriEntities, updEntities, added, deleted, EntityUtil.entityReferenceMatch); } } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java index 0fbb168c3e0..21d533efb5b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java @@ -74,7 +74,7 @@ import org.openmetadata.service.util.ResultList; @Collection(name = "charts") public class ChartResource extends EntityResource { public static final String COLLECTION_PATH = "v1/charts/"; - static final String FIELDS = "owner,followers,tags,domain,dataProducts,sourceHash"; + static final String FIELDS = "owner,followers,tags,domain,dataProducts,sourceHash,dashboards"; @Override public Chart addHref(UriInfo uriInfo, Chart chart) { @@ -530,6 +530,7 @@ public class ChartResource extends EntityResource { .withService(EntityUtil.getEntityReference(Entity.DASHBOARD_SERVICE, create.getService())) .withChartType(create.getChartType()) .withSourceUrl(create.getSourceUrl()) - .withSourceHash(create.getSourceHash()); + .withSourceHash(create.getSourceHash()) + .withDashboards(getEntityReferences(Entity.DASHBOARD, create.getDashboards())); } } diff --git a/openmetadata-service/src/main/resources/elasticsearch/en/chart_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/en/chart_index_mapping.json index 3b6766a2fa2..e15851eda83 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/en/chart_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/en/chart_index_mapping.json @@ -342,6 +342,54 @@ }, "descriptionStatus": { "type": "keyword" + }, + "dashboards": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "text" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text", + "analyzer": "om_analyzer" + }, + "deleted": { + "type": "text" + }, + "href": { + "type": "text" + } + } } } } diff --git a/openmetadata-service/src/main/resources/elasticsearch/indexMapping.json b/openmetadata-service/src/main/resources/elasticsearch/indexMapping.json index d57f6777ae9..386479da05b 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/indexMapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/indexMapping.json @@ -31,7 +31,7 @@ "indexName": "chart_search_index", "alias": "chart", "indexMappingFile": "/elasticsearch/%s/chart_index_mapping.json", - "parentAliases": ["dashboard", "dashboardService", "dataAsset"], + "parentAliases": ["dashboard", "dashboardService", "all", "dataAsset"], "childAliases": [] }, "dashboard": { diff --git a/openmetadata-service/src/main/resources/elasticsearch/jp/chart_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/jp/chart_index_mapping.json index c49b2fbb6a3..0082afda778 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/jp/chart_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/jp/chart_index_mapping.json @@ -349,6 +349,54 @@ }, "descriptionStatus": { "type": "keyword" + }, + "dashboards": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "text" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text", + "analyzer": "om_analyzer" + }, + "deleted": { + "type": "text" + }, + "href": { + "type": "text" + } + } } } } diff --git a/openmetadata-service/src/main/resources/elasticsearch/zh/chart_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/zh/chart_index_mapping.json index f5e978449f4..33a6d78c29d 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/zh/chart_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/zh/chart_index_mapping.json @@ -332,6 +332,54 @@ }, "descriptionStatus": { "type": "keyword" + }, + "dashboards": { + "properties": { + "id": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 36 + } + } + }, + "type": { + "type": "text" + }, + "name": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "displayName": { + "type": "keyword", + "fields": { + "keyword": { + "type": "keyword", + "normalizer": "lowercase_normalizer", + "ignore_above": 256 + } + } + }, + "fullyQualifiedName": { + "type": "text" + }, + "description": { + "type": "text", + "analyzer": "om_analyzer" + }, + "deleted": { + "type": "text" + }, + "href": { + "type": "text" + } + } } } } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index b0e0f88589e..f3d62b46455 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -333,6 +333,7 @@ public abstract class EntityResourceTest CHART_REFERENCES; + public static List DASHBOARD_REFERENCES; public static Database DATABASE; public static DatabaseSchema DATABASE_SCHEMA; diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/charts/ChartResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/charts/ChartResourceTest.java index 8a6a70f720d..110cd0b5567 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/charts/ChartResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/charts/ChartResourceTest.java @@ -15,10 +15,12 @@ package org.openmetadata.service.resources.charts; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.OK; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; 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.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED; @@ -29,6 +31,7 @@ import static org.openmetadata.service.util.TestUtils.assertResponse; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.HttpResponseException; @@ -37,8 +40,10 @@ import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.openmetadata.schema.api.data.CreateChart; +import org.openmetadata.schema.api.data.CreateDashboard; import org.openmetadata.schema.api.services.CreateDashboardService; import org.openmetadata.schema.entity.data.Chart; +import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.services.DashboardService; import org.openmetadata.schema.type.ChangeDescription; import org.openmetadata.schema.type.ChartType; @@ -46,6 +51,7 @@ import org.openmetadata.schema.type.EntityReference; import org.openmetadata.service.Entity; import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.charts.ChartResource.ChartList; +import org.openmetadata.service.resources.dashboards.DashboardResourceTest; import org.openmetadata.service.resources.services.DashboardServiceResourceTest; import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.ResultList; @@ -279,6 +285,99 @@ public class ChartResourceTest extends EntityResourceTest { chart, originalJson, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change); } + @Test + public void testChartWithDashboards() throws IOException { + DashboardResourceTest dashboardResourceTest = new DashboardResourceTest(); + // Create a new CreateChart request with a populated "dashboards" field + CreateChart request = + createRequest("chartWithDashboards") + .withService(METABASE_REFERENCE.getName()) + .withDashboards(DASHBOARD_REFERENCES); + Chart chart = createAndCheckEntity(request, ADMIN_AUTH_HEADERS); + + // Validate that the created Chart entity has the expected "dashboards" field + assertNotNull(chart.getDashboards()); + assertEquals(3, chart.getDashboards().size()); + assertEquals("dashboard0", chart.getDashboards().get(0).getName()); + assertEquals("dashboard1", chart.getDashboards().get(1).getName()); + assertEquals("dashboard2", chart.getDashboards().get(2).getName()); + + // Check that each dashboard contains the newly created chart in their charts field + for (EntityReference dashboardRef : chart.getDashboards()) { + Dashboard dashboard = + dashboardResourceTest.getEntity(dashboardRef.getId(), "charts", ADMIN_AUTH_HEADERS); + assertNotNull(dashboard.getCharts()); + assertTrue( + dashboard.getCharts().stream() + .map(EntityReference::getId) + .anyMatch(chart.getId()::equals)); + } + + // Create a new Dashboard entity + CreateDashboard createDashboardRequest = + new CreateDashboard().withName("dashboard3").withService(METABASE_REFERENCE.getName()); + Dashboard dashboard3 = + dashboardResourceTest.createAndCheckEntity(createDashboardRequest, ADMIN_AUTH_HEADERS); + + // Update the "dashboards" field of the Chart entity with PATCH request with newly created + // dashboard3 + String originalJson = JsonUtils.pojoToJson(chart); + ChangeDescription change = getChangeDescription(chart, MINOR_UPDATE); + fieldDeleted(change, "dashboards", chart.getDashboards()); + fieldAdded(change, "dashboards", List.of(dashboard3.getEntityReference())); + chart.withDashboards(List.of(dashboard3.getEntityReference())); + chart = patchEntityAndCheck(chart, originalJson, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change); + + // Retrieve the Chart entity and validate that the retrieved entity has the updated "dashboards" + // field + chart = getEntity(chart.getId(), "dashboards", ADMIN_AUTH_HEADERS); + assertNotNull(chart.getDashboards()); + assertEquals(1, chart.getDashboards().size()); + assertEquals("dashboard3", chart.getDashboards().get(0).getName()); + + // Verify dashboard3 contains the respective chart + dashboard3 = dashboardResourceTest.getEntity(dashboard3.getId(), "charts", ADMIN_AUTH_HEADERS); + assertTrue( + dashboard3.getCharts().stream() + .map(EntityReference::getId) + .anyMatch(chart.getId()::equals)); + + // Create a new Dashboard entity + createDashboardRequest = + new CreateDashboard().withName("dashboard4").withService(METABASE_REFERENCE.getName()); + Dashboard dashboard4 = + dashboardResourceTest.createAndCheckEntity(createDashboardRequest, ADMIN_AUTH_HEADERS); + + // Update the "dashboards" field of the Chart entity with PUT request with newly created + // dashboard4 + change = getChangeDescription(chart, MINOR_UPDATE); + fieldDeleted(change, "dashboards", chart.getDashboards()); + fieldAdded(change, "dashboards", List.of(dashboard4.getEntityReference())); + chart.withDashboards(List.of(dashboard4.getEntityReference())); + updateAndCheckEntity( + request.withDashboards(List.of(dashboard4.getEntityReference().getFullyQualifiedName())), + OK, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change); + + // Verify dashboard4 contains the respective chart + dashboard4 = dashboardResourceTest.getEntity(dashboard4.getId(), "charts", ADMIN_AUTH_HEADERS); + assertTrue( + dashboard4.getCharts().stream() + .map(EntityReference::getId) + .anyMatch(chart.getId()::equals)); + + // Delete the chart + deleteEntity(chart.getId(), ADMIN_AUTH_HEADERS); + // Check that dashboard4 does not contain the deleted chart in their charts field + dashboard4 = dashboardResourceTest.getEntity(dashboard4.getId(), "charts", ADMIN_AUTH_HEADERS); + assertTrue( + dashboard4.getCharts().stream() + .map(EntityReference::getId) + .anyMatch(chart.getId()::equals)); + } + @Override public void compareEntities(Chart expected, Chart patched, Map authHeaders) { assertReference(expected.getService(), patched.getService()); @@ -295,6 +394,8 @@ public class ChartResourceTest extends EntityResourceTest { ChartType expectedChartType = ChartType.fromValue(expected.toString()); ChartType actualChartType = ChartType.fromValue(actual.toString()); assertEquals(expectedChartType, actualChartType); + } else if (fieldName.contains("dashboards")) { + assertEntityReferencesFieldChange(expected, actual); } else { assertCommonFieldChange(fieldName, expected, actual); } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/DashboardServiceResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/DashboardServiceResourceTest.java index 3c04ec9d693..9c030305379 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/DashboardServiceResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/DashboardServiceResourceTest.java @@ -40,9 +40,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.schema.api.data.CreateChart; +import org.openmetadata.schema.api.data.CreateDashboard; import org.openmetadata.schema.api.data.CreateDashboardDataModel.DashboardServiceType; import org.openmetadata.schema.api.services.CreateDashboardService; import org.openmetadata.schema.entity.data.Chart; +import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.services.DashboardService; import org.openmetadata.schema.entity.services.connections.TestConnectionResult; import org.openmetadata.schema.entity.services.connections.TestConnectionResultStatus; @@ -52,6 +54,7 @@ import org.openmetadata.schema.type.ChangeDescription; import org.openmetadata.schema.type.DashboardConnection; import org.openmetadata.service.Entity; import org.openmetadata.service.resources.charts.ChartResourceTest; +import org.openmetadata.service.resources.dashboards.DashboardResourceTest; import org.openmetadata.service.resources.services.dashboard.DashboardServiceResource.DashboardServiceList; import org.openmetadata.service.secrets.masker.PasswordEntityMasker; import org.openmetadata.service.util.JsonUtils; @@ -292,9 +295,10 @@ public class DashboardServiceResourceTest public void setupDashboardServices(TestInfo test) throws HttpResponseException, URISyntaxException { - DashboardServiceResourceTest dashboardResourceTest = new DashboardServiceResourceTest(); + DashboardServiceResourceTest dashboardServiceResourceTest = new DashboardServiceResourceTest(); + DashboardResourceTest dashboardResourceTest = new DashboardResourceTest(); CreateDashboardService createDashboardService = - dashboardResourceTest + dashboardServiceResourceTest .createRequest("superset", "", "", null) .withServiceType(DashboardServiceType.Metabase); DashboardConnection dashboardConnection = @@ -312,7 +316,7 @@ public class DashboardServiceResourceTest METABASE_REFERENCE = dashboardService.getEntityReference(); CreateDashboardService lookerDashboardService = - dashboardResourceTest + dashboardServiceResourceTest .createRequest("looker", "", "", null) .withServiceType(DashboardServiceType.Looker); DashboardConnection lookerConnection = @@ -334,5 +338,16 @@ public class DashboardServiceResourceTest Chart chart = chartResourceTest.createEntity(createChart, ADMIN_AUTH_HEADERS); CHART_REFERENCES.add(chart.getFullyQualifiedName()); } + DASHBOARD_REFERENCES = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + CreateDashboard createDashboard1 = + dashboardResourceTest + .createRequest("dashboard" + i, "", "", null) + .withService(METABASE_REFERENCE.getName()); + createDashboard1.withDomain(DOMAIN.getFullyQualifiedName()); + Dashboard dashboard1 = + new DashboardResourceTest().createEntity(createDashboard1, ADMIN_AUTH_HEADERS); + DASHBOARD_REFERENCES.add(dashboard1.getFullyQualifiedName()); + } } } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createChart.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createChart.json index f6b295ce63d..a207bb73cc9 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/data/createChart.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createChart.json @@ -62,6 +62,14 @@ "type": "string", "minLength": 1, "maxLength": 32 + }, + "dashboards": { + "description": "List of fully qualified name of dashboards containing this Chart.", + "type": "array", + "items": { + "$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName" + }, + "default": null } }, "required": ["name", "service"], diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/chart.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/chart.json index 4c41ba8a1f3..883a5fb46f4 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/data/chart.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/chart.json @@ -160,6 +160,11 @@ "type": "string", "minLength": 1, "maxLength": 32 + }, + "dashboards": { + "description": "All the dashboards containing this chart.", + "$ref": "../../type/entityReferenceList.json", + "default": null } }, "required": ["id", "name", "service"], diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 014ea20b9a0..28c69f86de4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -10,8 +10,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import test from '@playwright/test'; +import test, { expect } from '@playwright/test'; import { SidebarItem } from '../../constant/sidebar'; +import { DashboardClass } from '../../support/entity/DashboardClass'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { TeamClass } from '../../support/team/TeamClass'; @@ -25,6 +26,7 @@ import { approveGlossaryTermTask, createGlossary, createGlossaryTerms, + goToAssetsTab, selectActiveGlossary, validateGlossaryTerm, verifyGlossaryDetails, @@ -136,6 +138,247 @@ test.describe('Glossary tests', () => { await afterAction(); }); + test('Add and Remove Assets', async ({ browser }) => { + const { page, afterAction, apiContext } = await performAdminLogin(browser); + + const glossary1 = new Glossary(); + const glossaryTerm1 = new GlossaryTerm(glossary1); + const glossaryTerm2 = new GlossaryTerm(glossary1); + glossary1.data.owner = { name: 'admin', type: 'user' }; + glossary1.data.mutuallyExclusive = true; + glossary1.data.terms = [glossaryTerm1, glossaryTerm2]; + + const glossary2 = new Glossary(); + const glossaryTerm3 = new GlossaryTerm(glossary2); + const glossaryTerm4 = new GlossaryTerm(glossary2); + glossary2.data.owner = { name: 'admin', type: 'user' }; + glossary2.data.terms = [glossaryTerm3, glossaryTerm4]; + + await glossary1.create(apiContext); + await glossary2.create(apiContext); + await glossaryTerm1.create(apiContext); + await glossaryTerm2.create(apiContext); + await glossaryTerm3.create(apiContext); + await glossaryTerm4.create(apiContext); + + const dashboardEntity = new DashboardClass(); + await dashboardEntity.create(apiContext); + + try { + await test.step('Add asset to glossary term using entity', async () => { + await sidebarClick(page, SidebarItem.GLOSSARY); + + await selectActiveGlossary(page, glossary2.data.displayName); + await goToAssetsTab(page, glossaryTerm3.data.displayName); + + await page.waitForSelector( + 'text=Adding a new Asset is easy, just give it a spin!' + ); + + await dashboardEntity.visitEntityPage(page); + + // Dashboard Entity Right Panel + await page.click( + '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="add-tag"]' + ); + + // Select 1st term + await page.click('[data-testid="tag-selector"] #tagsForm_tags'); + + const glossaryRequest = page.waitForResponse( + `/api/v1/search/query?q=*&index=glossary_term_search_index&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` + ); + await page.type( + '[data-testid="tag-selector"] #tagsForm_tags', + glossaryTerm1.data.name + ); + await glossaryRequest; + + await page.getByText(glossaryTerm1.data.displayName).click(); + await page.waitForSelector( + '[data-testid="tag-selector"]:has-text("' + + glossaryTerm1.data.displayName + + '")' + ); + + // Select 2nd term + await page.click('[data-testid="tag-selector"] #tagsForm_tags'); + + const glossaryRequest2 = page.waitForResponse( + `/api/v1/search/query?q=*&index=glossary_term_search_index&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` + ); + await page.type( + '[data-testid="tag-selector"] #tagsForm_tags', + glossaryTerm2.data.name + ); + await glossaryRequest2; + + await page.getByText(glossaryTerm2.data.displayName).click(); + + await page.waitForSelector( + '[data-testid="tag-selector"]:has-text("' + + glossaryTerm2.data.displayName + + '")' + ); + + const patchRequest = page.waitForResponse(`/api/v1/dashboards/*`); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + await patchRequest; + + await expect(page.getByRole('alert').first()).toContainText( + "mutually exclusive and can't be assigned together" + ); + + await page.getByLabel('close').first().click(); + + // Add non mutually exclusive tags + await page.click( + '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="add-tag"]' + ); + + // Select 1st term + await page.click('[data-testid="tag-selector"] #tagsForm_tags'); + + const glossaryRequest3 = page.waitForResponse( + `/api/v1/search/query?q=*&index=glossary_term_search_index&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` + ); + await page.type( + '[data-testid="tag-selector"] #tagsForm_tags', + glossaryTerm3.data.name + ); + await glossaryRequest3; + + await page.getByText(glossaryTerm3.data.displayName).click(); + await page.waitForSelector( + '[data-testid="tag-selector"]:has-text("' + + glossaryTerm3.data.displayName + + '")' + ); + + // Select 2nd term + await page.click('[data-testid="tag-selector"] #tagsForm_tags'); + + const glossaryRequest4 = page.waitForResponse( + `/api/v1/search/query?q=*&index=glossary_term_search_index&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` + ); + await page.type( + '[data-testid="tag-selector"] #tagsForm_tags', + glossaryTerm4.data.name + ); + await glossaryRequest4; + + await page.getByText(glossaryTerm4.data.displayName).click(); + + await page.waitForSelector( + '[data-testid="tag-selector"]:has-text("' + + glossaryTerm4.data.displayName + + '")' + ); + + const patchRequest2 = page.waitForResponse(`/api/v1/dashboards/*`); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + await patchRequest2; + + // Check if the terms are present + const glossaryContainer = await page.locator( + '[data-testid="entity-right-panel"] [data-testid="glossary-container"]' + ); + const glossaryContainerText = await glossaryContainer.innerText(); + + expect(glossaryContainerText).toContain(glossaryTerm3.data.displayName); + expect(glossaryContainerText).toContain(glossaryTerm4.data.displayName); + + // Check if the icons are present + + const icons = await page.locator( + '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="glossary-icon"]' + ); + + expect(await icons.count()).toBe(2); + + // Add Glossary to Dashboard Charts + await page.click( + '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"] > [data-testid="entity-tags"] [data-testid="add-tag"]' + ); + + await page.click('[data-testid="tag-selector"]'); + + const glossaryRequest5 = page.waitForResponse( + `/api/v1/search/query?q=*&index=glossary_term_search_index&from=0&size=25&deleted=false&track_total_hits=true&getHierarchy=true` + ); + await page.type( + '[data-testid="tag-selector"] #tagsForm_tags', + glossaryTerm3.data.name + ); + await glossaryRequest5; + + await page + .getByRole('tree') + .getByTestId(`tag-${glossaryTerm3.data.fullyQualifiedName}`) + .click(); + + await page.waitForSelector( + '[data-testid="tag-selector"]:has-text("' + + glossaryTerm3.data.displayName + + '")' + ); + + const patchRequest3 = page.waitForResponse(`/api/v1/charts/*`); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + await patchRequest3; + + // Check if the term is present + const tagSelectorText = await page + .locator( + '[data-testid="glossary-tags-0"] [data-testid="glossary-container"] [data-testid="tags"]' + ) + .innerText(); + + expect(tagSelectorText).toContain(glossaryTerm3.data.displayName); + + // Check if the icon is visible + const icon = await page.locator( + '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"] [data-testid="glossary-icon"]' + ); + + expect(await icon.isVisible()).toBe(true); + + await sidebarClick(page, SidebarItem.GLOSSARY); + + await selectActiveGlossary(page, glossary2.data.displayName); + await goToAssetsTab(page, glossaryTerm3.data.displayName, '2'); + + // Check if the selected asset are present + const assetContainer = await page.locator( + '[data-testid="table-container"] .assets-data-container' + ); + + const assetContainerText = await assetContainer.innerText(); + + expect(assetContainerText).toContain(dashboardEntity.entity.name); + 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 dashboardEntity.delete(apiContext); + await afterAction(); + } + }); + test.afterAll(async ({ browser }) => { const { afterAction, apiContext } = await performAdminLogin(browser); await user1.delete(apiContext); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardClass.ts index 49e64ede71c..c73865c8f56 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardClass.ts @@ -33,6 +33,11 @@ export class DashboardClass extends EntityClass { }, }, }; + charts = { + name: `pw-chart-${uuid()}`, + displayName: `PW Chart ${uuid()}`, + service: this.service.name, + }; entity = { name: `pw-dashboard-${uuid()}`, displayName: `pw-dashboard-${uuid()}`, @@ -41,6 +46,7 @@ export class DashboardClass extends EntityClass { serviceResponseData: unknown; entityResponseData: unknown; + chartsResponseData: unknown; constructor(name?: string) { super(EntityTypeEndpoint.Dashboard); @@ -55,16 +61,25 @@ export class DashboardClass extends EntityClass { data: this.service, } ); + const chartsResponse = await apiContext.post('/api/v1/charts', { + data: this.charts, + }); + const entityResponse = await apiContext.post('/api/v1/dashboards', { - data: this.entity, + data: { + ...this.entity, + charts: [`${this.service.name}.${this.charts.name}`], + }, }); this.serviceResponseData = await serviceResponse.json(); + this.chartsResponseData = await chartsResponse.json(); this.entityResponseData = await entityResponse.json(); return { service: serviceResponse.body, entity: entityResponse.body, + charts: chartsResponse.body, }; } @@ -90,9 +105,16 @@ export class DashboardClass extends EntityClass { )}?recursive=true&hardDelete=true` ); + const chartResponse = await apiContext.delete( + `/api/v1/charts/name/${encodeURIComponent( + this.chartsResponseData?.['fullyQualifiedName'] + )}?recursive=true&hardDelete=true` + ); + return { service: serviceResponse.body, entity: this.entityResponseData, + chart: chartResponse.body, }; } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 9cca56302e0..9233f2b84a1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -61,6 +61,35 @@ export const selectActiveGlossary = async ( } }; +export const selectActiveGlossaryTerm = async ( + page: Page, + glossaryTermName: string +) => { + const glossaryTermResponse = page.waitForResponse( + '/api/v1/glossaryTerms/name/*?fields=relatedTerms%2Creviewers%2Ctags%2Cowner%2Cchildren%2Cvotes%2Cdomain%2Cextension' + ); + await page.getByTestId(glossaryTermName).click(); + await glossaryTermResponse; + + expect( + page.locator('[data-testid="entity-header-display-name"]') + ).toContainText(glossaryTermName); +}; + +export const goToAssetsTab = async ( + page: Page, + displayName: string, + count = '0' +) => { + await selectActiveGlossaryTerm(page, displayName); + await page.getByTestId('assets').click(); + await page.waitForSelector('.ant-tabs-tab-active:has-text("Assets")'); + + await expect( + page.getByTestId('assets').getByTestId('filter-count') + ).toContainText(count); +}; + export const addMultiOwner = async (data: { page: Page; ownerNames: string | string[]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx index f077b0bb930..73c517872c0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppBar/Suggestions.tsx @@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PAGE_SIZE_BASE } from '../../constants/constants'; import { + ChartSource, DashboardSource, DataProductSource, GlossarySource, @@ -98,6 +99,8 @@ const Suggestions = ({ DataProductSource[] >([]); + const [chartSuggestions, setChartSuggestions] = useState([]); + const isMounting = useRef(true); const updateSuggestions = (options: Array