You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2023/08/17 14:01:33 UTC

[superset] branch master updated: feat: Moves Profile to Single Page App (SPA) (#25001)

This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 712e1f760c feat: Moves Profile to Single Page App (SPA) (#25001)
712e1f760c is described below

commit 712e1f760c3dc27d5a42a9fccebfb3570443abdb
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Aug 17 11:01:24 2023 -0300

    feat: Moves Profile to Single Page App (SPA) (#25001)
---
 superset-frontend/src/features/home/RightMenu.tsx  |   2 +-
 .../profile}/CreatedContent.test.tsx               |   2 +-
 .../profile}/CreatedContent.tsx                    |   0
 .../profile}/Favorites.test.tsx                    |   2 +-
 .../components => features/profile}/Favorites.tsx  |   2 +-
 .../profile}/RecentActivity.test.tsx               |   2 +-
 .../profile}/RecentActivity.tsx                    |   7 +-
 .../profile}/Security.test.tsx                     |   2 +-
 .../components => features/profile}/Security.tsx   |   0
 .../profile}/UserInfo.test.tsx                     |   2 +-
 .../components => features/profile}/UserInfo.tsx   |   0
 .../components => features/profile}/fixtures.tsx   |   0
 .../src/{ => features}/profile/types.ts            |   0
 .../Profile/Profile.test.tsx}                      |  13 +-
 .../components/App.tsx => pages/Profile/index.tsx} |  10 +-
 superset-frontend/src/profile/App.tsx              |  58 --------
 superset-frontend/src/profile/index.tsx            |  23 ---
 superset-frontend/src/views/routes.tsx             |   8 +
 superset-frontend/webpack.config.js                |   1 -
 superset/initialization/__init__.py                |   2 +
 superset/views/base.py                             |   2 +-
 superset/views/core.py                             |  20 +--
 superset/views/profile.py                          |  40 +++++
 tests/integration_tests/core_tests.py              | 134 +----------------
 tests/integration_tests/profile_tests.py           | 164 +++++++++++++++++++++
 25 files changed, 245 insertions(+), 251 deletions(-)

diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx
index d0b8e44ab7..831ae85ba3 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -474,7 +474,7 @@ const RightMenu = ({
             <Menu.ItemGroup key="user-section" title={t('User')}>
               {navbarRight.user_profile_url && (
                 <Menu.Item key="profile">
-                  <a href={navbarRight.user_profile_url}>{t('Profile')}</a>
+                  <Link to={navbarRight.user_profile_url}>{t('Profile')}</Link>
                 </Menu.Item>
               )}
               {navbarRight.user_info_url && (
diff --git a/superset-frontend/src/profile/components/CreatedContent.test.tsx b/superset-frontend/src/features/profile/CreatedContent.test.tsx
similarity index 95%
rename from superset-frontend/src/profile/components/CreatedContent.test.tsx
rename to superset-frontend/src/features/profile/CreatedContent.test.tsx
index 817448cf35..df49e98aff 100644
--- a/superset-frontend/src/profile/components/CreatedContent.test.tsx
+++ b/superset-frontend/src/features/profile/CreatedContent.test.tsx
@@ -20,8 +20,8 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
-import CreatedContent from 'src/profile/components/CreatedContent';
 import TableLoader from 'src/components/TableLoader';
+import CreatedContent from './CreatedContent';
 
 import { user } from './fixtures';
 
diff --git a/superset-frontend/src/profile/components/CreatedContent.tsx b/superset-frontend/src/features/profile/CreatedContent.tsx
similarity index 100%
rename from superset-frontend/src/profile/components/CreatedContent.tsx
rename to superset-frontend/src/features/profile/CreatedContent.tsx
diff --git a/superset-frontend/src/profile/components/Favorites.test.tsx b/superset-frontend/src/features/profile/Favorites.test.tsx
similarity index 96%
rename from superset-frontend/src/profile/components/Favorites.test.tsx
rename to superset-frontend/src/features/profile/Favorites.test.tsx
index 8b5eaf4e86..e21967f3bf 100644
--- a/superset-frontend/src/profile/components/Favorites.test.tsx
+++ b/superset-frontend/src/features/profile/Favorites.test.tsx
@@ -20,8 +20,8 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
-import Favorites from 'src/profile/components/Favorites';
 import TableLoader from 'src/components/TableLoader';
+import Favorites from './Favorites';
 
 import { user } from './fixtures';
 
diff --git a/superset-frontend/src/profile/components/Favorites.tsx b/superset-frontend/src/features/profile/Favorites.tsx
similarity index 98%
rename from superset-frontend/src/profile/components/Favorites.tsx
rename to superset-frontend/src/features/profile/Favorites.tsx
index 834f933071..f38f174779 100644
--- a/superset-frontend/src/profile/components/Favorites.tsx
+++ b/superset-frontend/src/features/profile/Favorites.tsx
@@ -22,7 +22,7 @@ import moment from 'moment';
 import { t } from '@superset-ui/core';
 import { DashboardResponse, BootstrapUser } from 'src/types/bootstrapTypes';
 import TableLoader from '../../components/TableLoader';
-import { Chart } from '../types';
+import { Chart } from './types';
 
 interface FavoritesProps {
   user: BootstrapUser;
diff --git a/superset-frontend/src/profile/components/RecentActivity.test.tsx b/superset-frontend/src/features/profile/RecentActivity.test.tsx
similarity index 95%
rename from superset-frontend/src/profile/components/RecentActivity.test.tsx
rename to superset-frontend/src/features/profile/RecentActivity.test.tsx
index 73fdeb6e84..a64c209296 100644
--- a/superset-frontend/src/profile/components/RecentActivity.test.tsx
+++ b/superset-frontend/src/features/profile/RecentActivity.test.tsx
@@ -18,8 +18,8 @@
  */
 import React from 'react';
 import { shallow } from 'enzyme';
-import RecentActivity from 'src/profile/components/RecentActivity';
 import TableLoader from 'src/components/TableLoader';
+import RecentActivity from './RecentActivity';
 
 import { user } from './fixtures';
 
diff --git a/superset-frontend/src/profile/components/RecentActivity.tsx b/superset-frontend/src/features/profile/RecentActivity.tsx
similarity index 91%
rename from superset-frontend/src/profile/components/RecentActivity.tsx
rename to superset-frontend/src/features/profile/RecentActivity.tsx
index 2810fb3544..d550d2a953 100644
--- a/superset-frontend/src/profile/components/RecentActivity.tsx
+++ b/superset-frontend/src/features/profile/RecentActivity.tsx
@@ -20,10 +20,9 @@ import React from 'react';
 import moment from 'moment';
 import { t } from '@superset-ui/core';
 import rison from 'rison';
-
-import TableLoader from '../../components/TableLoader';
-import { ActivityResult } from '../types';
-import { BootstrapUser } from '../../types/bootstrapTypes';
+import TableLoader from 'src/components/TableLoader';
+import { BootstrapUser } from 'src/types/bootstrapTypes';
+import { ActivityResult } from './types';
 
 interface RecentActivityProps {
   user: BootstrapUser;
diff --git a/superset-frontend/src/profile/components/Security.test.tsx b/superset-frontend/src/features/profile/Security.test.tsx
similarity index 97%
rename from superset-frontend/src/profile/components/Security.test.tsx
rename to superset-frontend/src/features/profile/Security.test.tsx
index 31eda0aae7..af4706375c 100644
--- a/superset-frontend/src/profile/components/Security.test.tsx
+++ b/superset-frontend/src/features/profile/Security.test.tsx
@@ -18,9 +18,9 @@
  */
 import React from 'react';
 import { styledMount as mount } from 'spec/helpers/theming';
-import Security from 'src/profile/components/Security';
 import Label from 'src/components/Label';
 import { user, userNoPerms } from './fixtures';
+import Security from './Security';
 
 describe('Security', () => {
   const mockedProps = {
diff --git a/superset-frontend/src/profile/components/Security.tsx b/superset-frontend/src/features/profile/Security.tsx
similarity index 100%
rename from superset-frontend/src/profile/components/Security.tsx
rename to superset-frontend/src/features/profile/Security.tsx
diff --git a/superset-frontend/src/profile/components/UserInfo.test.tsx b/superset-frontend/src/features/profile/UserInfo.test.tsx
similarity index 97%
rename from superset-frontend/src/profile/components/UserInfo.test.tsx
rename to superset-frontend/src/features/profile/UserInfo.test.tsx
index 9d8a904cb5..6bbc2b52d1 100644
--- a/superset-frontend/src/profile/components/UserInfo.test.tsx
+++ b/superset-frontend/src/features/profile/UserInfo.test.tsx
@@ -19,7 +19,7 @@
 import React from 'react';
 import Gravatar from 'react-gravatar';
 import { mount } from 'enzyme';
-import UserInfo from 'src/profile/components/UserInfo';
+import UserInfo from './UserInfo';
 
 import { user } from './fixtures';
 
diff --git a/superset-frontend/src/profile/components/UserInfo.tsx b/superset-frontend/src/features/profile/UserInfo.tsx
similarity index 100%
rename from superset-frontend/src/profile/components/UserInfo.tsx
rename to superset-frontend/src/features/profile/UserInfo.tsx
diff --git a/superset-frontend/src/profile/components/fixtures.tsx b/superset-frontend/src/features/profile/fixtures.tsx
similarity index 100%
rename from superset-frontend/src/profile/components/fixtures.tsx
rename to superset-frontend/src/features/profile/fixtures.tsx
diff --git a/superset-frontend/src/profile/types.ts b/superset-frontend/src/features/profile/types.ts
similarity index 100%
rename from superset-frontend/src/profile/types.ts
rename to superset-frontend/src/features/profile/types.ts
diff --git a/superset-frontend/src/profile/components/App.test.tsx b/superset-frontend/src/pages/Profile/Profile.test.tsx
similarity index 79%
rename from superset-frontend/src/profile/components/App.test.tsx
rename to superset-frontend/src/pages/Profile/Profile.test.tsx
index 4f7a4caa1e..cf1446c29a 100644
--- a/superset-frontend/src/profile/components/App.test.tsx
+++ b/superset-frontend/src/pages/Profile/Profile.test.tsx
@@ -19,26 +19,25 @@
 import React from 'react';
 import { Row, Col } from 'src/components';
 import { shallow } from 'enzyme';
-import App from 'src/profile/components/App';
+import Profile from 'src/pages/Profile';
+import { user } from 'src/features/profile/fixtures';
 
-import { user } from './fixtures';
-
-describe('App', () => {
+describe('Profile', () => {
   const mockedProps = {
     user,
   };
   it('is valid', () => {
-    expect(React.isValidElement(<App {...mockedProps} />)).toBe(true);
+    expect(React.isValidElement(<Profile {...mockedProps} />)).toBe(true);
   });
 
   it('renders 2 Col', () => {
-    const wrapper = shallow(<App {...mockedProps} />);
+    const wrapper = shallow(<Profile {...mockedProps} />);
     expect(wrapper.find(Row)).toExist();
     expect(wrapper.find(Col)).toHaveLength(2);
   });
 
   it('renders 4 Tabs', () => {
-    const wrapper = shallow(<App {...mockedProps} />);
+    const wrapper = shallow(<Profile {...mockedProps} />);
     expect(wrapper.find('[tab]')).toHaveLength(4);
   });
 });
diff --git a/superset-frontend/src/profile/components/App.tsx b/superset-frontend/src/pages/Profile/index.tsx
similarity index 90%
rename from superset-frontend/src/profile/components/App.tsx
rename to superset-frontend/src/pages/Profile/index.tsx
index 08130176fe..511017941a 100644
--- a/superset-frontend/src/profile/components/App.tsx
+++ b/superset-frontend/src/pages/Profile/index.tsx
@@ -21,11 +21,11 @@ import { t, styled } from '@superset-ui/core';
 import { Row, Col } from 'src/components';
 import Tabs from 'src/components/Tabs';
 import { BootstrapUser } from 'src/types/bootstrapTypes';
-import Favorites from './Favorites';
-import UserInfo from './UserInfo';
-import Security from './Security';
-import RecentActivity from './RecentActivity';
-import CreatedContent from './CreatedContent';
+import Favorites from 'src/features/profile/Favorites';
+import UserInfo from 'src/features/profile/UserInfo';
+import Security from 'src/features/profile/Security';
+import RecentActivity from 'src/features/profile/RecentActivity';
+import CreatedContent from 'src/features/profile/CreatedContent';
 
 interface AppProps {
   user: BootstrapUser;
diff --git a/superset-frontend/src/profile/App.tsx b/superset-frontend/src/profile/App.tsx
deleted file mode 100644
index ed331ce737..0000000000
--- a/superset-frontend/src/profile/App.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * 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 { hot } from 'react-hot-loader/root';
-import thunk from 'redux-thunk';
-import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
-import { Provider } from 'react-redux';
-import { ThemeProvider } from '@superset-ui/core';
-import { GlobalStyles } from 'src/GlobalStyles';
-import App from 'src/profile/components/App';
-import messageToastReducer from 'src/components/MessageToasts/reducers';
-import { initEnhancer } from 'src/reduxUtils';
-import setupApp from 'src/setup/setupApp';
-import setupExtensions from 'src/setup/setupExtensions';
-import { theme } from 'src/preamble';
-import ToastContainer from 'src/components/MessageToasts/ToastContainer';
-import getBootstrapData from 'src/utils/getBootstrapData';
-
-setupApp();
-setupExtensions();
-
-const bootstrapData = getBootstrapData();
-
-const store = createStore(
-  combineReducers({
-    messageToasts: messageToastReducer,
-  }),
-  {},
-  compose(applyMiddleware(thunk), initEnhancer(false)),
-);
-
-const Application = () => (
-  <Provider store={store}>
-    <ThemeProvider theme={theme}>
-      <GlobalStyles />
-      <App user={bootstrapData.user} />
-      <ToastContainer />
-    </ThemeProvider>
-  </Provider>
-);
-
-export default hot(Application);
diff --git a/superset-frontend/src/profile/index.tsx b/superset-frontend/src/profile/index.tsx
deleted file mode 100644
index c257009e64..0000000000
--- a/superset-frontend/src/profile/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * 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 ReactDOM from 'react-dom';
-import App from './App';
-
-ReactDOM.render(<App />, document.getElementById('app'));
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index 23a25a5d85..197284d3ac 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -119,6 +119,10 @@ const RowLevelSecurityList = lazy(
     ),
 );
 
+const Profile = lazy(
+  () => import(/* webpackChunkName: "Profile" */ 'src/pages/Profile'),
+);
+
 type Routes = {
   path: string;
   Component: React.ComponentType;
@@ -217,6 +221,10 @@ export const routes: Routes = [
     path: '/rowlevelsecurity/list',
     Component: RowLevelSecurityList,
   },
+  {
+    path: '/profile',
+    Component: Profile,
+  },
 ];
 
 if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index fb137c06cb..a47670d9d4 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -212,7 +212,6 @@ const config = {
     spa: addPreamble('/src/views/index.tsx'),
     embedded: addPreamble('/src/embedded/index.tsx'),
     sqllab: addPreamble('/src/SqlLab/index.tsx'),
-    profile: addPreamble('/src/profile/index.tsx'),
     showSavedQuery: [path.join(APP_DIR, '/src/showSavedQuery/index.jsx')],
   },
   output,
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 828be78401..c390a6f779 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -182,6 +182,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         from superset.views.key_value import KV
         from superset.views.log.api import LogRestApi
         from superset.views.log.views import LogModelView
+        from superset.views.profile import ProfileView
         from superset.views.redirects import R
         from superset.views.sql_lab.views import (
             SavedQueryView,
@@ -309,6 +310,7 @@ class SupersetAppInitializer:  # pylint: disable=too-many-public-methods
         appbuilder.add_view_no_menu(ExplorePermalinkView)
         appbuilder.add_view_no_menu(KV)
         appbuilder.add_view_no_menu(R)
+        appbuilder.add_view_no_menu(ProfileView)
         appbuilder.add_view_no_menu(SavedQueryView)
         appbuilder.add_view_no_menu(SavedQueryViewApi)
         appbuilder.add_view_no_menu(SliceAsync)
diff --git a/superset/views/base.py b/superset/views/base.py
index a2c62df41b..d1c865374a 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -406,7 +406,7 @@ def menu_data(user: User) -> dict[str, Any]:
             "user_login_url": appbuilder.get_url_for_login,
             "user_profile_url": None
             if user.is_anonymous or is_feature_enabled("MENU_HIDE_USER_INFO")
-            else "/superset/profile/",
+            else "/profile/",
             "locale": session.get("locale", "en"),
         },
     }
diff --git a/superset/views/core.py b/superset/views/core.py
index d377f94d65..366ec6d664 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -80,7 +80,6 @@ from superset.utils.core import (
     base_json_conv,
     DatasourceType,
     get_user_id,
-    get_username,
     ReservedUrlParameters,
 )
 from superset.views.base import (
@@ -985,24 +984,9 @@ class Superset(BaseSupersetView):  # pylint: disable=too-many-public-methods
     @has_access
     @event_logger.log_this
     @expose("/profile/")
+    @deprecated(new_target="/profile")
     def profile(self) -> FlaskResponse:
-        """User profile page"""
-        user = g.user if hasattr(g, "user") and g.user else None
-        if not user or security_manager.is_guest_user(user) or user.is_anonymous:
-            abort(404)
-        payload = {
-            "user": bootstrap_user_data(user, include_perms=True),
-            "common": common_bootstrap_payload(user),
-        }
-
-        return self.render_template(
-            "superset/basic.html",
-            title=_("%(user)s's profile", user=get_username()),
-            entry="profile",
-            bootstrap_data=json.dumps(
-                payload, default=utils.pessimistic_json_iso_dttm_ser
-            ),
-        )
+        return redirect("/profile/")
 
     @has_access
     @event_logger.log_this
diff --git a/superset/views/profile.py b/superset/views/profile.py
new file mode 100644
index 0000000000..3308a0f645
--- /dev/null
+++ b/superset/views/profile.py
@@ -0,0 +1,40 @@
+# 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.
+from flask import abort, g
+from flask_appbuilder import permission_name
+from flask_appbuilder.api import expose
+from flask_appbuilder.security.decorators import has_access
+
+from superset import event_logger, security_manager
+from superset.superset_typing import FlaskResponse
+
+from .base import BaseSupersetView
+
+
+class ProfileView(BaseSupersetView):
+    route_base = "/profile"
+    class_permission_name = "Profile"
+
+    @expose("/")
+    @has_access
+    @permission_name("read")
+    @event_logger.log_this
+    def root(self) -> FlaskResponse:
+        user = g.user if hasattr(g, "user") and g.user else None
+        if not user or security_manager.is_guest_user(user) or user.is_anonymous:
+            abort(404)
+        return super().render_app_template()
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 51ca10e608..2f346564e9 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -26,12 +26,10 @@ from unittest import mock
 from urllib.parse import quote
 
 import pandas as pd
-import prison
 import pytest
 import pytz
 import sqlalchemy as sqla
 from flask_babel import lazy_gettext as _
-from sqlalchemy import Table
 from sqlalchemy.exc import SQLAlchemyError
 
 import superset.utils.database
@@ -56,7 +54,7 @@ from superset.utils import core as utils
 from superset.utils.core import backend
 from superset.utils.database import get_example_database
 from superset.views.database.views import DatabaseView
-from tests.integration_tests.conftest import CTAS_SCHEMA_NAME, with_feature_flags
+from tests.integration_tests.conftest import with_feature_flags
 from tests.integration_tests.fixtures.birth_names_dashboard import (
     load_birth_names_dashboard_with_slices,
     load_birth_names_data,
@@ -65,12 +63,10 @@ from tests.integration_tests.fixtures.energy_dashboard import (
     load_energy_table_data,
     load_energy_table_with_slice,
 )
-from tests.integration_tests.fixtures.public_role import public_role_like_gamma
 from tests.integration_tests.fixtures.world_bank_dashboard import (
     load_world_bank_dashboard_with_slices,
     load_world_bank_data,
 )
-from tests.integration_tests.insert_chart_mixin import InsertChartMixin
 from tests.integration_tests.test_app import app
 
 from .base_tests import SupersetTestCase
@@ -86,7 +82,7 @@ def cleanup():
     yield
 
 
-class TestCore(SupersetTestCase, InsertChartMixin):
+class TestCore(SupersetTestCase):
     def setUp(self):
         self.table_ids = {
             tbl.table_name: tbl.id for tbl in (db.session.query(SqlaTable).all())
@@ -107,25 +103,6 @@ class TestCore(SupersetTestCase, InsertChartMixin):
         )
         return dashboard
 
-    def insert_chart_created_by(self, username: str) -> Slice:
-        user = self.get_user(username)
-        dataset = db.session.query(SqlaTable).first()
-        chart = self.insert_chart(
-            f"create_title_test",
-            [user.id],
-            dataset.id,
-            created_by=user,
-        )
-        return chart
-
-    @pytest.fixture()
-    def insert_dashboard_created_by_admin(self):
-        with self.create_app().app_context():
-            dashboard = self.insert_dashboard_created_by("admin")
-            yield dashboard
-            db.session.delete(dashboard)
-            db.session.commit()
-
     @pytest.fixture()
     def insert_dashboard_created_by_gamma(self):
         dashboard = self.insert_dashboard_created_by("gamma")
@@ -133,14 +110,6 @@ class TestCore(SupersetTestCase, InsertChartMixin):
         db.session.delete(dashboard)
         db.session.commit()
 
-    @pytest.fixture()
-    def insert_chart_created_by_admin(self):
-        with self.create_app().app_context():
-            chart = self.insert_chart_created_by("admin")
-            yield chart
-            db.session.delete(chart)
-            db.session.commit()
-
     def test_login(self):
         resp = self.get_resp("/login/", data=dict(username="admin", password="general"))
         self.assertNotIn("User confirmation needed", resp)
@@ -513,100 +482,6 @@ class TestCore(SupersetTestCase, InsertChartMixin):
         for k in keys:
             self.assertIn(k, resp.keys())
 
-    @pytest.mark.usefixtures("insert_dashboard_created_by_admin")
-    @pytest.mark.usefixtures("insert_chart_created_by_admin")
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    def test_user_profile(self, username="admin"):
-        self.login(username=username)
-        slc = self.get_slice("Girls", db.session)
-        dashboard = db.session.query(Dashboard).filter_by(slug="births").first()
-        # Set a favorite dashboard
-        self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={})
-        # Set a favorite chart
-        self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={})
-
-        # Get favorite dashboards:
-        request_query = {
-            "columns": ["created_on_delta_humanized", "dashboard_title", "url"],
-            "filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}],
-            "keys": ["none"],
-            "order_column": "changed_on",
-            "order_direction": "desc",
-            "page": 0,
-            "page_size": 100,
-        }
-        url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
-        resp = self.client.get(url)
-        assert resp.json["count"] == 1
-        assert resp.json["result"][0]["dashboard_title"] == "USA Births Names"
-
-        # Get Favorite Charts
-        request_query = {
-            "filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}],
-            "order_column": "slice_name",
-            "order_direction": "asc",
-            "page": 0,
-            "page_size": 25,
-        }
-        url = f"api/v1/chart/?q={prison.dumps(request_query)}"
-        resp = self.client.get(url)
-        assert resp.json["count"] == 1
-        assert resp.json["result"][0]["id"] == slc.id
-
-        # Get recent activity
-        url = "/api/v1/log/recent_activity/?q=(page_size:50)"
-        resp = self.client.get(url)
-        # TODO data for recent activity varies for sqlite, we should be able to assert
-        # the returned data
-        assert resp.status_code == 200
-
-        # Get dashboards created by the user
-        request_query = {
-            "columns": ["created_on_delta_humanized", "dashboard_title", "url"],
-            "filters": [
-                {"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"}
-            ],
-            "keys": ["none"],
-            "order_column": "changed_on",
-            "order_direction": "desc",
-            "page": 0,
-            "page_size": 100,
-        }
-        url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
-        resp = self.client.get(url)
-        assert resp.json["result"][0]["dashboard_title"] == "create_title_test"
-
-        # Get charts created by the user
-        request_query = {
-            "columns": ["created_on_delta_humanized", "slice_name", "url"],
-            "filters": [
-                {"col": "created_by", "opr": "chart_created_by_me", "value": "me"}
-            ],
-            "keys": ["none"],
-            "order_column": "changed_on_delta_humanized",
-            "order_direction": "desc",
-            "page": 0,
-            "page_size": 100,
-        }
-        url = f"/api/v1/chart/?q={prison.dumps(request_query)}"
-        resp = self.client.get(url)
-        assert resp.json["count"] == 1
-        assert resp.json["result"][0]["slice_name"] == "create_title_test"
-
-        resp = self.get_resp(f"/superset/profile/")
-        self.assertIn('"app"', resp)
-
-    def test_user_profile_gamma(self):
-        self.login(username="gamma")
-        resp = self.get_resp(f"/superset/profile/")
-        self.assertIn('"app"', resp)
-
-    @pytest.mark.usefixtures("public_role_like_gamma")
-    def test_user_profile_anonymous(self):
-        self.logout()
-        resp = self.client.get("/superset/profile/")
-        assert resp.status_code == 404
-
     @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_slice_id_is_always_logged_correctly_on_web_request(self):
         # explore case
@@ -1280,6 +1155,11 @@ class TestCore(SupersetTestCase, InsertChartMixin):
             is True
         )
 
+    def test_redirect_new_profile(self):
+        self.login(username="admin")
+        resp = self.client.get("/superset/profile/")
+        assert resp.status_code == 302
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/integration_tests/profile_tests.py b/tests/integration_tests/profile_tests.py
new file mode 100644
index 0000000000..aa5448139e
--- /dev/null
+++ b/tests/integration_tests/profile_tests.py
@@ -0,0 +1,164 @@
+# 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 prison
+import pytest
+
+from superset import db
+from superset.connectors.sqla.models import SqlaTable
+from superset.models.dashboard import Dashboard
+from superset.models.slice import Slice
+from tests.integration_tests.fixtures.birth_names_dashboard import (
+    load_birth_names_dashboard_with_slices,
+    load_birth_names_data,
+)
+from tests.integration_tests.fixtures.public_role import public_role_like_gamma
+from tests.integration_tests.insert_chart_mixin import InsertChartMixin
+
+from .base_tests import SupersetTestCase
+
+
+class TestProfile(SupersetTestCase, InsertChartMixin):
+    def insert_dashboard_created_by(self, username: str) -> Dashboard:
+        user = self.get_user(username)
+        dashboard = self.insert_dashboard(
+            f"create_title_test",
+            f"create_slug_test",
+            [user.id],
+            created_by=user,
+        )
+        return dashboard
+
+    @pytest.fixture()
+    def insert_dashboard_created_by_admin(self):
+        with self.create_app().app_context():
+            dashboard = self.insert_dashboard_created_by("admin")
+            yield dashboard
+            db.session.delete(dashboard)
+            db.session.commit()
+
+    def insert_chart_created_by(self, username: str) -> Slice:
+        user = self.get_user(username)
+        dataset = db.session.query(SqlaTable).first()
+        chart = self.insert_chart(
+            f"create_title_test",
+            [user.id],
+            dataset.id,
+            created_by=user,
+        )
+        return chart
+
+    @pytest.fixture()
+    def insert_chart_created_by_admin(self):
+        with self.create_app().app_context():
+            chart = self.insert_chart_created_by("admin")
+            yield chart
+            db.session.delete(chart)
+            db.session.commit()
+
+    @pytest.mark.usefixtures("insert_dashboard_created_by_admin")
+    @pytest.mark.usefixtures("insert_chart_created_by_admin")
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_user_profile(self, username="admin"):
+        self.login(username=username)
+        slc = self.get_slice("Girls", db.session)
+        dashboard = db.session.query(Dashboard).filter_by(slug="births").first()
+        # Set a favorite dashboard
+        self.client.post(f"/api/v1/dashboard/{dashboard.id}/favorites/", json={})
+        # Set a favorite chart
+        self.client.post(f"/api/v1/chart/{slc.id}/favorites/", json={})
+
+        # Get favorite dashboards:
+        request_query = {
+            "columns": ["created_on_delta_humanized", "dashboard_title", "url"],
+            "filters": [{"col": "id", "opr": "dashboard_is_favorite", "value": True}],
+            "keys": ["none"],
+            "order_column": "changed_on",
+            "order_direction": "desc",
+            "page": 0,
+            "page_size": 100,
+        }
+        url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
+        resp = self.client.get(url)
+        assert resp.json["count"] == 1
+        assert resp.json["result"][0]["dashboard_title"] == "USA Births Names"
+
+        # Get Favorite Charts
+        request_query = {
+            "filters": [{"col": "id", "opr": "chart_is_favorite", "value": True}],
+            "order_column": "slice_name",
+            "order_direction": "asc",
+            "page": 0,
+            "page_size": 25,
+        }
+        url = f"api/v1/chart/?q={prison.dumps(request_query)}"
+        resp = self.client.get(url)
+        assert resp.json["count"] == 1
+        assert resp.json["result"][0]["id"] == slc.id
+
+        # Get recent activity
+        url = "/api/v1/log/recent_activity/?q=(page_size:50)"
+        resp = self.client.get(url)
+        # TODO data for recent activity varies for sqlite, we should be able to assert
+        # the returned data
+        assert resp.status_code == 200
+
+        # Get dashboards created by the user
+        request_query = {
+            "columns": ["created_on_delta_humanized", "dashboard_title", "url"],
+            "filters": [
+                {"col": "created_by", "opr": "dashboard_created_by_me", "value": "me"}
+            ],
+            "keys": ["none"],
+            "order_column": "changed_on",
+            "order_direction": "desc",
+            "page": 0,
+            "page_size": 100,
+        }
+        url = f"/api/v1/dashboard/?q={prison.dumps(request_query)}"
+        resp = self.client.get(url)
+        assert resp.json["result"][0]["dashboard_title"] == "create_title_test"
+
+        # Get charts created by the user
+        request_query = {
+            "columns": ["created_on_delta_humanized", "slice_name", "url"],
+            "filters": [
+                {"col": "created_by", "opr": "chart_created_by_me", "value": "me"}
+            ],
+            "keys": ["none"],
+            "order_column": "changed_on_delta_humanized",
+            "order_direction": "desc",
+            "page": 0,
+            "page_size": 100,
+        }
+        url = f"/api/v1/chart/?q={prison.dumps(request_query)}"
+        resp = self.client.get(url)
+        assert resp.json["count"] == 1
+        assert resp.json["result"][0]["slice_name"] == "create_title_test"
+
+        resp = self.get_resp(f"/profile/")
+        self.assertIn('"app"', resp)
+
+    def test_user_profile_gamma(self):
+        self.login(username="gamma")
+        resp = self.get_resp(f"/profile/")
+        self.assertIn('"app"', resp)
+
+    @pytest.mark.usefixtures("public_role_like_gamma")
+    def test_user_profile_anonymous(self):
+        self.logout()
+        resp = self.client.get("/profile/")
+        assert resp.status_code == 404