Fix # 16475 : Support dashboards field in chart entity (#16646)

* Fix # 16475 : Add "all" alias for chart_search_index

* Backend : support "dashboards" field for Chart entity

* Backend : support "dashboards" field for Chart entity

* support to show charts in suggestions and glossary asset and redirect their dashboard page

* localization keys

* added unit test for component

* minor fix

* Add backend tests

* added playwright test for chart in glossary

* minor change

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
This commit is contained in:
sonika-shah 2024-07-01 20:27:34 +05:30 committed by GitHub
parent f766ba872d
commit b16460fba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1252 additions and 16 deletions

View File

@ -13,6 +13,11 @@
package org.openmetadata.service.jdbi3; 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.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction; 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.entity.services.DashboardService;
import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.Relationship;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.charts.ChartResource; 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.EntityUtil.Fields;
import org.openmetadata.service.util.FullyQualifiedName; import org.openmetadata.service.util.FullyQualifiedName;
@Slf4j @Slf4j
public class ChartRepository extends EntityRepository<Chart> { public class ChartRepository extends EntityRepository<Chart> {
private static final String CHART_UPDATE_FIELDS = "dashboards";
private static final String CHART_PATCH_FIELDS = "dashboards";
public ChartRepository() { public ChartRepository() {
super( super(
ChartResource.COLLECTION_PATH, ChartResource.COLLECTION_PATH,
Entity.CHART, Entity.CHART,
Chart.class, Chart.class,
Entity.getCollectionDAO().chartDAO(), Entity.getCollectionDAO().chartDAO(),
"", CHART_PATCH_FIELDS,
""); CHART_UPDATE_FIELDS);
supportsSearch = true; supportsSearch = true;
} }
@ -50,26 +61,35 @@ public class ChartRepository extends EntityRepository<Chart> {
DashboardService dashboardService = Entity.getEntity(chart.getService(), "", Include.ALL); DashboardService dashboardService = Entity.getEntity(chart.getService(), "", Include.ALL);
chart.setService(dashboardService.getEntityReference()); chart.setService(dashboardService.getEntityReference());
chart.setServiceType(dashboardService.getServiceType()); chart.setServiceType(dashboardService.getServiceType());
chart.setDashboards(EntityUtil.getEntityReferences(chart.getDashboards(), Include.NON_DELETED));
} }
@Override @Override
public void storeEntity(Chart chart, boolean update) { public void storeEntity(Chart chart, boolean update) {
// Relationships and fields such as tags are not stored as part of json // Relationships and fields such as tags are not stored as part of json
EntityReference service = chart.getService(); EntityReference service = chart.getService();
chart.withService(null); List<EntityReference> dashboards = chart.getDashboards();
chart.withService(null).withDashboards(null);
store(chart, update); store(chart, update);
chart.withService(service); chart.withService(service).withDashboards(dashboards);
} }
@Override @Override
@SneakyThrows @SneakyThrows
public void storeRelationships(Chart chart) { public void storeRelationships(Chart chart) {
addServiceRelationship(chart, chart.getService()); 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 @Override
public void setFields(Chart chart, Fields fields) { public void setFields(Chart chart, Fields fields) {
chart.withService(getContainer(chart.getId())); chart.withService(getContainer(chart.getId()));
chart.setDashboards(
fields.contains("dashboards") ? getRelatedEntities(chart, Entity.DASHBOARD) : null);
} }
@Override @Override
@ -94,6 +114,12 @@ public class ChartRepository extends EntityRepository<Chart> {
return Entity.getEntity(entity.getService(), fields, Include.ALL); return Entity.getEntity(entity.getService(), fields, Include.ALL);
} }
private List<EntityReference> getRelatedEntities(Chart chart, String entityType) {
return chart == null
? Collections.emptyList()
: findFrom(chart.getId(), Entity.CHART, Relationship.HAS, entityType);
}
public class ChartUpdater extends ColumnEntityUpdater { public class ChartUpdater extends ColumnEntityUpdater {
public ChartUpdater(Chart chart, Chart updated, Operation operation) { public ChartUpdater(Chart chart, Chart updated, Operation operation) {
super(chart, updated, operation); super(chart, updated, operation);
@ -105,6 +131,32 @@ public class ChartRepository extends EntityRepository<Chart> {
recordChange("chartType", original.getChartType(), updated.getChartType()); recordChange("chartType", original.getChartType(), updated.getChartType());
recordChange("sourceUrl", original.getSourceUrl(), updated.getSourceUrl()); recordChange("sourceUrl", original.getSourceUrl(), updated.getSourceUrl());
recordChange("sourceHash", original.getSourceHash(), updated.getSourceHash()); recordChange("sourceHash", original.getSourceHash(), updated.getSourceHash());
update(
Entity.DASHBOARD,
"dashboards",
listOrEmpty(updated.getDashboards()),
listOrEmpty(original.getDashboards()));
}
private void update(
String entityType,
String field,
List<EntityReference> updEntities,
List<EntityReference> 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<EntityReference> added = new ArrayList<>();
List<EntityReference> deleted = new ArrayList<>();
recordListChange(
field, oriEntities, updEntities, added, deleted, EntityUtil.entityReferenceMatch);
} }
} }
} }

View File

@ -74,7 +74,7 @@ import org.openmetadata.service.util.ResultList;
@Collection(name = "charts") @Collection(name = "charts")
public class ChartResource extends EntityResource<Chart, ChartRepository> { public class ChartResource extends EntityResource<Chart, ChartRepository> {
public static final String COLLECTION_PATH = "v1/charts/"; 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 @Override
public Chart addHref(UriInfo uriInfo, Chart chart) { public Chart addHref(UriInfo uriInfo, Chart chart) {
@ -530,6 +530,7 @@ public class ChartResource extends EntityResource<Chart, ChartRepository> {
.withService(EntityUtil.getEntityReference(Entity.DASHBOARD_SERVICE, create.getService())) .withService(EntityUtil.getEntityReference(Entity.DASHBOARD_SERVICE, create.getService()))
.withChartType(create.getChartType()) .withChartType(create.getChartType())
.withSourceUrl(create.getSourceUrl()) .withSourceUrl(create.getSourceUrl())
.withSourceHash(create.getSourceHash()); .withSourceHash(create.getSourceHash())
.withDashboards(getEntityReferences(Entity.DASHBOARD, create.getDashboards()));
} }
} }

View File

@ -342,6 +342,54 @@
}, },
"descriptionStatus": { "descriptionStatus": {
"type": "keyword" "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"
}
}
} }
} }
} }

View File

