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 40 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
40 changes: 20 additions & 20 deletions superset-frontend/src/views/components/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,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();
});

Expand All @@ -202,7 +202,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);
});
Expand All @@ -212,7 +212,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();
});
Expand All @@ -223,7 +223,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');
Expand All @@ -237,7 +237,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
Expand All @@ -263,14 +263,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();
Expand All @@ -281,21 +281,21 @@ 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);
});

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

Expand All @@ -307,7 +307,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();
Expand All @@ -321,7 +321,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();
});

Expand All @@ -333,7 +333,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');
Expand All @@ -348,7 +348,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}`);
Expand All @@ -367,7 +367,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);
Expand All @@ -381,7 +381,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);
});
Expand All @@ -394,19 +394,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();
});
79 changes: 53 additions & 26 deletions superset-frontend/src/views/components/MenuRight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,47 @@
* 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';
import DatabaseModal from '../CRUD/data/database/DatabaseModal';
import {
ExtentionConfigs,
GlobalMenuDataOptions,
RightMenuProps,
} from './types';
import { MenuObjectProps } from './Menu';

export const dropdownItems: MenuObjectProps[] = [
{
label: t('Data'),
icon: 'fa-database',
childs: [
{
icon: 'fa-upload',
label: t('Connect Database'),
name: GlobalMenuDataOptions.DB_CONNECTION,
},
{
label: t('Connect Google Sheet'),
name: GlobalMenuDataOptions.GOOGLE_SHEETS,
},
{
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',
Expand Down Expand Up @@ -107,13 +115,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,
Expand All @@ -123,16 +124,22 @@ 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);
const [showModal, setShowModal] = useState<boolean>(false);
const [engine, setEngine] = useState<string>('');
const {
CSV_EXTENSIONS,
COLUMNAR_EXTENSIONS,
EXCEL_EXTENSIONS,
SHOW_GLOBAL_GSHEETS,
} = useSelector<any, ExtentionConfigs>(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,
[GlobalMenuDataOptions.GOOGLE_SHEETS]: SHOW_GLOBAL_GSHEETS,
[GlobalMenuDataOptions.DB_CONNECTION]: true, // todo(hugh): add permission check here for database creation
Copy link
Member

Choose a reason for hiding this comment

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

instead of a separate map here, how about we move these values into the upper dropdownItems map with a key of something like has_perm? Then below instead of configMap[item.name] === true you can say item.has_perm

};
const canSql = findPermission('can_sqllab', 'Superset', roles);
const canDashboard = findPermission('can_write', 'Dashboard', roles);
Expand All @@ -144,9 +151,25 @@ const RightMenu = ({
{menu.label}
</>
);

const handleMenuSelection = (itemChose: any) => {
Copy link
Member

Choose a reason for hiding this comment

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

Is itemChose a complicated type or would this be able to be defined with more specific than any?

if (itemChose.key === 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?

} else if (itemChose.key === 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={handleMenuSelection}>
{!navbarRight.user_is_anonymous && showActionDropdown && (
<SubMenu
data-test="new-dropdown"
Expand All @@ -163,13 +186,17 @@ const RightMenu = ({
className="data-menu"
title={menuIconAndLabel(menu)}
>
{menu.childs.map(item =>
{menu.childs.map((item, idx) =>
typeof item !== 'string' &&
item.name &&
configMap[item.name] === true ? (
<Menu.Item key={item.name}>
<a href={item.url}> {item.label} </a>
</Menu.Item>
<>
{idx === 2 && <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.

maybe for future-proofing this, we can say idx > 1 instead of comparing it to 2 specifically?

Copy link
Member Author

Choose a reason for hiding this comment

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

if we go above idx > 1, this would render after every menu item after the first 2 items. We just want it to render it once right after the db connection buttons. Thats the one of the main reason i split out the db connection buttons from the config.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I see the complication. Again I would suggest separating the concerns between the data at the top and the rendering at the bottom. You could add the logic to the data with something like:

      {
          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,
        },

and then you can render the divider if typeof item === 'string'
or make it an object with a type of "divider"...

Copy link
Member

Choose a reason for hiding this comment

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

This is something we can discuss later.

<Menu.Item key={item.name}>
{item.url && <a href={item.url}> {item.label} </a>}
{item.url === undefined && item.label}
Copy link
Member

Choose a reason for hiding this comment

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

nit, but instead of two lines can you combine these by making it a ternary statement?

</Menu.Item>
</>
) : null,
)}
</SubMenu>
Expand Down
38 changes: 38 additions & 0 deletions superset-frontend/src/views/components/types.ts
Original file line number Diff line number Diff line change
@@ -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: Array<any>;
COLUMNAR_EXTENSIONS: Array<any>;
EXCEL_EXTENSIONS: Array<any>;
SHOW_GLOBAL_GSHEETS: 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',
}
6 changes: 6 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,10 @@ def common_bootstrap_payload() -> Dict[str, Any]:
ReportRecipientType.EMAIL,
]

# verify client has google sheets installed
available_specs = get_available_engine_specs()
frontend_config["SHOW_GLOBAL_GSHEETS"] = bool(available_specs[GSheetsEngineSpec])
Copy link
Member

Choose a reason for hiding this comment

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

bump of an earlier comment.. can we name this something more generic like HAS_GSHEETS_INSTALLED so that it's less tied to the client side functionality?


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