diff --git a/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
index 25f0fbd4cc371..18f23321ca0e9 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/data/database/DatabaseList_spec.jsx
@@ -24,6 +24,7 @@ import { styledMount as mount } from 'spec/helpers/theming';
import DatabaseList from 'src/views/CRUD/data/database/DatabaseList';
import DatabaseModal from 'src/views/CRUD/data/database/DatabaseModal';
+import DeleteModal from 'src/components/DeleteModal';
import SubMenu from 'src/components/Menu/SubMenu';
import ListView from 'src/components/ListView';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
@@ -36,6 +37,7 @@ const store = mockStore({});
const databasesInfoEndpoint = 'glob:*/api/v1/database/_info*';
const databasesEndpoint = 'glob:*/api/v1/database/?*';
const databaseEndpoint = 'glob:*/api/v1/database/*';
+const databaseRelatedEndpoint = 'glob:*/api/v1/database/*/related_objects*';
const mockdatabases = [...new Array(3)].map((_, i) => ({
changed_by: {
@@ -62,6 +64,16 @@ fetchMock.get(databasesEndpoint, {
});
fetchMock.delete(databaseEndpoint, {});
+fetchMock.get(databaseRelatedEndpoint, {
+ charts: {
+ count: 0,
+ result: [],
+ },
+ dashboards: {
+ count: 0,
+ result: [],
+ },
+});
describe('DatabaseList', () => {
const wrapper = mount(, { context: { store } });
@@ -100,6 +112,10 @@ describe('DatabaseList', () => {
});
await waitForComponentToPaint(wrapper);
+ expect(wrapper.find(DeleteModal).props().description).toMatchInlineSnapshot(
+ `"The database db 0 is linked to 0 charts that appear on 0 dashboards. Are you sure you want to continue? Deleting the database will break those objects."`,
+ );
+
act(() => {
wrapper
.find('#delete')
@@ -114,6 +130,9 @@ describe('DatabaseList', () => {
await waitForComponentToPaint(wrapper);
+ expect(fetchMock.calls(/database\/0\/related_objects/, 'GET')).toHaveLength(
+ 1,
+ );
expect(fetchMock.calls(/database\/0/, 'DELETE')).toHaveLength(1);
});
});
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
index 646ed3753697c..982851d683d47 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
@@ -23,8 +23,8 @@ import React, { useState, useMemo } from 'react';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/messageToasts/enhancers/withToasts';
-import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
+import DeleteModal from 'src/components/DeleteModal';
import TooltipWrapper from 'src/components/TooltipWrapper';
import Icon from 'src/components/Icon';
import ListView, { Filters } from 'src/components/ListView';
@@ -34,6 +34,10 @@ import { DatabaseObject } from './types';
const PAGE_SIZE = 25;
+interface DatabaseDeleteObject extends DatabaseObject {
+ chart_count: number;
+ dashboard_count: number;
+}
interface DatabaseListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
@@ -63,10 +67,34 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
addDangerToast,
);
const [databaseModalOpen, setDatabaseModalOpen] = useState(false);
+ const [
+ databaseCurrentlyDeleting,
+ setDatabaseCurrentlyDeleting,
+ ] = useState(null);
const [currentDatabase, setCurrentDatabase] = useState(
null,
);
+ const openDatabaseDeleteModal = (database: DatabaseObject) =>
+ SupersetClient.get({
+ endpoint: `/api/v1/database/${database.id}/related_objects/`,
+ })
+ .then(({ json = {} }) => {
+ setDatabaseCurrentlyDeleting({
+ ...database,
+ chart_count: json.charts.count,
+ dashboard_count: json.dashboards.count,
+ });
+ })
+ .catch(
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching database related data: %s',
+ errMsg,
+ ),
+ ),
+ );
+
function handleDatabaseDelete({ id, database_name: dbName }: DatabaseObject) {
SupersetClient.delete({
endpoint: `/api/v1/database/${id}`,
@@ -198,41 +226,28 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
},
{
Cell: ({ row: { original } }: any) => {
- const handleDelete = () => handleDatabaseDelete(original);
+ const handleDelete = () => openDatabaseDeleteModal(original);
if (!canDelete) {
return null;
}
return (
{canDelete && (
-
- {t('Are you sure you want to delete')}{' '}
- {original.database_name}?
- >
- }
- onConfirm={handleDelete}
+
- {confirmDelete => (
-
-
-
-
-
- )}
-
+
+
+
+
)}
);
@@ -258,6 +273,24 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
/* TODO: add database logic here */
}}
/>
+ {databaseCurrentlyDeleting && (
+ {
+ if (databaseCurrentlyDeleting) {
+ handleDatabaseDelete(databaseCurrentlyDeleting);
+ }
+ }}
+ onHide={() => setDatabaseCurrentlyDeleting(null)}
+ open
+ title={t('Delete Database?')}
+ />
+ )}
className="database-list-view"
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 9dd5438950b21..c3355f538f98f 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -36,12 +36,14 @@
DatabaseUpdateFailedError,
)
from superset.databases.commands.update import UpdateDatabaseCommand
+from superset.databases.dao import DatabaseDAO
from superset.databases.decorators import check_datasource_access
from superset.databases.filters import DatabaseFilter
from superset.databases.schemas import (
database_schemas_query_schema,
DatabasePostSchema,
DatabasePutSchema,
+ DatabaseRelatedObjectsResponse,
SchemasResponseSchema,
SelectStarResponseSchema,
TableMetadataResponseSchema,
@@ -63,6 +65,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
"table_metadata",
"select_star",
"schemas",
+ "related_objects",
}
class_permission_name = "DatabaseView"
resource_name = "database"
@@ -148,6 +151,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
}
openapi_spec_tag = "Database"
openapi_spec_component_schemas = (
+ DatabaseRelatedObjectsResponse,
TableMetadataResponseSchema,
SelectStarResponseSchema,
SchemasResponseSchema,
@@ -501,3 +505,60 @@ def select_star(
return self.response(404, message="Table not found on the database")
self.incr_stats("success", self.select_star.__name__)
return self.response(200, result=result)
+
+ @expose("//related_objects/", methods=["GET"])
+ @protect()
+ @safe
+ @statsd_metrics
+ def related_objects(self, pk: int) -> Response:
+ """Get charts and dashboards count associated to a database
+ ---
+ get:
+ description:
+ Get charts and dashboards count associated to a database
+ parameters:
+ - in: path
+ name: pk
+ schema:
+ type: integer
+ responses:
+ 200:
+ 200:
+ description: Query result
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/DatabaseRelatedObjectsResponse"
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ dataset = DatabaseDAO.find_by_id(pk)
+ if not dataset:
+ return self.response_404()
+ data = DatabaseDAO.get_related_objects(pk)
+ charts = [
+ {
+ "id": chart.id,
+ "slice_name": chart.slice_name,
+ "viz_type": chart.viz_type,
+ }
+ for chart in data["charts"]
+ ]
+ dashboards = [
+ {
+ "id": dashboard.id,
+ "json_metadata": dashboard.json_metadata,
+ "slug": dashboard.slug,
+ "title": dashboard.dashboard_title,
+ }
+ for dashboard in data["dashboards"]
+ ]
+ return self.response(
+ 200,
+ charts={"count": len(charts), "result": charts},
+ dashboards={"count": len(dashboards), "result": dashboards},
+ )
diff --git a/superset/databases/dao.py b/superset/databases/dao.py
index 88009800ed2cc..804ac129e4b06 100644
--- a/superset/databases/dao.py
+++ b/superset/databases/dao.py
@@ -15,11 +15,14 @@
# specific language governing permissions and limitations
# under the License.
import logging
+from typing import Any, Dict
from superset.dao.base import BaseDAO
from superset.databases.filters import DatabaseFilter
from superset.extensions import db
from superset.models.core import Database
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
logger = logging.getLogger(__name__)
@@ -41,3 +44,28 @@ def validate_update_uniqueness(database_id: int, database_name: str) -> bool:
Database.database_name == database_name, Database.id != database_id,
)
return not db.session.query(database_query.exists()).scalar()
+
+ @classmethod
+ def get_related_objects(cls, database_id: int) -> Dict[str, Any]:
+ datasets = cls.find_by_id(database_id).tables
+ dataset_ids = [dataset.id for dataset in datasets]
+
+ charts = (
+ db.session.query(Slice)
+ .filter(
+ Slice.datasource_id.in_(dataset_ids), Slice.datasource_type == "table"
+ )
+ .all()
+ )
+ chart_ids = [chart.id for chart in charts]
+
+ dashboards = (
+ (
+ db.session.query(Dashboard)
+ .join(Dashboard.slices)
+ .filter(Slice.id.in_(chart_ids))
+ )
+ .distinct()
+ .all()
+ )
+ return dict(charts=charts, dashboards=dashboards)
diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py
index dff09f9c015c6..a6a182f73dded 100644
--- a/superset/databases/schemas.py
+++ b/superset/databases/schemas.py
@@ -339,3 +339,35 @@ class SelectStarResponseSchema(Schema):
class SchemasResponseSchema(Schema):
result = fields.List(fields.String(description="A database schema name"))
+
+
+class DatabaseRelatedChart(Schema):
+ id = fields.Integer()
+ slice_name = fields.String()
+ viz_type = fields.String()
+
+
+class DatabaseRelatedDashboard(Schema):
+ id = fields.Integer()
+ json_metadata = fields.Dict()
+ slug = fields.String()
+ title = fields.String()
+
+
+class DatabaseRelatedCharts(Schema):
+ count = fields.Integer(description="Chart count")
+ result = fields.List(
+ fields.Nested(DatabaseRelatedChart), description="A list of dashboards"
+ )
+
+
+class DatabaseRelatedDashboards(Schema):
+ count = fields.Integer(description="Dashboard count")
+ result = fields.List(
+ fields.Nested(DatabaseRelatedDashboard), description="A list of dashboards"
+ )
+
+
+class DatabaseRelatedObjectsResponse(Schema):
+ charts = fields.Nested(DatabaseRelatedCharts)
+ dashboards = fields.Nested(DatabaseRelatedDashboards)
diff --git a/tests/databases/api_tests.py b/tests/databases/api_tests.py
index 642f57c3d0347..6d82202df690d 100644
--- a/tests/databases/api_tests.py
+++ b/tests/databases/api_tests.py
@@ -651,3 +651,35 @@ def test_database_schemas_invalid_query(self):
f"api/v1/database/{database.id}/schemas/?q={prison.dumps({'force': 'nop'})}"
)
self.assertEqual(rv.status_code, 400)
+
+ def test_get_database_related_objects(self):
+ """
+ Database API: Test get chart and dashboard count related to a database
+ :return:
+ """
+ self.login(username="admin")
+ database = get_example_database()
+ uri = f"api/v1/database/{database.id}/related_objects/"
+ rv = self.get_assert_metric(uri, "related_objects")
+ self.assertEqual(rv.status_code, 200)
+ response = json.loads(rv.data.decode("utf-8"))
+ self.assertEqual(response["charts"]["count"], 33)
+ self.assertEqual(response["dashboards"]["count"], 6)
+
+ def test_get_database_related_objects_not_found(self):
+ """
+ Database API: Test related objects not found
+ """
+ max_id = db.session.query(func.max(Database.id)).scalar()
+ # id does not exist and we get 404
+ invalid_id = max_id + 1
+ uri = f"api/v1/database/{invalid_id}/related_objects/"
+ self.login(username="admin")
+ rv = self.client.get(uri)
+ self.assertEqual(rv.status_code, 404)
+ self.logout()
+ self.login(username="gamma")
+ database = get_example_database()
+ uri = f"api/v1/database/{database.id}/related_objects/"
+ rv = self.client.get(uri)
+ self.assertEqual(rv.status_code, 404)