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:

Reply via email to