This is an automated email from the ASF dual-hosted git repository.
beto pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new d999802 Front end for VERSIONED_EXPORT (#11559)
d999802 is described below
commit d999802795eded6de03e6a4b3894990c353640b8
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed Nov 4 14:22:12 2020 -0800
Front end for VERSIONED_EXPORT (#11559)
---
superset-frontend/src/featureFlags.ts | 1 +
.../src/views/CRUD/chart/ChartCard.tsx | 14 ++++-
.../src/views/CRUD/chart/ChartList.tsx | 55 +++++++++++++-----
.../src/views/CRUD/data/database/DatabaseList.tsx | 65 ++++++++++++++++------
.../src/views/CRUD/data/dataset/DatasetList.tsx | 63 ++++++++++++++++-----
superset-frontend/src/views/CRUD/utils.tsx | 8 +++
superset/config.py | 1 +
superset/views/database/views.py | 4 +-
8 files changed, 162 insertions(+), 49 deletions(-)
diff --git a/superset-frontend/src/featureFlags.ts
b/superset-frontend/src/featureFlags.ts
index 516eedc..b64a5ed 100644
--- a/superset-frontend/src/featureFlags.ts
+++ b/superset-frontend/src/featureFlags.ts
@@ -31,6 +31,7 @@ export enum FeatureFlag {
ENABLE_REACT_CRUD_VIEWS = 'ENABLE_REACT_CRUD_VIEWS',
DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML',
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
+ VERSIONED_EXPORT = 'VERSIONED_EXPORT',
}
export type FeatureFlagMap = {
diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
index 3536e5a..fd307a4 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
@@ -18,6 +18,7 @@
*/
import React from 'react';
import { t } from '@superset-ui/core';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import Icon from 'src/components/Icon';
import Chart from 'src/types/Chart';
@@ -27,7 +28,7 @@ import Label from 'src/components/Label';
import { Dropdown, Menu } from 'src/common/components';
import FaveStar from 'src/components/FaveStar';
import FacePile from 'src/components/FacePile';
-import { handleChartDelete } from '../utils';
+import { handleBulkChartExport, handleChartDelete } from '../utils';
interface ChartCardProps {
chart: Chart;
@@ -56,6 +57,8 @@ export default function ChartCard({
}: ChartCardProps) {
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
+ const canExport =
+ hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const menu = (
<Menu>
@@ -92,6 +95,15 @@ export default function ChartCard({
</ConfirmStatusChange>
</Menu.Item>
)}
+ {canExport && (
+ <Menu.Item
+ role="button"
+ tabIndex={0}
+ onClick={() => handleBulkChartExport([chart])}
+ >
+ <ListViewCard.MenuIcon name="share" /> {t('Export')}
+ </Menu.Item>
+ )}
{canEdit && (
<Menu.Item
data-test="chart-list-edit-option"
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 8f42b03..a7e727c 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -24,6 +24,7 @@ import { isFeatureEnabled, FeatureFlag } from
'src/featureFlags';
import {
createFetchRelated,
createErrorHandler,
+ handleBulkChartExport,
handleChartDelete,
} from 'src/views/CRUD/utils';
import {
@@ -123,6 +124,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 handleBulkChartDelete(chartsToDelete: Chart[]) {
@@ -245,6 +248,10 @@ function ChartList(props: ChartListProps) {
refreshData,
);
const openEditModal = () => openChartEditModal(original);
+ const handleExport = () => handleBulkChartExport([original]);
+ if (!canEdit && !canDelete && !canExport) {
+ return null;
+ }
return (
<span className="actions">
@@ -277,6 +284,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"
@@ -302,7 +325,7 @@ function ChartList(props: ChartListProps) {
hidden: !canEdit && !canDelete,
},
],
- [canEdit, canDelete, favoriteStatus],
+ [canEdit, canDelete, canExport, favoriteStatus],
);
const filters: Filters = [
@@ -434,7 +457,7 @@ function ChartList(props: ChartListProps) {
);
}
const subMenuButtons: SubMenuProps['buttons'] = [];
- if (canDelete) {
+ if (canDelete || canExport) {
subMenuButtons.push({
name: t('Bulk Select'),
buttonStyle: 'secondary',
@@ -471,17 +494,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 962de6e..8040789 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',
@@ -143,6 +147,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(
() => [
@@ -238,25 +248,12 @@ function DatabaseList({ addDangerToast, addSuccessToast
}: DatabaseListProps) {
Cell: ({ row: { original } }: any) => {
const handleEdit = () => handleDatabaseEdit(original);
const handleDelete = () => openDatabaseDeleteModal(original);
-
+ 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"
@@ -274,6 +271,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>
);
},
@@ -283,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 08cd9bd..8cf4345 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -113,6 +113,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 }];
@@ -282,7 +283,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Cell: ({ row: { original } }: any) => {
const handleEdit = () => openDatasetEditModal(original);
const handleDelete = () => openDatasetDeleteModal(original);
-
+ const handleExport = () => handleBulkDatasetExport([original]);
+ if (!canEdit && !canDelete && !canExport) {
+ return null;
+ }
return (
<span className="actions">
{canDelete && (
@@ -301,7 +305,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"
@@ -327,7 +346,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true,
},
],
- [canEdit, canDelete, openDatasetEditModal],
+ [canEdit, canDelete, canExport, openDatasetEditModal],
);
const filterTypes: Filters = useMemo(
@@ -408,7 +427,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const buttonArr: Array<ButtonProps> = [];
- if (canDelete) {
+ if (canDelete || canExport) {
buttonArr.push({
name: t('Bulk Select'),
onClick: toggleBulkSelect,
@@ -473,6 +492,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} />
@@ -515,17 +542,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-frontend/src/views/CRUD/utils.tsx
b/superset-frontend/src/views/CRUD/utils.tsx
index 1fc457f..27e32df 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -183,6 +183,14 @@ export function handleChartDelete(
);
}
+export function handleBulkChartExport(chartsToExport: Chart[]) {
+ return window.location.assign(
+ `/api/v1/chart/export/?q=${rison.encode(
+ chartsToExport.map(({ id }) => id),
+ )}`,
+ );
+}
+
export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
return window.location.assign(
`/api/v1/dashboard/export/?q=${rison.encode(
diff --git a/superset/config.py b/superset/config.py
index 012c7f7..936230d 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -323,6 +323,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
# When True, this escapes HTML (rather than rendering it) in Markdown
components
"ESCAPE_MARKDOWN_HTML": False,
"SIP_34_ANNOTATIONS_UI": False,
+ "VERSIONED_EXPORT": False,
}
# Set the default view to card/grid view if thumbnail support is enabled.
diff --git a/superset/views/database/views.py b/superset/views/database/views.py
index 3f60ce5..1a418db 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: