diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java index a2ebb9b2c39..95d1a7ac2ab 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java @@ -589,7 +589,7 @@ public final class Entity { public static void restoreEntity(String updatedBy, String entityType, UUID entityId) { EntityRepository dao = getEntityRepository(entityType); - dao.restoreEntity(updatedBy, entityType, entityId); + dao.restoreEntity(updatedBy, entityId); } public static String getEntityTypeFromClass(Class clz) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DashboardRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DashboardRepository.java index 6a2a85d0e82..7986887f698 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DashboardRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DashboardRepository.java @@ -16,6 +16,7 @@ package org.openmetadata.service.jdbi3; import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; import static org.openmetadata.schema.type.Include.ALL; import static org.openmetadata.schema.type.Include.NON_DELETED; +import static org.openmetadata.service.Entity.CHART; import static org.openmetadata.service.Entity.DASHBOARD; import static org.openmetadata.service.Entity.FIELD_DESCRIPTION; import static org.openmetadata.service.Entity.FIELD_TAGS; @@ -25,7 +26,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.sqlobject.transaction.Transaction; import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.entity.data.Chart; @@ -46,6 +50,7 @@ import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.FullyQualifiedName; +@Slf4j public class DashboardRepository extends EntityRepository { private static final String DASHBOARD_UPDATE_FIELDS = "charts,dataModels"; private static final String DASHBOARD_PATCH_FIELDS = "charts,dataModels"; @@ -197,6 +202,144 @@ public class DashboardRepository extends EntityRepository { fields.contains("usageSummary") ? dashboard.getUsageSummary() : null); } + // Override soft delete behavior to handle charts through HAS relation. + @Transaction + @Override + protected void deleteChildren( + UUID dashboardId, boolean recursive, boolean hardDelete, String updatedBy) { + super.deleteChildren(dashboardId, recursive, hardDelete, updatedBy); + + // Load all charts linked to this dashboard + List chartRecords = + daoCollection + .relationshipDAO() + .findTo(dashboardId, DASHBOARD, Relationship.HAS.ordinal(), CHART); + if (chartRecords.isEmpty()) { + return; + } + + // Batch-load dashboard relationships for these charts + List dashboardRelationships = + daoCollection + .relationshipDAO() + .findFromBatch( + chartRecords.stream() + .map(record -> record.getId().toString()) + .distinct() + .collect(Collectors.toList()), + Relationship.HAS.ordinal(), + DASHBOARD); + + Set nonDeletedDashboards = + daoCollection + .dashboardDAO() + .findEntitiesByIds( + dashboardRelationships.stream() + .map(rel -> UUID.fromString(rel.getFromId())) + .distinct() + .collect(Collectors.toList()), + Include.NON_DELETED) + .stream() + .map(Dashboard::getId) + .filter(id -> !id.equals(dashboardId)) // (excluding the current dashboard + .collect(Collectors.toSet()); + + // For deletion: get charts whose linked dashboards (excluding the current dashboard) + // have no other non‑deleted dashboards. + List filteredChartRecordsToBeDeleted = + new ArrayList<>(); + + for (CollectionDAO.EntityRelationshipRecord record : chartRecords) { + UUID chartId = record.getId(); + boolean hasOtherNonDeletedDashboard = false; + + for (CollectionDAO.EntityRelationshipObject rel : dashboardRelationships) { + UUID relFromId = UUID.fromString(rel.getFromId()); + UUID relToId = UUID.fromString(rel.getToId()); + if (relToId.equals(chartId) && nonDeletedDashboards.contains(relFromId)) { + hasOtherNonDeletedDashboard = true; + break; + } + } + + if (!hasOtherNonDeletedDashboard) { + filteredChartRecordsToBeDeleted.add(record); + } + } + + deleteChildren(filteredChartRecordsToBeDeleted, hardDelete, updatedBy); + } + + // Override restore behavior to handle charts through HAS relation. + @Transaction + @Override + protected void restoreChildren(UUID dashboardId, String updatedBy) { + super.restoreChildren(dashboardId, updatedBy); + + // Load all charts linked to this dashboard + List chartRecords = + daoCollection + .relationshipDAO() + .findTo(dashboardId, DASHBOARD, Relationship.HAS.ordinal(), CHART); + if (chartRecords.isEmpty()) { + return; + } + + // Batch-load dashboard relationships for these charts + List dashboardRelationships = + daoCollection + .relationshipDAO() + .findFromBatch( + chartRecords.stream() + .map(record -> record.getId().toString()) + .distinct() + .collect(Collectors.toList()), + Relationship.HAS.ordinal(), + DASHBOARD); + + Set deletedDashboards = + daoCollection + .dashboardDAO() + .findEntitiesByIds( + dashboardRelationships.stream() + .map(rel -> UUID.fromString(rel.getFromId())) + .distinct() + .collect(Collectors.toList()), + Include.DELETED) + .stream() + .map(Dashboard::getId) + .filter(id -> !id.equals(dashboardId)) // (excluding the current dashboard + .collect(Collectors.toSet()); + + // For restore: get charts whose linked dashboards (excluding the current dashboard) + // are all non‑deleted. + List filteredChartRecordsToBeRestored = + new ArrayList<>(); + + for (CollectionDAO.EntityRelationshipRecord chartRecord : chartRecords) { + UUID chartId = chartRecord.getId(); + boolean hasOtherDeletedDashboard = false; + + for (CollectionDAO.EntityRelationshipObject relationship : dashboardRelationships) { + UUID relFromId = UUID.fromString(relationship.getFromId()); + UUID relToId = UUID.fromString(relationship.getToId()); + if (relToId.equals(chartId) && deletedDashboards.contains(relFromId)) { + hasOtherDeletedDashboard = true; + break; + } + } + + if (!hasOtherDeletedDashboard) { + filteredChartRecordsToBeRestored.add(chartRecord); + } + } + + for (CollectionDAO.EntityRelationshipRecord record : filteredChartRecordsToBeRestored) { + LOG.info("Recursively restoring {} {}", record.getType(), record.getId()); + Entity.restoreEntity(updatedBy, record.getType(), record.getId()); + } + } + @Override public void restorePatchAttributes(Dashboard original, Dashboard updated) { // Patch can't make changes to following fields. Ignore the changes diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java index 58509b638fe..74b965e7be8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java @@ -1172,7 +1172,7 @@ public abstract class EntityRepository { updated.setUpdatedAt(System.currentTimeMillis()); // If the entity state is soft-deleted, recursively undelete the entity and it's children if (Boolean.TRUE.equals(original.getDeleted())) { - restoreEntity(updated.getUpdatedBy(), entityType, original.getId()); + restoreEntity(updated.getUpdatedBy(), original.getId()); } // Update the attributes and relationships of an entity @@ -1196,7 +1196,7 @@ public abstract class EntityRepository { updated.setUpdatedAt(System.currentTimeMillis()); // If the entity state is soft-deleted, recursively undelete the entity and it's children if (Boolean.TRUE.equals(original.getDeleted())) { - restoreEntity(updated.getUpdatedBy(), entityType, original.getId()); + restoreEntity(updated.getUpdatedBy(), original.getId()); } // Update the attributes and relationships of an entity @@ -1571,7 +1571,7 @@ public abstract class EntityRepository { } @Transaction - private void deleteChildren(UUID id, boolean recursive, boolean hardDelete, String updatedBy) { + protected void deleteChildren(UUID id, boolean recursive, boolean hardDelete, String updatedBy) { // If an entity being deleted contains other **non-deleted** children entities, it can't be // deleted List childrenRecords = @@ -2364,22 +2364,9 @@ public abstract class EntityRepository { } @Transaction - public final PutResponse restoreEntity(String updatedBy, String entityType, UUID id) { + public final PutResponse restoreEntity(String updatedBy, UUID id) { // If an entity being restored contains other **deleted** children entities, restore them - List records = - daoCollection.relationshipDAO().findTo(id, entityType, Relationship.CONTAINS.ordinal()); - - if (!records.isEmpty()) { - // Restore all the contained entities - for (EntityRelationshipRecord entityRelationshipRecord : records) { - LOG.info( - "Recursively restoring {} {}", - entityRelationshipRecord.getType(), - entityRelationshipRecord.getId()); - Entity.restoreEntity( - updatedBy, entityRelationshipRecord.getType(), entityRelationshipRecord.getId()); - } - } + restoreChildren(id, updatedBy); // Finally set entity deleted flag to false LOG.info("Restoring the {} {}", entityType, id); @@ -2399,6 +2386,20 @@ public abstract class EntityRepository { } } + @Transaction + protected void restoreChildren(UUID id, String updatedBy) { + // Restore deleted children entities + List records = + daoCollection.relationshipDAO().findTo(id, entityType, Relationship.CONTAINS.ordinal()); + if (!records.isEmpty()) { + // Recursively restore all contained entities + for (CollectionDAO.EntityRelationshipRecord record : records) { + LOG.info("Recursively restoring {} {}", record.getType(), record.getId()); + Entity.restoreEntity(updatedBy, record.getType(), record.getId()); + } + } + } + public final void addRelationship( UUID fromId, UUID toId, String fromEntity, String toEntity, Relationship relationship) { addRelationship(fromId, toId, fromEntity, toEntity, relationship, false); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index 1087e286aa6..d3aa36b0865 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -555,7 +555,7 @@ public abstract class EntityResource response = - repository.restoreEntity(securityContext.getUserPrincipal().getName(), entityType, id); + repository.restoreEntity(securityContext.getUserPrincipal().getName(), id); repository.restoreFromSearch(response.getEntity()); addHref(uriInfo, response.getEntity()); LOG.info( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java index 8d324381dd6..dda0475d339 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java @@ -1022,16 +1022,6 @@ public class SearchRepository { new ImmutablePair<>( REMOVE_TAGS_CHILDREN_SCRIPT, Collections.singletonMap("fqn", entity.getFullyQualifiedName()))); - case Entity.DASHBOARD -> { - String scriptTxt = - String.format( - "if (ctx._source.dashboards.size() == 1) { ctx._source.put('deleted', '%s') }", - true); - searchClient.softDeleteOrRestoreChildren( - indexMapping.getChildAliases(clusterAlias), - scriptTxt, - List.of(new ImmutablePair<>("dashboards.id", docId))); - } case Entity.TEST_SUITE -> { TestSuite testSuite = (TestSuite) entity; if (Boolean.TRUE.equals(testSuite.getBasic())) { @@ -1082,16 +1072,6 @@ public class SearchRepository { indexMapping.getChildAliases(clusterAlias), scriptTxt, List.of(new ImmutablePair<>("service.id", docId))); - case Entity.DASHBOARD -> { - scriptTxt = - String.format( - "if (ctx._source.dashboards.size() == 1) { ctx._source.put('deleted', '%s') }", - delete); - searchClient.softDeleteOrRestoreChildren( - indexMapping.getChildAliases(clusterAlias), - scriptTxt, - List.of(new ImmutablePair<>("dashboards.id", docId))); - } default -> { List indexNames = indexMapping.getChildAliases(clusterAlias); if (!indexNames.isEmpty()) { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dashboards/DashboardResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dashboards/DashboardResourceTest.java index 6cb82bb873a..e349a6c94c4 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/dashboards/DashboardResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/dashboards/DashboardResourceTest.java @@ -17,12 +17,15 @@ import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; import static jakarta.ws.rs.core.Response.Status.OK; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openmetadata.service.Entity.FIELD_DELETED; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound; 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.MINOR_UPDATE; import static org.openmetadata.service.util.TestUtils.assertEntityReferenceNames; @@ -33,6 +36,7 @@ import static org.openmetadata.service.util.TestUtils.assertResponseContains; import java.io.IOException; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,14 +45,18 @@ 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.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.EntityReference; +import org.openmetadata.schema.type.Include; import org.openmetadata.service.Entity; import org.openmetadata.service.resources.EntityResourceTest; +import org.openmetadata.service.resources.charts.ChartResourceTest; import org.openmetadata.service.resources.dashboards.DashboardResource.DashboardList; import org.openmetadata.service.resources.services.DashboardServiceResourceTest; import org.openmetadata.service.util.FullyQualifiedName; @@ -58,6 +66,7 @@ import org.openmetadata.service.util.TestUtils; @Slf4j public class DashboardResourceTest extends EntityResourceTest { public static final String SUPERSET_INVALID_SERVICE = "invalid_superset_service"; + private final ChartResourceTest chartResourceTest = new ChartResourceTest(); public DashboardResourceTest() { super( @@ -184,6 +193,223 @@ public class DashboardResourceTest extends EntityResourceTest chartFqns = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + CreateChart createChart = + chartResourceTest.createRequest(test, i).withService(METABASE_REFERENCE.getName()); + Chart chart = chartResourceTest.createEntity(createChart, ADMIN_AUTH_HEADERS); + chartFqns.add(chart.getFullyQualifiedName()); + } + + // Create a dashboard with these charts + CreateDashboard create = + createRequest(test) + .withService(METABASE_REFERENCE.getFullyQualifiedName()) + .withCharts(chartFqns); + Dashboard dashboard = createAndCheckEntity(create, ADMIN_AUTH_HEADERS); + + // Verify the dashboard has charts + assertNotNull(dashboard.getCharts()); + assertFalse(dashboard.getCharts().isEmpty()); + + // Store all chart IDs for verification after dashboard deletion + List charts = dashboard.getCharts(); + + // For each chart, verify it belongs only to this dashboard + for (EntityReference chartRef : charts) { + // Get the chart entity with dashboards field included + Chart chart = chartResourceTest.getEntity(chartRef.getId(), "dashboards", ADMIN_AUTH_HEADERS); + + // Check chart has exactly one dashboard + assertEquals( + 1, + chart.getDashboards().size(), + "Chart should belong to exactly one dashboard before deletion test"); + + // Verify it's the dashboard we created + assertEquals( + dashboard.getId(), + chart.getDashboards().getFirst().getId(), + "Chart should belong to our test dashboard"); + } + + // Delete the dashboard + dashboard = deleteAndCheckEntity(dashboard, ADMIN_AUTH_HEADERS); + + // Now verify that the charts are actually soft deleted and not hard deleted + List deletedCharts = new ArrayList<>(); + for (EntityReference chartRef : charts) { + // Get the chart with include=deleted + Map queryParams = new HashMap<>(); + queryParams.put("include", Include.DELETED.value()); + + Chart deletedChart = + chartResourceTest.getEntity(chartRef.getId(), queryParams, "", ADMIN_AUTH_HEADERS); + + // Verify the chart is marked as deleted + assertTrue(deletedChart.getDeleted(), "Chart should be marked as deleted"); + assertEquals( + chartRef.getId(), deletedChart.getId(), "Found chart should match the original chart ID"); + deletedCharts.add(deletedChart); + } + + // Restore the dashboard + ChangeDescription change = getChangeDescription(dashboard, MINOR_UPDATE); + fieldUpdated(change, FIELD_DELETED, true, false); + restoreAndCheckEntity(dashboard, ADMIN_AUTH_HEADERS, change); + + // Verify charts are also restored when their parent dashboard is restored + for (Chart deletedChart : deletedCharts) { + // Verify chart is accessible again + Chart restoredChart = + chartResourceTest.getEntity(deletedChart.getId(), "dashboards", ADMIN_AUTH_HEADERS); + assertFalse( + restoredChart.getDeleted(), + "Chart should not be marked as deleted after dashboard restoration"); + + // Verify chart is associated with the restored dashboard + assertEquals( + 1, + restoredChart.getDashboards().size(), + "Chart should have exactly one dashboard after restoration"); + assertEquals( + dashboard.getId(), + restoredChart.getDashboards().getFirst().getId(), + "Chart should be associated with the restored dashboard"); + } + } + + @Test + void test_chartWithMultipleDashboards_deleteAndRestoreOneDashboard(TestInfo test) + throws IOException { + // Create charts first using ChartResourceTest + List chartFqns = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + CreateChart createChart = + chartResourceTest.createRequest(test, i).withService(METABASE_REFERENCE.getName()); + Chart chart = chartResourceTest.createEntity(createChart, ADMIN_AUTH_HEADERS); + chartFqns.add(chart.getFullyQualifiedName()); + } + + // Create first dashboard with all charts + CreateDashboard create1 = + createRequest(getEntityName(test) + "-first") + .withService(METABASE_REFERENCE.getFullyQualifiedName()) + .withCharts(chartFqns); + Dashboard dashboard1 = createAndCheckEntity(create1, ADMIN_AUTH_HEADERS); + + // Create second dashboard with the same charts + CreateDashboard create2 = + createRequest(getEntityName(test) + "-second") + .withService(METABASE_REFERENCE.getFullyQualifiedName()) + .withCharts(chartFqns); + final Dashboard dashboard2 = createAndCheckEntity(create2, ADMIN_AUTH_HEADERS); + + // Store all chart IDs for verification after dashboard deletion + List charts = dashboard1.getCharts(); + + // Delete the first dashboard + final Dashboard deletedDashboard1 = deleteAndCheckEntity(dashboard1, ADMIN_AUTH_HEADERS); + + // Verify charts are still accessible (not deleted) + for (EntityReference chartRef : charts) { + // Get the chart entity with dashboards field included + Chart chart = chartResourceTest.getEntity(chartRef.getId(), "dashboards", ADMIN_AUTH_HEADERS); + + // Check chart still exists + assertNotNull(chart); + + // Verify the chart itself isn't deleted + assertFalse(chart.getDeleted(), "Chart should not be marked as deleted"); + + // Count non-deleted dashboards + long nonDeletedDashboards = + chart.getDashboards().stream().filter(d -> !Boolean.TRUE.equals(d.getDeleted())).count(); + assertEquals( + 1, + nonDeletedDashboards, + "Chart should belong to exactly one non-deleted dashboard after first dashboard deletion"); + + // Verify one of the dashboards is the non-deleted second dashboard + boolean hasSecondDashboard = + chart.getDashboards().stream() + .anyMatch( + d -> + d.getId().equals(dashboard2.getId()) && !Boolean.TRUE.equals(d.getDeleted())); + assertTrue(hasSecondDashboard, "Chart should still belong to second test dashboard"); + } + + // Restore the first dashboard + ChangeDescription change = getChangeDescription(deletedDashboard1, MINOR_UPDATE); + fieldUpdated(change, FIELD_DELETED, true, false); + restoreAndCheckEntity(deletedDashboard1, ADMIN_AUTH_HEADERS, change); + + // Verify charts now have associations with both dashboards + for (EntityReference chartRef : charts) { + // Get the chart entity with dashboards field included + Chart chart = chartResourceTest.getEntity(chartRef.getId(), "dashboards", ADMIN_AUTH_HEADERS); + + // Verify chart is still not deleted + assertFalse(chart.getDeleted(), "Chart should not be marked as deleted"); + + // Count non-deleted dashboards - should be 2 now + long nonDeletedDashboards = + chart.getDashboards().stream().filter(d -> !Boolean.TRUE.equals(d.getDeleted())).count(); + assertEquals( + 2, + nonDeletedDashboards, + "Chart should belong to two non-deleted dashboards after first dashboard restoration"); + + // Verify both dashboards are associated with the chart + boolean hasFirstDashboard = + chart.getDashboards().stream() + .anyMatch( + d -> + d.getId().equals(deletedDashboard1.getId()) + && !Boolean.TRUE.equals(d.getDeleted())); + boolean hasSecondDashboard = + chart.getDashboards().stream() + .anyMatch( + d -> + d.getId().equals(dashboard2.getId()) && !Boolean.TRUE.equals(d.getDeleted())); + assertTrue( + hasFirstDashboard, + "Chart should be associated with the first dashboard after restoration"); + assertTrue(hasSecondDashboard, "Chart should still be associated with the second dashboard"); + } + + // Now delete both dashboards + deleteEntity(deletedDashboard1.getId(), true, false, ADMIN_AUTH_HEADERS); + deleteEntity(dashboard2.getId(), true, false, ADMIN_AUTH_HEADERS); + + // Verify charts are now soft deleted + for (EntityReference chartRef : charts) { + // Verify chart cannot be retrieved normally + assertResponse( + () -> chartResourceTest.getEntity(chartRef.getId(), "", ADMIN_AUTH_HEADERS), + NOT_FOUND, + entityNotFound(Entity.CHART, chartRef.getId())); + + // Verify chart can be retrieved with include=deleted parameter + Map queryParams = new HashMap<>(); + queryParams.put("include", Include.DELETED.value()); + + Chart deletedChart = + chartResourceTest.getEntity(chartRef.getId(), queryParams, "", ADMIN_AUTH_HEADERS); + + // Verify the chart is marked as deleted + assertTrue( + deletedChart.getDeleted(), + "Chart should be marked as deleted after all dashboards are deleted"); + assertEquals( + chartRef.getId(), deletedChart.getId(), "Found chart should match the original chart ID"); + } + } + @Override public Dashboard validateGetWithDifferentFields(Dashboard dashboard, boolean byName) throws HttpResponseException {