mirror of
https://github.com/apache/superset.git
synced 2024-09-18 11:39:49 -04:00
feat(rightmenu): Add Datasets to + Menu and Hide Databases when one has been connected (#21530)
This commit is contained in:
parent
175ec854b9
commit
c19708b432
@ -17,7 +17,6 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { FunctionComponent, useState, useEffect } from 'react';
|
import React, { FunctionComponent, useState, useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
import { useSingleViewResource } from 'src/views/CRUD/hooks';
|
||||||
import Modal from 'src/components/Modal';
|
import Modal from 'src/components/Modal';
|
||||||
@ -29,6 +28,7 @@ import {
|
|||||||
LocalStorageKeys,
|
LocalStorageKeys,
|
||||||
setItem,
|
setItem,
|
||||||
} from 'src/utils/localStorageHelpers';
|
} from 'src/utils/localStorageHelpers';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
type DatasetAddObject = {
|
type DatasetAddObject = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -42,6 +42,7 @@ interface DatasetModalProps {
|
|||||||
onDatasetAdd?: (dataset: DatasetAddObject) => void;
|
onDatasetAdd?: (dataset: DatasetAddObject) => void;
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
history?: any; // So we can render the modal when not using SPA
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableSelectorContainer = styled.div`
|
const TableSelectorContainer = styled.div`
|
||||||
@ -54,8 +55,8 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
|||||||
onDatasetAdd,
|
onDatasetAdd,
|
||||||
onHide,
|
onHide,
|
||||||
show,
|
show,
|
||||||
|
history,
|
||||||
}) => {
|
}) => {
|
||||||
const history = useHistory();
|
|
||||||
const [currentDatabase, setCurrentDatabase] = useState<
|
const [currentDatabase, setCurrentDatabase] = useState<
|
||||||
DatabaseObject | undefined
|
DatabaseObject | undefined
|
||||||
>();
|
>();
|
||||||
@ -128,8 +129,16 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
|||||||
if (onDatasetAdd) {
|
if (onDatasetAdd) {
|
||||||
onDatasetAdd({ id: response.id, ...response });
|
onDatasetAdd({ id: response.id, ...response });
|
||||||
}
|
}
|
||||||
history.push(`/chart/add?dataset=${currentTableName}`);
|
// We need to be able to work with no SPA routes opening the modal
|
||||||
cleanup();
|
// So useHistory wont be available always thus we check for it
|
||||||
|
if (!isEmpty(history)) {
|
||||||
|
history?.push(`/chart/add?dataset=${currentTableName}`);
|
||||||
|
cleanup();
|
||||||
|
} else {
|
||||||
|
window.location.href = `/chart/add?dataset=${currentTableName}`;
|
||||||
|
cleanup();
|
||||||
|
onHide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -725,6 +725,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
show={datasetAddModalOpen}
|
show={datasetAddModalOpen}
|
||||||
onHide={closeDatasetAddModal}
|
onHide={closeDatasetAddModal}
|
||||||
onDatasetAdd={refreshData}
|
onDatasetAdd={refreshData}
|
||||||
|
history={history}
|
||||||
/>
|
/>
|
||||||
{datasetCurrentlyDeleting && (
|
{datasetCurrentlyDeleting && (
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
|
@ -19,22 +19,90 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as reactRedux from 'react-redux';
|
import * as reactRedux from 'react-redux';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import { styledMount as mount } from 'spec/helpers/theming';
|
import userEvent from '@testing-library/user-event';
|
||||||
import RightMenu from './RightMenu';
|
import RightMenu from './RightMenu';
|
||||||
import { RightMenuProps } from './types';
|
import { GlobalMenuDataOptions, RightMenuProps } from './types';
|
||||||
|
|
||||||
jest.mock('react-redux', () => ({
|
jest.mock('react-redux', () => ({
|
||||||
...jest.requireActual('react-redux'),
|
...jest.requireActual('react-redux'),
|
||||||
useSelector: jest.fn(),
|
useSelector: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('src/views/CRUD/data/database/DatabaseModal', () => () => <span />);
|
||||||
|
jest.mock('src/views/CRUD/data/dataset/AddDatasetModal.tsx', () => () => (
|
||||||
|
<span />
|
||||||
|
));
|
||||||
|
|
||||||
|
const dropdownItems = [
|
||||||
|
{
|
||||||
|
label: 'Data',
|
||||||
|
icon: 'fa-database',
|
||||||
|
childs: [
|
||||||
|
{
|
||||||
|
label: 'Connect database',
|
||||||
|
name: GlobalMenuDataOptions.DB_CONNECTION,
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Create dataset',
|
||||||
|
name: GlobalMenuDataOptions.DATASET_CREATION,
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Connect Google Sheet',
|
||||||
|
name: GlobalMenuDataOptions.GOOGLE_SHEETS,
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Upload CSV to database',
|
||||||
|
name: 'Upload a CSV',
|
||||||
|
url: '/csvtodatabaseview/form',
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Upload columnar file to database',
|
||||||
|
name: 'Upload a Columnar file',
|
||||||
|
url: '/columnartodatabaseview/form',
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Upload Excel file to database',
|
||||||
|
name: 'Upload Excel',
|
||||||
|
url: '/exceltodatabaseview/form',
|
||||||
|
perm: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SQL query',
|
||||||
|
url: '/superset/sqllab?new=true',
|
||||||
|
icon: 'fa-fw fa-search',
|
||||||
|
perm: 'can_sqllab',
|
||||||
|
view: 'Superset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Chart',
|
||||||
|
url: '/chart/add',
|
||||||
|
icon: 'fa-fw fa-bar-chart',
|
||||||
|
perm: 'can_write',
|
||||||
|
view: 'Chart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dashboard',
|
||||||
|
url: '/dashboard/new',
|
||||||
|
icon: 'fa-fw fa-dashboard',
|
||||||
|
perm: 'can_write',
|
||||||
|
view: 'Dashboard',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const createProps = (): RightMenuProps => ({
|
const createProps = (): RightMenuProps => ({
|
||||||
align: 'flex-end',
|
align: 'flex-end',
|
||||||
navbarRight: {
|
navbarRight: {
|
||||||
show_watermark: false,
|
show_watermark: false,
|
||||||
bug_report_url: '/report/',
|
bug_report_url: undefined,
|
||||||
documentation_url: '/docs/',
|
documentation_url: undefined,
|
||||||
languages: {
|
languages: {
|
||||||
en: {
|
en: {
|
||||||
flag: 'us',
|
flag: 'us',
|
||||||
@ -47,8 +115,8 @@ const createProps = (): RightMenuProps => ({
|
|||||||
url: '/lang/it',
|
url: '/lang/it',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
show_language_picker: true,
|
show_language_picker: false,
|
||||||
user_is_anonymous: true,
|
user_is_anonymous: false,
|
||||||
user_info_url: '/users/userinfo/',
|
user_info_url: '/users/userinfo/',
|
||||||
user_logout_url: '/logout/',
|
user_logout_url: '/logout/',
|
||||||
user_login_url: '/login/',
|
user_login_url: '/login/',
|
||||||
@ -58,38 +126,15 @@ const createProps = (): RightMenuProps => ({
|
|||||||
version_sha: 'randomSHA',
|
version_sha: 'randomSHA',
|
||||||
build_number: 'randomBuildNumber',
|
build_number: 'randomBuildNumber',
|
||||||
},
|
},
|
||||||
settings: [
|
settings: [],
|
||||||
{
|
|
||||||
name: 'Security',
|
|
||||||
icon: 'fa-cogs',
|
|
||||||
label: 'Security',
|
|
||||||
index: 1,
|
|
||||||
childs: [
|
|
||||||
{
|
|
||||||
name: 'List Users',
|
|
||||||
icon: 'fa-user',
|
|
||||||
label: 'List Users',
|
|
||||||
url: '/users/list/',
|
|
||||||
index: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isFrontendRoute: () => true,
|
isFrontendRoute: () => true,
|
||||||
environmentTag: {
|
environmentTag: {
|
||||||
color: 'error.base',
|
color: 'error.base',
|
||||||
text: 'Development',
|
text: 'Development2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
const mockNonExamplesDB = [...new Array(2)].map((_, i) => ({
|
||||||
const useStateMock = jest.spyOn(React, 'useState');
|
|
||||||
|
|
||||||
let setShowModal: any;
|
|
||||||
let setEngine: any;
|
|
||||||
let setAllowUploads: any;
|
|
||||||
|
|
||||||
const mockNonGSheetsDBs = [...new Array(2)].map((_, i) => ({
|
|
||||||
changed_by: {
|
changed_by: {
|
||||||
first_name: `user`,
|
first_name: `user`,
|
||||||
last_name: `${i}`,
|
last_name: `${i}`,
|
||||||
@ -108,161 +153,205 @@ const mockNonGSheetsDBs = [...new Array(2)].map((_, i) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockGsheetsDbs = [...new Array(2)].map((_, i) => ({
|
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||||
changed_by: {
|
|
||||||
first_name: `user`,
|
|
||||||
last_name: `${i}`,
|
|
||||||
},
|
|
||||||
database_name: `db ${i}`,
|
|
||||||
backend: 'gsheets',
|
|
||||||
allow_run_async: true,
|
|
||||||
allow_dml: false,
|
|
||||||
allow_file_upload: true,
|
|
||||||
expose_in_sqllab: false,
|
|
||||||
changed_on_delta_humanized: `${i} day(s) ago`,
|
|
||||||
changed_on: new Date().toISOString,
|
|
||||||
id: i,
|
|
||||||
engine_information: {
|
|
||||||
supports_file_upload: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('RightMenu', () => {
|
beforeEach(async () => {
|
||||||
const mockedProps = createProps();
|
useSelectorMock.mockReset();
|
||||||
|
fetchMock.get(
|
||||||
beforeEach(async () => {
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
useSelectorMock.mockReset();
|
{ result: [], count: 0 },
|
||||||
useStateMock.mockReset();
|
);
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
|
||||||
{ result: [], database_count: 0 },
|
{ result: [], count: 0 },
|
||||||
);
|
);
|
||||||
// By default we get file extensions to be uploaded
|
});
|
||||||
useSelectorMock.mockReturnValue({
|
|
||||||
CSV_EXTENSIONS: ['csv'],
|
afterEach(fetchMock.restore);
|
||||||
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
|
|
||||||
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
|
const resetUseSelectorMock = () => {
|
||||||
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
|
useSelectorMock.mockReturnValueOnce({
|
||||||
});
|
createdOn: '2021-04-27T18:12:38.952304',
|
||||||
setShowModal = jest.fn();
|
email: 'admin',
|
||||||
setEngine = jest.fn();
|
firstName: 'admin',
|
||||||
setAllowUploads = jest.fn();
|
isActive: true,
|
||||||
const mockSetStateModal: any = (x: any) => [x, setShowModal];
|
lastName: 'admin',
|
||||||
const mockSetStateEngine: any = (x: any) => [x, setEngine];
|
permissions: {},
|
||||||
const mockSetStateAllow: any = (x: any) => [x, setAllowUploads];
|
roles: {
|
||||||
useStateMock.mockImplementationOnce(mockSetStateModal);
|
Admin: [
|
||||||
useStateMock.mockImplementationOnce(mockSetStateEngine);
|
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
||||||
useStateMock.mockImplementationOnce(mockSetStateAllow);
|
['can_write', 'Database'], // So we can write DBs
|
||||||
});
|
['can_write', 'Dataset'], // So we can write Datasets
|
||||||
afterEach(fetchMock.restore);
|
['can_write', 'Chart'], // So we can write Datasets
|
||||||
it('renders', async () => {
|
],
|
||||||
const wrapper = mount(<RightMenu {...mockedProps} />);
|
},
|
||||||
await waitForComponentToPaint(wrapper);
|
userId: 1,
|
||||||
expect(wrapper.find(RightMenu)).toExist();
|
username: 'admin',
|
||||||
});
|
});
|
||||||
it('If user has permission to upload files we query the existing DBs that has allow_file_upload as True', async () => {
|
|
||||||
useSelectorMock.mockReturnValueOnce({
|
// By default we get file extensions to be uploaded
|
||||||
createdOn: '2021-04-27T18:12:38.952304',
|
useSelectorMock.mockReturnValueOnce('1');
|
||||||
email: 'admin',
|
// By default we get file extensions to be uploaded
|
||||||
firstName: 'admin',
|
useSelectorMock.mockReturnValueOnce({
|
||||||
isActive: true,
|
CSV_EXTENSIONS: ['csv'],
|
||||||
lastName: 'admin',
|
EXCEL_EXTENSIONS: ['xls', 'xlsx'],
|
||||||
permissions: {},
|
COLUMNAR_EXTENSIONS: ['parquet', 'zip'],
|
||||||
roles: {
|
ALLOWED_EXTENSIONS: ['parquet', 'zip', 'xls', 'xlsx', 'csv'],
|
||||||
Admin: [
|
});
|
||||||
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
};
|
||||||
],
|
|
||||||
},
|
test('renders', async () => {
|
||||||
userId: 1,
|
const mockedProps = createProps();
|
||||||
username: 'admin',
|
// Initial Load
|
||||||
});
|
resetUseSelectorMock();
|
||||||
// Second call we get the dashboardId
|
const { container } = render(<RightMenu {...mockedProps} />, {
|
||||||
useSelectorMock.mockReturnValueOnce('1');
|
useRedux: true,
|
||||||
const wrapper = mount(<RightMenu {...mockedProps} />);
|
useQueryParams: true,
|
||||||
await waitForComponentToPaint(wrapper);
|
});
|
||||||
const callsD = fetchMock.calls(/database\/\?q/);
|
// expect(await screen.findByText(/Settings/i)).toBeInTheDocument();
|
||||||
expect(callsD).toHaveLength(1);
|
await waitFor(() => expect(container).toBeInTheDocument());
|
||||||
expect(callsD[0][0]).toMatchInlineSnapshot(
|
});
|
||||||
`"http://localhost/api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))"`,
|
|
||||||
);
|
test('If user has permission to upload files AND connect DBs we query existing DBs that has allow_file_upload as True and DBs that are not examples', async () => {
|
||||||
});
|
const mockedProps = createProps();
|
||||||
it('If user has no permission to upload files the query API should not be called', async () => {
|
// Initial Load
|
||||||
useSelectorMock.mockReturnValueOnce({
|
resetUseSelectorMock();
|
||||||
createdOn: '2021-04-27T18:12:38.952304',
|
const { container } = render(<RightMenu {...mockedProps} />, {
|
||||||
email: 'admin',
|
useRedux: true,
|
||||||
firstName: 'admin',
|
useQueryParams: true,
|
||||||
isActive: true,
|
});
|
||||||
lastName: 'admin',
|
await waitFor(() => expect(container).toBeVisible());
|
||||||
permissions: {},
|
const callsD = fetchMock.calls(/database\/\?q/);
|
||||||
roles: {
|
expect(callsD).toHaveLength(2);
|
||||||
Admin: [['can_write', 'Chart']], // no file permissions
|
expect(callsD[0][0]).toMatchInlineSnapshot(
|
||||||
},
|
`"http://localhost/api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))"`,
|
||||||
userId: 1,
|
);
|
||||||
username: 'admin',
|
expect(callsD[1][0]).toMatchInlineSnapshot(
|
||||||
});
|
`"http://localhost/api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))"`,
|
||||||
// Second call we get the dashboardId
|
);
|
||||||
useSelectorMock.mockReturnValueOnce('1');
|
});
|
||||||
const wrapper = mount(<RightMenu {...mockedProps} />);
|
|
||||||
await waitForComponentToPaint(wrapper);
|
test('If only examples DB exist we must show the Connect Database option', async () => {
|
||||||
const callsD = fetchMock.calls(/database\/\?q/);
|
const mockedProps = createProps();
|
||||||
expect(callsD).toHaveLength(0);
|
fetchMock.get(
|
||||||
});
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
it('If user has permission to upload files but there are only gsheets and clickhouse DBs', async () => {
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
fetchMock.get(
|
{ overwriteRoutes: true },
|
||||||
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
);
|
||||||
{ result: [...mockGsheetsDbs], database_count: 2 },
|
fetchMock.get(
|
||||||
{ overwriteRoutes: true },
|
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
|
||||||
);
|
{ result: [], count: 0 },
|
||||||
useSelectorMock.mockReturnValueOnce({
|
{ overwriteRoutes: true },
|
||||||
createdOn: '2021-04-27T18:12:38.952304',
|
);
|
||||||
email: 'admin',
|
// Initial Load
|
||||||
firstName: 'admin',
|
resetUseSelectorMock();
|
||||||
isActive: true,
|
// setAllowUploads called
|
||||||
lastName: 'admin',
|
resetUseSelectorMock();
|
||||||
permissions: {},
|
render(<RightMenu {...mockedProps} />, {
|
||||||
roles: {
|
useRedux: true,
|
||||||
Admin: [
|
useQueryParams: true,
|
||||||
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
useRouter: true,
|
||||||
],
|
});
|
||||||
},
|
const dropdown = screen.getByTestId('new-dropdown-icon');
|
||||||
userId: 1,
|
userEvent.hover(dropdown);
|
||||||
username: 'admin',
|
const dataMenu = await screen.findByText(dropdownItems[0].label);
|
||||||
});
|
userEvent.hover(dataMenu);
|
||||||
// Second call we get the dashboardId
|
expect(await screen.findByText('Connect database')).toBeInTheDocument();
|
||||||
useSelectorMock.mockReturnValueOnce('1');
|
expect(screen.queryByText('Create dataset')).not.toBeInTheDocument();
|
||||||
const wrapper = mount(<RightMenu {...mockedProps} />);
|
});
|
||||||
await waitForComponentToPaint(wrapper);
|
|
||||||
const callsD = fetchMock.calls(/database\/\?q/);
|
test('If more than just examples DB exist we must show the Create dataset option', async () => {
|
||||||
expect(callsD).toHaveLength(1);
|
const mockedProps = createProps();
|
||||||
expect(setAllowUploads).toHaveBeenCalledWith(false);
|
fetchMock.get(
|
||||||
});
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
it('If user has permission to upload files and some DBs with allow_file_upload are not gsheets nor clickhouse', async () => {
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
fetchMock.get(
|
{ overwriteRoutes: true },
|
||||||
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
);
|
||||||
{ result: [...mockNonGSheetsDBs, ...mockGsheetsDbs], database_count: 2 },
|
fetchMock.get(
|
||||||
{ overwriteRoutes: true },
|
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
|
||||||
);
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
useSelectorMock.mockReturnValueOnce({
|
{ overwriteRoutes: true },
|
||||||
createdOn: '2021-04-27T18:12:38.952304',
|
);
|
||||||
email: 'admin',
|
// Initial Load
|
||||||
firstName: 'admin',
|
resetUseSelectorMock();
|
||||||
isActive: true,
|
// setAllowUploads called
|
||||||
lastName: 'admin',
|
resetUseSelectorMock();
|
||||||
permissions: {},
|
// setNonExamplesDBConnected called
|
||||||
roles: {
|
resetUseSelectorMock();
|
||||||
Admin: [
|
render(<RightMenu {...mockedProps} />, {
|
||||||
['can_this_form_get', 'CsvToDatabaseView'], // So we can upload CSV
|
useRedux: true,
|
||||||
],
|
useQueryParams: true,
|
||||||
},
|
useRouter: true,
|
||||||
userId: 1,
|
});
|
||||||
username: 'admin',
|
const dropdown = screen.getByTestId('new-dropdown-icon');
|
||||||
});
|
userEvent.hover(dropdown);
|
||||||
// Second call we get the dashboardId
|
const dataMenu = await screen.findByText(dropdownItems[0].label);
|
||||||
useSelectorMock.mockReturnValueOnce('1');
|
userEvent.hover(dataMenu);
|
||||||
const wrapper = mount(<RightMenu {...mockedProps} />);
|
expect(await screen.findByText('Create dataset')).toBeInTheDocument();
|
||||||
await waitForComponentToPaint(wrapper);
|
expect(screen.queryByText('Connect database')).not.toBeInTheDocument();
|
||||||
const callsD = fetchMock.calls(/database\/\?q/);
|
});
|
||||||
expect(callsD).toHaveLength(1);
|
|
||||||
expect(setAllowUploads).toHaveBeenCalledWith(true);
|
test('If there is a DB with allow_file_upload set as True the option should be enabled', async () => {
|
||||||
});
|
const mockedProps = createProps();
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
|
||||||
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
// Initial load
|
||||||
|
resetUseSelectorMock();
|
||||||
|
// setAllowUploads called
|
||||||
|
resetUseSelectorMock();
|
||||||
|
// setNonExamplesDBConnected called
|
||||||
|
resetUseSelectorMock();
|
||||||
|
render(<RightMenu {...mockedProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
useQueryParams: true,
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
|
const dropdown = screen.getByTestId('new-dropdown-icon');
|
||||||
|
userEvent.hover(dropdown);
|
||||||
|
const dataMenu = await screen.findByText(dropdownItems[0].label);
|
||||||
|
userEvent.hover(dataMenu);
|
||||||
|
expect(
|
||||||
|
(await screen.findByText('Upload CSV to database')).closest('a'),
|
||||||
|
).toHaveAttribute('href', '/csvtodatabaseview/form');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('If there is NOT a DB with allow_file_upload set as True the option should be disabled', async () => {
|
||||||
|
const mockedProps = createProps();
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
|
||||||
|
{ result: [], count: 0 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
fetchMock.get(
|
||||||
|
'glob:*api/v1/database/?q=(filters:!((col:database_name,opr:neq,value:examples)))',
|
||||||
|
{ result: [...mockNonExamplesDB], count: 2 },
|
||||||
|
{ overwriteRoutes: true },
|
||||||
|
);
|
||||||
|
// Initial load
|
||||||
|
resetUseSelectorMock();
|
||||||
|
// setAllowUploads called
|
||||||
|
resetUseSelectorMock();
|
||||||
|
// setNonExamplesDBConnected called
|
||||||
|
resetUseSelectorMock();
|
||||||
|
render(<RightMenu {...mockedProps} />, {
|
||||||
|
useRedux: true,
|
||||||
|
useQueryParams: true,
|
||||||
|
useRouter: true,
|
||||||
|
});
|
||||||
|
const dropdown = screen.getByTestId('new-dropdown-icon');
|
||||||
|
userEvent.hover(dropdown);
|
||||||
|
const dataMenu = await screen.findByText(dropdownItems[0].label);
|
||||||
|
userEvent.hover(dataMenu);
|
||||||
|
expect(await screen.findByText('Upload CSV to database')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
(await screen.findByText('Upload CSV to database')).closest('a'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,12 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { Fragment, useEffect } from 'react';
|
import React, { Fragment, useState, useEffect } from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useQueryParams, BooleanParam } from 'use-query-params';
|
import { useQueryParams, BooleanParam } from 'use-query-params';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
t,
|
t,
|
||||||
@ -48,6 +49,7 @@ import {
|
|||||||
RightMenuProps,
|
RightMenuProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { MenuObjectProps } from './Menu';
|
import { MenuObjectProps } from './Menu';
|
||||||
|
import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal';
|
||||||
|
|
||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
|
|
||||||
@ -91,6 +93,13 @@ const tagStyles = (theme: SupersetTheme) => css`
|
|||||||
color: ${theme.colors.grayscale.light5};
|
color: ${theme.colors.grayscale.light5};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const styledChildMenu = (theme: SupersetTheme) => css`
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.primary.base} !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const { SubMenu } = Menu;
|
const { SubMenu } = Menu;
|
||||||
|
|
||||||
const RightMenu = ({
|
const RightMenu = ({
|
||||||
@ -101,7 +110,13 @@ const RightMenu = ({
|
|||||||
environmentTag,
|
environmentTag,
|
||||||
setQuery,
|
setQuery,
|
||||||
}: RightMenuProps & {
|
}: RightMenuProps & {
|
||||||
setQuery: ({ databaseAdded }: { databaseAdded: boolean }) => void;
|
setQuery: ({
|
||||||
|
databaseAdded,
|
||||||
|
datasetAdded,
|
||||||
|
}: {
|
||||||
|
databaseAdded?: boolean;
|
||||||
|
datasetAdded?: boolean;
|
||||||
|
}) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||||
state => state.user,
|
state => state.user,
|
||||||
@ -118,12 +133,14 @@ const RightMenu = ({
|
|||||||
ALLOWED_EXTENSIONS,
|
ALLOWED_EXTENSIONS,
|
||||||
HAS_GSHEETS_INSTALLED,
|
HAS_GSHEETS_INSTALLED,
|
||||||
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
|
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
|
||||||
const [showModal, setShowModal] = React.useState<boolean>(false);
|
const [showDatabaseModal, setShowDatabaseModal] = useState<boolean>(false);
|
||||||
const [engine, setEngine] = React.useState<string>('');
|
const [showDatasetModal, setShowDatasetModal] = useState<boolean>(false);
|
||||||
|
const [engine, setEngine] = useState<string>('');
|
||||||
const canSql = findPermission('can_sqllab', 'Superset', roles);
|
const canSql = findPermission('can_sqllab', 'Superset', roles);
|
||||||
const canDashboard = findPermission('can_write', 'Dashboard', roles);
|
const canDashboard = findPermission('can_write', 'Dashboard', roles);
|
||||||
const canChart = findPermission('can_write', 'Chart', roles);
|
const canChart = findPermission('can_write', 'Chart', roles);
|
||||||
const canDatabase = findPermission('can_write', 'Database', roles);
|
const canDatabase = findPermission('can_write', 'Database', roles);
|
||||||
|
const canDataset = findPermission('can_write', 'Dataset', roles);
|
||||||
|
|
||||||
const { canUploadData, canUploadCSV, canUploadColumnar, canUploadExcel } =
|
const { canUploadData, canUploadCSV, canUploadColumnar, canUploadExcel } =
|
||||||
uploadUserPerms(
|
uploadUserPerms(
|
||||||
@ -135,7 +152,9 @@ const RightMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const showActionDropdown = canSql || canChart || canDashboard;
|
const showActionDropdown = canSql || canChart || canDashboard;
|
||||||
const [allowUploads, setAllowUploads] = React.useState<boolean>(false);
|
const [allowUploads, setAllowUploads] = useState<boolean>(false);
|
||||||
|
const [nonExamplesDBConnected, setNonExamplesDBConnected] =
|
||||||
|
useState<boolean>(false);
|
||||||
const isAdmin = isUserAdmin(user);
|
const isAdmin = isUserAdmin(user);
|
||||||
const showUploads = allowUploads || isAdmin;
|
const showUploads = allowUploads || isAdmin;
|
||||||
const dropdownItems: MenuObjectProps[] = [
|
const dropdownItems: MenuObjectProps[] = [
|
||||||
@ -146,7 +165,12 @@ const RightMenu = ({
|
|||||||
{
|
{
|
||||||
label: t('Connect database'),
|
label: t('Connect database'),
|
||||||
name: GlobalMenuDataOptions.DB_CONNECTION,
|
name: GlobalMenuDataOptions.DB_CONNECTION,
|
||||||
perm: canDatabase,
|
perm: canDatabase && !nonExamplesDBConnected,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Create dataset'),
|
||||||
|
name: GlobalMenuDataOptions.DATASET_CREATION,
|
||||||
|
perm: canDataset && nonExamplesDBConnected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('Connect Google Sheet'),
|
label: t('Connect Google Sheet'),
|
||||||
@ -217,12 +241,29 @@ const RightMenu = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const existsNonExamplesDatabases = () => {
|
||||||
|
const payload = {
|
||||||
|
filters: [{ col: 'database_name', opr: 'neq', value: 'examples' }],
|
||||||
|
};
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/database/?q=${rison.encode(payload)}`,
|
||||||
|
}).then(({ json }: Record<string, any>) => {
|
||||||
|
setNonExamplesDBConnected(json.count >= 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canUploadData) {
|
if (canUploadData) {
|
||||||
checkAllowUploads();
|
checkAllowUploads();
|
||||||
}
|
}
|
||||||
}, [canUploadData]);
|
}, [canUploadData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canDatabase || canDataset) {
|
||||||
|
existsNonExamplesDatabases();
|
||||||
|
}
|
||||||
|
}, [canDatabase, canDataset]);
|
||||||
|
|
||||||
const menuIconAndLabel = (menu: MenuObjectProps) => (
|
const menuIconAndLabel = (menu: MenuObjectProps) => (
|
||||||
<>
|
<>
|
||||||
<i data-test={`menu-item-${menu.label}`} className={`fa ${menu.icon}`} />
|
<i data-test={`menu-item-${menu.label}`} className={`fa ${menu.icon}`} />
|
||||||
@ -232,16 +273,22 @@ const RightMenu = ({
|
|||||||
|
|
||||||
const handleMenuSelection = (itemChose: any) => {
|
const handleMenuSelection = (itemChose: any) => {
|
||||||
if (itemChose.key === GlobalMenuDataOptions.DB_CONNECTION) {
|
if (itemChose.key === GlobalMenuDataOptions.DB_CONNECTION) {
|
||||||
setShowModal(true);
|
setShowDatabaseModal(true);
|
||||||
} else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) {
|
} else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) {
|
||||||
setShowModal(true);
|
setShowDatabaseModal(true);
|
||||||
setEngine('Google Sheets');
|
setEngine('Google Sheets');
|
||||||
|
} else if (itemChose.key === GlobalMenuDataOptions.DATASET_CREATION) {
|
||||||
|
setShowDatasetModal(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnHideModal = () => {
|
const handleOnHideModal = () => {
|
||||||
setEngine('');
|
setEngine('');
|
||||||
setShowModal(false);
|
setShowDatabaseModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnHideDatasetModalModal = () => {
|
||||||
|
setShowDatasetModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDisabled = isAdmin && !allowUploads;
|
const isDisabled = isAdmin && !allowUploads;
|
||||||
@ -259,21 +306,33 @@ const RightMenu = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Item key={item.name}>
|
<Menu.Item key={item.name} css={styledChildMenu}>
|
||||||
{item.url ? <a href={item.url}> {item.label} </a> : item.label}
|
{item.url ? <a href={item.url}> {item.label} </a> : item.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMenuOpen = (openKeys: string[]) => {
|
const onMenuOpen = (openKeys: string[]) => {
|
||||||
if (openKeys.length && canUploadData) {
|
// We should query the API only if opening Data submenus
|
||||||
return checkAllowUploads();
|
// because the rest don't need this information. Not using
|
||||||
|
// "Data" directly since we might change the label later on?
|
||||||
|
if (
|
||||||
|
openKeys.length > 1 &&
|
||||||
|
!isEmpty(
|
||||||
|
openKeys?.filter((key: string) =>
|
||||||
|
key.includes(`sub2_${dropdownItems?.[0]?.label}`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (canUploadData) checkAllowUploads();
|
||||||
|
if (canDatabase || canDataset) existsNonExamplesDatabases();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const RightMenuExtension = extensionsRegistry.get('navbar.right');
|
const RightMenuExtension = extensionsRegistry.get('navbar.right');
|
||||||
|
|
||||||
const handleDatabaseAdd = () => setQuery({ databaseAdded: true });
|
const handleDatabaseAdd = () => setQuery({ databaseAdded: true });
|
||||||
|
const handleDatasetAdd = () => setQuery({ datasetAdded: true });
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -282,11 +341,18 @@ const RightMenu = ({
|
|||||||
{canDatabase && (
|
{canDatabase && (
|
||||||
<DatabaseModal
|
<DatabaseModal
|
||||||
onHide={handleOnHideModal}
|
onHide={handleOnHideModal}
|
||||||
show={showModal}
|
show={showDatabaseModal}
|
||||||
dbEngine={engine}
|
dbEngine={engine}
|
||||||
onDatabaseAdd={handleDatabaseAdd}
|
onDatabaseAdd={handleDatabaseAdd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{canDataset && (
|
||||||
|
<AddDatasetModal
|
||||||
|
onHide={handleOnHideDatasetModalModal}
|
||||||
|
show={showDatasetModal}
|
||||||
|
onDatasetAdd={handleDatasetAdd}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{environmentTag?.text && (
|
{environmentTag?.text && (
|
||||||
<Label
|
<Label
|
||||||
css={{ borderRadius: `${theme.gridUnit * 125}px` }}
|
css={{ borderRadius: `${theme.gridUnit * 125}px` }}
|
||||||
@ -331,7 +397,7 @@ const RightMenu = ({
|
|||||||
{menu?.childs?.map?.((item, idx) =>
|
{menu?.childs?.map?.((item, idx) =>
|
||||||
typeof item !== 'string' && item.name && item.perm ? (
|
typeof item !== 'string' && item.name && item.perm ? (
|
||||||
<Fragment key={item.name}>
|
<Fragment key={item.name}>
|
||||||
{idx === 2 && <Menu.Divider />}
|
{idx === 3 && <Menu.Divider />}
|
||||||
{buildMenuItem(item)}
|
{buildMenuItem(item)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : null,
|
) : null,
|
||||||
@ -486,6 +552,7 @@ const RightMenu = ({
|
|||||||
const RightMenuWithQueryWrapper: React.FC<RightMenuProps> = props => {
|
const RightMenuWithQueryWrapper: React.FC<RightMenuProps> = props => {
|
||||||
const [, setQuery] = useQueryParams({
|
const [, setQuery] = useQueryParams({
|
||||||
databaseAdded: BooleanParam,
|
databaseAdded: BooleanParam,
|
||||||
|
datasetAdded: BooleanParam,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <RightMenu setQuery={setQuery} {...props} />;
|
return <RightMenu setQuery={setQuery} {...props} />;
|
||||||
|
@ -40,4 +40,5 @@ export interface RightMenuProps {
|
|||||||
export enum GlobalMenuDataOptions {
|
export enum GlobalMenuDataOptions {
|
||||||
GOOGLE_SHEETS = 'gsheets',
|
GOOGLE_SHEETS = 'gsheets',
|
||||||
DB_CONNECTION = 'dbconnection',
|
DB_CONNECTION = 'dbconnection',
|
||||||
|
DATASET_CREATION = 'datasetCreation',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user