feat: DBC-UI Globally available across the app 🌎 (#18722)

* more data nav menu

* fix lint and fix nav css

* update test and remove icons

* Update superset-frontend/src/views/components/Menu.test.tsx

Co-authored-by: Hugh A. Miles II <hughmil3s@gmail.com>

* Apply suggestions from code review

* use backend app.link to show new nav changes

* fix lint

* update test

* usetheme and remove chaining

* add more suggestions

* fix lint

* working global db connection

* add allowed extensions to bootstrap and hard code links

* remove backend links

* fix test

* apply stashed gsheets

* fix check for google sheets

* setup gsheets

* add extensions to frontend conf

* fix test and add be changes

* remove package json changes

* test is python test passes

* update python test and reremove app links

* fix tslint issues

* fix other linting tools

* fix pylint

* fix test

* fix

* refactor

* fix lint

* working fixed test

* clean up test

* address concerns

* address concerns

* change to tenarary

Co-authored-by: Phillip Kelley-Dotson <pkelleydotson@yahoo.com>
This commit is contained in:
Hugh A. Miles II 2022-02-24 13:00:44 -08:00 committed by GitHub
parent 822dd6de5d
commit 209e3f4554
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 232 additions and 102 deletions

View File

@ -21,7 +21,64 @@ import * as reactRedux from 'react-redux';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Menu } from './Menu';
import { dropdownItems } from './MenuRight';
const dropdownItems = [
{
label: 'Data',
icon: 'fa-database',
childs: [
{
label: 'Connect Database',
name: 'dbconnect',
perm: true,
},
{
label: 'Connect Google Sheet',
name: 'gsheets',
perm: true,
},
{
label: 'Upload a CSV',
name: 'Upload a CSV',
url: '/csvtodatabaseview/form',
perm: true,
},
{
label: 'Upload a Columnar File',
name: 'Upload a Columnar file',
url: '/columnartodatabaseview/form',
perm: true,
},
{
label: 'Upload Excel',
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 user = {
createdOn: '2021-04-27T18:12:38.952304',
@ -185,13 +242,13 @@ beforeEach(() => {
test('should render', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
const { container } = render(<Menu {...mockedProps} />);
const { container } = render(<Menu {...mockedProps} />, { useRedux: true });
expect(container).toBeInTheDocument();
});
test('should render the navigation', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
@ -202,7 +259,7 @@ test('should render the brand', () => {
brand: { alt, icon },
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
const image = screen.getByAltText(alt);
expect(image).toHaveAttribute('src', icon);
});
@ -212,7 +269,7 @@ test('should render all the top navbar menu items', () => {
const {
data: { menu },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
menu.forEach(item => {
expect(screen.getByText(item.label)).toBeInTheDocument();
});
@ -223,7 +280,7 @@ test('should render the top navbar child menu items', async () => {
const {
data: { menu },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
const sources = screen.getByText('Sources');
userEvent.hover(sources);
const datasets = await screen.findByText('Datasets');
@ -237,7 +294,7 @@ test('should render the top navbar child menu items', async () => {
test('should render the dropdown items', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...notanonProps} />);
render(<Menu {...notanonProps} />, { useRedux: true });
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
// todo (philip): test data submenu
@ -263,14 +320,14 @@ test('should render the dropdown items', async () => {
test('should render the Settings', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
const settings = await screen.findByText('Settings');
expect(settings).toBeInTheDocument();
});
test('should render the Settings menu item', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const label = await screen.findByText('Security');
expect(label).toBeInTheDocument();
@ -281,7 +338,7 @@ test('should render the Settings dropdown child menu items', async () => {
const {
data: { settings },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const listUsers = await screen.findByText('List Users');
expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url);
@ -289,13 +346,13 @@ test('should render the Settings dropdown child menu items', async () => {
test('should render the plus menu (+) when user is not anonymous', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...notanonProps} />);
render(<Menu {...notanonProps} />, { useRedux: true });
expect(screen.getByTestId('new-dropdown')).toBeInTheDocument();
});
test('should NOT render the plus menu (+) when user is anonymous', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
});
@ -307,7 +364,7 @@ test('should render the user actions when user is not anonymous', async () => {
},
} = mockedProps;
render(<Menu {...notanonProps} />);
render(<Menu {...notanonProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const user = await screen.findByText('User');
expect(user).toBeInTheDocument();
@ -321,7 +378,7 @@ test('should render the user actions when user is not anonymous', async () => {
test('should NOT render the user actions when user is anonymous', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
expect(screen.queryByText('User')).not.toBeInTheDocument();
});
@ -333,7 +390,7 @@ test('should render the Profile link when available', async () => {
},
} = mockedProps;
render(<Menu {...notanonProps} />);
render(<Menu {...notanonProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const profile = await screen.findByText('Profile');
@ -348,7 +405,7 @@ test('should render the About section and version_string, sha or build_number wh
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const about = await screen.findByText('About');
const version = await screen.findByText(`Version: ${version_string}`);
@ -367,7 +424,7 @@ test('should render the Documentation link when available', async () => {
navbar_right: { documentation_url },
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
userEvent.hover(screen.getByText('Settings'));
const doc = await screen.findByTitle('Documentation');
expect(doc).toHaveAttribute('href', documentation_url);
@ -381,7 +438,7 @@ test('should render the Bug Report link when available', async () => {
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
const bugReport = await screen.findByTitle('Report a bug');
expect(bugReport).toHaveAttribute('href', bug_report_url);
});
@ -394,19 +451,19 @@ test('should render the Login link when user is anonymous', () => {
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
const login = screen.getByText('Login');
expect(login).toHaveAttribute('href', user_login_url);
});
test('should render the Language Picker', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
expect(screen.getByLabelText('Languages')).toBeInTheDocument();
});
test('should hide create button without proper roles', () => {
useSelectorMock.mockReturnValue({ roles: [] });
render(<Menu {...notanonProps} />);
render(<Menu {...mockedProps} />, { useRedux: true });
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
});

View File

@ -79,7 +79,7 @@ interface MenuObjectChildProps {
index?: number;
url?: string;
isFrontendRoute?: boolean;
perm?: string;
perm?: string | boolean;
view?: string;
}

View File

@ -16,67 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useState } from 'react';
import { MainNav as Menu } from 'src/common/components';
import { t, styled, css, SupersetTheme } from '@superset-ui/core';
import { Link } from 'react-router-dom';
import Icons from 'src/components/Icons';
import findPermission from 'src/dashboard/util/findPermission';
import { useSelector } from 'react-redux';
import {
UserWithPermissionsAndRoles,
CommonBootstrapData,
} from 'src/types/bootstrapTypes';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import LanguagePicker from './LanguagePicker';
import { NavBarProps, MenuObjectProps } from './Menu';
export const dropdownItems: MenuObjectProps[] = [
{
label: t('Data'),
icon: 'fa-database',
childs: [
{
icon: 'fa-upload',
label: t('Upload a CSV'),
name: 'Upload a CSV',
url: '/csvtodatabaseview/form',
},
{
icon: 'fa-upload',
label: t('Upload a Columnar File'),
name: 'Upload a Columnar file',
url: '/columnartodatabaseview/form',
},
{
icon: 'fa-upload',
label: t('Upload Excel'),
name: 'Upload Excel',
url: '/exceltodatabaseview/form',
},
],
},
{
label: t('SQL query'),
url: '/superset/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
},
{
label: t('Chart'),
url: '/chart/add',
icon: 'fa-fw fa-bar-chart',
perm: 'can_write',
view: 'Chart',
},
{
label: t('Dashboard'),
url: '/dashboard/new',
icon: 'fa-fw fa-dashboard',
perm: 'can_write',
view: 'Dashboard',
},
];
import DatabaseModal from '../CRUD/data/database/DatabaseModal';
import {
ExtentionConfigs,
GlobalMenuDataOptions,
RightMenuProps,
} from './types';
import { MenuObjectProps } from './Menu';
const versionInfoStyles = (theme: SupersetTheme) => css`
padding: ${theme.gridUnit * 1.5}px ${theme.gridUnit * 4}px
@ -107,13 +62,6 @@ const StyledAnchor = styled.a`
const { SubMenu } = Menu;
interface RightMenuProps {
align: 'flex-start' | 'flex-end';
settings: MenuObjectProps[];
navbarRight: NavBarProps;
isFrontendRoute: (path?: string) => boolean;
}
const RightMenu = ({
align,
settings,
@ -123,30 +71,106 @@ const RightMenu = ({
const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
// @ts-ignore
const { CSV_EXTENSIONS, COLUMNAR_EXTENSIONS, EXCEL_EXTENSIONS } = useSelector<
any,
CommonBootstrapData
>(state => state.common.conf);
// if user has any of these roles the dropdown will appear
const configMap = {
'Upload a CSV': CSV_EXTENSIONS,
'Upload a Columnar file': COLUMNAR_EXTENSIONS,
'Upload Excel': EXCEL_EXTENSIONS,
};
const {
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
EXCEL_EXTENSIONS,
HAS_GSHEETS_INSTALLED,
} = useSelector<any, ExtentionConfigs>(state => state.common.conf);
const [showModal, setShowModal] = 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 showActionDropdown = canSql || canChart || canDashboard;
const dropdownItems: MenuObjectProps[] = [
{
label: t('Data'),
icon: 'fa-database',
childs: [
{
label: t('Connect Database'),
name: GlobalMenuDataOptions.DB_CONNECTION,
perm: true,
},
{
label: t('Connect Google Sheet'),
name: GlobalMenuDataOptions.GOOGLE_SHEETS,
perm: HAS_GSHEETS_INSTALLED,
},
{
label: t('Upload a CSV'),
name: 'Upload a CSV',
url: '/csvtodatabaseview/form',
perm: CSV_EXTENSIONS,
},
{
label: t('Upload a Columnar File'),
name: 'Upload a Columnar file',
url: '/columnartodatabaseview/form',
perm: COLUMNAR_EXTENSIONS,
},
{
label: t('Upload Excel'),
name: 'Upload Excel',
url: '/exceltodatabaseview/form',
perm: EXCEL_EXTENSIONS,
},
],
},
{
label: t('SQL query'),
url: '/superset/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
},
{
label: t('Chart'),
url: '/chart/add',
icon: 'fa-fw fa-bar-chart',
perm: 'can_write',
view: 'Chart',
},
{
label: t('Dashboard'),
url: '/dashboard/new',
icon: 'fa-fw fa-dashboard',
perm: 'can_write',
view: 'Dashboard',
},
];
const menuIconAndLabel = (menu: MenuObjectProps) => (
<>
<i data-test={`menu-item-${menu.label}`} className={`fa ${menu.icon}`} />
{menu.label}
</>
);
const handleMenuSelection = (itemChose: any) => {
if (itemChose.key === GlobalMenuDataOptions.DB_CONNECTION) {
setShowModal(true);
} else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) {
setShowModal(true);
setEngine('Google Sheets');
}
};
const handleOnHideModal = () => {
setEngine('');
setShowModal(false);
};
return (
<StyledDiv align={align}>
<Menu mode="horizontal">
<DatabaseModal
onHide={handleOnHideModal}
show={showModal}
dbEngine={engine}
/>
<Menu selectable={false} mode="horizontal" onClick={handleMenuSelection}>
{!navbarRight.user_is_anonymous && showActionDropdown && (
<SubMenu
data-test="new-dropdown"
@ -163,13 +187,18 @@ const RightMenu = ({
className="data-menu"
title={menuIconAndLabel(menu)}
>
{menu.childs.map(item =>
typeof item !== 'string' &&
item.name &&
configMap[item.name] === true ? (
<Menu.Item key={item.name}>
<a href={item.url}> {item.label} </a>
</Menu.Item>
{menu.childs.map((item, idx) =>
typeof item !== 'string' && item.name && item.perm ? (
<>
{idx === 2 && <Menu.Divider />}
<Menu.Item key={item.name}>
{item.url ? (
<a href={item.url}> {item.label} </a>
) : (
item.label
)}
</Menu.Item>
</>
) : null,
)}
</SubMenu>

View File

@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { NavBarProps, MenuObjectProps } from './Menu';
export interface ExtentionConfigs {
CSV_EXTENSIONS: boolean;
COLUMNAR_EXTENSIONS: boolean;
EXCEL_EXTENSIONS: boolean;
HAS_GSHEETS_INSTALLED: boolean;
}
export interface RightMenuProps {
align: 'flex-start' | 'flex-end';
settings: MenuObjectProps[];
navbarRight: NavBarProps;
isFrontendRoute: (path?: string) => boolean;
}
export enum GlobalMenuDataOptions {
GOOGLE_SHEETS = 'gsheets',
DB_CONNECTION = 'dbconnection',
}

View File

@ -62,6 +62,8 @@ from superset import (
from superset.commands.exceptions import CommandException, CommandInvalidError
from superset.connectors.sqla import models
from superset.datasets.commands.exceptions import get_dataset_exist_error_msg
from superset.db_engine_specs import get_available_engine_specs
from superset.db_engine_specs.gsheets import GSheetsEngineSpec
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from superset.exceptions import (
SupersetErrorException,
@ -366,6 +368,10 @@ def common_bootstrap_payload() -> Dict[str, Any]:
ReportRecipientType.EMAIL,
]
# verify client has google sheets installed
available_specs = get_available_engine_specs()
frontend_config["HAS_GSHEETS_INSTALLED"] = bool(available_specs[GSheetsEngineSpec])
bootstrap_data = {
"flash_messages": messages,
"conf": frontend_config,