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