Add Chart and Dashboard Service entities

This commit is contained in:
Suresh Srinivas 2021-08-26 00:20:18 -07:00
parent 0e4a3b26f9
commit 3244960083
7 changed files with 779 additions and 40 deletions

View File

@ -17,13 +17,11 @@
package org.openmetadata.catalog.jdbi3;
import org.openmetadata.catalog.entity.data.Chart;
import org.openmetadata.catalog.entity.data.Database;
import org.openmetadata.catalog.entity.data.Table;
import org.openmetadata.catalog.entity.services.DashboardService;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.exception.EntityNotFoundException;
import org.openmetadata.catalog.jdbi3.TeamRepository.TeamDAO;
import org.openmetadata.catalog.jdbi3.UserRepository.UserDAO;
import org.openmetadata.catalog.resources.charts.ChartResource;
import org.openmetadata.catalog.resources.dashboards.DashboardResource;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.entity.data.Dashboard;
@ -59,6 +57,9 @@ public abstract class DashboardRepository {
private static final Fields DASHBOARD_PATCH_FIELDS = new Fields(DashboardResource.FIELD_LIST,
"owner,service,tags,charts");
public static String getFQN(EntityReference service, Dashboard dashboard) {
return (service.getName() + "." + dashboard.getName());
}
@CreateSqlObject
abstract DashboardDAO dashboardDAO();
@ -111,6 +112,12 @@ public abstract class DashboardRepository {
return dashboards;
}
@Transaction
public Dashboard getByName(String fqn, Fields fields) throws IOException {
Dashboard dashboard = EntityUtil.validate(fqn, dashboardDAO().findByFQN(fqn), Dashboard.class);
return setFields(dashboard, fields);
}
@Transaction
public Dashboard create(Dashboard dashboard, EntityReference service, EntityReference owner) throws IOException {
getService(service); // Validate service
@ -120,7 +127,8 @@ public abstract class DashboardRepository {
@Transaction
public PutResponse<Dashboard> createOrUpdate(Dashboard updatedDashboard, EntityReference service,
EntityReference newOwner) throws IOException {
String fqn = service.getName() + "." + updatedDashboard.getName();
getService(service); // Validate service
String fqn = getFQN(service, updatedDashboard);
Dashboard storedDashboard = JsonUtils.readValue(dashboardDAO().findByFQN(fqn), Dashboard.class);
if (storedDashboard == null) {
return new PutResponse<>(Status.CREATED, createInternal(updatedDashboard, service, newOwner));
@ -134,11 +142,12 @@ public abstract class DashboardRepository {
// Update owner relationship
setFields(storedDashboard, DASHBOARD_UPDATE_FIELDS); // First get the ownership information
updateRelationships(storedDashboard, updatedDashboard);
updateOwner(storedDashboard, storedDashboard.getOwner(), newOwner);
// Service can't be changed in update since service name is part of FQN and
// change to a different service will result in a different FQN and creation of a new database under the new service
storedDashboard.setService(service);
applyTags(updatedDashboard);
return new PutResponse<>(Response.Status.OK, storedDashboard);
}
@ -216,16 +225,24 @@ public abstract class DashboardRepository {
return dashboard;
}
private EntityReference getService(Dashboard dashboard) {
private EntityReference getService(Dashboard dashboard) throws IOException {
return dashboard == null ? null : getService(EntityUtil.getService(relationshipDAO(), dashboard.getId()));
}
private EntityReference getService(EntityReference service) {
// TODO What are the dashboard services?
private EntityReference getService(EntityReference service) throws IOException {
String id = service.getId().toString();
if (service.getType().equalsIgnoreCase(Entity.DASHBOARD_SERVICE)) {
DashboardService serviceInstance = EntityUtil.validate(id, dashboardServiceDAO().findById(id),
DashboardService.class);
service.setDescription(serviceInstance.getDescription());
service.setName(serviceInstance.getName());
} else {
throw new IllegalArgumentException(String.format("Invalid service type %s for the chart", service.getType()));
}
return service;
}
public void setService(Dashboard dashboard, EntityReference service) {
public void setService(Dashboard dashboard, EntityReference service) throws IOException {
if (service != null && dashboard != null) {
getService(service); // Populate service details
relationshipDAO().insert(service.getId().toString(), dashboard.getId().toString(), service.getType(),
@ -303,9 +320,11 @@ public abstract class DashboardRepository {
private void addRelationships(Dashboard dashboard) throws IOException {
// Add relationship from dashboard to chart
String dashboardId = dashboard.getId().toString();
for (EntityReference chart: dashboard.getCharts()) {
relationshipDAO().insert(dashboardId, chart.getId().toString(), Entity.DASHBOARD, Entity.CHART,
Relationship.CONTAINS.ordinal());
if (dashboard.getCharts() != null) {
for (EntityReference chart : dashboard.getCharts()) {
relationshipDAO().insert(dashboardId, chart.getId().toString(), Entity.DASHBOARD, Entity.CHART,
Relationship.CONTAINS.ordinal());
}
}
// Add owner relationship
EntityUtil.setOwner(relationshipDAO(), dashboard.getId(), Entity.DASHBOARD, dashboard.getOwner());
@ -314,14 +333,6 @@ public abstract class DashboardRepository {
applyTags(dashboard);
}
private void updateRelationships(Dashboard origDashboard, Dashboard updatedDashboard) throws IOException {
// Add owner relationship
origDashboard.setOwner(getOwner(origDashboard));
EntityUtil.updateOwner(relationshipDAO(), origDashboard.getOwner(), updatedDashboard.getOwner(),
origDashboard.getId(), Entity.TABLE);
applyTags(updatedDashboard);
}
private Dashboard validateDashboard(String id) throws IOException {
return EntityUtil.validate(id, dashboardDAO().findById(id), Dashboard.class);
}
@ -351,7 +362,7 @@ public abstract class DashboardRepository {
List<String> listBefore(@Bind("fqnPrefix") String fqnPrefix, @Bind("limit") int limit,
@Bind("before") String before);
@SqlQuery("SELECT json FROM chart_entity WHERE " +
@SqlQuery("SELECT json FROM dashboard_entity WHERE " +
"(fullyQualifiedName LIKE CONCAT(:fqnPrefix, '.%') OR :fqnPrefix IS NULL) AND " +
"fullyQualifiedName > :after " +
"ORDER BY fullyQualifiedName " +

View File

@ -92,7 +92,7 @@ public abstract class UsageRepository {
@Transaction
public EntityUsage getByName(String entityType, String fqn, String date, int days) throws IOException {
EntityReference ref = EntityUtil.getEntityReferenceByName(entityType, fqn, tableDAO(), databaseDAO(),
metricsDAO(), reportDAO(), topicDAO(), chartDAO());
metricsDAO(), reportDAO(), topicDAO(), chartDAO(), dashboardDAO());
List<UsageDetails> usageDetails = usageDAO().getUsageById(ref.getId().toString(), date, days - 1);
return new EntityUsage().withUsage(usageDetails).withEntity(ref);
}
@ -108,7 +108,7 @@ public abstract class UsageRepository {
@Transaction
public void createByName(String entityType, String fullyQualifiedName, DailyCount usage) throws IOException {
EntityReference ref = EntityUtil.getEntityReferenceByName(entityType, fullyQualifiedName, tableDAO(),
databaseDAO(), metricsDAO(), reportDAO(), topicDAO(), chartDAO());
databaseDAO(), metricsDAO(), reportDAO(), topicDAO(), chartDAO(), dashboardDAO());
addUsage(entityType, ref.getId().toString(), usage);
LOG.info("Usage successfully posted by name");
}

View File

@ -98,6 +98,7 @@ public class ChartResource {
EntityUtil.addHref(uriInfo, chart.getOwner());
EntityUtil.addHref(uriInfo, chart.getService());
EntityUtil.addHref(uriInfo, chart.getFollowers());
return chart;
}

View File

@ -26,6 +26,7 @@ import org.openmetadata.catalog.entity.data.Dashboard;
import org.openmetadata.catalog.jdbi3.DashboardRepository;
import org.openmetadata.catalog.resources.Collection;
import org.openmetadata.catalog.security.SecurityUtil;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.EntityUtil.Fields;
import org.openmetadata.catalog.util.RestUtil;
@ -77,20 +78,30 @@ import java.util.UUID;
@Consumes(MediaType.APPLICATION_JSON)
@Collection(name = "dashboards", repositoryClass = "org.openmetadata.catalog.jdbi3.DashboardRepository")
public class DashboardResource {
public static final String COLLECTION_PATH = "/v1/dashboards/";
public static final String DASHBOARD_COLLECTION_PATH = "v1/dashboards/";
private final List<String> attributes = RestUtil.getAttributes(Dashboard.class);
private final List<String> relationships = RestUtil.getAttributes(Dashboard.class);
private final DashboardRepository dao;
private final CatalogAuthorizer authorizer;
private static List<Dashboard> addHref(UriInfo uriInfo, List<Dashboard> dashboards) {
public static void addHref(UriInfo uriInfo, EntityReference ref) {
ref.withHref(RestUtil.getHref(uriInfo, DASHBOARD_COLLECTION_PATH, ref.getId()));
}
public static List<Dashboard> addHref(UriInfo uriInfo, List<Dashboard> dashboards) {
Optional.ofNullable(dashboards).orElse(Collections.emptyList()).forEach(i -> addHref(uriInfo, i));
return dashboards;
}
private static Dashboard addHref(UriInfo uriInfo, Dashboard dashboard) {
dashboard.setHref(RestUtil.getHref(uriInfo, COLLECTION_PATH, dashboard.getId()));
public static Dashboard addHref(UriInfo uriInfo, Dashboard dashboard) {
dashboard.setHref(RestUtil.getHref(uriInfo, DASHBOARD_COLLECTION_PATH, dashboard.getId()));
EntityUtil.addHref(uriInfo, dashboard.getOwner());
EntityUtil.addHref(uriInfo, dashboard.getService());
if (dashboard.getCharts() != null) {
EntityUtil.addHref(uriInfo, dashboard.getCharts());
}
EntityUtil.addHref(uriInfo, dashboard.getFollowers());
return dashboard;
}
@ -113,7 +124,7 @@ public class DashboardResource {
}
}
static final String FIELDS = "owner,service,followers,tags,usageSummary";
static final String FIELDS = "owner,service,charts,followers,tags,usageSummary";
public static final List<String> FIELD_LIST = Arrays.asList(FIELDS.replaceAll(" ", "")
.split(","));
@ -196,6 +207,27 @@ public class DashboardResource {
return addHref(uriInfo, dao.get(id, fields));
}
@GET
@Path("/name/{fqn}")
@Operation(summary = "Get a dashboard by name", tags = "dashboards",
description = "Get a dashboard by fully qualified name.",
responses = {
@ApiResponse(responseCode = "200", description = "The dashboard",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Chart.class))),
@ApiResponse(responseCode = "404", description = "Dashboard for instance {id} is not found")
})
public Dashboard getByName(@Context UriInfo uriInfo, @PathParam("fqn") String fqn,
@Context SecurityContext securityContext,
@Parameter(description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields") String fieldsParam) throws IOException {
Fields fields = new Fields(FIELD_LIST, fieldsParam);
Dashboard dashboard = dao.getByName(fqn, fields);
return addHref(uriInfo, dashboard);
}
@POST
@Operation(summary = "Create a dashboard", tags = "dashboards",
description = "Create a new dashboard.",
@ -209,9 +241,10 @@ public class DashboardResource {
@Valid CreateDashboard create) throws IOException {
SecurityUtil.checkAdminOrBotRole(authorizer, securityContext);
Dashboard dashboard = new Dashboard().withId(UUID.randomUUID()).withName(create.getName())
.withService(create.getService()).withCharts(create.getCharts())
.withDashboardUrl(create.getDashboardUrl()).withTags(create.getTags());
addHref(uriInfo, dao.create(dashboard, dashboard.getService(), dashboard.getOwner()));
.withDescription(create.getDescription()).withService(create.getService()).withCharts(create.getCharts())
.withDashboardUrl(create.getDashboardUrl()).withTags(create.getTags())
.withOwner(create.getOwner());
dashboard = addHref(uriInfo, dao.create(dashboard, dashboard.getService(), dashboard.getOwner()));
return Response.created(dashboard.getHref()).entity(dashboard).build();
}
@ -249,15 +282,17 @@ public class DashboardResource {
schema = @Schema(implementation = Dashboard.class))),
@ApiResponse(responseCode = "400", description = "Bad request")
})
public Response createOrUpdate(@Context UriInfo uriInfo, @Context SecurityContext securityContext,
@Valid Dashboard create) throws IOException {
public Response createOrUpdate(@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Valid CreateDashboard create) throws IOException {
Dashboard dashboard = new Dashboard().withId(UUID.randomUUID()).withName(create.getName())
.withService(create.getService()).withCharts(create.getCharts())
.withDashboardUrl(create.getDashboardUrl()).withTags(create.getTags());
addHref(uriInfo, dao.create(dashboard, dashboard.getService(), dashboard.getOwner()));
.withDescription(create.getDescription()).withService(create.getService()).withCharts(create.getCharts())
.withDashboardUrl(create.getDashboardUrl()).withTags(create.getTags())
.withOwner(create.getOwner());
PutResponse<Dashboard> response = dao.createOrUpdate(dashboard, dashboard.getService(), dashboard.getOwner());
addHref(uriInfo, response.getEntity());
return Response.status(response.getStatus()).entity(response.getEntity()).build();
dashboard = addHref(uriInfo, response.getEntity());
return Response.status(response.getStatus()).entity(dashboard).build();
}
@PUT

View File

@ -45,6 +45,7 @@ import org.openmetadata.catalog.jdbi3.TopicRepository.TopicDAO;
import org.openmetadata.catalog.jdbi3.UsageRepository.UsageDAO;
import org.openmetadata.catalog.jdbi3.UserRepository.UserDAO;
import org.openmetadata.catalog.resources.charts.ChartResource;
import org.openmetadata.catalog.resources.dashboards.DashboardResource;
import org.openmetadata.catalog.resources.databases.DatabaseResource;
import org.openmetadata.catalog.resources.databases.TableResource;
import org.openmetadata.catalog.resources.feeds.MessageParser.EntityLink;
@ -135,6 +136,9 @@ public final class EntityUtil {
case Entity.CHART:
ChartResource.addHref(uriInfo, ref);
break;
case Entity.DASHBOARD:
DashboardResource.addHref(uriInfo, ref);
break;
case Entity.MESSAGING_SERVICE:
MessagingServiceResource.addHref(uriInfo, ref);
break;
@ -273,7 +277,8 @@ public final class EntityUtil {
public static EntityReference getEntityReferenceByName(String entity, String fqn, TableDAO tableDAO,
DatabaseDAO databaseDAO, MetricsDAO metricsDAO,
ReportDAO reportDAO, TopicDAO topicDAO, ChartDAO chartDAO)
ReportDAO reportDAO, TopicDAO topicDAO, ChartDAO chartDAO,
DashboardDAO dashboardDAO)
throws IOException {
if (entity.equalsIgnoreCase(Entity.TABLE)) {
Table instance = EntityUtil.validate(fqn, tableDAO.findByFQN(fqn), Table.class);
@ -299,6 +304,10 @@ public final class EntityUtil {
Chart instance = EntityUtil.validate(fqn, chartDAO.findByFQN(fqn), Chart.class);
return new EntityReference().withId(instance.getId()).withName(instance.getName()).withType(Entity.CHART)
.withDescription(instance.getDescription());
} else if (entity.equalsIgnoreCase(Entity.DASHBOARD)) {
Dashboard instance = EntityUtil.validate(fqn, dashboardDAO.findByFQN(fqn), Dashboard.class);
return new EntityReference().withId(instance.getId()).withName(instance.getName()).withType(Entity.DASHBOARD)
.withDescription(instance.getDescription());
}
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityNotFound(entity, fqn));
}
@ -331,6 +340,9 @@ public final class EntityUtil {
} else if (clazz.toString().toLowerCase().endsWith(Entity.CHART.toLowerCase())) {
Chart instance = (Chart) entity;
return getEntityReference(instance);
} else if (clazz.toString().toLowerCase().endsWith(Entity.DASHBOARD.toLowerCase())) {
Dashboard instance = (Dashboard) entity;
return getEntityReference(instance);
} else if (clazz.toString().toLowerCase().endsWith(Entity.MESSAGING_SERVICE.toLowerCase())) {
MessagingService instance = (MessagingService) entity;
return getEntityReference(instance);

View File

@ -0,0 +1,668 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.openmetadata.catalog.resources.dashboards;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.openmetadata.catalog.CatalogApplicationTest;
import org.openmetadata.catalog.Entity;
import org.openmetadata.catalog.api.data.CreateDashboard;
import org.openmetadata.catalog.api.services.CreateDashboardService;
import org.openmetadata.catalog.api.services.CreateDashboardService.DashboardServiceType;
import org.openmetadata.catalog.entity.data.Dashboard;
import org.openmetadata.catalog.entity.services.DashboardService;
import org.openmetadata.catalog.entity.teams.Team;
import org.openmetadata.catalog.entity.teams.User;
import org.openmetadata.catalog.exception.CatalogExceptionMessage;
import org.openmetadata.catalog.resources.dashboards.DashboardResource.DashboardList;
import org.openmetadata.catalog.resources.services.DashboardServiceResourceTest;
import org.openmetadata.catalog.resources.teams.TeamResourceTest;
import org.openmetadata.catalog.resources.teams.UserResourceTest;
import org.openmetadata.catalog.type.EntityReference;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.TestUtils;
import org.openmetadata.common.utils.JsonSchemaUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.json.JsonPatch;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response.Status;
import java.util.Map;
import java.util.UUID;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.CONFLICT;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.entityNotFound;
import static org.openmetadata.catalog.exception.CatalogExceptionMessage.readOnlyAttribute;
import static org.openmetadata.catalog.util.TestUtils.adminAuthHeaders;
import static org.openmetadata.catalog.util.TestUtils.assertEntityPagination;
import static org.openmetadata.catalog.util.TestUtils.assertResponse;
import static org.openmetadata.catalog.util.TestUtils.authHeaders;
public class DashboardResourceTest extends CatalogApplicationTest {
private static final Logger LOG = LoggerFactory.getLogger(DashboardResourceTest.class);
public static User USER1;
public static EntityReference USER_OWNER1;
public static Team TEAM1;
public static EntityReference TEAM_OWNER1;
public static EntityReference SUPERSET_REFERENCE;
public static EntityReference LOOKER_REFERENCE;
@BeforeAll
public static void setup(TestInfo test) throws HttpResponseException {
USER1 = UserResourceTest.createUser(UserResourceTest.create(test), authHeaders("test@open-metadata.org"));
USER_OWNER1 = new EntityReference().withId(USER1.getId()).withType("user");
TEAM1 = TeamResourceTest.createTeam(TeamResourceTest.create(test), adminAuthHeaders());
TEAM_OWNER1 = new EntityReference().withId(TEAM1.getId()).withType("team");
CreateDashboardService createService = new CreateDashboardService().withName("superset")
.withServiceType(DashboardServiceType.Superset).withDashboardUrl(TestUtils.DASHBOARD_URL);
DashboardService service = DashboardServiceResourceTest.createService(createService, adminAuthHeaders());
SUPERSET_REFERENCE = EntityUtil.getEntityReference(service);
createService.withName("looker").withServiceType(DashboardServiceType.Looker);
service = DashboardServiceResourceTest.createService(createService, adminAuthHeaders());
LOOKER_REFERENCE = EntityUtil.getEntityReference(service);
}
@Test
public void post_dashboardWithLongName_400_badRequest(TestInfo test) {
// Create dashboard with mandatory name field empty
CreateDashboard create = create(test).withName(TestUtils.LONG_ENTITY_NAME);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "[name size must be between 1 and 64]");
}
@Test
public void post_DashboardWithoutName_400_badRequest(TestInfo test) {
// Create Dashboard with mandatory name field empty
CreateDashboard create = create(test).withName("");
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "[name size must be between 1 and 64]");
}
@Test
public void post_DashboardAlreadyExists_409_conflict(TestInfo test) throws HttpResponseException {
CreateDashboard create = create(test);
createDashboard(create, adminAuthHeaders());
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
assertResponse(exception, CONFLICT, CatalogExceptionMessage.ENTITY_ALREADY_EXISTS);
}
@Test
public void post_validDashboards_as_admin_200_OK(TestInfo test) throws HttpResponseException {
// Create team with different optional fields
CreateDashboard create = create(test);
createAndCheckDashboard(create, adminAuthHeaders());
create.withName(getDashboardName(test, 1)).withDescription("description");
createAndCheckDashboard(create, adminAuthHeaders());
}
@Test
public void post_DashboardWithUserOwner_200_ok(TestInfo test) throws HttpResponseException {
createAndCheckDashboard(create(test).withOwner(USER_OWNER1), adminAuthHeaders());
}
@Test
public void post_DashboardWithTeamOwner_200_ok(TestInfo test) throws HttpResponseException {
createAndCheckDashboard(create(test).withOwner(TEAM_OWNER1), adminAuthHeaders());
}
@Test
public void post_Dashboard_as_non_admin_401(TestInfo test) {
CreateDashboard create = create(test);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, authHeaders("test@open-metadata.org")));
assertResponse(exception, FORBIDDEN, "Principal: CatalogPrincipal{name='test'} is not admin");
}
@Test
public void post_DashboardWithoutRequiredService_4xx(TestInfo test) {
CreateDashboard create = create(test).withService(null);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
TestUtils.assertResponseContains(exception, BAD_REQUEST, "service must not be null");
}
@Test
public void post_DashboardWithInvalidOwnerType_4xx(TestInfo test) {
EntityReference owner = new EntityReference().withId(TEAM1.getId()); /* No owner type is set */
CreateDashboard create = create(test).withOwner(owner);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
TestUtils.assertResponseContains(exception, BAD_REQUEST, "type must not be null");
}
@Test
public void post_DashboardWithNonExistentOwner_4xx(TestInfo test) {
EntityReference owner = new EntityReference().withId(TestUtils.NON_EXISTENT_ENTITY).withType("user");
CreateDashboard create = create(test).withOwner(owner);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
createDashboard(create, adminAuthHeaders()));
assertResponse(exception, NOT_FOUND, entityNotFound("User", TestUtils.NON_EXISTENT_ENTITY));
}
@Test
public void post_DashboardWithDifferentService_200_ok(TestInfo test) throws HttpResponseException {
EntityReference[] differentServices = {SUPERSET_REFERENCE, LOOKER_REFERENCE};
// Create Dashboard for each service and test APIs
for (EntityReference service : differentServices) {
createAndCheckDashboard(create(test).withService(service), adminAuthHeaders());
// List Dashboards by filtering on service name and ensure right Dashboards are returned in the response
DashboardList list = listDashboards("service", service.getName(), adminAuthHeaders());
for (Dashboard db : list.getData()) {
assertEquals(service.getName(), db.getService().getName());
}
}
}
@Test
public void get_DashboardListWithInvalidLimitOffset_4xx() {
// Limit must be >= 1 and <= 1000,000
HttpResponseException exception = assertThrows(HttpResponseException.class, ()
-> listDashboards(null, null, -1, null, null, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "[query param limit must be greater than or equal to 1]");
exception = assertThrows(HttpResponseException.class, ()
-> listDashboards(null, null, 0, null, null, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "[query param limit must be greater than or equal to 1]");
exception = assertThrows(HttpResponseException.class, ()
-> listDashboards(null, null, 1000001, null, null, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "[query param limit must be less than or equal to 1000000]");
}
@Test
public void get_DashboardListWithInvalidPaginationCursors_4xx() {
// Passing both before and after cursors is invalid
HttpResponseException exception = assertThrows(HttpResponseException.class, ()
-> listDashboards(null, null, 1, "", "", adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, "Only one of before or after query parameter allowed");
}
@Test
public void get_DashboardListWithValidLimitOffset_4xx(TestInfo test) throws HttpResponseException {
// Create a large number of Dashboards
int maxDashboards = 40;
for (int i = 0; i < maxDashboards; i++) {
createDashboard(create(test, i), adminAuthHeaders());
}
// List all Dashboards
DashboardList allDashboards = listDashboards(null, null, 1000000, null,
null, adminAuthHeaders());
int totalRecords = allDashboards.getData().size();
printDashboards(allDashboards);
// List limit number Dashboards at a time at various offsets and ensure right results are returned
for (int limit = 1; limit < maxDashboards; limit++) {
String after = null;
String before;
int pageCount = 0;
int indexInAllDashboards = 0;
DashboardList forwardPage;
DashboardList backwardPage;
do { // For each limit (or page size) - forward scroll till the end
LOG.info("Limit {} forward scrollCount {} afterCursor {}", limit, pageCount, after);
forwardPage = listDashboards(null, null, limit, null, after, adminAuthHeaders());
printDashboards(forwardPage);
after = forwardPage.getPaging().getAfter();
before = forwardPage.getPaging().getBefore();
assertEntityPagination(allDashboards.getData(), forwardPage, limit, indexInAllDashboards);
if (pageCount == 0) { // CASE 0 - First page is being returned. There is no before cursor
assertNull(before);
} else {
// Make sure scrolling back based on before cursor returns the correct result
backwardPage = listDashboards(null, null, limit, before, null, adminAuthHeaders());
assertEntityPagination(allDashboards.getData(), backwardPage, limit, (indexInAllDashboards - limit));
}
indexInAllDashboards += forwardPage.getData().size();
pageCount++;
} while (after != null);
// We have now reached the last page - test backward scroll till the beginning
pageCount = 0;
indexInAllDashboards = totalRecords - limit - forwardPage.getData().size();
do {
LOG.info("Limit {} backward scrollCount {} beforeCursor {}", limit, pageCount, before);
forwardPage = listDashboards(null, null, limit, before, null, adminAuthHeaders());
printDashboards(forwardPage);
before = forwardPage.getPaging().getBefore();
assertEntityPagination(allDashboards.getData(), forwardPage, limit, indexInAllDashboards);
pageCount++;
indexInAllDashboards -= forwardPage.getData().size();
} while (before != null);
}
}
private void printDashboards(DashboardList list) {
list.getData().forEach(Dashboard -> LOG.info("DB {}", Dashboard.getFullyQualifiedName()));
LOG.info("before {} after {} ", list.getPaging().getBefore(), list.getPaging().getAfter());
}
@Test
public void put_DashboardUpdateWithNoChange_200(TestInfo test) throws HttpResponseException {
// Create a Dashboard with POST
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withOwner(USER_OWNER1);
createAndCheckDashboard(request, adminAuthHeaders());
// Update Dashboard two times successfully with PUT requests
updateAndCheckDashboard(request, OK, adminAuthHeaders());
updateAndCheckDashboard(request, OK, adminAuthHeaders());
}
@Test
public void put_DashboardCreate_200(TestInfo test) throws HttpResponseException {
// Create a new Dashboard with put
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withOwner(USER_OWNER1);
updateAndCheckDashboard(request.withName(test.getDisplayName()).withDescription(null), CREATED, adminAuthHeaders());
}
@Test
public void put_DashboardCreate_as_owner_200(TestInfo test) throws HttpResponseException {
// Create a new Dashboard with put
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withOwner(USER_OWNER1);
// Add Owner as admin
createAndCheckDashboard(request, adminAuthHeaders());
//Update the table as Owner
updateAndCheckDashboard(request.withName(test.getDisplayName()).withDescription(null),
CREATED, authHeaders(USER1.getEmail()));
}
@Test
public void put_DashboardNullDescriptionUpdate_200(TestInfo test) throws HttpResponseException {
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withDescription(null);
createAndCheckDashboard(request, adminAuthHeaders());
// Update null description with a new description
Dashboard db = updateAndCheckDashboard(request.withDescription("newDescription"), OK, adminAuthHeaders());
assertEquals("newDescription", db.getDescription());
}
@Test
public void put_DashboardEmptyDescriptionUpdate_200(TestInfo test) throws HttpResponseException {
// Create table with empty description
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withDescription("");
createAndCheckDashboard(request, adminAuthHeaders());
// Update empty description with a new description
Dashboard db = updateAndCheckDashboard(request.withDescription("newDescription"), OK, adminAuthHeaders());
assertEquals("newDescription", db.getDescription());
}
@Test
public void put_DashboardNonEmptyDescriptionUpdate_200(TestInfo test) throws HttpResponseException {
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withDescription("description");
createAndCheckDashboard(request, adminAuthHeaders());
// Updating description is ignored when backend already has description
Dashboard db = updateDashboard(request.withDescription("newDescription"), OK, adminAuthHeaders());
assertEquals("description", db.getDescription());
}
@Test
public void put_DashboardUpdateOwner_200(TestInfo test) throws HttpResponseException {
CreateDashboard request = create(test).withService(SUPERSET_REFERENCE).withDescription("");
createAndCheckDashboard(request, adminAuthHeaders());
// Change ownership from USER_OWNER1 to TEAM_OWNER1
updateAndCheckDashboard(request.withOwner(TEAM_OWNER1), OK, adminAuthHeaders());
// Remove ownership
Dashboard db = updateAndCheckDashboard(request.withOwner(null), OK, adminAuthHeaders());
assertNull(db.getOwner());
}
@Test
public void get_nonExistentDashboard_404_notFound() {
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
getDashboard(TestUtils.NON_EXISTENT_ENTITY, adminAuthHeaders()));
assertResponse(exception, NOT_FOUND,
entityNotFound(Entity.DASHBOARD, TestUtils.NON_EXISTENT_ENTITY));
}
@Test
public void get_DashboardWithDifferentFields_200_OK(TestInfo test) throws HttpResponseException {
CreateDashboard create = create(test).withDescription("description").withOwner(USER_OWNER1)
.withService(SUPERSET_REFERENCE);
Dashboard Dashboard = createAndCheckDashboard(create, adminAuthHeaders());
validateGetWithDifferentFields(Dashboard, false);
}
@Test
public void get_DashboardByNameWithDifferentFields_200_OK(TestInfo test) throws HttpResponseException {
CreateDashboard create = create(test).withDescription("description").withOwner(USER_OWNER1)
.withService(SUPERSET_REFERENCE);
Dashboard Dashboard = createAndCheckDashboard(create, adminAuthHeaders());
validateGetWithDifferentFields(Dashboard, true);
}
@Test
public void patch_DashboardAttributes_200_ok(TestInfo test) throws HttpResponseException, JsonProcessingException {
// Create Dashboard without description, owner
Dashboard Dashboard = createDashboard(create(test), adminAuthHeaders());
assertNull(Dashboard.getDescription());
assertNull(Dashboard.getOwner());
assertNotNull(Dashboard.getService());
Dashboard = getDashboard(Dashboard.getId(), "service,owner,usageSummary", adminAuthHeaders());
Dashboard.getService().setHref(null); // href is readonly and not patchable
// Add description, owner when previously they were null
Dashboard = patchDashboardAttributesAndCheck(Dashboard, "description", TEAM_OWNER1, adminAuthHeaders());
Dashboard.setOwner(TEAM_OWNER1); // Get rid of href and name returned in the response for owner
Dashboard.setService(SUPERSET_REFERENCE); // Get rid of href and name returned in the response for service
// Replace description, tier, owner
Dashboard = patchDashboardAttributesAndCheck(Dashboard, "description1", USER_OWNER1, adminAuthHeaders());
Dashboard.setOwner(USER_OWNER1); // Get rid of href and name returned in the response for owner
Dashboard.setService(SUPERSET_REFERENCE); // Get rid of href and name returned in the response for service
// Remove description, tier, owner
patchDashboardAttributesAndCheck(Dashboard, null, null, adminAuthHeaders());
}
@Test
public void patch_DashboardIDChange_400(TestInfo test) throws HttpResponseException, JsonProcessingException {
// Ensure Dashboard ID can't be changed using patch
Dashboard Dashboard = createDashboard(create(test), adminAuthHeaders());
UUID DashboardId = Dashboard.getId();
String DashboardJson = JsonUtils.pojoToJson(Dashboard);
Dashboard.setId(UUID.randomUUID());
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardId, DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "id"));
// ID can't be deleted
Dashboard.setId(null);
exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardId, DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "id"));
}
@Test
public void patch_DashboardNameChange_400(TestInfo test) throws HttpResponseException, JsonProcessingException {
// Ensure Dashboard name can't be changed using patch
Dashboard Dashboard = createDashboard(create(test), adminAuthHeaders());
String DashboardJson = JsonUtils.pojoToJson(Dashboard);
Dashboard.setName("newName");
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "name"));
// Name can't be removed
Dashboard.setName(null);
exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "name"));
}
@Test
public void patch_DashboardRemoveService_400(TestInfo test) throws HttpResponseException, JsonProcessingException {
// Ensure service corresponding to Dashboard can't be changed by patch operation
Dashboard Dashboard = createDashboard(create(test), adminAuthHeaders());
Dashboard.getService().setHref(null); // Remove href from returned response as it is read-only field
String DashboardJson = JsonUtils.pojoToJson(Dashboard);
Dashboard.setService(LOOKER_REFERENCE);
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "service"));
// Service relationship can't be removed
Dashboard.setService(null);
exception = assertThrows(HttpResponseException.class, () ->
patchDashboard(DashboardJson, Dashboard, adminAuthHeaders()));
assertResponse(exception, BAD_REQUEST, readOnlyAttribute(Entity.DASHBOARD, "service"));
}
// TODO listing tables test:1
// TODO Change service?
@Test
public void delete_emptyDashboard_200_ok(TestInfo test) throws HttpResponseException {
Dashboard Dashboard = createDashboard(create(test), adminAuthHeaders());
deleteDashboard(Dashboard.getId(), adminAuthHeaders());
}
@Test
public void delete_nonEmptyDashboard_4xx() {
// TODO
}
@Test
public void delete_nonExistentDashboard_404() {
HttpResponseException exception = assertThrows(HttpResponseException.class, () ->
deleteDashboard(TestUtils.NON_EXISTENT_ENTITY, adminAuthHeaders()));
assertResponse(exception, NOT_FOUND, entityNotFound(Entity.DASHBOARD, TestUtils.NON_EXISTENT_ENTITY));
}
public static Dashboard createAndCheckDashboard(CreateDashboard create,
Map<String, String> authHeaders) throws HttpResponseException {
Dashboard Dashboard = createDashboard(create, authHeaders);
validateDashboard(Dashboard, create.getDescription(), create.getOwner(), create.getService());
return getAndValidate(Dashboard.getId(), create, authHeaders);
}
public static Dashboard updateAndCheckDashboard(CreateDashboard create,
Status status,
Map<String, String> authHeaders) throws HttpResponseException {
Dashboard updatedDashboard = updateDashboard(create, status, authHeaders);
validateDashboard(updatedDashboard, create.getDescription(), create.getOwner(), create.getService());
// GET the newly updated Dashboard and validate
return getAndValidate(updatedDashboard.getId(), create, authHeaders);
}
// Make sure in GET operations the returned Dashboard has all the required information passed during creation
public static Dashboard getAndValidate(UUID DashboardId,
CreateDashboard create,
Map<String, String> authHeaders) throws HttpResponseException {
// GET the newly created Dashboard by ID and validate
Dashboard Dashboard = getDashboard(DashboardId, "service,owner", authHeaders);
validateDashboard(Dashboard, create.getDescription(), create.getOwner(), create.getService());
// GET the newly created Dashboard by name and validate
String fqn = Dashboard.getFullyQualifiedName();
Dashboard = getDashboardByName(fqn, "service,owner", authHeaders);
return validateDashboard(Dashboard, create.getDescription(), create.getOwner(), create.getService());
}
public static Dashboard updateDashboard(CreateDashboard create,
Status status,
Map<String, String> authHeaders) throws HttpResponseException {
return TestUtils.put(getResource("dashboards"),
create, Dashboard.class, status, authHeaders);
}
public static Dashboard createDashboard(CreateDashboard create,
Map<String, String> authHeaders) throws HttpResponseException {
return TestUtils.post(getResource("dashboards"), create, Dashboard.class, authHeaders);
}
/** Validate returned fields GET .../dashboards/{id}?fields="..." or GET .../dashboards/name/{fqn}?fields="..." */
private void validateGetWithDifferentFields(Dashboard Dashboard, boolean byName) throws HttpResponseException {
// .../Dashboards?fields=owner
String fields = "owner";
Dashboard = byName ? getDashboardByName(Dashboard.getFullyQualifiedName(), fields, adminAuthHeaders()) :
getDashboard(Dashboard.getId(), fields, adminAuthHeaders());
assertNotNull(Dashboard.getOwner());
assertNull(Dashboard.getService());
assertNull(Dashboard.getCharts());
// .../Dashboards?fields=owner,service
fields = "owner,service";
Dashboard = byName ? getDashboardByName(Dashboard.getFullyQualifiedName(), fields, adminAuthHeaders()) :
getDashboard(Dashboard.getId(), fields, adminAuthHeaders());
assertNotNull(Dashboard.getOwner());
assertNotNull(Dashboard.getService());
assertNull(Dashboard.getCharts());
// .../Dashboards?fields=owner,service,tables
fields = "owner,service,charts,usageSummary";
Dashboard = byName ? getDashboardByName(Dashboard.getFullyQualifiedName(), fields, adminAuthHeaders()) :
getDashboard(Dashboard.getId(), fields, adminAuthHeaders());
assertNotNull(Dashboard.getOwner());
assertNotNull(Dashboard.getService());
assertNotNull(Dashboard.getCharts());
TestUtils.validateEntityReference(Dashboard.getCharts());
assertNotNull(Dashboard.getUsageSummary());
}
private static Dashboard validateDashboard(Dashboard dashboard, String expectedDescription,
EntityReference expectedOwner, EntityReference expectedService) {
assertNotNull(dashboard.getId());
assertNotNull(dashboard.getHref());
assertEquals(expectedDescription, dashboard.getDescription());
// Validate owner
if (expectedOwner != null) {
TestUtils.validateEntityReference(dashboard.getOwner());
assertEquals(expectedOwner.getId(), dashboard.getOwner().getId());
assertEquals(expectedOwner.getType(), dashboard.getOwner().getType());
assertNotNull(dashboard.getOwner().getHref());
}
// Validate service
if (expectedService != null) {
TestUtils.validateEntityReference(dashboard.getService());
assertEquals(expectedService.getId(), dashboard.getService().getId());
assertEquals(expectedService.getType(), dashboard.getService().getType());
}
return dashboard;
}
private Dashboard patchDashboardAttributesAndCheck(Dashboard dashboard, String newDescription,
EntityReference newOwner, Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
String DashboardJson = JsonUtils.pojoToJson(dashboard);
// Update the table attributes
dashboard.setDescription(newDescription);
dashboard.setOwner(newOwner);
// Validate information returned in patch response has the updates
Dashboard updatedDashboard = patchDashboard(DashboardJson, dashboard, authHeaders);
validateDashboard(updatedDashboard, dashboard.getDescription(), newOwner, null);
// GET the table and Validate information returned
Dashboard getDashboard = getDashboard(dashboard.getId(), "service,owner", authHeaders);
validateDashboard(getDashboard, dashboard.getDescription(), newOwner, null);
return updatedDashboard;
}
private Dashboard patchDashboard(UUID dashboardId, String originalJson, Dashboard updatedDashboard,
Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
String updateDashboardJson = JsonUtils.pojoToJson(updatedDashboard);
JsonPatch patch = JsonSchemaUtil.getJsonPatch(originalJson, updateDashboardJson);
return TestUtils.patch(getResource("dashboards/" + dashboardId), patch, Dashboard.class, authHeaders);
}
private Dashboard patchDashboard(String originalJson,
Dashboard updatedDashboard,
Map<String, String> authHeaders)
throws JsonProcessingException, HttpResponseException {
return patchDashboard(updatedDashboard.getId(), originalJson, updatedDashboard, authHeaders);
}
public static void getDashboard(UUID id, Map<String, String> authHeaders) throws HttpResponseException {
getDashboard(id, null, authHeaders);
}
public static Dashboard getDashboard(UUID id, String fields, Map<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = getResource("dashboards/" + id);
target = fields != null ? target.queryParam("fields", fields): target;
return TestUtils.get(target, Dashboard.class, authHeaders);
}
public static Dashboard getDashboardByName(String fqn, String fields, Map<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = getResource("dashboards/name/" + fqn);
target = fields != null ? target.queryParam("fields", fields): target;
return TestUtils.get(target, Dashboard.class, authHeaders);
}
public static DashboardList listDashboards(String fields, String serviceParam, Map<String, String> authHeaders)
throws HttpResponseException {
return listDashboards(fields, serviceParam, null, null, null, authHeaders);
}
public static DashboardList listDashboards(String fields, String serviceParam, Integer limitParam,
String before, String after, Map<String, String> authHeaders)
throws HttpResponseException {
WebTarget target = getResource("dashboards");
target = fields != null ? target.queryParam("fields", fields): target;
target = serviceParam != null ? target.queryParam("service", serviceParam): target;
target = limitParam != null ? target.queryParam("limit", limitParam): target;
target = before != null ? target.queryParam("before", before) : target;
target = after != null ? target.queryParam("after", after) : target;
return TestUtils.get(target, DashboardList.class, authHeaders);
}
private void deleteDashboard(UUID id, Map<String, String> authHeaders) throws HttpResponseException {
TestUtils.delete(getResource("dashboards/" + id), authHeaders);
// Ensure deleted Dashboard does not exist
HttpResponseException exception = assertThrows(HttpResponseException.class, () -> getDashboard(id, authHeaders));
assertResponse(exception, NOT_FOUND, entityNotFound(Entity.DASHBOARD, id));
}
public static String getDashboardName(TestInfo test) {
return String.format("dash_%s", test.getDisplayName());
}
public static String getDashboardName(TestInfo test, int index) {
return String.format("dash%d_%s", index, test.getDisplayName());
}
public static CreateDashboard create(TestInfo test) {
return new CreateDashboard().withName(getDashboardName(test)).withService(SUPERSET_REFERENCE);
}
public static CreateDashboard create(TestInfo test, int index) {
return new CreateDashboard().withName(getDashboardName(test, index)).withService(SUPERSET_REFERENCE);
}
}

View File

@ -49,6 +49,8 @@ public final class TestUtils {
public static final String LONG_ENTITY_NAME = "012345678901234567890123456789012345678901234567890123456789012345";
public static final UUID NON_EXISTENT_ENTITY = UUID.randomUUID();
public static JdbcInfo JDBC_INFO;
public static URI DASHBOARD_URL;
static {
try {
JDBC_INFO = new JdbcInfo().withConnectionUrl(new URI("jdbc:service://")).withDriverClass("driverClass");
@ -58,6 +60,15 @@ public final class TestUtils {
}
}
static {
try {
DASHBOARD_URL = new URI("http://localhost:8088");
} catch (URISyntaxException e) {
DASHBOARD_URL = null;
e.printStackTrace();
}
}
private TestUtils() {
}
@ -169,7 +180,8 @@ public final class TestUtils {
assertNotNull(ref.getName());
assertNotNull(ref.getType());
// Ensure data entities use fully qualified name
if (List.of("table", "database", "metrics", "dashboard", "pipeline", "report", "topic").contains(ref.getName())) {
if (List.of("table", "database", "metrics", "dashboard", "pipeline", "report", "topic", "chart")
.contains(ref.getName())) {
ref.getName().contains("."); // FullyQualifiedName has "." as separator
}
}