Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 42 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
681c20e
more data nav menu
pkdotson Feb 9, 2022
a4bbd5e
fix lint and fix nav css
pkdotson Feb 9, 2022
2ae8696
update test and remove icons
pkdotson Feb 9, 2022
237263a
Update superset-frontend/src/views/components/Menu.test.tsx
pkdotson Feb 10, 2022
04d1ff9
Apply suggestions from code review
hughhhh Feb 10, 2022
1c0f383
use backend app.link to show new nav changes
pkdotson Feb 10, 2022
760d8c6
Merge branch 'chore-change-nav' of https://github.com/preset-io/super…
pkdotson Feb 10, 2022
717c5bb
fix lint
pkdotson Feb 10, 2022
2502a9a
update test
pkdotson Feb 10, 2022
123eff6
usetheme and remove chaining
pkdotson Feb 11, 2022
785f74b
add more suggestions
pkdotson Feb 14, 2022
757d502
fix lint
pkdotson Feb 14, 2022
2780d60
Merge branch 'master' of https://github.com/apache/superset into hugh…
hughhhh Feb 14, 2022
8052f1f
Merge branch 'chore-change-nav' of https://github.com/preset-io/super…
hughhhh Feb 14, 2022
b68aa95
working global db connection
hughhhh Feb 14, 2022
a6c0bbe
add allowed extensions to bootstrap and hard code links
pkdotson Feb 15, 2022
0e9abe9
remove backend links
pkdotson Feb 15, 2022
70e7ba4
fix conflicts
hughhhh Feb 15, 2022
117261e
fix test
pkdotson Feb 15, 2022
4fc95cd
apply stashed gsheets
hughhhh Feb 15, 2022
dca3be1
fix check for google sheets
hughhhh Feb 15, 2022
b45184b
setup gsheets
hughhhh Feb 15, 2022
c42491d
add extensions to frontend conf
pkdotson Feb 16, 2022
9eac285
fix test and add be changes
pkdotson Feb 16, 2022
a3cc781
fix
hughhhh Feb 16, 2022
e64d6eb
remove package json changes
hughhhh Feb 16, 2022
6e9eb0c
test is python test passes
pkdotson Feb 16, 2022
9121e01
update python test and reremove app links
pkdotson Feb 16, 2022
e9152e4
Merge branch 'chore-change-nav' of https://github.com/preset-io/super…
hughhhh Feb 16, 2022
5b27726
fix merge conflicts
hughhhh Feb 17, 2022
9b2bf5f
fix tslint issues
hughhhh Feb 17, 2022
56902d3
fix other linting tools
hughhhh Feb 17, 2022
3af0404
fix pylint
hughhhh Feb 17, 2022
e43fbd9
fix test
hughhhh Feb 17, 2022
0f55d41
fix
hughhhh Feb 18, 2022
5af39b6
refactor
hughhhh Feb 18, 2022
0507aaa
fix lint
hughhhh Feb 18, 2022
bd4a8bf
working fixed test
hughhhh Feb 18, 2022
58d7607
clean up test
hughhhh Feb 18, 2022
b1aeaa4
address concerns
hughhhh Feb 22, 2022
e2b0570
address concerns
hughhhh Feb 24, 2022
192a7f4
change to tenarary
hughhhh Feb 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 101 additions & 20 deletions superset-frontend/src/views/components/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import React from 'react';
import * as reactRedux from 'react-redux';
import { render, screen } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { store } from '../store';
import { Menu } from './Menu';
import { dropdownItems } from './MenuRight';

Expand Down Expand Up @@ -185,13 +186,21 @@ beforeEach(() => {

test('should render', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
const { container } = render(<Menu {...mockedProps} />);
const { container } = render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(container).toBeInTheDocument();
});

test('should render the navigation', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(screen.getByRole('navigation')).toBeInTheDocument();
});