@ -31,7 +31,7 @@
"indexName": "chart_search_index", "indexName": "chart_search_index",
"alias": "chart", "alias": "chart",
"indexMappingFile": "/elasticsearch/%s/chart_index_mapping.json", "indexMappingFile": "/elasticsearch/%s/chart_index_mapping.json",
"parentAliases": ["dashboard", "dashboardService", "dataAsset"], "parentAliases": ["dashboard", "dashboardService", "all", "dataAsset"],
"childAliases": [] "childAliases": []
}, },
"dashboard": { "dashboard": {

View File

@ -349,6 +349,54 @@
}, },
"descriptionStatus": { "descriptionStatus": {
"type": "keyword" "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"
}
}
} }
} }
} }

View File

@ -332,6 +332,54 @@
}, },
"descriptionStatus": { "descriptionStatus": {
"type": "keyword" "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"
}
}
} }
} }
} }

View File

@ -333,6 +333,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
public static EntityReference METABASE_REFERENCE; public static EntityReference METABASE_REFERENCE;
public static EntityReference LOOKER_REFERENCE; public static EntityReference LOOKER_REFERENCE;
public static List<String> CHART_REFERENCES; public static List<String> CHART_REFERENCES;
public static List<String> DASHBOARD_REFERENCES;
public static Database DATABASE; public static Database DATABASE;
public static DatabaseSchema DATABASE_SCHEMA; public static DatabaseSchema DATABASE_SCHEMA;

View File

@ -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.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.OK; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.openmetadata.service.security.SecurityUtil.authHeaders; import static org.openmetadata.service.security.SecurityUtil.authHeaders;
import static org.openmetadata.service.util.EntityUtil.fieldAdded; 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.fieldUpdated;
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; 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.CHANGE_CONSOLIDATED;
@ -29,6 +31,7 @@ import static org.openmetadata.service.util.TestUtils.assertResponse;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException; 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.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.api.parallel.ExecutionMode;
import org.openmetadata.schema.api.data.CreateChart; 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.api.services.CreateDashboardService;
import org.openmetadata.schema.entity.data.Chart; 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.DashboardService;
import org.openmetadata.schema.type.ChangeDescription; import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.ChartType; 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.Entity;
import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.charts.ChartResource.ChartList; 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.resources.services.DashboardServiceResourceTest;
import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.ResultList; import org.openmetadata.service.util.ResultList;
@ -279,6 +285,99 @@ public class ChartResourceTest extends EntityResourceTest<Chart, CreateChart> {
chart, originalJson, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change); 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 @Override
public void compareEntities(Chart expected, Chart patched, Map<String, String> authHeaders) { public void compareEntities(Chart expected, Chart patched, Map<String, String> authHeaders) {
assertReference(expected.getService(), patched.getService()); assertReference(expected.getService(), patched.getService());
@ -295,6 +394,8 @@ public class ChartResourceTest extends EntityResourceTest<Chart, CreateChart> {
ChartType expectedChartType = ChartType.fromValue(expected.toString()); ChartType expectedChartType = ChartType.fromValue(expected.toString());
ChartType actualChartType = ChartType.fromValue(actual.toString()); ChartType actualChartType = ChartType.fromValue(actual.toString());
assertEquals(expectedChartType, actualChartType); assertEquals(expectedChartType, actualChartType);
} else if (fieldName.contains("dashboards")) {
assertEntityReferencesFieldChange(expected, actual);
} else { } else {
assertCommonFieldChange(fieldName, expected, actual); assertCommonFieldChange(fieldName, expected, actual);
} }

View File

@ -40,9 +40,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.common.utils.CommonUtil;
import org.openmetadata.schema.api.data.CreateChart; 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.data.CreateDashboardDataModel.DashboardServiceType;
import org.openmetadata.schema.api.services.CreateDashboardService; import org.openmetadata.schema.api.services.CreateDashboardService;
import org.openmetadata.schema.entity.data.Chart; 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.DashboardService;
import org.openmetadata.schema.entity.services.connections.TestConnectionResult; import org.openmetadata.schema.entity.services.connections.TestConnectionResult;
import org.openmetadata.schema.entity.services.connections.TestConnectionResultStatus; 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.schema.type.DashboardConnection;
import org.openmetadata.service.Entity; import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.charts.ChartResourceTest; 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.resources.services.dashboard.DashboardServiceResource.DashboardServiceList;
import org.openmetadata.service.secrets.masker.PasswordEntityMasker; import org.openmetadata.service.secrets.masker.PasswordEntityMasker;
import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.JsonUtils;
@ -292,9 +295,10 @@ public class DashboardServiceResourceTest
public void setupDashboardServices(TestInfo test) public void setupDashboardServices(TestInfo test)
throws HttpResponseException, URISyntaxException { throws HttpResponseException, URISyntaxException {
DashboardServiceResourceTest dashboardResourceTest = new DashboardServiceResourceTest(); DashboardServiceResourceTest dashboardServiceResourceTest = new DashboardServiceResourceTest();
DashboardResourceTest dashboardResourceTest = new DashboardResourceTest();
CreateDashboardService createDashboardService = CreateDashboardService createDashboardService =
dashboardResourceTest dashboardServiceResourceTest
.createRequest("superset", "", "", null) .createRequest("superset", "", "", null)
.withServiceType(DashboardServiceType.Metabase); .withServiceType(DashboardServiceType.Metabase);
DashboardConnection dashboardConnection = DashboardConnection dashboardConnection =
@ -312,7 +316,7 @@ public class DashboardServiceResourceTest
METABASE_REFERENCE = dashboardService.getEntityReference(); METABASE_REFERENCE = dashboardService.getEntityReference();
CreateDashboardService lookerDashboardService = CreateDashboardService lookerDashboardService =
dashboardResourceTest dashboardServiceResourceTest
.createRequest("looker", "", "", null) .createRequest("looker", "", "", null)
.withServiceType(DashboardServiceType.Looker); .withServiceType(DashboardServiceType.Looker);
DashboardConnection lookerConnection = DashboardConnection lookerConnection =
@ -334,5 +338,16 @@ public class DashboardServiceResourceTest
Chart chart = chartResourceTest.createEntity(createChart, ADMIN_AUTH_HEADERS); Chart chart = chartResourceTest.createEntity(createChart, ADMIN_AUTH_HEADERS);
CHART_REFERENCES.add(chart.getFullyQualifiedName()); 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());
}
} }
} }

View File

@ -62,6 +62,14 @@
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"maxLength": 32 "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"], "required": ["name", "service"],

View File

