Skip to content

Commit

Permalink
feat(dashboard): dashboard/id/datasets endpoint (apache#13523)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
suddjian authored and Allan Caetano de Oliveira committed May 21, 2021
1 parent dbfa7a5 commit da387b6
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 0 deletions.
1 change: 1 addition & 0 deletions superset/connectors/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ def data(self) -> Dict[str, Any]:
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
Expand Down
1 change: 1 addition & 0 deletions superset/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,5 @@ class RouteMethod: # pylint: disable=too-few-public-methods
"data": "read",
"data_from_cache": "read",
"get_charts": "read",
"get_datasets": "read",
}
55 changes: 55 additions & 0 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
FilterRelatedRoles,
)
from superset.dashboards.schemas import (
DashboardDatasetSchema,
DashboardGetResponseSchema,
DashboardPostSchema,
DashboardPutSchema,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: []]]

Expand All @@ -189,6 +192,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
openapi_spec_component_schemas = (
ChartEntityResponseSchema,
DashboardGetResponseSchema,
DashboardDatasetSchema,
GetFavStarIdsSchema,
)
apispec_parameter_schemas = {
Expand Down Expand Up @@ -252,6 +256,57 @@ def get(self, id_or_slug: str) -> Response:
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
Expand Down
24 changes: 24 additions & 0 deletions superset/dashboards/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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__)
Expand Down Expand Up @@ -57,6 +58,29 @@ def get_by_id_or_slug(id_or_slug: str) -> Dashboard:
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 = (
Expand Down
45 changes: 45 additions & 0 deletions superset/dashboards/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/dashboards/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,32 @@ def create_dashboard_with_report(self):
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")
Expand Down

0 comments on commit da387b6

Please sign in to comment.