feat(rightmenu): Add Datasets to + Menu and Hide Databases when one has been connected (#21530)

This commit is contained in:
Antonio Rivero Martinez 2022-10-24 13:45:23 -03:00 committed by GitHub
parent 175ec854b9
commit c19708b432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 374 additions and 207 deletions

View File

@ -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();
}
}); });
}; };

View File

@ -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

View File

@ -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();
}); });

View File

@ -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} />;

View File

@ -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',
} }