@ -160,6 +160,11 @@
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"maxLength": 32 "maxLength": 32
},
"dashboards": {
"description": "All the dashboards containing this chart.",
"$ref": "../../type/entityReferenceList.json",
"default": null
} }
}, },
"required": ["id", "name", "service"], "required": ["id", "name", "service"],

View File

@ -10,8 +10,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import test from '@playwright/test'; import test, { expect } from '@playwright/test';
import { SidebarItem } from '../../constant/sidebar'; import { SidebarItem } from '../../constant/sidebar';
import { DashboardClass } from '../../support/entity/DashboardClass';
import { Glossary } from '../../support/glossary/Glossary'; import { Glossary } from '../../support/glossary/Glossary';
import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm';
import { TeamClass } from '../../support/team/TeamClass'; import { TeamClass } from '../../support/team/TeamClass';
@ -25,6 +26,7 @@ import {
approveGlossaryTermTask, approveGlossaryTermTask,
createGlossary, createGlossary,
createGlossaryTerms, createGlossaryTerms,
goToAssetsTab,
selectActiveGlossary, selectActiveGlossary,
validateGlossaryTerm, validateGlossaryTerm,
verifyGlossaryDetails, verifyGlossaryDetails,
@ -136,6 +138,247 @@ test.describe('Glossary tests', () => {
await afterAction(); 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 }) => { test.afterAll(async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser); const { afterAction, apiContext } = await performAdminLogin(browser);
await user1.delete(apiContext); await user1.delete(apiContext);

View File

@ -33,6 +33,11 @@ export class DashboardClass extends EntityClass {
}, },
}, },
}; };
charts = {
name: `pw-chart-${uuid()}`,
displayName: `PW Chart ${uuid()}`,
service: this.service.name,
};
entity = { entity = {
name: `pw-dashboard-${uuid()}`, name: `pw-dashboard-${uuid()}`,
displayName: `pw-dashboard-${uuid()}`, displayName: `pw-dashboard-${uuid()}`,
@ -41,6 +46,7 @@ export class DashboardClass extends EntityClass {
serviceResponseData: unknown; serviceResponseData: unknown;
entityResponseData: unknown; entityResponseData: unknown;
chartsResponseData: unknown;
constructor(name?: string) { constructor(name?: string) {
super(EntityTypeEndpoint.Dashboard); super(EntityTypeEndpoint.Dashboard);
@ -55,16 +61,25 @@ export class DashboardClass extends EntityClass {
data: this.service, data: this.service,
} }
); );
const chartsResponse = await apiContext.post('/api/v1/charts', {
data: this.charts,
});
const entityResponse = await apiContext.post('/api/v1/dashboards', { 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.serviceResponseData = await serviceResponse.json();
this.chartsResponseData = await chartsResponse.json();
this.entityResponseData = await entityResponse.json(); this.entityResponseData = await entityResponse.json();
return { return {
service: serviceResponse.body, service: serviceResponse.body,
entity: entityResponse.body, entity: entityResponse.body,
charts: chartsResponse.body,
}; };
} }
@ -90,9 +105,16 @@ export class DashboardClass extends EntityClass {
)}?recursive=true&hardDelete=true` )}?recursive=true&hardDelete=true`
); );
const chartResponse = await apiContext.delete(
`/api/v1/charts/name/${encodeURIComponent(
this.chartsResponseData?.['fullyQualifiedName']
)}?recursive=true&hardDelete=true`
);
return { return {
service: serviceResponse.body, service: serviceResponse.body,
entity: this.entityResponseData, entity: this.entityResponseData,
chart: chartResponse.body,
}; };
} }
} }

View File

