feat: allow uploads in crud view (#18953)

* feat: allow uploads in crud view

* fix merge conflict and fix ts

* fix import

* fix tests

* fix lint

* remove unused var

* fix underline flash and alignment

* fix offset

* fix icon alignment

* fix labels and css issues

* make drowdown primary all the time

* make global

* fix lables

* add upload perms to utils

* remove unused code

* add suggested changes

* update menuright
This commit is contained in:
Phillip Kelley-Dotson 2022-03-21 13:01:36 -07:00 committed by GitHub
parent 9ae51f7a48
commit d771ddbb94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 181 additions and 28 deletions

View File

@ -18,6 +18,7 @@
*/
import React from 'react';
import thunk from 'redux-thunk';
import * as redux from 'react-redux';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { Provider } from 'react-redux';
@ -40,6 +41,17 @@ import { act } from 'react-dom/test-utils';
const mockStore = configureStore([thunk]);
const store = mockStore({});
const mockAppState = {
common: {
config: {
CSV_EXTENSIONS: ['csv'],
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
},
},
};
const databasesInfoEndpoint = 'glob:*/api/v1/database/_info*';
const databasesEndpoint = 'glob:*/api/v1/database/?*';
const databaseEndpoint = 'glob:*/api/v1/database/*';
@ -94,12 +106,22 @@ fetchMock.get(databaseRelatedEndpoint, {
},
});
const useSelectorMock = jest.spyOn(redux, 'useSelector');
describe('DatabaseList', () => {
useSelectorMock.mockReturnValue({
CSV_EXTENSIONS: ['csv'],
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
});
const wrapper = mount(
<Provider store={store}>
<DatabaseList user={mockUser} />
</Provider>,
);
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
});
@ -195,6 +217,7 @@ describe('RTL', () => {
<DatabaseList user={mockUser} />
</QueryParamProvider>,
{ useRedux: true },
mockAppState,
);
});

View File

