mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
Front end for VERSIONED_EXPORT (#11559)
This commit is contained in:
parent
128ddfabb6
commit
d999802795
@ -31,6 +31,7 @@ export enum FeatureFlag {
|
|||||||
ENABLE_REACT_CRUD_VIEWS = 'ENABLE_REACT_CRUD_VIEWS',
|
ENABLE_REACT_CRUD_VIEWS = 'ENABLE_REACT_CRUD_VIEWS',
|
||||||
DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML',
|
DISPLAY_MARKDOWN_HTML = 'DISPLAY_MARKDOWN_HTML',
|
||||||
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
|
ESCAPE_MARKDOWN_HTML = 'ESCAPE_MARKDOWN_HTML',
|
||||||
|
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FeatureFlagMap = {
|
export type FeatureFlagMap = {
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||||
import Icon from 'src/components/Icon';
|
import Icon from 'src/components/Icon';
|
||||||
import Chart from 'src/types/Chart';
|
import Chart from 'src/types/Chart';
|
||||||
@ -27,7 +28,7 @@ import Label from 'src/components/Label';
|
|||||||
import { Dropdown, Menu } from 'src/common/components';
|
import { Dropdown, Menu } from 'src/common/components';
|
||||||
import FaveStar from 'src/components/FaveStar';
|
import FaveStar from 'src/components/FaveStar';
|
||||||
import FacePile from 'src/components/FacePile';
|
import FacePile from 'src/components/FacePile';
|
||||||
import { handleChartDelete } from '../utils';
|
import { handleBulkChartExport, handleChartDelete } from '../utils';
|
||||||
|
|
||||||
interface ChartCardProps {
|
interface ChartCardProps {
|
||||||
chart: Chart;
|
chart: Chart;
|
||||||
@ -56,6 +57,8 @@ export default function ChartCard({
|
|||||||
}: ChartCardProps) {
|
}: ChartCardProps) {
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
|
const canExport =
|
||||||
|
hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -92,6 +95,15 @@ export default function ChartCard({
|
|||||||
</ConfirmStatusChange>
|
</ConfirmStatusChange>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
|
{canExport && (
|
||||||
|
<Menu.Item
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => handleBulkChartExport([chart])}
|
||||||
|
>
|
||||||
|
<ListViewCard.MenuIcon name="share" /> {t('Export')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
data-test="chart-list-edit-option"
|
data-test="chart-list-edit-option"
|
||||||
|
@ -24,6 +24,7 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
|||||||
import {
|
import {
|
||||||
createFetchRelated,
|
createFetchRelated,
|
||||||
createErrorHandler,
|
createErrorHandler,
|
||||||
|
handleBulkChartExport,
|
||||||
handleChartDelete,
|
handleChartDelete,
|
||||||
} from 'src/views/CRUD/utils';
|
} from 'src/views/CRUD/utils';
|
||||||
import {
|
import {
|
||||||
@ -123,6 +124,8 @@ function ChartList(props: ChartListProps) {
|
|||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
|
const canExport =
|
||||||
|
hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
||||||
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
|
|
||||||
function handleBulkChartDelete(chartsToDelete: Chart[]) {
|
function handleBulkChartDelete(chartsToDelete: Chart[]) {
|
||||||
@ -245,6 +248,10 @@ function ChartList(props: ChartListProps) {
|
|||||||
refreshData,
|
refreshData,
|
||||||
);
|
);
|
||||||
const openEditModal = () => openChartEditModal(original);
|
const openEditModal = () => openChartEditModal(original);
|
||||||
|
const handleExport = () => handleBulkChartExport([original]);
|
||||||
|
if (!canEdit && !canDelete && !canExport) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="actions">
|
<span className="actions">
|
||||||
@ -277,6 +284,22 @@ function ChartList(props: ChartListProps) {
|
|||||||
)}
|
)}
|
||||||
</ConfirmStatusChange>
|
</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 && (
|
{canEdit && (
|
||||||
<TooltipWrapper
|
<TooltipWrapper
|
||||||
label="edit-action"
|
label="edit-action"
|
||||||
@ -302,7 +325,7 @@ function ChartList(props: ChartListProps) {
|
|||||||
hidden: !canEdit && !canDelete,
|
hidden: !canEdit && !canDelete,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[canEdit, canDelete, favoriteStatus],
|
[canEdit, canDelete, canExport, favoriteStatus],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters: Filters = [
|
const filters: Filters = [
|
||||||
@ -434,7 +457,7 @@ function ChartList(props: ChartListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const subMenuButtons: SubMenuProps['buttons'] = [];
|
const subMenuButtons: SubMenuProps['buttons'] = [];
|
||||||
if (canDelete) {
|
if (canDelete || canExport) {
|
||||||
subMenuButtons.push({
|
subMenuButtons.push({
|
||||||
name: t('Bulk Select'),
|
name: t('Bulk Select'),
|
||||||
buttonStyle: 'secondary',
|
buttonStyle: 'secondary',
|
||||||
@ -471,17 +494,23 @@ function ChartList(props: ChartListProps) {
|
|||||||
onConfirm={handleBulkChartDelete}
|
onConfirm={handleBulkChartDelete}
|
||||||
>
|
>
|
||||||
{confirmDelete => {
|
{confirmDelete => {
|
||||||
const bulkActions: ListViewProps['bulkActions'] = canDelete
|
const bulkActions: ListViewProps['bulkActions'] = [];
|
||||||
? [
|
if (canDelete) {
|
||||||
{
|
bulkActions.push({
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
name: t('Delete'),
|
name: t('Delete'),
|
||||||
onSelect: confirmDelete,
|
type: 'danger',
|
||||||
type: 'danger',
|
onSelect: confirmDelete,
|
||||||
},
|
});
|
||||||
]
|
}
|
||||||
: [];
|
if (canExport) {
|
||||||
|
bulkActions.push({
|
||||||
|
key: 'export',
|
||||||
|
name: t('Export'),
|
||||||
|
type: 'primary',
|
||||||
|
onSelect: handleBulkChartExport,
|
||||||
|
});
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ListView<Chart>
|
<ListView<Chart>
|
||||||
bulkActions={bulkActions}
|
bulkActions={bulkActions}
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
import { SupersetClient, t, styled } from '@superset-ui/core';
|
import { SupersetClient, t, styled } from '@superset-ui/core';
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import rison from 'rison';
|
||||||
|
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||||
import { createErrorHandler } from 'src/views/CRUD/utils';
|
import { createErrorHandler } from 'src/views/CRUD/utils';
|
||||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||||
@ -119,6 +121,8 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
|
const canExport =
|
||||||
|
hasPerm('can_mulexport') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
|
||||||
|
|
||||||
const menuData: SubMenuProps = {
|
const menuData: SubMenuProps = {
|
||||||
activeChild: 'Databases',
|
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 initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -238,25 +248,12 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
const handleEdit = () => handleDatabaseEdit(original);
|
const handleEdit = () => handleDatabaseEdit(original);
|
||||||
const handleDelete = () => openDatabaseDeleteModal(original);
|
const handleDelete = () => openDatabaseDeleteModal(original);
|
||||||
|
const handleExport = () => handleDatabaseExport(original);
|
||||||
|
if (!canEdit && !canDelete && !canExport) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span className="actions">
|
<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 && (
|
{canDelete && (
|
||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
@ -274,6 +271,38 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
</span>
|
</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>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -283,7 +312,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
|
|||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[canDelete, canEdit],
|
[canDelete, canEdit, canExport],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filters: Filters = useMemo(
|
const filters: Filters = useMemo(
|
||||||
|
@ -113,6 +113,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
const canEdit = hasPerm('can_edit');
|
const canEdit = hasPerm('can_edit');
|
||||||
const canDelete = hasPerm('can_delete');
|
const canDelete = hasPerm('can_delete');
|
||||||
const canCreate = hasPerm('can_add');
|
const canCreate = hasPerm('can_add');
|
||||||
|
const canExport = hasPerm('can_mulexport');
|
||||||
|
|
||||||
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||||
|
|
||||||
@ -282,7 +283,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
Cell: ({ row: { original } }: any) => {
|
Cell: ({ row: { original } }: any) => {
|
||||||
const handleEdit = () => openDatasetEditModal(original);
|
const handleEdit = () => openDatasetEditModal(original);
|
||||||
const handleDelete = () => openDatasetDeleteModal(original);
|
const handleDelete = () => openDatasetDeleteModal(original);
|
||||||
|
const handleExport = () => handleBulkDatasetExport([original]);
|
||||||
|
if (!canEdit && !canDelete && !canExport) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span className="actions">
|
<span className="actions">
|
||||||
{canDelete && (
|
{canDelete && (
|
||||||
@ -301,7 +305,22 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</TooltipWrapper>
|
</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 && (
|
{canEdit && (
|
||||||
<TooltipWrapper
|
<TooltipWrapper
|
||||||
label="edit-action"
|
label="edit-action"
|
||||||
@ -327,7 +346,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[canEdit, canDelete, openDatasetEditModal],
|
[canEdit, canDelete, canExport, openDatasetEditModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterTypes: Filters = useMemo(
|
const filterTypes: Filters = useMemo(
|
||||||
@ -408,7 +427,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
|
|
||||||
const buttonArr: Array<ButtonProps> = [];
|
const buttonArr: Array<ButtonProps> = [];
|
||||||
|
|
||||||
if (canDelete) {
|
if (canDelete || canExport) {
|
||||||
buttonArr.push({
|
buttonArr.push({
|
||||||
name: t('Bulk Select'),
|
name: t('Bulk Select'),
|
||||||
onClick: toggleBulkSelect,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubMenu {...menuData} />
|
<SubMenu {...menuData} />
|
||||||
@ -515,17 +542,23 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
onConfirm={handleBulkDatasetDelete}
|
onConfirm={handleBulkDatasetDelete}
|
||||||
>
|
>
|
||||||
{confirmDelete => {
|
{confirmDelete => {
|
||||||
const bulkActions: ListViewProps['bulkActions'] = canDelete
|
const bulkActions: ListViewProps['bulkActions'] = [];
|
||||||
? [
|
if (canDelete) {
|
||||||
{
|
bulkActions.push({
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
name: t('Delete'),
|
name: t('Delete'),
|
||||||
onSelect: confirmDelete,
|
onSelect: confirmDelete,
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
},
|
});
|
||||||
]
|
}
|
||||||
: [];
|
if (canExport) {
|
||||||
|
bulkActions.push({
|
||||||
|
key: 'export',
|
||||||
|
name: t('Export'),
|
||||||
|
type: 'primary',
|
||||||
|
onSelect: handleBulkDatasetExport,
|
||||||
|
});
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ListView<Dataset>
|
<ListView<Dataset>
|
||||||
className="dataset-list-view"
|
className="dataset-list-view"
|
||||||
|
@ -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[]) {
|
export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
|
||||||
return window.location.assign(
|
return window.location.assign(
|
||||||
`/api/v1/dashboard/export/?q=${rison.encode(
|
`/api/v1/dashboard/export/?q=${rison.encode(
|
||||||
|
@ -323,6 +323,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
|
|||||||
# When True, this escapes HTML (rather than rendering it) in Markdown components
|
# When True, this escapes HTML (rather than rendering it) in Markdown components
|
||||||
"ESCAPE_MARKDOWN_HTML": False,
|
"ESCAPE_MARKDOWN_HTML": False,
|
||||||
"SIP_34_ANNOTATIONS_UI": False,
|
"SIP_34_ANNOTATIONS_UI": False,
|
||||||
|
"VERSIONED_EXPORT": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set the default view to card/grid view if thumbnail support is enabled.
|
# Set the default view to card/grid view if thumbnail support is enabled.
|
||||||
|
@ -50,7 +50,7 @@ stats_logger = config["STATS_LOGGER"]
|
|||||||
|
|
||||||
def sqlalchemy_uri_form_validator(_: _, field: StringField) -> None:
|
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)
|
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:
|
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:
|
if field.data:
|
||||||
try:
|
try:
|
||||||
|
Loading…
Reference in New Issue
Block a user