@ -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: { export const addMultiOwner = async (data: {
page: Page; page: Page;
ownerNames: string | string[]; ownerNames: string | string[];

View File

@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PAGE_SIZE_BASE } from '../../constants/constants'; import { PAGE_SIZE_BASE } from '../../constants/constants';
import { import {
ChartSource,
DashboardSource, DashboardSource,
DataProductSource, DataProductSource,
GlossarySource, GlossarySource,
@ -98,6 +99,8 @@ const Suggestions = ({
DataProductSource[] DataProductSource[]
>([]); >([]);
const [chartSuggestions, setChartSuggestions] = useState<ChartSource[]>([]);
const isMounting = useRef(true); const isMounting = useRef(true);
const updateSuggestions = (options: Array<Option>) => { const updateSuggestions = (options: Array<Option>) => {
@ -127,6 +130,8 @@ const Suggestions = ({
setDataProductSuggestions( setDataProductSuggestions(
filterOptionsByIndex(options, SearchIndex.DATA_PRODUCT) filterOptionsByIndex(options, SearchIndex.DATA_PRODUCT)
); );
setChartSuggestions(filterOptionsByIndex(options, SearchIndex.CHART));
}; };
const getSuggestionsForIndex = ( const getSuggestionsForIndex = (
@ -189,6 +194,10 @@ const Suggestions = ({
suggestions: dataProductSuggestions, suggestions: dataProductSuggestions,
searchIndex: SearchIndex.DATA_PRODUCT, searchIndex: SearchIndex.DATA_PRODUCT,
}, },
{
suggestions: chartSuggestions,
searchIndex: SearchIndex.CHART,
},
...searchClassBase.getEntitiesSuggestions(options ?? []), ...searchClassBase.getEntitiesSuggestions(options ?? []),
].map(({ suggestions, searchIndex }) => ].map(({ suggestions, searchIndex }) =>
getSuggestionsForIndex(suggestions, searchIndex) getSuggestionsForIndex(suggestions, searchIndex)

View File

@ -0,0 +1,92 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Col, Divider, Row, Typography } from 'antd';
import { get } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { ExplorePageTabs } from '../../../../enums/Explore.enum';
import { Chart } from '../../../../generated/entity/data/chart';
import {
getFormattedEntityData,
getSortedTagsWithHighlight,
} from '../../../../utils/EntitySummaryPanelUtils';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityOverview,
} from '../../../../utils/EntityUtils';
import SummaryTagsDescription from '../../../common/SummaryTagsDescription/SummaryTagsDescription.component';
import { SearchedDataProps } from '../../../SearchedData/SearchedData.interface';
import CommonEntitySummaryInfo from '../CommonEntitySummaryInfo/CommonEntitySummaryInfo';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface ChartsSummaryProps {
entityDetails: Chart;
highlights?: SearchedDataProps['data'][number]['highlight'];
}
const ChartSummary = ({ entityDetails, highlights }: ChartsSummaryProps) => {
const { t } = useTranslation();
const entityInfo = useMemo(
() => getEntityOverview(ExplorePageTabs.CHARTS, entityDetails),
[entityDetails]
);
const formattedDashboardData: BasicEntityInfo[] = useMemo(
() =>
getFormattedEntityData(
SummaryEntityType.DASHBOARD,
entityDetails.dashboards
),
[entityDetails.dashboards]
);
return (
<>
<Row className="m-md m-t-0" gutter={[0, 4]}>
<Col span={24}>
<CommonEntitySummaryInfo
componentType={DRAWER_NAVIGATION_OPTIONS.explore}
entityInfo={entityInfo}
/>
</Col>
</Row>
<Divider className="m-y-xs" />
<SummaryTagsDescription
entityDetail={entityDetails}
tags={getSortedTagsWithHighlight(
entityDetails.tags,
get(highlights, 'tag.name')
)}
/>
<Divider className="m-y-xs" />
<Row className="m-md" gutter={[0, 8]}>
<Col span={24}>
<Typography.Text
className="summary-panel-section-title"
data-testid="charts-header">
{t('label.dashboard-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedDashboardData} />
</Col>
</Row>
</>
);
};
export default ChartSummary;

View File

@ -0,0 +1,57 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { MOCK_CHART_DATA } from '../../../../mocks/Chart.mock';
import ChartSummary from './ChartSummary.component';
jest.mock('../SummaryList/SummaryList.component', () =>
jest.fn().mockImplementation(() => <p>SummaryList</p>)
);
jest.mock('../CommonEntitySummaryInfo/CommonEntitySummaryInfo', () =>
jest.fn().mockImplementation(() => <p>testCommonEntitySummaryInfo</p>)
);
jest.mock(
'../../../common/SummaryTagsDescription/SummaryTagsDescription.component',
() => jest.fn().mockImplementation(() => <p>SummaryTagsDescription</p>)
);
jest.mock('../../../../utils/EntityUtils', () => ({
getEntityOverview: jest.fn(),
DRAWER_NAVIGATION_OPTIONS: {
explore: 'explore',
},
}));
jest.mock('../../../../utils/EntitySummaryPanelUtils', () => ({
getSortedTagsWithHighlight: jest.fn(),
getFormattedEntityData: jest.fn(),
}));
describe('ChartSummary component tests', () => {
it('Component should render properly', async () => {
await act(async () => {
render(<ChartSummary entityDetails={MOCK_CHART_DATA} />, {
wrapper: MemoryRouter,
});
});
expect(screen.getByTestId('charts-header')).toBeInTheDocument();
expect(screen.getByText('SummaryList')).toBeInTheDocument();
expect(screen.getByText('SummaryTagsDescription')).toBeInTheDocument();
expect(screen.getByText('testCommonEntitySummaryInfo')).toBeInTheDocument();
});
});

View File

@ -24,6 +24,7 @@ import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../enums/common.enum';
import { EntityType } from '../../../enums/entity.enum'; import { EntityType } from '../../../enums/entity.enum';
import { ExplorePageTabs } from '../../../enums/Explore.enum'; import { ExplorePageTabs } from '../../../enums/Explore.enum';
import { Tag } from '../../../generated/entity/classification/tag'; import { Tag } from '../../../generated/entity/classification/tag';
import { Chart } from '../../../generated/entity/data/chart';
import { Container } from '../../../generated/entity/data/container'; import { Container } from '../../../generated/entity/data/container';
import { Dashboard } from '../../../generated/entity/data/dashboard'; import { Dashboard } from '../../../generated/entity/data/dashboard';
import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel';
@ -50,6 +51,7 @@ import searchClassBase from '../../../utils/SearchClassBase';
import { stringToHTML } from '../../../utils/StringsUtils'; import { stringToHTML } from '../../../utils/StringsUtils';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../common/Loader/Loader'; import Loader from '../../common/Loader/Loader';
import ChartSummary from './ChartSummary/ChartSummary.component';
import ContainerSummary from './ContainerSummary/ContainerSummary.component'; import ContainerSummary from './ContainerSummary/ContainerSummary.component';
import DashboardSummary from './DashboardSummary/DashboardSummary.component'; import DashboardSummary from './DashboardSummary/DashboardSummary.component';
import DatabaseSchemaSummary from './DatabaseSchemaSummary/DatabaseSchemaSummary.component'; import DatabaseSchemaSummary from './DatabaseSchemaSummary/DatabaseSchemaSummary.component';
@ -149,6 +151,14 @@ export default function EntitySummaryPanel({
/> />
); );
case EntityType.CHART:
return (
<ChartSummary
entityDetails={entity as Chart}
highlights={highlights}
/>
);
case EntityType.PIPELINE: case EntityType.PIPELINE:
return ( return (
<PipelineSummary <PipelineSummary

View File

@ -72,6 +72,14 @@ jest.mock('./MlModelSummary/MlModelSummary.component', () =>
)) ))
); );
jest.mock('./ChartSummary/ChartSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="ChartSummary">ChartSummary</div>
))
);
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => ({ tab: 'table' })), useParams: jest.fn().mockImplementation(() => ({ tab: 'table' })),
Link: jest.fn().mockImplementation(({ children }) => <>{children}</>), Link: jest.fn().mockImplementation(({ children }) => <>{children}</>),
@ -186,4 +194,24 @@ describe('EntitySummaryPanel component tests', () => {
expect(mlModelSummary).toBeInTheDocument(); expect(mlModelSummary).toBeInTheDocument();
}); });
it('ChartSummary should render for chart data', async () => {
await act(async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: {
...mockMlModelEntityDetails,
entityType: EntityType.CHART,
},
}}
handleClosePanel={mockHandleClosePanel}
/>
);
});
const chartSummary = screen.getByTestId('ChartSummary');
expect(chartSummary).toBeInTheDocument();
});
}); });

View File

@ -91,6 +91,11 @@ export interface DataProductSource extends CommonSource {
data_product_name: string; data_product_name: string;
} }
export interface ChartSource extends CommonSource {
chart_id: string;
chart_name: string;
}
export interface Option { export interface Option {
_index: string; _index: string;
_id: string; _id: string;
@ -105,7 +110,8 @@ export interface Option {
GlossarySource & GlossarySource &
TagSource & TagSource &
SearchIndexSource & SearchIndexSource &
DataProductSource; DataProductSource &
ChartSource;
} }
export type SearchSuggestions = export type SearchSuggestions =
@ -120,4 +126,5 @@ export type SearchSuggestions =
| SearchIndexSource[] | SearchIndexSource[]
| StoredProcedureSearchSource[] | StoredProcedureSearchSource[]
| DashboardDataModelSearchSource[] | DashboardDataModelSearchSource[]
| DataProductSource[]; | DataProductSource[]
| ChartSource[];

View File

@ -18,4 +18,5 @@ export enum SummaryEntityType {
MLFEATURE = 'mlFeature', MLFEATURE = 'mlFeature',
SCHEMAFIELD = 'schemaField', SCHEMAFIELD = 'schemaField',
FIELD = 'field', FIELD = 'field',
DASHBOARD = 'dashboard',
} }

