diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index 516eedc80f..b64a5ed649 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 3536e5aa22..fd307a42bc 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 = ( @@ -92,6 +95,15 @@ export default function ChartCard({ )} + {canExport && ( + handleBulkChartExport([chart])} + > + {t('Export')} + + )} {canEdit && ( openChartEditModal(original); + const handleExport = () => handleBulkChartExport([original]); + if (!canEdit && !canDelete && !canExport) { + return null; + } return ( @@ -277,6 +284,22 @@ function ChartList(props: ChartListProps) { )} )} + {canExport && ( + + + + + + )} {canEdit && ( {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 ( 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 962de6ead2..80407899ce 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 ( - {canEdit && ( - - - - - - )} {canDelete && ( )} + {canExport && ( + + + + + + )} + {canEdit && ( + + + + + + )} ); }, @@ -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 08cd9bdcc1..8cf43458c2 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 = ({ 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 = ({ Cell: ({ row: { original } }: any) => { const handleEdit = () => openDatasetEditModal(original); const handleDelete = () => openDatasetDeleteModal(original); - + const handleExport = () => handleBulkDatasetExport([original]); + if (!canEdit && !canDelete && !canExport) { + return null; + } return ( {canDelete && ( @@ -301,7 +305,22 @@ const DatasetList: FunctionComponent = ({ )} - + {canExport && ( + + + + + + )} {canEdit && ( = ({ disableSortBy: true, }, ], - [canEdit, canDelete, openDatasetEditModal], + [canEdit, canDelete, canExport, openDatasetEditModal], ); const filterTypes: Filters = useMemo( @@ -408,7 +427,7 @@ const DatasetList: FunctionComponent = ({ const buttonArr: Array = []; - if (canDelete) { + if (canDelete || canExport) { buttonArr.push({ name: t('Bulk Select'), onClick: toggleBulkSelect, @@ -473,6 +492,14 @@ const DatasetList: FunctionComponent = ({ ); }; + const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { + return window.location.assign( + `/api/v1/dataset/export/?q=${rison.encode( + datasetsToExport.map(({ id }) => id), + )}`, + ); + }; + return ( <> @@ -515,17 +542,23 @@ const DatasetList: FunctionComponent = ({ 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 ( className="dataset-list-view" diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 1fc457fdd3..27e32df3f5 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 012c7f7af7..936230d8b5 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 3f60ce56e3..1a418dbfbb 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: