feat: deprecate old API and create new API for dashes created by me (#19434)
* feat: deprecate old API and create new API for dashes created by me * add tests * fix previous test * fix test and lint * fix sqlite test * fix lint * fix lint * lint * fix tests * fix tests * use dashboards get list instead * clean unnecessary marshmallow schema * Update superset/views/core.py Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * lint Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
parent
8da2c9b96a
commit
d6d2777ada
|
|
@ -16,13 +16,14 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import rison from 'rison';
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
import TableLoader from '../../components/TableLoader';
|
||||
import { Slice } from '../types';
|
||||
import { User, Dashboard } from '../../types/bootstrapTypes';
|
||||
import { User, DashboardResponse } from '../../types/bootstrapTypes';
|
||||
|
||||
interface CreatedContentProps {
|
||||
user: User;
|
||||
|
|
@ -49,17 +50,27 @@ class CreatedContent extends React.PureComponent<CreatedContentProps> {
|
|||
}
|
||||
|
||||
renderDashboardTable() {
|
||||
const mutator = (data: Dashboard[]) =>
|
||||
data.map(dash => ({
|
||||
dashboard: <a href={dash.url}>{dash.title}</a>,
|
||||
created: moment.utc(dash.dttm).fromNow(),
|
||||
_created: dash.dttm,
|
||||
const search = [{ col: 'created_by', opr: 'created_by_me', value: 'me' }];
|
||||
const query = rison.encode({
|
||||
keys: ['none'],
|
||||
columns: ['created_on_delta_humanized', 'dashboard_title', 'url'],
|
||||
filters: search,
|
||||
order_column: 'changed_on',
|
||||
order_direction: 'desc',
|
||||
page: 0,
|
||||
page_size: 100,
|
||||
});
|
||||
const mutator = (data: DashboardResponse) =>
|
||||
data.result.map(dash => ({
|
||||
dashboard: <a href={dash.url}>{dash.dashboard_title}</a>,
|
||||
created: dash.created_on_delta_humanized,
|
||||
_created: dash.created_on_delta_humanized,
|
||||
}));
|
||||
return (
|
||||
<TableLoader
|
||||
className="table-condensed"
|
||||
mutator={mutator}
|
||||
dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`}
|
||||
dataEndpoint={`/api/v1/dashboard/?q=${query}`}
|
||||
noDataText={t('No dashboards')}
|
||||
columns={['dashboard', 'created']}
|
||||
sortable
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ export type Dashboard = {
|
|||
creator_url?: string;
|
||||
};
|
||||
|
||||
export type DashboardData = {
|
||||
dashboard_title?: string;
|
||||
created_on_delta_humanized?: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type DashboardResponse = {
|
||||
result: DashboardData[];
|
||||
};
|
||||
|
||||
export interface CommonBootstrapData {
|
||||
flash_messages: string[][];
|
||||
conf: JsonObject;
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ MODEL_VIEW_RW_METHOD_PERMISSION_MAP = {
|
|||
|
||||
MODEL_API_RW_METHOD_PERMISSION_MAP = {
|
||||
"bulk_delete": "write",
|
||||
"created_by_me": "read",
|
||||
"delete": "write",
|
||||
"distinct": "read",
|
||||
"get": "read",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ from superset.dashboards.dao import DashboardDAO
|
|||
from superset.dashboards.filters import (
|
||||
DashboardAccessFilter,
|
||||
DashboardCertifiedFilter,
|
||||
DashboardCreatedByMeFilter,
|
||||
DashboardFavoriteFilter,
|
||||
DashboardTitleOrSlugFilter,
|
||||
FilterRelatedRoles,
|
||||
|
|
@ -139,6 +140,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
"set_embedded",
|
||||
"delete_embedded",
|
||||
"thumbnail",
|
||||
"created_by_me",
|
||||
}
|
||||
resource_name = "dashboard"
|
||||
allow_browser_login = True
|
||||
|
|
@ -166,6 +168,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
"changed_by_url",
|
||||
"changed_on_utc",
|
||||
"changed_on_delta_humanized",
|
||||
"created_on_delta_humanized",
|
||||
"created_by.first_name",
|
||||
"created_by.id",
|
||||
"created_by.last_name",
|
||||
|
|
@ -179,13 +182,14 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
"roles.name",
|
||||
"is_managed_externally",
|
||||
]
|
||||
list_select_columns = list_columns + ["changed_on", "changed_by_fk"]
|
||||
list_select_columns = list_columns + ["changed_on", "created_on", "changed_by_fk"]
|
||||
order_columns = [
|
||||
"changed_by.first_name",
|
||||
"changed_on_delta_humanized",
|
||||
"created_by.first_name",
|
||||
"dashboard_title",
|
||||
"published",
|
||||
"changed_on",
|
||||
]
|
||||
|
||||
add_columns = [
|
||||
|
|
@ -215,6 +219,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
search_filters = {
|
||||
"dashboard_title": [DashboardTitleOrSlugFilter],
|
||||
"id": [DashboardFavoriteFilter, DashboardCertifiedFilter],
|
||||
"created_by": [DashboardCreatedByMeFilter],
|
||||
}
|
||||
base_order = ("changed_on", "desc")
|
||||
|
||||
|
|
@ -226,7 +231,9 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
embedded_response_schema = EmbeddedDashboardResponseSchema()
|
||||
embedded_config_schema = EmbeddedDashboardConfigSchema()
|
||||
|
||||
base_filters = [["id", DashboardAccessFilter, lambda: []]]
|
||||
base_filters = [
|
||||
["id", DashboardAccessFilter, lambda: []],
|
||||
]
|
||||
|
||||
order_rel_fields = {
|
||||
"slices": ("slice_name", "asc"),
|
||||
|
|
@ -307,8 +314,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
properties:
|
||||
result:
|
||||
$ref: '#/components/schemas/DashboardGetResponseSchema'
|
||||
302:
|
||||
description: Redirects to the current digest
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
|
|
@ -364,8 +369,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DashboardDatasetSchema'
|
||||
302:
|
||||
description: Redirects to the current digest
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
|
|
@ -427,8 +430,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ChartEntityResponseSchema'
|
||||
302:
|
||||
description: Redirects to the current digest
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
|
|
@ -489,8 +490,6 @@ class DashboardRestApi(BaseSupersetModelRestApi):
|
|||
type: number
|
||||
result:
|
||||
$ref: '#/components/schemas/{{self.__class__.__name__}}.post'
|
||||
302:
|
||||
description: Redirects to the current digest
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,21 @@ class DashboardTitleOrSlugFilter(BaseFilter): # pylint: disable=too-few-public-
|
|||
)
|
||||
|
||||
|
||||
class DashboardCreatedByMeFilter(BaseFilter): # pylint: disable=too-few-public-methods
|
||||
name = _("Created by me")
|
||||
arg_name = "created_by_me"
|
||||
|
||||
def apply(self, query: Query, value: Any) -> Query:
|
||||
return query.filter(
|
||||
or_(
|
||||
Dashboard.created_by_fk # pylint: disable=comparison-with-callable
|
||||
== g.user.get_user_id(),
|
||||
Dashboard.changed_by_fk # pylint: disable=comparison-with-callable
|
||||
== g.user.get_user_id(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DashboardFavoriteFilter( # pylint: disable=too-few-public-methods
|
||||
BaseFavoriteFilter
|
||||
):
|
||||
|
|
|
|||
|
|
@ -420,6 +420,10 @@ class AuditMixinNullable(AuditMixin):
|
|||
def changed_on_delta_humanized(self) -> str:
|
||||
return self.changed_on_humanized
|
||||
|
||||
@renders("created_on")
|
||||
def created_on_delta_humanized(self) -> str:
|
||||
return self.created_on_humanized
|
||||
|
||||
@renders("changed_on")
|
||||
def changed_on_utc(self) -> str:
|
||||
# Convert naive datetime to UTC
|
||||
|
|
@ -429,6 +433,10 @@ class AuditMixinNullable(AuditMixin):
|
|||
def changed_on_humanized(self) -> str:
|
||||
return humanize.naturaltime(datetime.now() - self.changed_on)
|
||||
|
||||
@property
|
||||
def created_on_humanized(self) -> str:
|
||||
return humanize.naturaltime(datetime.now() - self.created_on)
|
||||
|
||||
@renders("changed_on")
|
||||
def modified(self) -> Markup:
|
||||
return Markup(f'<span class="no-wrap">{self.changed_on_humanized}</span>')
|
||||
|
|
|
|||
|
|
@ -1587,16 +1587,24 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
@event_logger.log_this
|
||||
@expose("/created_dashboards/<int:user_id>/", methods=["GET"])
|
||||
def created_dashboards(self, user_id: int) -> FlaskResponse:
|
||||
logging.warning(
|
||||
"%s.created_dashboards "
|
||||
"This API endpoint is deprecated and will be removed in version 3.0.0",
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
error_obj = self.get_user_activity_access_error(user_id)
|
||||
if error_obj:
|
||||
return error_obj
|
||||
Dash = Dashboard
|
||||
qry = (
|
||||
db.session.query(Dash)
|
||||
db.session.query(Dashboard)
|
||||
.filter( # pylint: disable=comparison-with-callable
|
||||
or_(Dash.created_by_fk == user_id, Dash.changed_by_fk == user_id)
|
||||
or_(
|
||||
Dashboard.created_by_fk == user_id,
|
||||
Dashboard.changed_by_fk == user_id,
|
||||
)
|
||||
)
|
||||
.order_by(Dash.changed_on.desc())
|
||||
.order_by(Dashboard.changed_on.desc())
|
||||
)
|
||||
payload = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"""Unit tests for Superset"""
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from random import random
|
||||
|
||||
|
|
@ -139,9 +140,20 @@ class TestDashboard(SupersetTestCase):
|
|||
self.login(username="admin")
|
||||
dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
|
||||
url = "/dashboard/new/"
|
||||
resp = self.get_resp(url)
|
||||
response = self.client.get(url, follow_redirects=False)
|
||||
dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
|
||||
self.assertEqual(dash_count_before + 1, dash_count_after)
|
||||
group = re.match(
|
||||
r"http:\/\/localhost\/superset\/dashboard\/([0-9]*)\/\?edit=true",
|
||||
response.headers["Location"],
|
||||
)
|
||||
assert group is not None
|
||||
|
||||
# Cleanup
|
||||
created_dashboard_id = int(group[1])
|
||||
created_dashboard = db.session.query(Dashboard).get(created_dashboard_id)
|
||||
db.session.delete(created_dashboard)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
|
||||
def test_save_dash(self, username="admin"):
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"""Unit tests for Superset"""
|
||||
import json
|
||||
from io import BytesIO
|
||||
from time import sleep
|
||||
from typing import List, Optional
|
||||
from unittest.mock import patch
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
|
|
@ -27,7 +28,6 @@ from tests.integration_tests.insert_chart_mixin import InsertChartMixin
|
|||
import pytest
|
||||
import prison
|
||||
import yaml
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from freezegun import freeze_time
|
||||
from sqlalchemy import and_
|
||||
|
|
@ -160,6 +160,27 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
|||
db.session.delete(fav_dashboard)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.fixture()
|
||||
def create_created_by_admin_dashboards(self):
|
||||
with self.create_app().app_context():
|
||||
dashboards = []
|
||||
admin = self.get_user("admin")
|
||||
for cx in range(2):
|
||||
dashboard = self.insert_dashboard(
|
||||
f"create_title{cx}",
|
||||
f"create_slug{cx}",
|
||||
[admin.id],
|
||||
created_by=admin,
|
||||
)
|
||||
sleep(1)
|
||||
dashboards.append(dashboard)
|
||||
|
||||
yield dashboards
|
||||
|
||||
for dashboard in dashboards:
|
||||
db.session.delete(dashboard)
|
||||
db.session.commit()
|
||||
|
||||
@pytest.fixture()
|
||||
def create_dashboard_with_report(self):
|
||||
with self.create_app().app_context():
|
||||
|
|
@ -674,7 +695,41 @@ class TestDashboardApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixi
|
|||
rv = self.get_assert_metric(uri, "get_list")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["count"], 6)
|
||||
self.assertEqual(data["count"], 5)
|
||||
|
||||
@pytest.mark.usefixtures("create_created_by_admin_dashboards")
|
||||
def test_get_dashboards_created_by_me(self):
|
||||
"""
|
||||
Dashboard API: Test get dashboards created by current user
|
||||
"""
|
||||
query = {
|
||||
"columns": ["created_on_delta_humanized", "dashboard_title", "url"],
|
||||
"filters": [{"col": "created_by", "opr": "created_by_me", "value": "me"}],
|
||||
"order_column": "changed_on",
|
||||
"order_direction": "desc",
|
||||
"page": 0,
|
||||
"page_size": 100,
|
||||
}
|
||||
uri = f"api/v1/dashboard/?q={prison.dumps(query)}"
|
||||
self.login(username="admin")
|
||||
rv = self.client.get(uri)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
assert rv.status_code == 200
|
||||
assert len(data["result"]) == 2
|
||||
assert list(data["result"][0].keys()) == query["columns"]
|
||||
expected_results = [
|
||||
{
|
||||
"dashboard_title": "create_title1",
|
||||
"url": "/superset/dashboard/create_slug1/",
|
||||
},
|
||||
{
|
||||
"dashboard_title": "create_title0",
|
||||
"url": "/superset/dashboard/create_slug0/",
|
||||
},
|
||||
]
|
||||
for idx, response_item in enumerate(data["result"]):
|
||||
for key, value in expected_results[idx].items():
|
||||
assert response_item[key] == value
|
||||
|
||||
def create_dashboard_import(self):
|
||||
buf = BytesIO()
|
||||
|
|
|
|||
Loading…
Reference in New Issue