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 : (
+
+ ),
+ )}
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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 === '-' ? (
+
+ ) : (
+
+ ),
+ )}
+
+ );
+}
+
+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 (
+
+
+
+ }
+ >
+
+
+
+ );
+}
+
+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 '