@ -18,10 +18,11 @@
*/
import { SupersetClient, t, styled } from '@superset-ui/core';
import React, { useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import Loading from 'src/components/Loading';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { useListViewResource } from 'src/views/CRUD/hooks';
import { createErrorHandler } from 'src/views/CRUD/utils';
import { createErrorHandler, uploadUserPerms } from 'src/views/CRUD/utils';
import withToasts from 'src/components/MessageToasts/withToasts';
import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu';
import DeleteModal from 'src/components/DeleteModal';
@ -31,6 +32,8 @@ import ListView, { FilterOperator, Filters } from 'src/components/ListView';
import { commonMenuData } from 'src/views/CRUD/data/common';
import ImportModelsModal from 'src/components/ImportModal/index';
import handleResourceExport from 'src/utils/export';
import { ExtentionConfigs } from 'src/views/components/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import DatabaseModal from './DatabaseModal';
import { DatabaseObject } from './types';
@ -103,6 +106,15 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
const [importingDatabase, showImportModal] = useState<boolean>(false);
const [passwordFields, setPasswordFields] = useState<string[]>([]);
const [preparingExport, setPreparingExport] = useState<boolean>(false);
const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
const {
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
EXCEL_EXTENSIONS,
ALLOWED_EXTENSIONS,
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
const openDatabaseImportModal = () => {
showImportModal(true);
@ -171,8 +183,49 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
const canExport =
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const { canUploadCSV, canUploadColumnar, canUploadExcel } = uploadUserPerms(
roles,
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
EXCEL_EXTENSIONS,
ALLOWED_EXTENSIONS,
);
const uploadDropdownMenu = [
{
label: t('Upload file to database'),
childs: [
{
label: t('Upload CSV'),
name: 'Upload CSV file',
url: '/csvtodatabaseview/form',
perm: canUploadCSV,
},
{
label: t('Upload columnar file'),
name: 'Upload columnar file',
url: '/columnartodatabaseview/form',
perm: canUploadColumnar,
},
{
label: t('Upload Excel file'),
name: 'Upload Excel file',
url: '/exceltodatabaseview/form',
perm: canUploadExcel,
},
],
},
];
const filteredDropDown = uploadDropdownMenu.map(link => {
// eslint-disable-next-line no-param-reassign
link.childs = link.childs.filter(item => item.perm);
return link;
});
const menuData: SubMenuProps = {
activeChild: 'Databases',
dropDownLinks: filteredDropDown,
...commonMenuData,
};
@ -222,6 +275,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
}
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const columns = useMemo(
() => [
{

View File

@ -32,6 +32,7 @@ import rison from 'rison';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { FetchDataConfig } from 'src/components/ListView';
import SupersetText from 'src/utils/textUtils';
import findPermission from 'src/dashboard/util/findPermission';
import { Dashboard, Filters } from './types';
// Modifies the rison encoding slightly to match the backend's rison encoding/decoding. Applies globally.
@ -420,3 +421,21 @@ export const checkUploadExtensions = (
}
return false;
};
export const uploadUserPerms = (
roles: Record<string, [string, string][]>,
csvExt: Array<string>,
colExt: Array<string>,
excelExt: Array<string>,
allowedExt: Array<string>,
) => ({
canUploadCSV:
findPermission('can_this_form_get', 'CsvToDatabaseView', roles) &&
checkUploadExtensions(csvExt, allowedExt),
canUploadColumnar:
checkUploadExtensions(colExt, allowedExt) &&
findPermission('can_this_form_get', 'ColumnarToDatabaseView', roles),
canUploadExcel:
checkUploadExtensions(excelExt, allowedExt) &&
findPermission('can_this_form_get', 'ExcelToDatabaseView', roles),
});

View File

@ -66,7 +66,7 @@ export interface MenuProps {
isFrontendRoute?: (path?: string) => boolean;
}
interface MenuObjectChildProps {
export interface MenuObjectChildProps {
label: string;
name?: string;
icon?: string;

View File

@ -26,7 +26,7 @@ import { useSelector } from 'react-redux';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import LanguagePicker from './LanguagePicker';
import DatabaseModal from '../CRUD/data/database/DatabaseModal';
import { checkUploadExtensions } from '../CRUD/utils';
import { uploadUserPerms } from '../CRUD/utils';
import {
ExtentionConfigs,
GlobalMenuDataOptions,
@ -86,20 +86,12 @@ const RightMenu = ({
const canChart = findPermission('can_write', 'Chart', roles);
const canDatabase = findPermission('can_write', 'Database', roles);
const canUploadCSV = findPermission(
'can_this_form_get',
'CsvToDatabaseView',
roles,
);
const canUploadColumnar = findPermission(
'can_this_form_get',
'ColumnarToDatabaseView',
roles,
);
const canUploadExcel = findPermission(
'can_this_form_get',
'ExcelToDatabaseView',
const { canUploadCSV, canUploadColumnar, canUploadExcel } = uploadUserPerms(
roles,
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
EXCEL_EXTENSIONS,
ALLOWED_EXTENSIONS,
);
const canUpload = canUploadCSV || canUploadColumnar || canUploadExcel;
@ -123,25 +115,19 @@ const RightMenu = ({
label: t('Upload CSV to database'),
name: 'Upload a CSV',
url: '/csvtodatabaseview/form',
perm:
checkUploadExtensions(CSV_EXTENSIONS, ALLOWED_EXTENSIONS) &&
canUploadCSV,
perm: canUploadCSV,
},
{
label: t('Upload columnar file to database'),
name: 'Upload a Columnar file',
url: '/columnartodatabaseview/form',
perm:
checkUploadExtensions(COLUMNAR_EXTENSIONS, ALLOWED_EXTENSIONS) &&
canUploadColumnar,
perm: canUploadColumnar,
},
{
label: t('Upload Excel file to database'),
name: 'Upload Excel',
url: '/exceltodatabaseview/form',
perm:
checkUploadExtensions(EXCEL_EXTENSIONS, ALLOWED_EXTENSIONS) &&
canUploadExcel,
perm: canUploadExcel,
},
],
},

View File

@ -43,6 +43,19 @@ const mockedProps = {
usesRouter: false,
},
],
dropDownLinks: [
{
label: 'test a upload',
childs: [
{
label: 'Upload Test',
name: 'Upload Test',
url: '/test/form',
perm: true,
},
],
},
],
};
test('should render', () => {
@ -74,6 +87,13 @@ test('should render all the tabs links', () => {
});
});
test('should render dropdownlinks', async () => {
render(<SubMenu {...mockedProps} />);
userEvent.hover(screen.getByText('test a upload'));
const label = await screen.findByText('test a upload');
expect(label).toBeInTheDocument();
});
test('should render the buttons', () => {
const mockFunc = jest.fn();
const buttons = [
@ -94,7 +114,7 @@ test('should render the buttons', () => {
};
render(<SubMenu {...buttonsProps} />);
const testButton = screen.getByText(buttons[0].name);
expect(screen.getAllByRole('button')).toHaveLength(2);
expect(screen.getAllByRole('button')).toHaveLength(3);
userEvent.click(testButton);
expect(mockFunc).toHaveBeenCalled();
});

View File

@ -22,8 +22,10 @@ import { styled } from '@superset-ui/core';
import cx from 'classnames';
import { debounce } from 'lodash';
import { Row } from 'src/components';
import { Menu, MenuMode } from 'src/components/Menu';
import { Menu, MenuMode, MainNav as DropdownMenu } from 'src/components/Menu';
import Button, { OnClickHandler } from 'src/components/Button';
import Icons from 'src/components/Icons';
import { MenuObjectProps } from './Menu';
const StyledHeader = styled.div`
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
@ -39,11 +41,21 @@ const StyledHeader = styled.div`
.nav-right {
display: flex;
align-items: center;
padding: 14px 0;
padding: ${({ theme }) => theme.gridUnit * 3.5}px 0;
margin-right: ${({ theme }) => theme.gridUnit * 3}px;
float: right;
position: absolute;
right: 0;
ul.ant-menu-root {
padding: 0px;
}
li[role='menuitem'] {
border: 0;
border-bottom: none;
&:hover {
border-bottom: transparent;
}
}
}
.nav-right-collapse {
display: flex;
@ -58,8 +70,10 @@ const StyledHeader = styled.div`
.ant-menu-horizontal {
line-height: inherit;
.ant-menu-item {
border-bottom: none;
&:hover {
border-bottom: none;
text-decoration: none;
}
}
}
@ -117,6 +131,17 @@ const StyledHeader = styled.div`
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
}
}
.ant-menu-submenu {
span[role='img'] {
position: absolute;
right: ${({ theme }) => -theme.gridUnit + -2}px;
top: ${({ theme }) => theme.gridUnit + 1}px !important;
}
}
.dropdown-menu-links > div.ant-menu-submenu-title,
.ant-menu-submenu-open.ant-menu-submenu-active > div.ant-menu-submenu-title {
color: ${({ theme }) => theme.colors.primary.dark1};
}
`;
type MenuChild = {
@ -152,8 +177,11 @@ export interface SubMenuProps {
* otherwise, a 'You should not use <Link> outside a <Router>' error will be thrown */
usesRouter?: boolean;
color?: string;
dropDownLinks?: Array<MenuObjectProps>;
}
const { SubMenu } = DropdownMenu;
const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
const [showMenu, setMenu] = useState<MenuMode>('horizontal');
const [navRightStyle, setNavRightStyle] = useState('nav-right');
@ -177,6 +205,7 @@ const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
props.buttons.length >= 3 &&
window.innerWidth >= 795
) {
// eslint-disable-next-line no-unused-expressions
setNavRightStyle('nav-right');
} else if (
props.buttons &&
@ -231,6 +260,28 @@ const SubMenuComponent: React.FunctionComponent<SubMenuProps> = props => {
})}
</Menu>
<div className={navRightStyle}>
<Menu mode="horizontal" triggerSubMenuAction="click">
{props.dropDownLinks?.map((link, i) => (
<SubMenu
key={i}
title={link.label}
icon={<Icons.TriangleDown />}
popupOffset={[10, 20]}
className="dropdown-menu-links"
>
{link.childs?.map(item => {
if (typeof item === 'object') {
return (
<DropdownMenu.Item key={item.label}>
<a href={item.url}>{item.label}</a>
</DropdownMenu.Item>
);
}
return null;
})}
</SubMenu>
))}
</Menu>
{props.buttons?.map((btn, i) => (
<Button
key={i}