View File

@ -24,6 +24,7 @@ export enum ExplorePageTabs {
PIPELINES = 'pipelines', PIPELINES = 'pipelines',
MLMODELS = 'mlmodels', MLMODELS = 'mlmodels',
CONTAINERS = 'containers', CONTAINERS = 'containers',
CHARTS = 'charts',
GLOSSARY = 'glossaries', GLOSSARY = 'glossaries',
TAG = 'tags', TAG = 'tags',
DATA_PRODUCT = 'dataProducts', DATA_PRODUCT = 'dataProducts',

View File

@ -142,6 +142,7 @@
"chart": "Diagramm", "chart": "Diagramm",
"chart-entity": "{{entity}} Diagramm", "chart-entity": "{{entity}} Diagramm",
"chart-plural": "Diagramme", "chart-plural": "Diagramme",
"chart-type": "Chart type",
"check-status": "Status überprüfen", "check-status": "Status überprüfen",
"children": "Kinder", "children": "Kinder",
"children-lowercase": "kinder", "children-lowercase": "kinder",

View File

@ -142,6 +142,7 @@
"chart": "Chart", "chart": "Chart",
"chart-entity": "Chart {{entity}}", "chart-entity": "Chart {{entity}}",
"chart-plural": "Charts", "chart-plural": "Charts",
"chart-type": "Chart type",
"check-status": "Check status", "check-status": "Check status",
"children": "Children", "children": "Children",
"children-lowercase": "children", "children-lowercase": "children",

View File

@ -142,6 +142,7 @@
"chart": "Gráfico", "chart": "Gráfico",
"chart-entity": "Gráfico {{entity}}", "chart-entity": "Gráfico {{entity}}",
"chart-plural": "Gráficos", "chart-plural": "Gráficos",
"chart-type": "Chart type",
"check-status": "Verificar estado", "check-status": "Verificar estado",
"children": "Hijos", "children": "Hijos",
"children-lowercase": "hijos", "children-lowercase": "hijos",

View File

@ -142,6 +142,7 @@
"chart": "Graphique", "chart": "Graphique",
"chart-entity": "Graphique {{entity}}", "chart-entity": "Graphique {{entity}}",
"chart-plural": "Graphiques", "chart-plural": "Graphiques",
"chart-type": "Chart type",
"check-status": "Vérifier le Statut", "check-status": "Vérifier le Statut",
"children": "Enfants", "children": "Enfants",
"children-lowercase": "enfants", "children-lowercase": "enfants",

View File

@ -142,6 +142,7 @@
"chart": "תרשים", "chart": "תרשים",
"chart-entity": "תרשים {{entity}}", "chart-entity": "תרשים {{entity}}",
"chart-plural": "תרשימים", "chart-plural": "תרשימים",
"chart-type": "Chart type",
"check-status": "בדוק סטטוס", "check-status": "בדוק סטטוס",
"children": "ילדים", "children": "ילדים",
"children-lowercase": "ילדים", "children-lowercase": "ילדים",

View File

@ -142,6 +142,7 @@
"chart": "チャート", "chart": "チャート",
"chart-entity": "{{entity}}のチャート", "chart-entity": "{{entity}}のチャート",
"chart-plural": "チャート", "chart-plural": "チャート",
"chart-type": "Chart type",
"check-status": "ステータスチェック", "check-status": "ステータスチェック",
"children": "Children", "children": "Children",
"children-lowercase": "children", "children-lowercase": "children",

View File

@ -142,6 +142,7 @@
"chart": "Chart", "chart": "Chart",
"chart-entity": "Chart {{entity}}", "chart-entity": "Chart {{entity}}",
"chart-plural": "Charts", "chart-plural": "Charts",
"chart-type": "Chart type",
"check-status": "Status controleren", "check-status": "Status controleren",
"children": "Kinderen", "children": "Kinderen",
"children-lowercase": "kinderen", "children-lowercase": "kinderen",

View File

@ -142,6 +142,7 @@
"chart": "Gráfico", "chart": "Gráfico",
"chart-entity": "Gráfico {{entity}}", "chart-entity": "Gráfico {{entity}}",
"chart-plural": "Gráficos", "chart-plural": "Gráficos",
"chart-type": "Chart type",
"check-status": "Verificar status", "check-status": "Verificar status",
"children": "Filhos", "children": "Filhos",
"children-lowercase": "filhos", "children-lowercase": "filhos",

View File

@ -142,6 +142,7 @@
"chart": "Диаграмма", "chart": "Диаграмма",
"chart-entity": "Диаграмма {{entity}}", "chart-entity": "Диаграмма {{entity}}",
"chart-plural": "Диаграммы", "chart-plural": "Диаграммы",
"chart-type": "Chart type",
"check-status": "Проверить статус", "check-status": "Проверить статус",
"children": "Наследники", "children": "Наследники",
"children-lowercase": "наследники", "children-lowercase": "наследники",

View File

@ -142,6 +142,7 @@
"chart": "图表", "chart": "图表",
"chart-entity": "图表{{entity}}", "chart-entity": "图表{{entity}}",
"chart-plural": "图表", "chart-plural": "图表",
"chart-type": "Chart type",
"check-status": "检查状态", "check-status": "检查状态",
"children": "子级", "children": "子级",
"children-lowercase": "子级", "children-lowercase": "子级",

View File

@ -0,0 +1,102 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Chart,
ChartType,
DashboardServiceType,
LabelType,
State,
TagSource,
} from '../generated/entity/data/chart';
export const MOCK_CHART_DATA: Chart = {
id: 'f4464d71-f900-4f8c-aca1-b7b95cc077e1',
name: '127',
displayName: 'Are you an ethnic minority in your city?',
fullyQualifiedName: 'sample_superset.127',
description: '',
version: 0.2,
updatedAt: 1718702267352,
updatedBy: 'admin',
chartType: ChartType.Other,
sourceUrl:
'http://localhost:8088/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D',
followers: [],
tags: [
{
tagFQN: 'Glossary.Glossary=Term',
displayName: 'Glossary=Term',
name: 'Glossary=Term',
labelType: LabelType.Manual,
description: 'Glossary=Term',
style: {},
source: TagSource.Glossary,
state: State.Confirmed,
},
],
service: {
deleted: false,
displayName: 'sample_superset',
name: 'sample_superset',
id: 'f70b7a78-8327-4565-a069-6cac70fa99cf',
type: 'dashboardService',
fullyQualifiedName: 'sample_superset',
},
serviceType: DashboardServiceType.Superset,
deleted: false,
dataProducts: [],
votes: {
upVoters: [],
downVoters: [],
upVotes: 0,
downVotes: 0,
},
dashboards: [
{
deleted: false,
displayName: 'deck.gl Demo',
name: '10',
description: '',
id: '77a0ac8a-ca1a-4f21-9a37-406faa482008',
type: 'dashboard',
fullyQualifiedName: 'sample_superset.10',
},
{
deleted: false,
displayName: 'Misc Charts',
name: '12',
description: '',
id: '54e2f981-ba28-4393-b21b-f85a8b7e63e9',
type: 'dashboard',
fullyQualifiedName: 'sample_superset.12',
},
{
deleted: false,
displayName: 'Slack Dashboard',
name: '33',
description: '',
id: '35e7b4db-9daa-4711-b4f0-a273fc966050',
type: 'dashboard',
fullyQualifiedName: 'sample_superset.33',
},
{
deleted: false,
displayName: 'Video Game Sales',
name: '51',
description: '',
id: '135fdd19-471c-4edf-a226-066c54f2f889',
type: 'dashboard',
fullyQualifiedName: 'sample_superset.51',
},
],
};

