diff --git a/superset/assets/cypress.json b/superset/assets/cypress.json index 9c1d3f657c7..09ee3a7d930 100644 --- a/superset/assets/cypress.json +++ b/superset/assets/cypress.json @@ -7,5 +7,6 @@ "video": false, "videoUploadOnPasses": false, "viewportWidth": 1280, - "viewportHeight": 800 + "viewportHeight": 800, + "requestTimeout": 10000 } diff --git a/superset/assets/spec/javascripts/components/Menu_spec.jsx b/superset/assets/spec/javascripts/components/Menu_spec.jsx new file mode 100644 index 00000000000..ce7e912370d --- /dev/null +++ b/superset/assets/spec/javascripts/components/Menu_spec.jsx @@ -0,0 +1,140 @@ +/** + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { Nav } from 'react-bootstrap'; + +import Menu from '../../../src/components/Menu/Menu'; + +const defaultProps = { + data: { + menu: [ + { + name: 'Security', + icon: 'fa-cogs', + label: 'Security', + childs: [ + { + name: 'List Users', + icon: 'fa-user', + label: 'List Users', + url: '/users/list/', + }, + ], + }, + { + name: 'Sources', + icon: 'fa-table', + label: 'Sources', + childs: [ + { + name: 'Tables', + icon: 'fa-table', + label: 'Tables', + url: '/tablemodelview/list/?_flt_1_is_sqllab_view=y', + }, + '-', + { + name: 'Databases', + icon: 'fa-database', + label: 'Databases', + url: '/databaseview/list/', + }, + ], + }, + { + name: 'Charts', + icon: 'fa-bar-chart', + label: 'Charts', + url: '/chart/list/', + }, + { + name: 'Dashboards', + icon: 'fa-dashboard', + label: 'Dashboards', + url: '/dashboard/list/', + }, + ], + brand: { + path: '/superset/profile/admin/', + icon: '/static/assets/images/superset-logo@2x.png', + alt: 'Superset', + }, + navbar_right: { + bug_report_url: null, + documentation_url: null, + languages: { + en: { + flag: 'us', + name: 'English', + url: '/lang/en', + }, + it: { + flag: 'it', + name: 'Italian', + url: '/lang/it', + }, + }, + show_language_picker: true, + user_is_anonymous: false, + user_info_url: '/users/userinfo/', + user_logout_url: '/logout/', + user_login_url: '/login/', + locale: 'en', + }, + }, +}; + +describe('Menu', () => { + let wrapper; + + const getWrapper = (overrideProps = {}) => { + const props = { + ...defaultProps, + ...overrideProps, + }; + return shallow(); + }; + + beforeEach(() => { + wrapper = getWrapper(); + }); + + it('renders the brand', () => { + expect(wrapper.find('.navbar-brand')).toHaveLength(1); + }); + + it('renders 2 navs', () => { + expect(wrapper.find(Nav)).toHaveLength(2); + }); + + it('renders a logged out view', () => { + const loggedOutWrapper = getWrapper({ + data: { + ...defaultProps.data, + navbar_right: { + ...defaultProps.data.navbar_right, + user_is_anonymous: true, + }, + }, + }); + expect(loggedOutWrapper.find('i.fa-sign-in')).toHaveLength(1); + expect(loggedOutWrapper.find('i.fa-user')).toHaveLength(0); + }); +}); diff --git a/superset/assets/src/components/Menu/LanguagePicker.jsx b/superset/assets/src/components/Menu/LanguagePicker.jsx new file mode 100644 index 00000000000..d5101294dd8 --- /dev/null +++ b/superset/assets/src/components/Menu/LanguagePicker.jsx @@ -0,0 +1,52 @@ +/** + * 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 React from 'react'; +import PropTypes from 'prop-types'; +import { NavDropdown, MenuItem } from 'react-bootstrap'; + +const propTypes = { + locale: PropTypes.string.isRequired, + languages: PropTypes.object.isRequired, +}; + +export default function LanguagePicker({ locale, languages }) { + return ( + + + + } + > + {Object.keys(languages).map(langKey => + langKey === locale ? null : ( + + {' '} +
+ - {languages[langKey].name} +
+
+ ), + )} +
+ ); +} + +LanguagePicker.propTypes = propTypes; diff --git a/superset/assets/src/components/Menu/Menu.jsx b/superset/assets/src/components/Menu/Menu.jsx new file mode 100644 index 00000000000..7c4ea0890e3 --- /dev/null +++ b/superset/assets/src/components/Menu/Menu.jsx @@ -0,0 +1,102 @@ +/** + * 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 React from 'react'; +import PropTypes from 'prop-types'; +import { t } from '@superset-ui/translation'; +import { Nav, Navbar, NavItem } from 'react-bootstrap'; +import MenuObject from './MenuObject'; +import NewMenu from './NewMenu'; +import UserMenu from './UserMenu'; +import LanguagePicker from './LanguagePicker'; + +const propTypes = { + data: PropTypes.shape({ + menu: PropTypes.arrayOf(PropTypes.object).isRequired, + brand: PropTypes.shape({ + path: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, + }).isRequired, + navbar_right: PropTypes.shape({ + bug_report_url: PropTypes.string, + documentation_url: PropTypes.string, + languages: PropTypes.object, + show_language_picker: PropTypes.bool.isRequired, + user_is_anonymous: PropTypes.bool.isRequired, + user_info_url: PropTypes.string.isRequired, + user_login_url: PropTypes.string.isRequired, + user_logout_url: PropTypes.string.isRequired, + locale: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +export default function Menu({ data: { menu, brand, navbar_right: navbarRight } }) { + return ( +
+ + + + + {brand.alt} + + + + + + + +
+ ); +} + +Menu.propTypes = propTypes; diff --git a/superset/assets/src/components/Menu/MenuObject.jsx b/superset/assets/src/components/Menu/MenuObject.jsx new file mode 100644 index 00000000000..0e52a9b9e80 --- /dev/null +++ b/superset/assets/src/components/Menu/MenuObject.jsx @@ -0,0 +1,66 @@ +/** + * 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 React from 'react'; +import PropTypes from 'prop-types'; +import { NavItem, NavDropdown, MenuItem } from 'react-bootstrap'; + +const propTypes = { + label: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + url: PropTypes.string, + childs: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])), +}; + +export default function MenuObject({ label, icon, childs, url, index }) { + if (url) { + return ( + +   {label} + + ); + } + + const navTitle = ( + <> + +   {label} + + ); + return ( + + {childs.map((child, index1) => + child === '-' ? ( + + ) : ( + + +   {child.label} + + ), + )} + + ); +} + +MenuObject.propTypes = propTypes; diff --git a/superset/assets/src/components/Menu/NewMenu.jsx b/superset/assets/src/components/Menu/NewMenu.jsx new file mode 100644 index 00000000000..92b939aaa5b --- /dev/null +++ b/superset/assets/src/components/Menu/NewMenu.jsx @@ -0,0 +1,64 @@ +/** + * 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 React from 'react'; +import { t } from '@superset-ui/translation'; + +const propTypes = {}; + +const buttonStyle = { + marginTop: '12px', + marginRight: '30px', +}; + +export default function NewMenu() { + return ( +
  • + + +
  • + ); +} + +NewMenu.propTypes = propTypes; diff --git a/superset/assets/src/components/Menu/UserMenu.jsx b/superset/assets/src/components/Menu/UserMenu.jsx new file mode 100644 index 00000000000..a6097a0dbdb --- /dev/null +++ b/superset/assets/src/components/Menu/UserMenu.jsx @@ -0,0 +1,51 @@ +/** + * 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 React from 'react'; +import PropTypes from 'prop-types'; +import { NavDropdown, MenuItem } from 'react-bootstrap'; +import { t } from '@superset-ui/translation'; + +const propTypes = { + userInfoUrl: PropTypes.string.isRequired, + userLogoutUrl: PropTypes.string.isRequired, +}; + +export default function UserMenu({ userInfoUrl, userLogoutUrl }) { + return ( + + + + } + > + + + {t('Profile')} + + + + {t('Logout')} + + + ); +} + +UserMenu.propTypes = propTypes; diff --git a/superset/assets/src/welcome/App.jsx b/superset/assets/src/welcome/App.jsx index 1a712ca98be..55febfae5db 100644 --- a/superset/assets/src/welcome/App.jsx +++ b/superset/assets/src/welcome/App.jsx @@ -26,12 +26,14 @@ import messageToastReducer from '../messageToasts/reducers'; import { initEnhancer } from '../reduxUtils'; import setupApp from '../setup/setupApp'; import Welcome from './Welcome'; +import Menu from '../components/Menu/Menu'; setupApp(); const container = document.getElementById('app'); const bootstrap = JSON.parse(container.getAttribute('data-bootstrap')); const user = { ...bootstrap.user }; +const menu = { ...bootstrap.common.menu_data }; const store = createStore( combineReducers({ @@ -46,7 +48,10 @@ const store = createStore( const App = () => ( - + <> + + + ); diff --git a/superset/assets/stylesheets/less/cosmo/bootswatch.less b/superset/assets/stylesheets/less/cosmo/bootswatch.less index 085b4b0a3ee..a2cf346fc25 100644 --- a/superset/assets/stylesheets/less/cosmo/bootswatch.less +++ b/superset/assets/stylesheets/less/cosmo/bootswatch.less @@ -37,7 +37,7 @@ } } - b.caret { + .caret { display: inline-block; padding: 0 5px 18px 5px; } diff --git a/superset/templates/superset/welcome.html b/superset/templates/superset/welcome.html new file mode 100644 index 00000000000..5f051bbdd68 --- /dev/null +++ b/superset/templates/superset/welcome.html @@ -0,0 +1,28 @@ +{# + 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. +#} +{% extends "superset/basic.html" %} + +{% block navbar %} +{% endblock %} + +{% block tail_js %} + {% with filename="welcome" %} + {% include "superset/partials/_script_tag.html" %} + {% endwith %} +{% endblock %} diff --git a/superset/views/base.py b/superset/views/base.py index 5c6e25905cd..739de1c65bf 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -23,7 +23,7 @@ from typing import Any, Dict, Optional import simplejson as json import yaml -from flask import abort, flash, g, get_flashed_messages, redirect, Response +from flask import abort, flash, g, get_flashed_messages, redirect, Response, session from flask_appbuilder import BaseView, ModelView from flask_appbuilder.actions import action from flask_appbuilder.forms import DynamicForm @@ -34,7 +34,7 @@ from flask_wtf.form import FlaskForm from werkzeug.exceptions import HTTPException from wtforms.fields.core import Field, UnboundField -from superset import conf, db, get_feature_flags, security_manager +from superset import appbuilder, conf, db, get_feature_flags, security_manager from superset.exceptions import SupersetException, SupersetSecurityException from superset.translations.utils import get_language_pack from superset.utils import core as utils @@ -169,16 +169,63 @@ class BaseSupersetView(BaseView): mimetype="application/json", ) + def menu_data(self): + menu = appbuilder.menu.get_data() + root_path = "#" + logo_target_path = "" + if not g.user.is_anonymous: + try: + logo_target_path = ( + appbuilder.app.config.get("LOGO_TARGET_PATH") + or f"/profile/{g.user.username}/" + ) + # when user object has no username + except NameError as e: + logging.exception(e) + + if logo_target_path.startswith("/"): + root_path = f"/superset{logo_target_path}" + else: + root_path = logo_target_path + + languages = {} + for lang in appbuilder.languages: + languages[lang] = { + **appbuilder.languages[lang], + "url": appbuilder.get_url_for_locale(lang), + } + return { + "menu": menu, + "brand": { + "path": root_path, + "icon": appbuilder.app_icon, + "alt": appbuilder.app_name, + }, + "navbar_right": { + "bug_report_url": appbuilder.app.config.get("BUG_REPORT_URL"), + "documentation_url": appbuilder.app.config.get("DOCUMENTATION_URL"), + "languages": languages, + "show_language_picker": len(languages.keys()) > 1, + "user_is_anonymous": g.user.is_anonymous, + "user_info_url": appbuilder.get_url_for_userinfo, + "user_logout_url": appbuilder.get_url_for_logout, + "user_login_url": appbuilder.get_url_for_login, + "locale": session.get("locale", "en"), + }, + } + def common_bootstrap_payload(self): """Common data always sent to the client""" messages = get_flashed_messages(with_categories=True) locale = str(get_locale()) + return { "flash_messages": messages, "conf": {k: conf.get(k) for k in FRONTEND_CONF_KEYS}, "locale": locale, "language_pack": get_language_pack(locale), "feature_flags": get_feature_flags(), + "menu_data": self.menu_data(), } diff --git a/superset/views/core.py b/superset/views/core.py index 01fcc2cd970..7b24afcf010 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -2993,7 +2993,7 @@ class Superset(BaseSupersetView): } return self.render_template( - "superset/basic.html", + "superset/welcome.html", entry="welcome", bootstrap_data=json.dumps( payload, default=utils.pessimistic_json_iso_dttm_ser diff --git a/tests/core_tests.py b/tests/core_tests.py index 50605a90369..6b15e593f94 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -90,7 +90,7 @@ class CoreTests(SupersetTestCase): # Testing overrides resp = self.get_resp("/superset/slice/{}/?standalone=true".format(slc.id)) - assert "List Roles" not in resp + assert '