You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by be...@apache.org on 2020/10/26 16:42:42 UTC
[incubator-superset] branch SO-1002 updated: Front end for
VERSIONED_EXPORT (#11373)
This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch SO-1002
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/SO-1002 by this push:
new 168bd6c Front end for VERSIONED_EXPORT (#11373)
168bd6c is described below
commit 168bd6ce55c2163e6c5692c3169a1867b2d3adf1
Author: Beto Dealmeida <ro...@dealmeida.net>
AuthorDate: Mon Oct 26 09:38:06 2020 -0700
Front end for VERSIONED_EXPORT (#11373)
---
superset-frontend/src/featureFlags.ts | 1 +
.../src/views/CRUD/chart/ChartList.tsx | 70 +++++++++++++++++-----
.../src/views/CRUD/data/database/DatabaseList.tsx | 63 +++++++++++++------
.../src/views/CRUD/data/dataset/DatasetList.tsx | 61 ++++++++++++++-----
superset/charts/api.py | 1 +
superset/charts/commands/export.py | 6 +-
superset/config.py | 1 +
superset/dashboards/commands/export.py | 6 +-
superset/databases/api.py | 1 +
superset/datasets/api.py | 3 +
superset/views/database/views.py | 4 +-
11 files changed, 166 insertions(+), 51 deletions(-)
diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts
index 46a3455..9c62256 100644
--- a/superset-frontend/src/featureFlags.ts
+++ b/superset-frontend/src/featureFlags.ts
@@ -28,6 +28,7 @@ export enum FeatureFlag {
SQLLAB_BACKEND_PERSISTENCE = 'SQLLAB_BACKEND_PERSISTENCE',
THUMBNAILS = 'THUMBNAILS',
LISTVIEWS_DEFAULT_CARD_VIEW = 'LISTVIEWS_DEFAULT_CARD_VIEW',
+ VERSIONED_EXPORT = 'VERSIONED_EXPORT',
}
export type FeatureFlagMap = {
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index ae64ae5..e559525 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -113,6 +113,8 @@ function ChartList(props: ChartListProps) {
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
+ const canExport =
+ hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
function openChartEditModal(chart: Chart) {
@@ -168,6 +170,14 @@ function ChartList(props: ChartListProps) {
);
}
+ function handleBulkChartExport(chartsToExport: Chart[]) {
+ return window.location.assign(
+ `/api/v1/chart/export/?q=${rison.encode(
+ chartsToExport.map(({ id }) => id),
+ )}`,
+ );
+ }
+
function renderFaveStar(id: number) {
return (
<FaveStar
@@ -268,7 +278,8 @@ function ChartList(props: ChartListProps) {
Cell: ({ row: { original } }: any) => {
const handleDelete = () => handleChartDelete(original);
const openEditModal = () => openChartEditModal(original);
- if (!canEdit && !canDelete) {
+ const handleExport = () => handleBulkChartExport([original]);
+ if (!canEdit && !canDelete && !canExport) {
return null;
}
@@ -303,6 +314,22 @@ function ChartList(props: ChartListProps) {
)}
</ConfirmStatusChange>
)}
+ {canExport && (
+ <TooltipWrapper
+ label="export-action"
+ tooltip={t('Export')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleExport}
+ >
+ <Icon name="share" />
+ </span>
+ </TooltipWrapper>
+ )}
{canEdit && (
<TooltipWrapper
label="edit-action"
@@ -327,7 +354,7 @@ function ChartList(props: ChartListProps) {
disableSortBy: true,
},
],
- [canEdit, canDelete],
+ [canEdit, canDelete, canExport],
);
const filters: Filters = [
@@ -457,6 +484,15 @@ function ChartList(props: ChartListProps) {
</ConfirmStatusChange>
</Menu.Item>
)}
+ {canExport && (
+ <Menu.Item
+ role="button"
+ tabIndex={0}
+ onClick={() => handleBulkChartExport([chart])}
+ >
+ <ListViewCard.MenuIcon name="share" /> Export
+ </Menu.Item>
+ )}
{canEdit && (
<Menu.Item
data-test="chart-list-edit-option"
@@ -495,7 +531,7 @@ function ChartList(props: ChartListProps) {
);
}
const subMenuButtons: SubMenuProps['buttons'] = [];
- if (canDelete) {
+ if (canDelete || canExport) {
subMenuButtons.push({
name: t('Bulk Select'),
buttonStyle: 'secondary',
@@ -533,17 +569,23 @@ function ChartList(props: ChartListProps) {
onConfirm={handleBulkChartDelete}
>
{confirmDelete => {
- const bulkActions: ListViewProps['bulkActions'] = canDelete
- ? [
- {
- key: 'delete',
- name: t('Delete'),
- onSelect: confirmDelete,
- type: 'danger',
- },
- ]
- : [];
-
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ type: 'danger',
+ onSelect: confirmDelete,
+ });
+ }
+ if (canExport) {
+ bulkActions.push({
+ key: 'export',
+ name: t('Export'),
+ type: 'primary',
+ onSelect: handleBulkChartExport,
+ });
+ }
return (
<ListView<Chart>
bulkActions={bulkActions}
diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
index 4b436ec..a5711b8 100644
--- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
+++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx
@@ -18,6 +18,8 @@
*/
import { SupersetClient, t, styled } from '@superset-ui/core';
import React, { useState, useMemo } from 'react';
+import rison from 'rison';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createErrorHandler } from 'src/views/CRUD/utils';
import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -119,6 +121,8 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
+ const canExport =
+ hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const menuData: SubMenuProps = {
activeChild: 'Databases',
@@ -144,6 +148,12 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
];
}
+ function handleDatabaseExport(database: DatabaseObject) {
+ return window.location.assign(
+ `/api/v1/database/export/?q=${rison.encode([database.id])}`,
+ );
+ }
+
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const columns = useMemo(
() => [
@@ -239,27 +249,12 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleDatabaseEdit(original);
const handleDelete = () => openDatabaseDeleteModal(original);
- if (!canEdit && !canDelete) {
+ const handleExport = () => handleDatabaseExport(original);
+ if (!canEdit && !canDelete && !canExport) {
return null;
}
return (
<span className="actions">
- {canEdit && (
- <TooltipWrapper
- label="edit-action"
- tooltip={t('Edit')}
- placement="bottom"
- >
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={handleEdit}
- >
- <Icon name="edit-alt" />
- </span>
- </TooltipWrapper>
- )}
{canDelete && (
<span
role="button"
@@ -277,6 +272,38 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
</TooltipWrapper>
</span>
)}
+ {canExport && (
+ <TooltipWrapper
+ label="export-action"
+ tooltip={t('Export')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleExport}
+ >
+ <Icon name="share" />
+ </span>
+ </TooltipWrapper>
+ )}
+ {canEdit && (
+ <TooltipWrapper
+ label="edit-action"
+ tooltip={t('Edit')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleEdit}
+ >
+ <Icon name="edit-alt" />
+ </span>
+ </TooltipWrapper>
+ )}
</span>
);
},
@@ -285,7 +312,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
disableSortBy: true,
},
],
- [canDelete, canEdit],
+ [canDelete, canEdit, canExport],
);
const filters: Filters = useMemo(
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index 8b8fcb9..97be23b 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -103,6 +103,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
const canCreate = hasPerm('can_add');
+ const canExport = hasPerm('can_mulexport');
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
@@ -250,7 +251,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Cell: ({ row: { original } }: any) => {
const handleEdit = () => openDatasetEditModal(original);
const handleDelete = () => openDatasetDeleteModal(original);
- if (!canEdit && !canDelete) {
+ const handleExport = () => handleBulkDatasetExport([original]);
+ if (!canEdit && !canDelete && !canExport) {
return null;
}
return (
@@ -271,7 +273,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
</span>
</TooltipWrapper>
)}
-
+ {canExport && (
+ <TooltipWrapper
+ label="export-action"
+ tooltip={t('Export')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleExport}
+ >
+ <Icon name="share" />
+ </span>
+ </TooltipWrapper>
+ )}
{canEdit && (
<TooltipWrapper
label="edit-action"
@@ -296,7 +313,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true,
},
],
- [canEdit, canDelete, openDatasetEditModal],
+ [canEdit, canDelete, canExport, openDatasetEditModal],
);
const filterTypes: Filters = useMemo(
@@ -377,7 +394,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const buttonArr: Array<ButtonProps> = [];
- if (canDelete) {
+ if (canDelete || canExport) {
buttonArr.push({
name: t('Bulk Select'),
onClick: toggleBulkSelect,
@@ -443,6 +460,14 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
);
};
+ const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => {
+ return window.location.assign(
+ `/api/v1/dataset/export/?q=${rison.encode(
+ datasetsToExport.map(({ id }) => id),
+ )}`,
+ );
+ };
+
return (
<>
<SubMenu {...menuData} />
@@ -485,17 +510,23 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
onConfirm={handleBulkDatasetDelete}
>
{confirmDelete => {
- const bulkActions: ListViewProps['bulkActions'] = canDelete
- ? [
- {
- key: 'delete',
- name: t('Delete'),
- onSelect: confirmDelete,
- type: 'danger',
- },
- ]
- : [];
-
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ onSelect: confirmDelete,
+ type: 'danger',
+ });
+ }
+ if (canExport) {
+ bulkActions.push({
+ key: 'export',
+ name: t('Export'),
+ type: 'primary',
+ onSelect: handleBulkDatasetExport,
+ });
+ }
return (
<ListView<Dataset>
className="dataset-list-view"
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 3ef27d0..b5873aa 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -174,6 +174,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
apispec_parameter_schemas = {
"screenshot_query_schema": screenshot_query_schema,
"get_delete_ids_schema": get_delete_ids_schema,
+ "get_export_ids_schema": get_export_ids_schema,
}
""" Add extra schemas to the OpenAPI components schema section """
openapi_spec_methods = openapi_spec_methods_override
diff --git a/superset/charts/commands/export.py b/superset/charts/commands/export.py
index 00e0fd4..807a0a3 100644
--- a/superset/charts/commands/export.py
+++ b/superset/charts/commands/export.py
@@ -74,8 +74,12 @@ class ExportChartsCommand(BaseCommand):
def run(self) -> Iterator[Tuple[str, str]]:
self.validate()
+ seen = set()
for chart in self._models:
- yield from self.export_chart(chart)
+ for file_name, file_content in self.export_chart(chart):
+ if file_name not in seen:
+ yield file_name, file_content
+ seen.add(file_name)
def validate(self) -> None:
self._models = ChartDAO.find_by_ids(self.chart_ids)
diff --git a/superset/config.py b/superset/config.py
index 8554e18..4f48ab8 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -310,6 +310,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"TAGGING_SYSTEM": False,
"SQLLAB_BACKEND_PERSISTENCE": False,
"LISTVIEWS_DEFAULT_CARD_VIEW": False,
+ "VERSIONED_EXPORT": False,
}
# Set the default view to card/grid view if thumbnail support is enabled.
diff --git a/superset/dashboards/commands/export.py b/superset/dashboards/commands/export.py
index 0f3cbc1..60fb0d3 100644
--- a/superset/dashboards/commands/export.py
+++ b/superset/dashboards/commands/export.py
@@ -71,8 +71,12 @@ class ExportDashboardsCommand(BaseCommand):
def run(self) -> Iterator[Tuple[str, str]]:
self.validate()
+ seen = set()
for dashboard in self._models:
- yield from self.export_dashboard(dashboard)
+ for file_name, file_content in self.export_dashboard(dashboard):
+ if file_name not in seen:
+ yield file_name, file_content
+ seen.add(file_name)
def validate(self) -> None:
self._models = DashboardDAO.find_by_ids(self.dashboard_ids)
diff --git a/superset/databases/api.py b/superset/databases/api.py
index ca1eb3b..f043455 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -165,6 +165,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
apispec_parameter_schemas = {
"database_schemas_query_schema": database_schemas_query_schema,
+ "get_export_ids_schema": get_export_ids_schema,
}
openapi_spec_tag = "Database"
openapi_spec_component_schemas = (
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index fff69bf..e7bf184 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -164,6 +164,9 @@ class DatasetRestApi(BaseSupersetModelRestApi):
allowed_rel_fields = {"database", "owners"}
allowed_distinct_fields = {"schema"}
+ apispec_parameter_schemas = {
+ "get_export_ids_schema": get_export_ids_schema,
+ }
openapi_spec_component_schemas = (DatasetRelatedObjectsResponse,)
@expose("/", methods=["POST"])
diff --git a/superset/views/database/views.py b/superset/views/database/views.py
index 0961381..a903c9b 100644
--- a/superset/views/database/views.py
+++ b/superset/views/database/views.py
@@ -50,7 +50,7 @@ stats_logger = config["STATS_LOGGER"]
def sqlalchemy_uri_form_validator(_: _, field: StringField) -> None:
"""
- Check if user has submitted a valid SQLAlchemy URI
+ Check if user has submitted a valid SQLAlchemy URI
"""
sqlalchemy_uri_validator(field.data, exception=ValidationError)
@@ -58,7 +58,7 @@ def sqlalchemy_uri_form_validator(_: _, field: StringField) -> None:
def certificate_form_validator(_: _, field: StringField) -> None:
"""
- Check if user has submitted a valid SSL certificate
+ Check if user has submitted a valid SSL certificate
"""
if field.data:
try: