mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 17:49:14 +00:00
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:
parent
f766ba872d
commit
b16460fba2
@ -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<Chart> {
|
||||
|
||||
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<Chart> {
|
||||
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<EntityReference> 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<Chart> {
|
||||
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 ChartUpdater(Chart chart, Chart updated, Operation operation) {
|
||||
super(chart, updated, operation);
|
||||
@ -105,6 +131,32 @@ public class ChartRepository extends EntityRepository<Chart> {
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ import org.openmetadata.service.util.ResultList;
|
||||
@Collection(name = "charts")
|
||||
public class ChartResource extends EntityResource<Chart, ChartRepository> {
|
||||
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<Chart, ChartRepository> {
|
||||
.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()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,6 +333,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
public static EntityReference METABASE_REFERENCE;
|
||||
public static EntityReference LOOKER_REFERENCE;
|
||||
public static List<String> CHART_REFERENCES;
|
||||
public static List<String> DASHBOARD_REFERENCES;
|
||||
|
||||
public static Database DATABASE;
|
||||
public static DatabaseSchema DATABASE_SCHEMA;
|
||||
|
||||
@ -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, CreateChart> {
|
||||
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<String, String> authHeaders) {
|
||||
assertReference(expected.getService(), patched.getService());
|
||||
@ -295,6 +394,8 @@ public class ChartResourceTest extends EntityResourceTest<Chart, CreateChart> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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<ChartSource[]>([]);
|
||||
|
||||
const isMounting = useRef(true);
|
||||
|
||||
const updateSuggestions = (options: Array<Option>) => {
|
||||
@ -127,6 +130,8 @@ const Suggestions = ({
|
||||
setDataProductSuggestions(
|
||||
filterOptionsByIndex(options, SearchIndex.DATA_PRODUCT)
|
||||
);
|
||||
|
||||
setChartSuggestions(filterOptionsByIndex(options, SearchIndex.CHART));
|
||||
};
|
||||
|
||||
const getSuggestionsForIndex = (
|
||||
@ -189,6 +194,10 @@ const Suggestions = ({
|
||||
suggestions: dataProductSuggestions,
|
||||
searchIndex: SearchIndex.DATA_PRODUCT,
|
||||
},
|
||||
{
|
||||
suggestions: chartSuggestions,
|
||||
searchIndex: SearchIndex.CHART,
|
||||
},
|
||||
...searchClassBase.getEntitiesSuggestions(options ?? []),
|
||||
].map(({ suggestions, searchIndex }) =>
|
||||
getSuggestionsForIndex(suggestions, searchIndex)
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -24,6 +24,7 @@ import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../enums/common.enum';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { ExplorePageTabs } from '../../../enums/Explore.enum';
|
||||
import { Tag } from '../../../generated/entity/classification/tag';
|
||||
import { Chart } from '../../../generated/entity/data/chart';
|
||||
import { Container } from '../../../generated/entity/data/container';
|
||||
import { Dashboard } from '../../../generated/entity/data/dashboard';
|
||||
import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel';
|
||||
@ -50,6 +51,7 @@ import searchClassBase from '../../../utils/SearchClassBase';
|
||||
import { stringToHTML } from '../../../utils/StringsUtils';
|
||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../common/Loader/Loader';
|
||||
import ChartSummary from './ChartSummary/ChartSummary.component';
|
||||
import ContainerSummary from './ContainerSummary/ContainerSummary.component';
|
||||
import DashboardSummary from './DashboardSummary/DashboardSummary.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:
|
||||
return (
|
||||
<PipelineSummary
|
||||
|
||||
@ -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', () => ({
|
||||
useParams: jest.fn().mockImplementation(() => ({ tab: 'table' })),
|
||||
Link: jest.fn().mockImplementation(({ children }) => <>{children}</>),
|
||||
@ -186,4 +194,24 @@ describe('EntitySummaryPanel component tests', () => {
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -91,6 +91,11 @@ export interface DataProductSource extends CommonSource {
|
||||
data_product_name: string;
|
||||
}
|
||||
|
||||
export interface ChartSource extends CommonSource {
|
||||
chart_id: string;
|
||||
chart_name: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
_index: string;
|
||||
_id: string;
|
||||
@ -105,7 +110,8 @@ export interface Option {
|
||||
GlossarySource &
|
||||
TagSource &
|
||||
SearchIndexSource &
|
||||
DataProductSource;
|
||||
DataProductSource &
|
||||
ChartSource;
|
||||
}
|
||||
|
||||
export type SearchSuggestions =
|
||||
@ -120,4 +126,5 @@ export type SearchSuggestions =
|
||||
| SearchIndexSource[]
|
||||
| StoredProcedureSearchSource[]
|
||||
| DashboardDataModelSearchSource[]
|
||||
| DataProductSource[];
|
||||
| DataProductSource[]
|
||||
| ChartSource[];
|
||||
|
||||
@ -18,4 +18,5 @@ export enum SummaryEntityType {
|
||||
MLFEATURE = 'mlFeature',
|
||||
SCHEMAFIELD = 'schemaField',
|
||||
FIELD = 'field',
|
||||
DASHBOARD = 'dashboard',
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ export enum ExplorePageTabs {
|
||||
PIPELINES = 'pipelines',
|
||||
MLMODELS = 'mlmodels',
|
||||
CONTAINERS = 'containers',
|
||||
CHARTS = 'charts',
|
||||
GLOSSARY = 'glossaries',
|
||||
TAG = 'tags',
|
||||
DATA_PRODUCT = 'dataProducts',
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Diagramm",
|
||||
"chart-entity": "{{entity}} Diagramm",
|
||||
"chart-plural": "Diagramme",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Status überprüfen",
|
||||
"children": "Kinder",
|
||||
"children-lowercase": "kinder",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Chart",
|
||||
"chart-entity": "Chart {{entity}}",
|
||||
"chart-plural": "Charts",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Check status",
|
||||
"children": "Children",
|
||||
"children-lowercase": "children",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Gráfico",
|
||||
"chart-entity": "Gráfico {{entity}}",
|
||||
"chart-plural": "Gráficos",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Verificar estado",
|
||||
"children": "Hijos",
|
||||
"children-lowercase": "hijos",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Graphique",
|
||||
"chart-entity": "Graphique {{entity}}",
|
||||
"chart-plural": "Graphiques",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Vérifier le Statut",
|
||||
"children": "Enfants",
|
||||
"children-lowercase": "enfants",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "תרשים",
|
||||
"chart-entity": "תרשים {{entity}}",
|
||||
"chart-plural": "תרשימים",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "בדוק סטטוס",
|
||||
"children": "ילדים",
|
||||
"children-lowercase": "ילדים",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "チャート",
|
||||
"chart-entity": "{{entity}}のチャート",
|
||||
"chart-plural": "チャート",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "ステータスチェック",
|
||||
"children": "Children",
|
||||
"children-lowercase": "children",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Chart",
|
||||
"chart-entity": "Chart {{entity}}",
|
||||
"chart-plural": "Charts",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Status controleren",
|
||||
"children": "Kinderen",
|
||||
"children-lowercase": "kinderen",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Gráfico",
|
||||
"chart-entity": "Gráfico {{entity}}",
|
||||
"chart-plural": "Gráficos",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Verificar status",
|
||||
"children": "Filhos",
|
||||
"children-lowercase": "filhos",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "Диаграмма",
|
||||
"chart-entity": "Диаграмма {{entity}}",
|
||||
"chart-plural": "Диаграммы",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "Проверить статус",
|
||||
"children": "Наследники",
|
||||
"children-lowercase": "наследники",
|
||||
|
||||
@ -142,6 +142,7 @@
|
||||
"chart": "图表",
|
||||
"chart-entity": "图表{{entity}}",
|
||||
"chart-plural": "图表",
|
||||
"chart-type": "Chart type",
|
||||
"check-status": "检查状态",
|
||||
"children": "子级",
|
||||
"children-lowercase": "子级",
|
||||
|
||||
102
openmetadata-ui/src/main/resources/ui/src/mocks/Chart.mock.ts
Normal file
102
openmetadata-ui/src/main/resources/ui/src/mocks/Chart.mock.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -26,11 +26,13 @@ import {
|
||||
mockEntityDataWithNestingResponse,
|
||||
mockEntityDataWithoutNesting,
|
||||
mockEntityDataWithoutNestingResponse,
|
||||
mockEntityReferenceDashboardData,
|
||||
mockGetHighlightOfListItemResponse,
|
||||
mockGetMapOfListHighlightsResponse,
|
||||
mockGetSummaryListItemTypeResponse,
|
||||
mockHighlights,
|
||||
mockInvalidDataResponse,
|
||||
mockLinkBasedSummaryTitleDashboardResponse,
|
||||
mockLinkBasedSummaryTitleResponse,
|
||||
mockListItemNameHighlight,
|
||||
mockTagFQNsForHighlight,
|
||||
@ -134,6 +136,14 @@ describe('EntitySummaryPanelUtils tests', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
} from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
|
||||
import { ICON_DIMENSION, NO_DATA_PLACEHOLDER } from '../constants/constants';
|
||||
import { SummaryListHighlightKeys } from '../constants/EntitySummaryPanelUtils.constant';
|
||||
import { EntityType } from '../enums/entity.enum';
|
||||
import { SummaryEntityType } from '../enums/EntitySummary.enum';
|
||||
import { Chart } from '../generated/entity/data/chart';
|
||||
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 { Column, TableConstraint } from '../generated/entity/data/table';
|
||||
import { Field } from '../generated/entity/data/topic';
|
||||
import { EntityReference } from '../generated/tests/testCase';
|
||||
import entityUtilClassBase from './EntityUtilClassBase';
|
||||
import { getEntityName } from './EntityUtils';
|
||||
import { stringToHTML } from './StringsUtils';
|
||||
|
||||
@ -65,6 +68,23 @@ export const getTitle = (
|
||||
: getEntityName(listItem) || NO_DATA_PLACEHOLDER;
|
||||
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 ? (
|
||||
<Link target="_blank" to={{ pathname: sourceUrl }}>
|
||||
<div className="d-flex items-center">
|
||||
|
||||
@ -12,11 +12,14 @@
|
||||
*/
|
||||
import { getEntityDetailsPath } from '../constants/constants';
|
||||
import { EntityTabs, EntityType } from '../enums/entity.enum';
|
||||
import { ExplorePageTabs } from '../enums/Explore.enum';
|
||||
import { TestSuite } from '../generated/tests/testCase';
|
||||
import { MOCK_CHART_DATA } from '../mocks/Chart.mock';
|
||||
import {
|
||||
columnSorter,
|
||||
getBreadcrumbForTestSuite,
|
||||
getEntityLinkFromType,
|
||||
getEntityOverview,
|
||||
highlightEntityNameAndDescription,
|
||||
} from './EntityUtils';
|
||||
import {
|
||||
@ -28,6 +31,7 @@ import {
|
||||
|
||||
jest.mock('../constants/constants', () => ({
|
||||
getEntityDetailsPath: jest.fn(),
|
||||
getServiceDetailsPath: jest.fn(),
|
||||
}));
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -68,6 +68,7 @@ import { SearchIndex } from '../enums/search.enum';
|
||||
import { ServiceCategory, ServiceCategoryPlural } from '../enums/service.enum';
|
||||
import { PrimaryTableDataTypes } from '../enums/table.enum';
|
||||
import { Classification } from '../generated/entity/classification/classification';
|
||||
import { Chart } from '../generated/entity/data/chart';
|
||||
import { Container } from '../generated/entity/data/container';
|
||||
import { Dashboard } from '../generated/entity/data/dashboard';
|
||||
import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel';
|
||||
@ -582,6 +583,67 @@ const getContainerOverview = (containerDetails: Container) => {
|
||||
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 {
|
||||
owner,
|
||||
@ -896,6 +958,9 @@ export const getEntityOverview = (
|
||||
case ExplorePageTabs.CONTAINERS: {
|
||||
return getContainerOverview(entityDetail as Container);
|
||||
}
|
||||
case ExplorePageTabs.CHARTS: {
|
||||
return getChartOverview(entityDetail as Chart);
|
||||
}
|
||||
|
||||
case ExplorePageTabs.DASHBOARD_DATA_MODEL: {
|
||||
return getDataModelOverview(entityDetail as DashboardDataModel);
|
||||
|
||||
@ -26,6 +26,8 @@ import {
|
||||
} from '../constants/AdvancedSearch.constants';
|
||||
import { EntityType } from '../enums/entity.enum';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { Chart } from '../generated/entity/data/chart';
|
||||
import { getEntityLinkFromType } from './EntityUtils';
|
||||
import { SearchClassBase } from './SearchClassBase';
|
||||
import { getTestSuiteDetailsPath, getTestSuiteFQN } from './TestSuiteUtils';
|
||||
|
||||
@ -34,6 +36,12 @@ jest.mock('./TestSuiteUtils', () => ({
|
||||
getTestSuiteFQN: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./EntityUtils', () => ({
|
||||
getEntityLinkFromType: jest.fn(),
|
||||
getEntityName: jest.fn(),
|
||||
getEntityBreadcrumbs: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SearchClassBase', () => {
|
||||
let searchClassBase: SearchClassBase;
|
||||
|
||||
@ -227,7 +235,59 @@ describe('SearchClassBase', () => {
|
||||
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({
|
||||
fullyQualifiedName: 'test.testSuite',
|
||||
entityType: EntityType.TABLE,
|
||||
|
||||
@ -57,6 +57,7 @@ import {
|
||||
import { EntityType } from '../enums/entity.enum';
|
||||
import { ExplorePageTabs } from '../enums/Explore.enum';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { Chart } from '../generated/entity/data/chart';
|
||||
import { TestSuite } from '../generated/tests/testCase';
|
||||
import { SearchSourceAlias } from '../interface/search.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) {
|
||||
return getEntityLinkFromType(
|
||||
entity.fullyQualifiedName,
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
import { EntityType } from '../enums/entity.enum';
|
||||
import { SearchIndex } from '../enums/search.enum';
|
||||
import { getEntityTypeFromSearchIndex } from './SearchUtils';
|
||||
import { getEntityTypeFromSearchIndex, getGroupLabel } from './SearchUtils';
|
||||
|
||||
describe('getEntityTypeFromSearchIndex', () => {
|
||||
it.each([
|
||||
@ -46,3 +46,79 @@ describe('getEntityTypeFromSearchIndex', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ import i18next from 'i18next';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
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 DataProductIcon } from '../assets/svg/ic-data-product.svg';
|
||||
import { ReactComponent as IconContainer } from '../assets/svg/ic-storage.svg';
|
||||
@ -159,6 +160,12 @@ export const getGroupLabel = (index: string) => {
|
||||
|
||||
break;
|
||||
|
||||
case SearchIndex.CHART:
|
||||
label = i18next.t('label.chart-plural');
|
||||
GroupIcon = IconChart;
|
||||
|
||||
break;
|
||||
|
||||
default: {
|
||||
const { label: indexLabel, GroupIcon: IndexIcon } =
|
||||
searchClassBase.getIndexGroupLabel(index);
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
State,
|
||||
TagSource,
|
||||
} from '../../generated/entity/data/table';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import { ReactComponent as IconExternalLink } from '../assets/svg/external-links.svg';
|
||||
|
||||
const { Text } = Typography;
|
||||
@ -54,6 +55,17 @@ export const mockLinkBasedSummaryTitleResponse = (
|
||||
</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 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[] = [
|
||||
{
|
||||
name: 'dim_address_task',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user