Expand All @@ -202,7 +211,11 @@ test('should render the brand', () => {
brand: { alt, icon },
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
const image = screen.getByAltText(alt);
expect(image).toHaveAttribute('src', icon);
});
Expand All @@ -212,7 +225,11 @@ test('should render all the top navbar menu items', () => {
const {
data: { menu },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
menu.forEach(item => {
expect(screen.getByText(item.label)).toBeInTheDocument();
});
Expand All @@ -223,7 +240,11 @@ test('should render the top navbar child menu items', async () => {
const {
data: { menu },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
const sources = screen.getByText('Sources');
userEvent.hover(sources);
const datasets = await screen.findByText('Datasets');
Expand All @@ -237,7 +258,11 @@ 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(
<reactRedux.Provider store={store}>
<Menu {...notanonProps} />
</reactRedux.Provider>,
);
const dropdown = screen.getByTestId('new-dropdown-icon');
userEvent.hover(dropdown);
// todo (philip): test data submenu
Expand All @@ -263,14 +288,22 @@ test('should render the dropdown items', async () => {

test('should render the Settings', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
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(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
userEvent.hover(screen.getByText('Settings'));
const label = await screen.findByText('Security');
expect(label).toBeInTheDocument();
Expand All @@ -281,21 +314,33 @@ test('should render the Settings dropdown child menu items', async () => {
const {
data: { settings },
} = mockedProps;
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
userEvent.hover(screen.getByText('Settings'));
const listUsers = await screen.findByText('List Users');
expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url);
});

test('should render the plus menu (+) when user is not anonymous', () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
render(<Menu {...notanonProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...notanonProps} />
</reactRedux.Provider>,
);
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(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
});

Expand All @@ -307,7 +352,11 @@ test('should render the user actions when user is not anonymous', async () => {
},
} = mockedProps;

render(<Menu {...notanonProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...notanonProps} />
</reactRedux.Provider>,
);
userEvent.hover(screen.getByText('Settings'));
const user = await screen.findByText('User');
expect(user).toBeInTheDocument();
Expand All @@ -321,7 +370,11 @@ 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(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(screen.queryByText('User')).not.toBeInTheDocument();
});

Expand All @@ -333,7 +386,11 @@ test('should render the Profile link when available', async () => {
},
} = mockedProps;

render(<Menu {...notanonProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...notanonProps} />
</reactRedux.Provider>,
);

userEvent.hover(screen.getByText('Settings'));
const profile = await screen.findByText('Profile');
Expand All @@ -348,7 +405,11 @@ test('should render the About section and version_string, sha or build_number wh
},
} = mockedProps;

render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
userEvent.hover(screen.getByText('Settings'));
const about = await screen.findByText('About');
const version = await screen.findByText(`Version: ${version_string}`);
Expand All @@ -367,7 +428,11 @@ test('should render the Documentation link when available', async () => {
navbar_right: { documentation_url },
},
} = mockedProps;
render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
userEvent.hover(screen.getByText('Settings'));
const doc = await screen.findByTitle('Documentation');
expect(doc).toHaveAttribute('href', documentation_url);
Expand All @@ -381,7 +446,11 @@ test('should render the Bug Report link when available', async () => {
},
} = mockedProps;

render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
const bugReport = await screen.findByTitle('Report a bug');
expect(bugReport).toHaveAttribute('href', bug_report_url);
});
Expand All @@ -394,19 +463,31 @@ test('should render the Login link when user is anonymous', () => {
},
} = mockedProps;

render(<Menu {...mockedProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
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(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(screen.getByLabelText('Languages')).toBeInTheDocument();
});

test('should hide create button without proper roles', () => {
useSelectorMock.mockReturnValue({ roles: [] });
render(<Menu {...notanonProps} />);
render(
<reactRedux.Provider store={store}>
<Menu {...mockedProps} />
</reactRedux.Provider>,
);
expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument();
});
48 changes: 46 additions & 2 deletions superset-frontend/src/views/components/MenuRight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* 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';
Expand All @@ -29,6 +29,7 @@ import {
} from 'src/types/bootstrapTypes';
import LanguagePicker from './LanguagePicker';
import { NavBarProps, MenuObjectProps } from './Menu';
import DatabaseModal from '../CRUD/data/database/DatabaseModal';

export const dropdownItems: MenuObjectProps[] = [
{
Expand Down Expand Up @@ -114,6 +115,11 @@ interface RightMenuProps {
isFrontendRoute: (path?: string) => boolean;
}

export enum GlobalMenuDataOptions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this live in a types.ts file?

GOOGLE_SHEETS = '1',
DB_CONNECTION = '2',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just fyi, you don't need to initialize the values if they're going to auto increment

}

const RightMenu = ({
align,
settings,
Expand All @@ -123,11 +129,19 @@ const RightMenu = ({
const { roles } = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
const [showModal, setShowModal] = useState<boolean>(false);
const [engine, setEngine] = useState<string>('');
// @ts-ignore
const { CSV_EXTENSIONS, COLUMNAR_EXTENSIONS, EXCEL_EXTENSIONS } = useSelector<
any,
CommonBootstrapData
>(state => state.common.conf);

// @ts-ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious what ts is complaining about here

const { SHOW_GLOBAL_GSHEETS } = useSelector<any, CommonBootstrapData>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to get rid of this ts-ignore if you replace CommonBootstrapData with a new type. For ex...
interface NewType { Show_GLOBAL_GSHEETS: BOOLEAN; } const { SHOW_GLOBAL_GSHEETS } = useSelector<any, NewType> {....
I just figured this out in my current open pr. lol

state => state.common.conf,
);

// if user has any of these roles the dropdown will appear
const configMap = {
'Upload a CSV': CSV_EXTENSIONS,
Expand All @@ -144,9 +158,30 @@ const RightMenu = ({
{menu.label}
</>
);

const handleMenuSelection = (itemChose: React.Key) => {
if (itemChose === GlobalMenuDataOptions.DB_CONNECTION) {
setShowModal(true);
setEngine('');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to clear out the value if google sheets was selected at some point? If so, can we reset the value on modal close instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried moving this logic to the into component, but since we are managing the state in the upper component (MenuRight) it doesn't reset.

Open to other strategies here, but was having a hard time getting it to work without reseting from the upper component

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to leave the logic here for which engine was chosen, but maybe instead of clearing it out on menu selection, we can clear out the engine value on the dbmodal onHide event?

}
if (itemChose === GlobalMenuDataOptions.GOOGLE_SHEETS) {
setShowModal(true);
setEngine('Google Sheets');
}
};

return (
<StyledDiv align={align}>
<Menu mode="horizontal">
<DatabaseModal
onHide={() => setShowModal(false)}
show={showModal}
dbEngine={engine}
/>
<Menu
selectable={false}
mode="horizontal"
onClick={({ key }) => handleMenuSelection(key)}
>
{!navbarRight.user_is_anonymous && showActionDropdown && (
<SubMenu
data-test="new-dropdown"
Expand All @@ -163,6 +198,15 @@ const RightMenu = ({
className="data-menu"
title={menuIconAndLabel(menu)}
>
<Menu.Item key={GlobalMenuDataOptions.DB_CONNECTION}>
Connect Database
</Menu.Item>
{SHOW_GLOBAL_GSHEETS && (
<Menu.Item key={GlobalMenuDataOptions.GOOGLE_SHEETS}>
Connect Google Sheet
</Menu.Item>
)}
<Menu.Divider />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest since we have the data object above that we're looping over, to try to move this into that data structure so that we can keep this as modular as possible.

{menu.childs.map(item =>
typeof item !== 'string' &&
item.name &&
Expand Down
8 changes: 8 additions & 0 deletions superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
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,
Expand Down Expand Up @@ -366,6 +368,12 @@ def common_bootstrap_payload() -> Dict[str, Any]:
ReportRecipientType.EMAIL,
]

available_specs = get_available_engine_specs()
if available_specs[GSheetsEngineSpec]:
frontend_config["SHOW_GLOBAL_GSHEETS"] = True
else:
frontend_config["SHOW_GLOBAL_GSHEETS"] = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this by dry-ed up a bit by something like:

available_specs = get_available_engine_specs()
frontend_config["SHOW_GLOBAL_GSHEETS"]=bool(available_specs[GSheetsEngineSpec])

Also nit, but can we decouple the backend config by naming this something more generic perhaps? Since the value is whether the driver is installed, the backend doesn't need to know how the client side is using this value, i.e., whether to show gsheets globally, just needs to know that the driver is installed.


bootstrap_data = {
"flash_messages": messages,
"conf": frontend_config,
Expand Down