feat(dashboard): dashboard/id/datasets endpoint (#13523)

* feat(dashboard) dashboard/id/datasets endpoint

* schema for dashboard datasets

* list instead of map

* finish dashboard dataset schema

* description

* better test

* add the dataset schema to the schema list

* lint
This commit is contained in:
David Aaron Suddjian
2021-03-11 17:43:33 -08:00
committed by GitHub
parent 7656b2e68b
commit 1b95ed7267
6 changed files with 152 additions and 0 deletions

View File

@@ -237,6 +237,7 @@ class BaseDatasource(
return {
# simple fields
"id": self.id,
"uid": self.uid,
"column_formats": self.column_formats,
"description": self.description,
"database": self.database.data, # pylint: disable=no-member

View File

@@ -115,4 +115,5 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
"data": "read",
"data_from_cache": "read",
"get_charts": "read",
"get_datasets": "read",
}

View File

@@ -58,6 +58,7 @@ from superset.dashboards.filters import (
FilterRelatedRoles,
)
from superset.dashboards.schemas import (
DashboardDatasetSchema,
DashboardGetResponseSchema,
DashboardPostSchema,
DashboardPutSchema,
@@ -93,6 +94,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
"bulk_delete", # not using RouteMethod since locally defined
"favorite_status",
"get_charts",
"get_datasets",
}
resource_name = "dashboard"
allow_browser_login = True
@@ -169,6 +171,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
edit_model_schema = DashboardPutSchema()
chart_entity_response_schema = ChartEntityResponseSchema()
dashboard_get_response_schema = DashboardGetResponseSchema()
dashboard_dataset_schema = DashboardDatasetSchema()
base_filters = [["slice", DashboardFilter, lambda: []]]
@@ -189,6 +192,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
openapi_spec_component_schemas = (
ChartEntityResponseSchema,
DashboardGetResponseSchema,
DashboardDatasetSchema,
GetFavStarIdsSchema,
)
apispec_parameter_schemas = {
@@ -252,6 +256,57 @@ class DashboardRestApi(BaseSupersetModelRestApi):
except DashboardNotFoundError:
return self.response_404()
@expose("/<id_or_slug>/datasets", methods=["GET"])
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get_datasets",
log_to_statsd=False,
)
def get_datasets(self, id_or_slug: str) -> Response:
"""Gets a dashboard's datasets
---
get:
description: >-
Returns a list of a dashboard's datasets. Each dataset includes only
the information necessary to render the dashboard's charts.
parameters:
- in: path
schema:
type: string
name: id_or_slug
description: Either the id of the dashboard, or its slug
responses:
200:
description: Dashboard dataset definitions
content:
application/json:
schema:
type: object
properties:
result:
type: array
items:
$ref: '#/components/schemas/DashboardDatasetSchema'
302:
description: Redirects to the current digest
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
404:
$ref: '#/components/responses/404'
"""
try:
datasets = DashboardDAO.get_datasets_for_dashboard(id_or_slug)
result = [
self.dashboard_dataset_schema.dump(dataset) for dataset in datasets
]
return self.response(200, result=result)
except DashboardNotFoundError:
return self.response_404()
@expose("/<pk>/charts", methods=["GET"])
@protect()
@safe

View File

@@ -29,6 +29,7 @@ from superset.extensions import db
from superset.models.core import FavStar, FavStarClassName
from superset.models.dashboard import Dashboard, id_or_slug_filter
from superset.models.slice import Slice
from superset.utils import core
from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
logger = logging.getLogger(__name__)
@@ -57,6 +58,29 @@ class DashboardDAO(BaseDAO):
raise DashboardNotFoundError()
return dashboard
@staticmethod
def get_datasets_for_dashboard(id_or_slug: str) -> List[Any]:
query = (
db.session.query(Dashboard)
.filter(id_or_slug_filter(id_or_slug))
.outerjoin(Slice, Dashboard.slices)
.outerjoin(Slice.table)
)
# Apply dashboard base filters
query = DashboardFilter("id", SQLAInterface(Dashboard, db.session)).apply(
query, None
)
dashboard = query.one_or_none()
if not dashboard:
raise DashboardNotFoundError()
datasource_slices = core.indexed(dashboard.slices, "datasource")
data = [
datasource.data_for_slices(slices)
for datasource, slices in datasource_slices.items()
if datasource
]
return data
@staticmethod
def get_charts_for_dashboard(dashboard_id: int) -> List[Slice]:
query = (

View File

@@ -158,6 +158,51 @@ class DashboardGetResponseSchema(Schema):
table_names = fields.String() # legacy nonsense
class DatabaseSchema(Schema):
id = fields.Int()
name = fields.String()
backend = fields.String()
allow_multi_schema_metadata_fetch = fields.Bool() # pylint: disable=invalid-name
allows_subquery = fields.Bool()
allows_cost_estimate = fields.Bool()
allows_virtual_table_explore = fields.Bool()
explore_database_id = fields.Int()
class DashboardDatasetSchema(Schema):
id = fields.Int()
uid = fields.Str()
column_formats = fields.Dict()
database = fields.Nested(DatabaseSchema)
default_endpoint = fields.String()
filter_select = fields.Bool()
filter_select_enabled = fields.Bool()
is_sqllab_view = fields.Bool()
name = fields.Str()
datasource_name = fields.Str()
table_name = fields.Str()
type = fields.Str()
schema = fields.Str()
offset = fields.Int()
cache_timeout = fields.Int()
params = fields.Str()
perm = fields.Str()
edit_url = fields.Str()
sql = fields.Str()
select_star = fields.Str()
main_dttm_col = fields.Str()
health_check_message = fields.Str()
fetch_values_predicate = fields.Str()
template_params = fields.Str()
owners = fields.List(fields.Int())
columns = fields.List(fields.Dict())
metrics = fields.List(fields.Dict())
order_by_choices = fields.List(fields.List(fields.Str()))
verbose_map = fields.Dict(fields.Str(), fields.Str())
time_grain_sqla = fields.List(fields.List(fields.Str()))
granularity_sqla = fields.List(fields.List(fields.Str()))
class BaseDashboardSchema(Schema):
# pylint: disable=no-self-use,unused-argument
@post_load

View File

@@ -170,6 +170,32 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
db.session.delete(dashboard)
db.session.commit()
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_get_dashboard_datasets(self):
self.login(username="admin")
uri = "api/v1/dashboard/world_health/datasets"
response = self.get_assert_metric(uri, "get_datasets")
self.assertEqual(response.status_code, 200)
data = json.loads(response.data.decode("utf-8"))
dashboard = Dashboard.get("world_health")
expected_dataset_ids = set([s.datasource_id for s in dashboard.slices])
actual_dataset_ids = set([dataset["id"] for dataset in data["result"]])
self.assertEqual(actual_dataset_ids, expected_dataset_ids)
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_get_dashboard_datasets_not_found(self):
self.login(username="alpha")
uri = "api/v1/dashboard/not_found/datasets"
response = self.get_assert_metric(uri, "get_datasets")
self.assertEqual(response.status_code, 404)
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
def test_get_dashboard_datasets_not_allowed(self):
self.login(username="gamma")
uri = "api/v1/dashboard/world_health/datasets"
response = self.get_assert_metric(uri, "get_datasets")
self.assertEqual(response.status_code, 404)
@pytest.mark.usefixtures("create_dashboards")
def get_dashboard_by_slug(self):
self.login(username="admin")