mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
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:
parent
9ae51f7a48
commit
d771ddbb94
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -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(
|
||||
() => [
|
||||
{
|
||||
|
@ -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),
|
||||
});
|
||||
|
@ -66,7 +66,7 @@ export interface MenuProps {
|
||||
isFrontendRoute?: (path?: string) => boolean;
|
||||
}
|
||||
|
||||
interface MenuObjectChildProps {
|
||||
export interface MenuObjectChildProps {
|
||||
label: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user