View File

@ -26,11 +26,13 @@ import {
mockEntityDataWithNestingResponse, mockEntityDataWithNestingResponse,
mockEntityDataWithoutNesting, mockEntityDataWithoutNesting,
mockEntityDataWithoutNestingResponse, mockEntityDataWithoutNestingResponse,
mockEntityReferenceDashboardData,
mockGetHighlightOfListItemResponse, mockGetHighlightOfListItemResponse,
mockGetMapOfListHighlightsResponse, mockGetMapOfListHighlightsResponse,
mockGetSummaryListItemTypeResponse, mockGetSummaryListItemTypeResponse,
mockHighlights, mockHighlights,
mockInvalidDataResponse, mockInvalidDataResponse,
mockLinkBasedSummaryTitleDashboardResponse,
mockLinkBasedSummaryTitleResponse, mockLinkBasedSummaryTitleResponse,
mockListItemNameHighlight, mockListItemNameHighlight,
mockTagFQNsForHighlight, mockTagFQNsForHighlight,
@ -134,6 +136,14 @@ describe('EntitySummaryPanelUtils tests', () => {
expect(linkBasedTitle).toEqual(mockLinkBasedSummaryTitleResponse); expect(linkBasedTitle).toEqual(mockLinkBasedSummaryTitleResponse);
}); });
it('getTitle should return title as link without icon if type: dashboard present in listItem', () => {
const linkBasedTitle = getTitle(mockEntityReferenceDashboardData);
expect(linkBasedTitle).toEqual(
mockLinkBasedSummaryTitleDashboardResponse
);
});
}); });
describe('getMapOfListHighlights', () => { describe('getMapOfListHighlights', () => {

View File

@ -24,6 +24,7 @@ import {
} from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface'; } from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { ICON_DIMENSION, NO_DATA_PLACEHOLDER } from '../constants/constants'; import { ICON_DIMENSION, NO_DATA_PLACEHOLDER } from '../constants/constants';
import { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant'; import { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant';
import { EntityType } from '../enums/entity.enum';
import { SummaryEntityType } from '../enums/EntitySummary.enum'; import { SummaryEntityType } from '../enums/EntitySummary.enum';
import { Chart } from '../generated/entity/data/chart'; import { Chart } from '../generated/entity/data/chart';
import { TagLabel } from '../generated/entity/data/container'; import { TagLabel } from '../generated/entity/data/container';
@ -31,6 +32,8 @@ import { MlFeature } from '../generated/entity/data/mlmodel';
import { Task } from '../generated/entity/data/pipeline'; import { Task } from '../generated/entity/data/pipeline';
import { Column, TableConstraint } from '../generated/entity/data/table'; import { Column, TableConstraint } from '../generated/entity/data/table';
import { Field } from '../generated/entity/data/topic'; import { Field } from '../generated/entity/data/topic';
import { EntityReference } from '../generated/tests/testCase';
import entityUtilClassBase from './EntityUtilClassBase';
import { getEntityName } from './EntityUtils'; import { getEntityName } from './EntityUtils';
import { stringToHTML } from './StringsUtils'; import { stringToHTML } from './StringsUtils';
@ -65,6 +68,23 @@ export const getTitle = (
: getEntityName(listItem) || NO_DATA_PLACEHOLDER; : getEntityName(listItem) || NO_DATA_PLACEHOLDER;
const sourceUrl = (listItem as Chart | Task).sourceUrl; const sourceUrl = (listItem as Chart | Task).sourceUrl;
if ((listItem as EntityReference).type === SummaryEntityType.DASHBOARD) {
return (
<Link
to={entityUtilClassBase.getEntityLink(
EntityType.DASHBOARD,
listItem.fullyQualifiedName ?? ''
)}>
<Text
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
{title}
</Text>
</Link>
);
}
return sourceUrl ? ( return sourceUrl ? (
<Link target="_blank" to={{ pathname: sourceUrl }}> <Link target="_blank" to={{ pathname: sourceUrl }}>
<div className="d-flex items-center"> <div className="d-flex items-center">

View File

@ -12,11 +12,14 @@
*/ */
import { getEntityDetailsPath } from '../constants/constants'; import { getEntityDetailsPath } from '../constants/constants';
import { EntityTabs, EntityType } from '../enums/entity.enum'; import { EntityTabs, EntityType } from '../enums/entity.enum';
import { ExplorePageTabs } from '../enums/Explore.enum';
import { TestSuite } from '../generated/tests/testCase'; import { TestSuite } from '../generated/tests/testCase';
import { MOCK_CHART_DATA } from '../mocks/Chart.mock';
import { import {
columnSorter, columnSorter,
getBreadcrumbForTestSuite, getBreadcrumbForTestSuite,
getEntityLinkFromType, getEntityLinkFromType,
getEntityOverview,
highlightEntityNameAndDescription, highlightEntityNameAndDescription,
} from './EntityUtils'; } from './EntityUtils';
import { import {
@ -28,6 +31,7 @@ import {
jest.mock('../constants/constants', () => ({ jest.mock('../constants/constants', () => ({
getEntityDetailsPath: jest.fn(), getEntityDetailsPath: jest.fn(),
getServiceDetailsPath: jest.fn(),
})); }));
jest.mock('./RouterUtils', () => ({ jest.mock('./RouterUtils', () => ({
@ -106,4 +110,25 @@ describe('EntityUtils unit tests', () => {
]); ]);
}); });
}); });
describe('getEntityOverview', () => {
it('should call getChartOverview and get ChartData if ExplorePageTabs is charts', () => {
const result = JSON.stringify(
getEntityOverview(ExplorePageTabs.CHARTS, MOCK_CHART_DATA)
);
expect(result).toContain('label.owner');
expect(result).toContain('label.chart');
expect(result).toContain('label.url-uppercase');
expect(result).toContain('Are you an ethnic minority in your city?');
expect(result).toContain(
`http://localhost:8088/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D`
);
expect(result).toContain('label.service');
expect(result).toContain('sample_superset');
expect(result).toContain('Other');
expect(result).toContain('label.service-type');
expect(result).toContain('Superset');
});
});
}); });

View File

@ -68,6 +68,7 @@ import { SearchIndex } from '../enums/search.enum';
import { ServiceCategory, ServiceCategoryPlural } from '../enums/service.enum'; import { ServiceCategory, ServiceCategoryPlural } from '../enums/service.enum';
import { PrimaryTableDataTypes } from '../enums/table.enum'; import { PrimaryTableDataTypes } from '../enums/table.enum';
import { Classification } from '../generated/entity/classification/classification'; import { Classification } from '../generated/entity/classification/classification';
import { Chart } from '../generated/entity/data/chart';
import { Container } from '../generated/entity/data/container'; import { Container } from '../generated/entity/data/container';
import { Dashboard } from '../generated/entity/data/dashboard'; import { Dashboard } from '../generated/entity/data/dashboard';
import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel';
@ -582,6 +583,67 @@ const getContainerOverview = (containerDetails: Container) => {
return overview; return overview;
}; };
const getChartOverview = (chartDetails: Chart) => {
const { owner, sourceUrl, chartType, service, serviceType, displayName } =
chartDetails;
const serviceDisplayName = getEntityName(service);
const overview = [
{
name: i18next.t('label.owner'),
value: <OwnerLabel hasPermission={false} owner={owner} />,
url: getOwnerValue(owner as EntityReference),
isLink: !isEmpty(owner?.name),
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: `${i18next.t('label.chart')} ${i18next.t('label.url-uppercase')}`,
value: stringToHTML(displayName ?? '') || NO_DATA,
url: sourceUrl,
isLink: true,
isExternal: true,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.service'),
value: serviceDisplayName || NO_DATA,
url: getServiceDetailsPath(
service?.name ?? '',
ServiceCategory.DASHBOARD_SERVICES
),
isExternal: false,
isLink: true,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.chart-type'),
value: chartType ?? NO_DATA,
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.explore,
DRAWER_NAVIGATION_OPTIONS.lineage,
],
},
{
name: i18next.t('label.service-type'),
value: serviceType ?? NO_DATA,
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.explore,
DRAWER_NAVIGATION_OPTIONS.lineage,
],
},
];
return overview;
};
const getDataModelOverview = (dataModelDetails: DashboardDataModel) => { const getDataModelOverview = (dataModelDetails: DashboardDataModel) => {
const { const {
owner, owner,
@ -896,6 +958,9 @@ export const getEntityOverview = (
case ExplorePageTabs.CONTAINERS: { case ExplorePageTabs.CONTAINERS: {
return getContainerOverview(entityDetail as Container); return getContainerOverview(entityDetail as Container);
} }
case ExplorePageTabs.CHARTS: {
return getChartOverview(entityDetail as Chart);
}
case ExplorePageTabs.DASHBOARD_DATA_MODEL: { case ExplorePageTabs.DASHBOARD_DATA_MODEL: {
return getDataModelOverview(entityDetail as DashboardDataModel); return getDataModelOverview(entityDetail as DashboardDataModel);

View File

@ -26,6 +26,8 @@ import {
} from '../constants/AdvancedSearch.constants'; } from '../constants/AdvancedSearch.constants';
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { Chart } from '../generated/entity/data/chart';
import { getEntityLinkFromType } from './EntityUtils';
import { SearchClassBase } from './SearchClassBase'; import { SearchClassBase } from './SearchClassBase';
import { getTestSuiteDetailsPath, getTestSuiteFQN } from './TestSuiteUtils'; import { getTestSuiteDetailsPath, getTestSuiteFQN } from './TestSuiteUtils';
@ -34,6 +36,12 @@ jest.mock('./TestSuiteUtils', () => ({
getTestSuiteFQN: jest.fn(), getTestSuiteFQN: jest.fn(),
})); }));
jest.mock('./EntityUtils', () => ({
getEntityLinkFromType: jest.fn(),
getEntityName: jest.fn(),
getEntityBreadcrumbs: jest.fn(),
}));
describe('SearchClassBase', () => { describe('SearchClassBase', () => {
let searchClassBase: SearchClassBase; let searchClassBase: SearchClassBase;
@ -227,7 +235,59 @@ describe('SearchClassBase', () => {
expect(getTestSuiteDetailsPath).toHaveBeenCalled(); expect(getTestSuiteDetailsPath).toHaveBeenCalled();
}); });
it('should call not getTestSuiteDetailsPath if entity type is not TestSuite', () => { it('should call getEntityLinkFromType with dashboard data if entity type is Chart', () => {
searchClassBase.getEntityLink({
id: '123',
service: {
id: '11',
type: 'dashboard',
fullyQualifiedName: 'superset',
name: 'superset',
},
fullyQualifiedName: 'test.chart',
entityType: EntityType.CHART,
name: 'chart',
dashboards: [
{
id: '12',
fullyQualifiedName: 'test.dashboard',
name: 'dashboard',
type: 'dashboard',
},
],
} as Chart);
expect(getEntityLinkFromType).toHaveBeenCalledWith(
'test.dashboard',
'dashboard',
{
fullyQualifiedName: 'test.dashboard',
id: '12',
name: 'dashboard',
type: 'dashboard',
}
);
});
it('should call not getEntityLinkFromType entity type is Chart and there is no dashboard in it', () => {
const result = searchClassBase.getEntityLink({
id: '123',
service: {
id: '11',
type: 'dashboard',
fullyQualifiedName: 'superset',
name: 'superset',
},
fullyQualifiedName: 'test.chart',
entityType: EntityType.CHART,
name: 'chart',
} as Chart);
expect(getEntityLinkFromType).not.toHaveBeenCalledWith();
expect(result).toBe('');
});
it('should not call getTestSuiteDetailsPath if entity type is not TestSuite', () => {
searchClassBase.getEntityLink({ searchClassBase.getEntityLink({
fullyQualifiedName: 'test.testSuite', fullyQualifiedName: 'test.testSuite',
entityType: EntityType.TABLE, entityType: EntityType.TABLE,

View File

@ -57,6 +57,7 @@ import {
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { ExplorePageTabs } from '../enums/Explore.enum'; import { ExplorePageTabs } from '../enums/Explore.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { Chart } from '../generated/entity/data/chart';
import { TestSuite } from '../generated/tests/testCase'; import { TestSuite } from '../generated/tests/testCase';
import { SearchSourceAlias } from '../interface/search.interface'; import { SearchSourceAlias } from '../interface/search.interface';
import { TabsInfoData } from '../pages/ExplorePage/ExplorePage.interface'; import { TabsInfoData } from '../pages/ExplorePage/ExplorePage.interface';
@ -344,6 +345,18 @@ class SearchClassBase {
}); });
} }
if (entity.entityType === EntityType.CHART) {
const dashboard = (entity as Chart).dashboards?.[0];
return dashboard
? getEntityLinkFromType(
dashboard.fullyQualifiedName ?? '',
EntityType.DASHBOARD,
dashboard as SourceType
)
: '';
}
if (entity.fullyQualifiedName && entity.entityType) { if (entity.fullyQualifiedName && entity.entityType) {
return getEntityLinkFromType( return getEntityLinkFromType(
entity.fullyQualifiedName, entity.fullyQualifiedName,

View File

@ -12,7 +12,7 @@
*/ */
import { EntityType } from '../enums/entity.enum'; import { EntityType } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
import { getEntityTypeFromSearchIndex } from './SearchUtils'; import { getEntityTypeFromSearchIndex, getGroupLabel } from './SearchUtils';
describe('getEntityTypeFromSearchIndex', () => { describe('getEntityTypeFromSearchIndex', () => {
it.each([ it.each([
@ -46,3 +46,79 @@ describe('getEntityTypeFromSearchIndex', () => {
expect(getEntityTypeFromSearchIndex('DUMMY_INDEX')).toBeNull(); expect(getEntityTypeFromSearchIndex('DUMMY_INDEX')).toBeNull();
}); });
}); });
describe('getGroupLabel', () => {
it('should return topic details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.TOPIC));
expect(result).toContain('label.topic-plural');
});
it('should return dashboard details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.DASHBOARD));
expect(result).toContain('label.dashboard-plural');
});
it('should return pipeline details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.PIPELINE));
expect(result).toContain('label.pipeline-plural');
});
it('should return ml-model details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.MLMODEL));
expect(result).toContain('label.ml-model-plural');
});
it('should return glossary-term details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.GLOSSARY_TERM));
expect(result).toContain('label.glossary-term-plural');
});
it('should return chart details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.CHART));
expect(result).toContain('label.chart-plural');
});
it('should return tag details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.TAG));
expect(result).toContain('label.tag-plural');
});
it('should return container details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.CONTAINER));
expect(result).toContain('label.container-plural');
});
it('should return stored-procedure details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.STORED_PROCEDURE));
expect(result).toContain('label.stored-procedure-plural');
});
it('should return data-model details if index type is chart', () => {
const result = JSON.stringify(
getGroupLabel(SearchIndex.DASHBOARD_DATA_MODEL)
);
expect(result).toContain('label.data-model-plural');
});
it('should return search-index details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.SEARCH_INDEX));
expect(result).toContain('label.search-index-plural');
});
it('should return data-product details if index type is chart', () => {
const result = JSON.stringify(getGroupLabel(SearchIndex.DATA_PRODUCT));
expect(result).toContain('label.data-product-plural');
});
});

View File

@ -17,6 +17,7 @@ import i18next from 'i18next';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ReactComponent as IconChart } from '../assets/svg/chart.svg';
import { ReactComponent as IconDashboard } from '../assets/svg/dashboard-grey.svg'; import { ReactComponent as IconDashboard } from '../assets/svg/dashboard-grey.svg';
import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.svg'; import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.svg';
import { ReactComponent as IconContainer } from '../assets/svg/ic-storage.svg'; import { ReactComponent as IconContainer } from '../assets/svg/ic-storage.svg';
@ -159,6 +160,12 @@ export const getGroupLabel = (index: string) => {
break; break;
case SearchIndex.CHART:
label = i18next.t('label.chart-plural');
GroupIcon = IconChart;
break;
default: { default: {
const { label: indexLabel, GroupIcon: IndexIcon } = const { label: indexLabel, GroupIcon: IndexIcon } =
searchClassBase.getIndexGroupLabel(index); searchClassBase.getIndexGroupLabel(index);

View File

@ -25,6 +25,7 @@ import {
State, State,
TagSource, TagSource,
} from '../../generated/entity/data/table'; } from '../../generated/entity/data/table';
import { EntityReference } from '../../generated/type/entityReference';
import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg'; import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg';
const { Text } = Typography; const { Text } = Typography;
@ -54,6 +55,17 @@ export const mockLinkBasedSummaryTitleResponse = (
</Link> </Link>
); );
export const mockLinkBasedSummaryTitleDashboardResponse = (
<Link to="/dashboard/sample_superset.10">
<Text
className="entity-title text-link-color font-medium m-r-xss"
data-testid="entity-title"
ellipsis={{ tooltip: true }}>
deck.gl Demo
</Text>
</Link>
);
export const mockGetSummaryListItemTypeResponse = 'PrestoOperator'; export const mockGetSummaryListItemTypeResponse = 'PrestoOperator';
export const mockTagsSortAndHighlightResponse = [ export const mockTagsSortAndHighlightResponse = [
@ -130,6 +142,16 @@ export const mockEntityDataWithoutNesting: Task[] = [
}, },
]; ];
export const mockEntityReferenceDashboardData: EntityReference = {
deleted: false,
description: '',
displayName: 'deck.gl Demo',
fullyQualifiedName: 'sample_superset.10',
id: '77a0ac8a-ca1a-4f21-9a37-406faa482008',
name: '10',
type: 'dashboard',
};
export const mockEntityDataWithoutNestingResponse: BasicEntityInfo[] = [ export const mockEntityDataWithoutNestingResponse: BasicEntityInfo[] = [
{ {
name: 'dim_address_task', name: 'dim_address_task',