Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Migrate copy_dash endpoint to api v1 #23112

Merged
merged 7 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions superset-frontend/src/dashboard/actions/dashboardState.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export function saveDashboardRequest(data, id, saveType) {
};

const onCopySuccess = response => {
const lastModifiedTime = response.json.last_modified_time;
const lastModifiedTime = response.json.result.last_modified_time;
if (lastModifiedTime) {
dispatch(saveDashboardRequestSuccess(lastModifiedTime));
}
Expand Down Expand Up @@ -450,24 +450,21 @@ export function saveDashboardRequest(data, id, saveType) {
});
}
// changing the data as the endpoint requires
const copyData = { ...cleanedData };
if (copyData.metadata) {
delete copyData.metadata;
if ('positions' in cleanedData && !('positions' in cleanedData.metadata)) {
cleanedData.metadata.positions = cleanedData.positions;
}
const finalCopyData = {
...copyData,
// the endpoint is expecting the metadata to be flat
...(cleanedData?.metadata || {}),
cleanedData.metadata.default_filters = safeStringify(serializedFilters);
cleanedData.metadata.filter_scopes = serializedFilterScopes;
const copyPayload = {
dashboard_title: cleanedData.dashboard_title,
css: cleanedData.css,
duplicate_slices: cleanedData.duplicate_slices,
json_metadata: JSON.stringify(cleanedData.metadata),
};

return SupersetClient.post({
endpoint: `/superset/copy_dash/${id}/`,
postPayload: {
data: {
...finalCopyData,
default_filters: safeStringify(serializedFilters),
filter_scopes: safeStringify(serializedFilterScopes),
},
},
endpoint: `/api/v1/dashboard/${id}/copy/`,
jsonPayload: copyPayload,
})
.then(response => onCopySuccess(response))
.catch(response => onError(response));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ describe('dashboardState actions', () => {
const thunk = saveDashboardRequest(newDashboardData, 1, 'save_dash');
thunk(dispatch, getState);
expect(postStub.callCount).toBe(1);
const { postPayload } = postStub.getCall(0).args[0];
expect(postPayload.data.positions[DASHBOARD_GRID_ID].parents).toBe(
mockParentsList,
);
const { jsonPayload } = postStub.getCall(0).args[0];
const parsedJsonMetadata = JSON.parse(jsonPayload.json_metadata);
expect(
parsedJsonMetadata.positions[DASHBOARD_GRID_ID].parents,
).toStrictEqual(mockParentsList);
});

describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => {
Expand Down
9 changes: 2 additions & 7 deletions superset-frontend/src/dashboard/components/SaveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,8 @@ class SaveModal extends React.PureComponent<SaveModalProps, SaveModalState> {
);
} else {
this.onSave(data, dashboardId, saveType).then((resp: JsonResponse) => {
if (
saveType === SAVE_TYPE_NEWDASHBOARD &&
resp &&
resp.json &&
resp.json.id
) {
window.location.href = `/superset/dashboard/${resp.json.id}/`;
if (saveType === SAVE_TYPE_NEWDASHBOARD && resp.json?.result?.id) {
window.location.href = `/superset/dashboard/${resp.json.result.id}/`;
}
});
this.modal?.current?.close?.();
Expand Down
1 change: 1 addition & 0 deletions superset/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods
"get_all_objects": "read",
"add_objects": "write",
"delete_object": "write",
"copy_dash": "write",
}

EXTRA_FORM_DATA_APPEND_KEYS = {
Expand Down
69 changes: 69 additions & 0 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
FilterRelatedRoles,
)
from superset.dashboards.schemas import (
DashboardCopySchema,
DashboardDatasetSchema,
DashboardGetResponseSchema,
DashboardPostSchema,
Expand Down Expand Up @@ -149,6 +150,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
"set_embedded",
"delete_embedded",
"thumbnail",
"copy_dash",
}
resource_name = "dashboard"
allow_browser_login = True
Expand Down Expand Up @@ -284,6 +286,7 @@ def ensure_thumbnails_enabled(self) -> Optional[Response]:
""" Override the name set for this collection of endpoints """
openapi_spec_component_schemas = (
ChartEntityResponseSchema,
DashboardCopySchema,
DashboardGetResponseSchema,
DashboardDatasetSchema,
GetFavStarIdsSchema,
Expand Down Expand Up @@ -1380,3 +1383,69 @@ def delete_embedded(self, dashboard: Dashboard) -> Response:
for embedded in dashboard.embedded:
DashboardDAO.delete(embedded)
return self.response(200, message="OK")

@expose("/<id_or_slug>/copy/", methods=["POST"])
@protect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this endpoint permission be can write on Dashboard add copy_dash to https://github.com/apache/superset/blob/master/superset/constants.py#L151

@safe
@permission_name("write")
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.copy_dash",
log_to_statsd=False,
)
@with_dashboard
def copy_dash(self, original_dash: Dashboard) -> Response:
"""Makes a copy of an existing dashboard
---
post:
summary: Makes a copy of an existing dashboard
parameters:
- in: path
schema:
type: string
name: id_or_slug
description: The dashboard id or slug
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardCopySchema'
responses:
200:
description: Id of new dashboard and last modified time
content:
application/json:
schema:
type: object
properties:
id:
type: number
last_modified_time:
type: number
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
404:
$ref: '#/components/responses/404'
500:
$ref: '#/components/responses/500'
"""
try:
data = DashboardCopySchema().load(request.json)
except ValidationError as error:
return self.response_400(message=error.messages)

dash = DashboardDAO.copy_dashboard(original_dash, data)
return self.response(
200,
result={
"id": dash.id,
"last_modified_time": dash.changed_on.replace(
microsecond=0
).timestamp(),
},
)
43 changes: 36 additions & 7 deletions superset/dashboards/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from flask import g
from flask_appbuilder.models.sqla.interface import SQLAInterface
from sqlalchemy.exc import SQLAlchemyError

Expand Down Expand Up @@ -262,13 +263,6 @@ def set_dash_metadata( # pylint: disable=too-many-locals
# positions have its own column, no need to store it in metadata
md.pop("positions", None)

# The css and dashboard_title properties are not part of the metadata
# TODO (geido): remove by refactoring/deprecating save_dash endpoint
if data.get("css") is not None:
dashboard.css = data.get("css")
if data.get("dashboard_title") is not None:
dashboard.dashboard_title = data.get("dashboard_title")

if new_filter_scopes:
md["filter_scopes"] = new_filter_scopes
else:
Expand Down Expand Up @@ -308,6 +302,41 @@ def favorited_ids(dashboards: List[Dashboard]) -> List[FavStar]:
.all()
]

@classmethod
def copy_dashboard(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about just def copy( we know it's a dashboard from the class

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to be explicit here

cls, original_dash: Dashboard, data: Dict[str, Any]
) -> Dashboard:
dash = Dashboard()
dash.owners = [g.user] if g.user else []
dash.dashboard_title = data["dashboard_title"]
dash.css = data.get("css")

metadata = json.loads(data["json_metadata"])
old_to_new_slice_ids: Dict[int, int] = {}
if data.get("duplicate_slices"):
# Duplicating slices as well, mapping old ids to new ones
for slc in original_dash.slices:
new_slice = slc.clone()
new_slice.owners = [g.user] if g.user else []
db.session.add(new_slice)
db.session.flush()
new_slice.dashboards.append(dash)
old_to_new_slice_ids[slc.id] = new_slice.id

# update chartId of layout entities
for value in metadata["positions"].values():
if isinstance(value, dict) and value.get("meta", {}).get("chartId"):
old_id = value["meta"]["chartId"]
new_id = old_to_new_slice_ids.get(old_id)
value["meta"]["chartId"] = new_id
else:
dash.slices = original_dash.slices

cls.set_dash_metadata(dash, metadata, old_to_new_slice_ids)
db.session.add(dash)
db.session.commit()
return dash

@staticmethod
def add_favorite(dashboard: Dashboard) -> None:
ids = DashboardDAO.favorited_ids([dashboard])
Expand Down
19 changes: 18 additions & 1 deletion superset/dashboards/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class DashboardPostSchema(BaseDashboardSchema):
position_json = fields.String(
description=position_json_description, validate=validate_json
)
css = fields.String()
css = fields.String(description=css_description)
json_metadata = fields.String(
description=json_metadata_description,
validate=validate_json_metadata,
Expand All @@ -280,6 +280,23 @@ class DashboardPostSchema(BaseDashboardSchema):
external_url = fields.String(allow_none=True)


class DashboardCopySchema(Schema):
dashboard_title = fields.String(
description=dashboard_title_description,
allow_none=True,
validate=Length(0, 500),
)
css = fields.String(description=css_description)
json_metadata = fields.String(
description=json_metadata_description,
validate=validate_json_metadata,
required=True,
)
duplicate_slices = fields.Boolean(
description="Whether or not to also copy all charts on the dashboard"
)


class DashboardPutSchema(BaseDashboardSchema):
dashboard_title = fields.String(
description=dashboard_title_description,
Expand Down
7 changes: 7 additions & 0 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,7 @@ def tables( # pylint: disable=no-self-use
@has_access_api
@event_logger.log_this
@expose("/copy_dash/<int:dashboard_id>/", methods=["GET", "POST"])
@deprecated()
def copy_dash( # pylint: disable=no-self-use
self, dashboard_id: int
) -> FlaskResponse:
Expand All @@ -1260,6 +1261,7 @@ def copy_dash( # pylint: disable=no-self-use

dash.owners = [g.user] if g.user else []
dash.dashboard_title = data["dashboard_title"]
dash.css = data.get("css")

old_to_new_slice_ids: Dict[int, int] = {}
if data["duplicate_slices"]:
Expand Down Expand Up @@ -1294,6 +1296,7 @@ def copy_dash( # pylint: disable=no-self-use
@has_access_api
@event_logger.log_this
@expose("/save_dash/<int:dashboard_id>/", methods=["GET", "POST"])
@deprecated()
def save_dash( # pylint: disable=no-self-use
self, dashboard_id: int
) -> FlaskResponse:
Expand All @@ -1320,6 +1323,10 @@ def save_dash( # pylint: disable=no-self-use
# remove to avoid confusion.
data.pop("last_modified_time", None)

if data.get("css") is not None:
dash.css = data["css"]
if data.get("dashboard_title") is not None:
dash.dashboard_title = data["dashboard_title"]
DashboardDAO.set_dash_metadata(dash, data)
session.merge(dash)
session.commit()
Expand Down
Loading