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

elizabeth 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 09b92e7d08 feat: Allow superset to be deployed under a prefixed URL 
(#30134)
09b92e7d08 is described below

commit 09b92e7d08bc96b6006938c00d9f8ce7a108102b
Author: Martyn Gigg <[email protected]>
AuthorDate: Wed Apr 9 21:43:44 2025 +0100

    feat: Allow superset to be deployed under a prefixed URL (#30134)
    
    Co-authored-by: Kamil Gabryjelski <[email protected]>
---
 .github/workflows/bashlib.sh                       |   8 +-
 .github/workflows/superset-e2e.yml                 |   3 +-
 Dockerfile                                         |   2 +-
 docker-compose.yml                                 |   8 +-
 docker/.env                                        |   1 +
 docker/docker-healthcheck.sh                       |  19 ++++
 docker/nginx/nginx.conf                            |  41 +-------
 docker/nginx/templates/superset.conf.template      |  57 +++++++++++
 docker/pythonpath_dev/superset_config.py           |   7 +-
 docs/docs/configuration/configuring-superset.mdx   |  39 ++++++++
 docs/docs/configuration/networking-settings.mdx    |   2 +-
 .../cypress/applitools/explore.test.ts             |   2 +-
 .../cypress/e2e/chart_list/list.test.ts            |   9 +-
 .../cypress/e2e/dashboard/_skip.url_params.test.ts |   4 +-
 .../cypress/e2e/dashboard/drilltodetail.test.ts    |   2 +-
 .../e2e/dashboard/horizontalFilterBar.test.ts      |   2 +-
 .../dashboard/nativeFilters.noInitState.test.ts    |   2 +-
 .../cypress/e2e/dashboard/tabs.test.ts             |   4 +-
 .../cypress-base/cypress/e2e/dashboard/utils.ts    |  28 +++---
 .../cypress/e2e/explore/_skip.AdhocFilters.test.ts |   6 +-
 .../cypress/e2e/explore/advanced_analytics.test.ts |   4 +-
 .../cypress/e2e/explore/control.test.ts            |   2 +-
 .../cypress-base/cypress/e2e/explore/utils.ts      |  20 ++--
 .../e2e/explore/visualizations/box_plot.test.js    |   2 +-
 .../e2e/explore/visualizations/bubble.test.js      |   2 +-
 .../e2e/explore/visualizations/compare.test.js     |   2 +-
 .../explore/visualizations/download_chart.test.js  |   2 +-
 .../e2e/explore/visualizations/gauge.test.js       |   2 +-
 .../e2e/explore/visualizations/graph.test.ts       |   2 +-
 .../cypress/e2e/explore/visualizations/pie.test.js |   2 +-
 .../e2e/explore/visualizations/pivot_table.test.js |   2 +-
 .../e2e/explore/visualizations/sunburst.test.js    |   2 +-
 .../e2e/explore/visualizations/time_table.js       |   2 +-
 .../e2e/explore/visualizations/world_map.test.js   |   2 +-
 .../e2e/sqllab/_skip.sourcePanel.index.test.js     |   2 +-
 .../cypress-base/cypress/e2e/sqllab/query.test.ts  |   6 +-
 .../cypress-base/cypress/utils/vizPlugins.ts       |   4 +-
 .../src/connection/SupersetClientClass.ts          |  34 +++----
 .../superset-ui-core/src/connection/constants.ts   |   2 +-
 .../superset-ui-core/src/connection/types.ts       |   2 +-
 .../test/connection/SupersetClientClass.test.ts    |  12 ++-
 .../test/query/api/v1/makeApi.test.ts              |   4 +-
 .../spec/helpers/setupSupersetClient.js            |   2 +-
 .../SqlLab/components/TabbedSqlEditors/index.tsx   |   5 +-
 .../src/components/Chart/chartAction.js            |   3 +-
 .../src/components/FacePile/index.tsx              |   3 +-
 superset-frontend/src/components/Tags/Tag.test.tsx |   9 +-
 superset-frontend/src/components/Tags/Tag.tsx      |   7 +-
 .../src/components/Tags/TagsList.test.tsx          |   9 +-
 superset-frontend/src/constants.ts                 |   2 +
 .../src/dashboard/actions/dashboardState.js        |   9 +-
 .../components/AddSliceCard/AddSliceCard.tsx       |   5 +-
 .../src/dashboard/components/DashboardGrid.jsx     |  17 ++--
 .../src/dashboard/components/SaveModal.tsx         |   3 +-
 .../src/dashboard/components/SliceAdder.tsx        |   9 +-
 .../components/nativeFilters/FilterBar/index.tsx   |  12 ++-
 .../nativeFilters/FilterBar/keyValue.tsx           |   2 +-
 superset-frontend/src/embedded/index.tsx           |   2 +
 .../components/ExploreViewContainer/index.jsx      |   3 +-
 .../explore/exploreUtils/getChartDataUri.test.ts   | 107 +++++++++++----------
 .../src/explore/exploreUtils/index.js              |  16 +--
 .../features/allEntities/AllEntitiesTable.test.tsx |   2 +
 .../src/features/charts/ChartCard.tsx              |   5 +-
 .../src/features/dashboards/DashboardCard.tsx      |   5 +-
 superset-frontend/src/features/home/ChartTable.tsx |   3 +-
 .../src/features/home/DashboardTable.tsx           |   3 +-
 superset-frontend/src/features/home/EmptyState.tsx |   3 +-
 superset-frontend/src/features/home/Menu.tsx       |   5 +-
 superset-frontend/src/features/home/RightMenu.tsx  |  11 ++-
 .../src/features/home/SavedQueries.tsx             |  11 ++-
 superset-frontend/src/features/tags/TagCard.tsx    |   5 +-
 .../src/{views/index.tsx => globals.d.ts}          |   5 +-
 .../src/middleware/loggerMiddleware.js             |   3 +-
 .../src/pages/AnnotationLayerList/index.tsx        |   3 +-
 .../src/pages/DashboardList/index.tsx              |   3 +-
 superset-frontend/src/preamble.ts                  |   4 +-
 .../src/{views/index.tsx => public-path.ts}        |   6 +-
 superset-frontend/src/theme.ts                     |   2 +
 superset-frontend/src/types/bootstrapTypes.ts      |   2 +
 .../src/{views/index.tsx => utils/assetUrl.ts}     |  12 ++-
 superset-frontend/src/utils/export.ts              |   7 +-
 superset-frontend/src/utils/getBootstrapData.ts    |  20 ++++
 .../{getBootstrapData.ts => navigationUtils.ts}    |  36 ++++---
 .../src/{views/index.tsx => utils/pathUtils.ts}    |  12 ++-
 superset-frontend/src/views/App.tsx                |   4 +-
 superset-frontend/src/views/CRUD/hooks.ts          |   5 +-
 superset-frontend/src/views/index.tsx              |   2 +
 superset-frontend/src/views/menu.tsx               |   1 +
 superset-frontend/webpack.config.js                |  29 +++++-
 superset/app.py                                    |  50 +++++++++-
 superset/initialization/__init__.py                |  17 ++--
 superset/templates/appbuilder/navbar.html          |   2 +-
 superset/templates/superset/base.html              |   8 +-
 superset/templates/superset/basic.html             |   8 +-
 .../templates/superset/partials/asset_bundle.html  |  57 +++++------
 superset/templates/superset/spa.html               |   2 +-
 superset/utils/urls.py                             |   3 +-
 superset/views/base.py                             |   6 +-
 superset/views/core.py                             |  22 +++--
 superset/views/dashboard/views.py                  |  11 ++-
 100 files changed, 636 insertions(+), 344 deletions(-)

diff --git a/.github/workflows/bashlib.sh b/.github/workflows/bashlib.sh
index 0d359e059e..deb56ab649 100644
--- a/.github/workflows/bashlib.sh
+++ b/.github/workflows/bashlib.sh
@@ -145,6 +145,7 @@ cypress-install() {
 
 cypress-run-all() {
   local USE_DASHBOARD=$1
+  local APP_ROOT=$2
   cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
 
   # Start Flask and run it in background
@@ -152,7 +153,12 @@ cypress-run-all() {
   # so errors can print to stderr.
   local flasklog="${HOME}/flask.log"
   local port=8081
-  export CYPRESS_BASE_URL="http://localhost:${port}";
+  CYPRESS_BASE_URL="http://localhost:${port}";
+  if [ -n "$APP_ROOT" ]; then
+    export SUPERSET_APP_ROOT=$APP_ROOT
+    CYPRESS_BASE_URL=${CYPRESS_BASE_URL}${APP_ROOT}
+  fi
+  export CYPRESS_BASE_URL
 
   nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
   local flaskProcessId=$!
diff --git a/.github/workflows/superset-e2e.yml 
b/.github/workflows/superset-e2e.yml
index 302ba63efc..a21e46b659 100644
--- a/.github/workflows/superset-e2e.yml
+++ b/.github/workflows/superset-e2e.yml
@@ -42,6 +42,7 @@ jobs:
       matrix:
         parallel_id: [0, 1, 2, 3, 4, 5]
         browser: ["chrome"]
+        app_root: ["", "/app/prefix"]
     env:
       SUPERSET_ENV: development
       SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -135,7 +136,7 @@ jobs:
           CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
           NODE_OPTIONS: "--max-old-space-size=4096"
         with:
-          run: cypress-run-all ${{ env.USE_DASHBOARD }}
+          run: cypress-run-all ${{ env.USE_DASHBOARD }} ${{ matrix.app_root }}
       - name: Upload Artifacts
         uses: actions/upload-artifact@v4
         if: failure()
diff --git a/Dockerfile b/Dockerfile
index f6a21d2a01..f2643e5dcc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -208,7 +208,7 @@ RUN rm superset/translations/*/*/*.po
 COPY --from=superset-node /app/superset/translations superset/translations
 COPY --from=python-translation-compiler /app/translations_mo 
superset/translations
 
-HEALTHCHECK CMD curl -f "http://localhost:${SUPERSET_PORT}/health";
+HEALTHCHECK CMD /app/docker/docker-healthcheck.sh
 CMD ["/app/docker/entrypoints/run-server.sh"]
 EXPOSE ${SUPERSET_PORT}
 
diff --git a/docker-compose.yml b/docker-compose.yml
index cb5953bee3..be4f191d20 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,7 +29,6 @@ x-superset-volumes: &superset-volumes
   - ./superset-frontend:/app/superset-frontend
   - superset_home:/app/superset_home
   - ./tests:/app/tests
-
 x-common-build: &common-build
   context: .
   target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
@@ -43,6 +42,11 @@ x-common-build: &common-build
 
 services:
   nginx:
+    env_file:
+      - path: docker/.env # default
+        required: true
+      - path: docker/.env-local # optional override
+        required: false
     image: nginx:latest
     container_name: superset_nginx
     restart: unless-stopped
@@ -52,6 +56,8 @@ services:
       - "host.docker.internal:host-gateway"
     volumes:
       - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+      - ./docker/nginx/templates:/etc/nginx/templates:ro
+
   redis:
     image: redis:7
     container_name: superset_cache
diff --git a/docker/.env b/docker/.env
index 6e06e0c60e..2ae0578bfc 100644
--- a/docker/.env
+++ b/docker/.env
@@ -54,6 +54,7 @@ REDIS_HOST=redis
 REDIS_PORT=6379
 
 FLASK_DEBUG=true
+SUPERSET_APP_ROOT="/"
 SUPERSET_ENV=development
 SUPERSET_LOAD_EXAMPLES=yes
 CYPRESS_CONFIG=false
diff --git a/docker/docker-healthcheck.sh b/docker/docker-healthcheck.sh
new file mode 100755
index 0000000000..36204fcef6
--- /dev/null
+++ b/docker/docker-healthcheck.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+curl -f "http://localhost:${SUPERSET_PORT}/${SUPERSET_APP_ROOT/\//}/health"; || 
exit 1
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index e60a07fbdf..63d91ba051 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -90,44 +90,5 @@ http {
 
     client_max_body_size 10m;
 
-    upstream superset_app {
-        server host.docker.internal:8088;
-        keepalive 100;
-    }
-
-    upstream superset_websocket {
-        server host.docker.internal:8080;
-        keepalive 100;
-    }
-
-    server {
-        listen 80 default_server;
-        server_name  _;
-
-        location /ws {
-            proxy_pass http://superset_websocket;
-            proxy_http_version 1.1;
-            proxy_set_header Upgrade $http_upgrade;
-            proxy_set_header Connection "Upgrade";
-            proxy_set_header Host $host;
-        }
-
-        location /static {
-            proxy_pass http://host.docker.internal:9000;  # Proxy to 
superset-node
-            proxy_http_version 1.1;
-            proxy_set_header Host $host;
-        }
-
-        location / {
-            proxy_pass http://superset_app;
-            proxy_set_header    Host                $host;
-            proxy_set_header    X-Real-IP           $remote_addr;
-            proxy_set_header    X-Forwarded-For     $remote_addr;
-            proxy_set_header    X-Forwarded-Host    $host;
-            proxy_set_header    X-Forwarded-Proto   $scheme;
-            proxy_http_version 1.1;
-            port_in_redirect off;
-            proxy_connect_timeout 300;
-        }
-    }
+    include /etc/nginx/conf.d/superset.conf;
 }
diff --git a/docker/nginx/templates/superset.conf.template 
b/docker/nginx/templates/superset.conf.template
new file mode 100644
index 0000000000..e87eb85ef7
--- /dev/null
+++ b/docker/nginx/templates/superset.conf.template
@@ -0,0 +1,57 @@
+# 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.
+
+upstream superset_app {
+    server host.docker.internal:8088;
+    keepalive 100;
+}
+
+upstream superset_websocket {
+    server host.docker.internal:8080;
+    keepalive 100;
+}
+
+server {
+    listen 80 default_server;
+    server_name  _;
+
+    location /ws {
+        proxy_pass http://superset_websocket;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "Upgrade";
+        proxy_set_header Host $host;
+    }
+
+    location ${SUPERSET_APP_ROOT}/static {
+        proxy_pass http://host.docker.internal:9000;  # Proxy to superset-node
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+    }
+
+    location ${SUPERSET_APP_ROOT} {
+        proxy_pass http://superset_app;
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_http_version 1.1;
+        port_in_redirect off;
+        proxy_connect_timeout 300;
+    }
+
+}
diff --git a/docker/pythonpath_dev/superset_config.py 
b/docker/pythonpath_dev/superset_config.py
index cd5e7f3976..705846ac48 100644
--- a/docker/pythonpath_dev/superset_config.py
+++ b/docker/pythonpath_dev/superset_config.py
@@ -71,6 +71,7 @@ CACHE_CONFIG = {
     "CACHE_REDIS_DB": REDIS_RESULTS_DB,
 }
 DATA_CACHE_CONFIG = CACHE_CONFIG
+THUMBNAIL_CACHE_CONFIG = CACHE_CONFIG
 
 
 class CeleryConfig:
@@ -100,9 +101,11 @@ CELERY_CONFIG = CeleryConfig
 
 FEATURE_FLAGS = {"ALERT_REPORTS": True}
 ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
-WEBDRIVER_BASEURL = "http://superset:8088/";  # When using docker compose 
baseurl should be http://superset_app:8088/  # noqa: E501
+WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', 
'/')}/"  # When using docker compose baseurl should be 
http://superset_nginx{ENV{BASEPATH}}/  # noqa: E501
 # The base URL for the email report hyperlinks.
-WEBDRIVER_BASEURL_USER_FRIENDLY = WEBDRIVER_BASEURL
+WEBDRIVER_BASEURL_USER_FRIENDLY = (
+    f"http://localhost:8888/{os.environ.get('SUPERSET_APP_ROOT', '/')}/"
+)
 SQLLAB_CTAS_NO_LIMIT = True
 
 log_level_text = os.getenv("SUPERSET_LOG_LEVEL", "INFO")
diff --git a/docs/docs/configuration/configuring-superset.mdx 
b/docs/docs/configuration/configuring-superset.mdx
index c718880d80..0fb208d4f8 100644
--- a/docs/docs/configuration/configuring-superset.mdx
+++ b/docs/docs/configuration/configuring-superset.mdx
@@ -215,6 +215,45 @@ In case the reverse proxy is used for providing SSL 
encryption, an explicit defi
 RequestHeader set X-Forwarded-Proto "https"
 ```
 
+## Configuring the application root
+
+*Please be advised that this feature is in BETA.*
+
+Superset supports running the application under a non-root path. The root path
+prefix can be specified in one of two ways:
+
+- Setting the `SUPERSET_APP_ROOT` environment variable to the desired prefix.
+- Customizing the [Flask 
entrypoint](https://github.com/apache/superset/blob/master/superset/app.py#L29)
+  by passing the `superset_app_root` variable.
+
+Note, the prefix should start with a `/`.
+
+### Customizing the Flask entrypoint
+
+To configure a prefix, e.g `/analytics`, pass the `superset_app_root` argument 
to
+`create_app` when calling flask run either through the `FLASK_APP`
+environment variable:
+
+```sh
+FLASK_APP="superset:create_app(superset_app_root='/analytics')"
+```
+
+or as part of the `--app` argument to `flask run`:
+
+```sh
+flask --app "superset.app:create_app(superset_app_root='/analytics')"
+```
+
+### Docker builds
+
+The [docker compose](/docs/installation/docker-compose#configuring-further) 
developer
+configuration includes an additional environmental variable,
+[`SUPERSET_APP_ROOT`](https://github.com/apache/superset/blob/master/docker/.env),
+to simplify the process of setting up a non-default root path across the 
services.
+
+In `docker/.env-local` set `SUPERSET_APP_ROOT` to the desired prefix and then 
bring the
+services up with `docker compose up --detach`.
+
 ## Custom OAuth2 Configuration
 
 Superset is built on Flask-AppBuilder (FAB), which supports many providers out 
of the box
diff --git a/docs/docs/configuration/networking-settings.mdx 
b/docs/docs/configuration/networking-settings.mdx
index 4921a277d4..eced46d487 100644
--- a/docs/docs/configuration/networking-settings.mdx
+++ b/docs/docs/configuration/networking-settings.mdx
@@ -138,4 +138,4 @@ of your additional middleware classes.
 
 For example, to use `AUTH_REMOTE_USER` from behind a proxy server like nginx, 
you have to add a
 simple middleware class to add the value of `HTTP_X_PROXY_REMOTE_USER` (or any 
other custom header
-from the proxy) to Gunicorn’s `REMOTE_USER` environment variable:
+from the proxy) to Gunicorn’s `REMOTE_USER` environment variable.
diff --git a/superset-frontend/cypress-base/cypress/applitools/explore.test.ts 
b/superset-frontend/cypress-base/cypress/applitools/explore.test.ts
index 233701cbff..9bdb1c390c 100644
--- a/superset-frontend/cypress-base/cypress/applitools/explore.test.ts
+++ b/superset-frontend/cypress-base/cypress/applitools/explore.test.ts
@@ -23,7 +23,7 @@ import {
 
 describe('explore view', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   afterEach(() => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
index 9f689c014b..cd99250a9e 100644
--- a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts
@@ -184,12 +184,13 @@ describe('Charts list', () => {
     });
 
     it('should allow to favorite/unfavorite', () => {
-      cy.intercept({ url: `/api/v1/chart/*/favorites/`, method: 'POST' }).as(
+      cy.intercept({ url: `**/api/v1/chart/*/favorites/`, method: 'POST' }).as(
         'select',
       );
-      cy.intercept({ url: `/api/v1/chart/*/favorites/`, method: 'DELETE' }).as(
-        'unselect',
-      );
+      cy.intercept({
+        url: `**/api/v1/chart/*/favorites/`,
+        method: 'DELETE',
+      }).as('unselect');
 
       setGridMode('card');
       orderAlphabetical();
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.url_params.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.url_params.test.ts
index 686c9e7536..38241613e2 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.url_params.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/_skip.url_params.test.ts
@@ -27,13 +27,13 @@ describe.skip('Dashboard form data', () => {
   });
 
   it('should apply url params to slice requests', () => {
-    cy.intercept('/api/v1/chart/data?*', request => {
+    cy.intercept('**/api/v1/chart/data?*', request => {
       // TODO: export url params to chart data API
       request.body.queries.forEach((query: { url_params: JsonObject }) => {
         expect(query.url_params).deep.eq(urlParams);
       });
     });
-    cy.intercept('/superset/explore_json/*', request => {
+    cy.intercept('**/superset/explore_json/*', request => {
       const requestParams = JSON.parse(
         parsePostForm(request.body).form_data as string,
       );
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
index eda6e56c45..e3250506bc 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/drilltodetail.test.ts
@@ -25,7 +25,7 @@ import {
 } from './utils';
 
 function interceptSamples() {
-  cy.intercept(`/datasource/samples*`).as('samples');
+  cy.intercept(`**/datasource/samples*`).as('samples');
 }
 
 function openModalFromMenu(chartType: string) {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
index b89e9f8a4b..e55c6ac18d 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
@@ -177,7 +177,7 @@ describe('Horizontal FilterBar', () => {
   });
 
   it.skip('should spot changes in "more filters" and apply their values', () 
=> {
-    cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
+    cy.intercept(`**/api/v1/chart/data?form_data=**`).as('chart');
     prepareDashboardFilters([
       { name: 'test_1', column: 'country_name', datasetId: 2 },
       { name: 'test_2', column: 'country_code', datasetId: 2 },
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
index 9778ae7445..8f3c5b024f 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
@@ -170,7 +170,7 @@ describe('Native filters', () => {
         testItems.datasetForNativeFilter,
       );
       saveNativeFilterSettings(WORLD_HEALTH_CHARTS);
-      cy.intercept(`/api/v1/chart/data?form_data=**`).as('chart');
+      cy.intercept(`**/api/v1/chart/data?form_data=**`).as('chart');
       cy.get(nativeFilters.modal.container).should('not.exist');
       // assert that native filter is created
       validateFilterNameOnDashboard(testItems.filterType.timeColumn);
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts
index ef4ea6419d..9b7c4086bd 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/tabs.test.ts
@@ -116,7 +116,7 @@ describe('Dashboard tabs', () => {
       });
     });
 
-    cy.intercept('/superset/explore_json/?*').as('legacyChartData');
+    cy.intercept('**/superset/explore_json/?*').as('legacyChartData');
     // click row level tab, send 1 more query
     cy.get('.ant-tabs-tab').contains('row tab 2').click();
 
@@ -131,7 +131,7 @@ describe('Dashboard tabs', () => {
       expect(requestParams.viz_type).eq(LINE_CHART.viz);
     });
 
-    cy.intercept('POST', '/api/v1/chart/data?*').as('v1ChartData');
+    cy.intercept('POST', '**/api/v1/chart/data?*').as('v1ChartData');
 
     // click top level tab, send 1 more query
     cy.get('.ant-tabs-tab').contains('Tab B').click();
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts 
b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
index 506858c74f..40c1d38272 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
@@ -125,63 +125,63 @@ export const valueNativeFilterOptions = [
 ];
 
 export function interceptGet() {
-  cy.intercept('GET', '/api/v1/dashboard/*').as('get');
+  cy.intercept('GET', '**/api/v1/dashboard/*').as('get');
 }
 
 export function interceptFiltering() {
-  cy.intercept('GET', `/api/v1/dashboard/?q=*`).as('filtering');
+  cy.intercept('GET', `**/api/v1/dashboard/?q=*`).as('filtering');
 }
 
 export function interceptBulkDelete() {
-  cy.intercept('DELETE', `/api/v1/dashboard/?q=*`).as('bulkDelete');
+  cy.intercept('DELETE', `**/api/v1/dashboard/?q=*`).as('bulkDelete');
 }
 
 export function interceptDelete() {
-  cy.intercept('DELETE', `/api/v1/dashboard/*`).as('delete');
+  cy.intercept('DELETE', `**/api/v1/dashboard/*`).as('delete');
 }
 
 export function interceptUpdate() {
-  cy.intercept('PUT', `/api/v1/dashboard/*`).as('update');
+  cy.intercept('PUT', `**/api/v1/dashboard/*`).as('update');
 }
 
 export function interceptExploreUpdate() {
-  cy.intercept('PUT', `/api/v1/chart/*`).as('chartUpdate');
+  cy.intercept('PUT', `**/api/v1/chart/*`).as('chartUpdate');
 }
 
 export function interceptPost() {
-  cy.intercept('POST', `/api/v1/dashboard/`).as('post');
+  cy.intercept('POST', `**/api/v1/dashboard/`).as('post');
 }
 
 export function interceptLog() {
-  cy.intercept('/superset/log/?explode=events&dashboard_id=*').as('logs');
+  cy.intercept('**/superset/log/?explode=events&dashboard_id=*').as('logs');
 }
 
 export function interceptFav() {
-  cy.intercept({ url: `/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
+  cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
     'select',
   );
 }
 
 export function interceptUnfav() {
-  cy.intercept({ url: `/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
+  cy.intercept({ url: `**/api/v1/dashboard/*/favorites/`, method: 'POST' }).as(
     'unselect',
   );
 }
 
 export function interceptDataset() {
-  cy.intercept('GET', `/api/v1/dataset/*`).as('getDataset');
+  cy.intercept('GET', `**/api/v1/dataset/*`).as('getDataset');
 }
 
 export function interceptCharts() {
-  cy.intercept('GET', `/api/v1/dashboard/*/charts`).as('getCharts');
+  cy.intercept('GET', `**/api/v1/dashboard/*/charts`).as('getCharts');
 }
 
 export function interceptDatasets() {
-  cy.intercept('GET', `/api/v1/dashboard/*/datasets`).as('getDatasets');
+  cy.intercept('GET', `**/api/v1/dashboard/*/datasets`).as('getDatasets');
 }
 
 export function interceptFilterState() {
-  cy.intercept('POST', `/api/v1/dashboard/*/filter_state*`).as(
+  cy.intercept('POST', `**/api/v1/dashboard/*/filter_state*`).as(
     'postFilterState',
   );
 }
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/_skip.AdhocFilters.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/explore/_skip.AdhocFilters.test.ts
index 0edcf77d54..b3d8c4958e 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/_skip.AdhocFilters.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/_skip.AdhocFilters.test.ts
@@ -18,11 +18,11 @@
  */
 describe.skip('AdhocFilters', () => {
   beforeEach(() => {
-    cy.intercept('GET', '/api/v1/datasource/table/*/column/name/values').as(
+    cy.intercept('GET', '**/api/v1/datasource/table/*/column/name/values').as(
       'filterValues',
     );
-    cy.intercept('POST', '/superset/explore_json/**').as('postJson');
-    cy.intercept('GET', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('postJson');
+    cy.intercept('GET', '**/superset/explore_json/**').as('getJson');
     cy.visitChartByName('Boys'); // a table chart
     cy.verifySliceSuccess({ waitAlias: '@postJson' });
   });
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/advanced_analytics.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/explore/advanced_analytics.test.ts
index 2d50460fd7..1db265cfe8 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/advanced_analytics.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/advanced_analytics.test.ts
@@ -21,8 +21,8 @@ import { interceptV1ChartData } from './utils';
 describe('Advanced analytics', () => {
   beforeEach(() => {
     interceptV1ChartData();
-    cy.intercept('PUT', '/api/v1/explore/**').as('putExplore');
-    cy.intercept('GET', '/explore/**').as('getExplore');
+    cy.intercept('PUT', '**/api/v1/explore/**').as('putExplore');
+    cy.intercept('GET', '**/explore/**').as('getExplore');
   });
 
   it('Create custom time compare', () => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts
index c792a310ef..bd56b6f76e 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts
@@ -146,7 +146,7 @@ describe('Test datatable', () => {
   });
   it('Datapane loads view samples', () => {
     cy.intercept(
-      'datasource/samples?force=false&datasource_type=table&datasource_id=*',
+      
'**/datasource/samples?force=false&datasource_type=table&datasource_id=*',
     ).as('Samples');
     cy.contains('Samples').click();
     cy.wait('@Samples');
diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/utils.ts 
b/superset-frontend/cypress-base/cypress/e2e/explore/utils.ts
index 298453a58e..8bc17aa2bc 100644
--- a/superset-frontend/cypress-base/cypress/e2e/explore/utils.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/explore/utils.ts
@@ -20,41 +20,41 @@
 import { interceptGet as interceptDashboardGet } from '../dashboard/utils';
 
 export function interceptFiltering() {
-  cy.intercept('GET', `/api/v1/chart/?q=*`).as('filtering');
+  cy.intercept('GET', `**/api/v1/chart/?q=*`).as('filtering');
 }
 
 export function interceptBulkDelete() {
-  cy.intercept('DELETE', `/api/v1/chart/?q=*`).as('bulkDelete');
+  cy.intercept('DELETE', `**/api/v1/chart/?q=*`).as('bulkDelete');
 }
 
 export function interceptDelete() {
-  cy.intercept('DELETE', `/api/v1/chart/*`).as('delete');
+  cy.intercept('DELETE', `**/api/v1/chart/*`).as('delete');
 }
 
 export function interceptFavoriteStatus() {
-  cy.intercept('GET', '/api/v1/chart/favorite_status/*').as('favoriteStatus');
+  cy.intercept('GET', 
'**/api/v1/chart/favorite_status/*').as('favoriteStatus');
 }
 
 export function interceptUpdate() {
-  cy.intercept('PUT', `/api/v1/chart/*`).as('update');
+  cy.intercept('PUT', `**/api/v1/chart/*`).as('update');
 }
 
 export const interceptV1ChartData = (alias = 'v1Data') => {
-  cy.intercept('/api/v1/chart/data*').as(alias);
+  cy.intercept('**/api/v1/chart/data*').as(alias);
 };
 
 export function interceptExploreJson(alias = 'getJson') {
-  cy.intercept('POST', `/superset/explore_json/**`).as(alias);
+  cy.intercept('POST', `**/superset/explore_json/**`).as(alias);
 }
 
 export const interceptFormDataKey = () => {
-  cy.intercept('POST', '/api/v1/explore/form_data').as('formDataKey');
+  cy.intercept('POST', '**/api/v1/explore/form_data').as('formDataKey');
 };
 
 export function interceptExploreGet() {
   cy.intercept({
     method: 'GET',
-    url: /api\/v1\/explore\/\?(form_data_key|dashboard_page_id|slice_id)=.*/,
+    url: 
/.*\/api\/v1\/explore\/\?(form_data_key|dashboard_page_id|slice_id)=.*/,
   }).as('getExplore');
 }
 
@@ -96,5 +96,5 @@ export function saveChartToDashboard(dashboardName: string) {
 
 export function visitSampleChartFromList(chartName: string) {
   cy.getBySel('table-row').contains(chartName).click();
-  cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+  cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
 }
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/box_plot.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/box_plot.test.js
index 1a8c1bca3e..be52c08d8b 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/box_plot.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/box_plot.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Box Plot', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data*').as('getJson');
+    cy.intercept('POST', '**/api/v1/chart/data*').as('getJson');
   });
 
   const BOX_PLOT_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/bubble.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/bubble.test.js
index 0d99150317..8edd040478 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/bubble.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/bubble.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Bubble', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   const BUBBLE_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
index a844b30179..347b54502d 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/compare.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Compare', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   const COMPARE_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js
index 76653dada9..804f35ca7b 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/download_chart.test.js
@@ -25,7 +25,7 @@ describe('Download Chart > Bar chart', () => {
   };
 
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   it('download chart with image works', () => {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/gauge.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/gauge.test.js
index 12c2d3522a..e9e6ba3955 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/gauge.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/gauge.test.js
@@ -19,7 +19,7 @@
 
 describe('Visualization > Gauge', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data*').as('getJson');
+    cy.intercept('POST', '**/api/v1/chart/data*').as('getJson');
   });
 
   const GAUGE_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/graph.test.ts
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/graph.test.ts
index 4f35bb4a20..eb038f1d3f 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/graph.test.ts
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/graph.test.ts
@@ -28,7 +28,7 @@ type adhocFilter = {
 
 describe('Visualization > Graph', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data*').as('getJson');
+    cy.intercept('POST', '**/api/v1/chart/data*').as('getJson');
   });
 
   const GRAPH_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pie.test.js 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pie.test.js
index e92b691e1a..5b8af481f6 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pie.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pie.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Pie', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data*').as('getJson');
+    cy.intercept('POST', '**/api/v1/chart/data*').as('getJson');
   });
 
   const PIE_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pivot_table.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pivot_table.test.js
index e1a4c7b9c8..aba2879433 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pivot_table.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/pivot_table.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Pivot Table', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data**').as('chartData');
+    cy.intercept('POST', '**/api/v1/chart/data**').as('chartData');
   });
 
   const PIVOT_TABLE_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/sunburst.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/sunburst.test.js
index 76eea9fc19..ddadae15c2 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/sunburst.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/sunburst.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > Sunburst', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/api/v1/chart/data**').as('chartData');
+    cy.intercept('POST', '**/api/v1/chart/data**').as('chartData');
   });
 
   const SUNBURST_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/time_table.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/time_table.js
index 5c8672192a..362f3daabb 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/time_table.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/time_table.js
@@ -20,7 +20,7 @@ import { FORM_DATA_DEFAULTS, NUM_METRIC } from 
'./shared.helper';
 
 describe('Visualization > Time TableViz', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'time_table' };
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/world_map.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/world_map.test.js
index 73a54a8e2c..e78b54ddf6 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/world_map.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/explore/visualizations/world_map.test.js
@@ -18,7 +18,7 @@
  */
 describe('Visualization > World Map', () => {
   beforeEach(() => {
-    cy.intercept('POST', '/superset/explore_json/**').as('getJson');
+    cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
   });
 
   const WORLD_MAP_FORM_DATA = {
diff --git 
a/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
 
b/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
index e746489ce1..4b24c84436 100644
--- 
a/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
+++ 
b/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
@@ -26,7 +26,7 @@ describe.skip('SqlLab datasource panel', () => {
   // TODO the test below is flaky, and has been disabled for the time being
   // (notice the `it.skip`)
   it('creates a table preview when a database, schema, and table are 
selected', () => {
-    cy.intercept('/superset/table/**').as('tableMetadata');
+    cy.intercept('**/superset/table/**').as('tableMetadata');
 
     // it should have dropdowns to select database, schema, and table
     cy.get('.sql-toolbar .Select').should('have.length', 3);
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts 
b/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
index 4f0d0bb8cd..389698dcb8 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
@@ -35,7 +35,7 @@ describe('SqlLab query panel', () => {
 
     cy.intercept({
       method: 'POST',
-      url: '/api/v1/sqllab/execute/',
+      url: '**/api/v1/sqllab/execute/',
     }).as('mockSQLResponse');
 
     cy.get('.TableSelector .Select:eq(0)').click();
@@ -79,7 +79,7 @@ describe('SqlLab query panel', () => {
   });
 
   it.skip('successfully saves a query', () => {
-    cy.intercept('api/v1/database/**/tables/**').as('getTables');
+    cy.intercept('**/api/v1/database/**/tables/**').as('getTables');
 
     const query =
       'SELECT ds, gender, name, num FROM main.birth_names ORDER BY name LIMIT 
3';
@@ -142,7 +142,7 @@ describe('SqlLab query panel', () => {
   });
 
   it.skip('Create a chart from a query', () => {
-    cy.intercept('/api/v1/sqllab/execute/').as('queryFinished');
+    cy.intercept('**/api/v1/sqllab/execute/').as('queryFinished');
     cy.intercept('**/api/v1/explore/**').as('explore');
     cy.intercept('**/api/v1/chart/**').as('chart');
     cy.intercept('**/tabstateview/**').as('tabstateview');
diff --git a/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts 
b/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts
index c67da1afd5..af06ad0299 100644
--- a/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts
+++ b/superset-frontend/cypress-base/cypress/utils/vizPlugins.ts
@@ -65,9 +65,9 @@ export function getChartDataRouteForSlice(slice: Slice) {
   const isLegacy = isLegacyChart(vizType);
   const formData = encodeURIComponent(`{"slice_id":${slice.slice_id}}`);
   if (isLegacy) {
-    return `/superset/explore_json/?*${formData}*`;
+    return `**/superset/explore_json/?*${formData}*`;
   }
-  return `/api/v1/chart/data?*${formData}*`;
+  return `**/api/v1/chart/data?*${formData}*`;
 }
 
 export function getChartAlias(slice: Slice): string {
diff --git 
a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
 
b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
index fd040faed0..92f2e50198 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
+++ 
b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts
@@ -31,11 +31,11 @@ import {
   RequestConfig,
   ParseMethod,
 } from './types';
-import { DEFAULT_FETCH_RETRY_OPTIONS, DEFAULT_BASE_URL } from './constants';
+import { DEFAULT_FETCH_RETRY_OPTIONS, DEFAULT_APP_ROOT } from './constants';
 
-const defaultUnauthorizedHandler = () => {
-  if (!window.location.pathname.startsWith('/login')) {
-    window.location.href = `/login?next=${window.location.href}`;
+const defaultUnauthorizedHandlerForPrefix = (appRoot: string) => () => {
+  if (!window.location.pathname.startsWith(`${appRoot}/login`)) {
+    window.location.href = `${appRoot}/login?next=${window.location.href}`;
   }
 };
 
@@ -52,7 +52,7 @@ export default class SupersetClientClass {
 
   fetchRetryOptions?: FetchRetryOptions;
 
-  baseUrl: string;
+  appRoot?: string;
 
   protocol: Protocol;
 
@@ -67,9 +67,9 @@ export default class SupersetClientClass {
   handleUnauthorized: () => void;
 
   constructor({
-    baseUrl = DEFAULT_BASE_URL,
     host,
     protocol,
+    appRoot = DEFAULT_APP_ROOT,
     headers = {},
     fetchRetryOptions = {},
     mode = 'same-origin',
@@ -78,17 +78,10 @@ export default class SupersetClientClass {
     csrfToken = undefined,
     guestToken = undefined,
     guestTokenHeaderName = 'X-GuestToken',
-    unauthorizedHandler = defaultUnauthorizedHandler,
+    unauthorizedHandler = undefined,
   }: ClientConfig = {}) {
-    const url = new URL(
-      host || protocol
-        ? `${protocol || 'https:'}//${host || 'localhost'}`
-        : baseUrl,
-      // baseUrl for API could also be relative, so we provide current 
location.href
-      // as the base of baseUrl
-      window.location.href,
-    );
-    this.baseUrl = url.href.replace(/\/+$/, ''); // always strip trailing slash
+    const url = new URL(`${protocol || 'https:'}//${host || 'localhost'}`);
+    this.appRoot = appRoot;
     this.host = url.host;
     this.protocol = url.protocol as Protocol;
     this.headers = { Accept: 'application/json', ...headers }; // defaulting 
accept to json
@@ -109,7 +102,10 @@ export default class SupersetClientClass {
     if (guestToken) {
       this.headers[guestTokenHeaderName] = guestToken;
     }
-    this.handleUnauthorized = unauthorizedHandler;
+    this.handleUnauthorized =
+      unauthorizedHandler !== undefined
+        ? unauthorizedHandler
+        : defaultUnauthorizedHandlerForPrefix(this.appRoot);
   }
 
   async init(force = false): CsrfPromise {
@@ -239,7 +235,7 @@ export default class SupersetClientClass {
       method: 'GET',
       mode: this.mode,
       timeout: this.timeout,
-      url: this.getUrl({ endpoint: 'api/v1/security/csrf_token/' }),
+      url: this.getUrl({ endpoint: '/api/v1/security/csrf_token/' }),
       parseMethod: 'json',
     }).then(({ json }) => {
       if (typeof json === 'object') {
@@ -271,7 +267,7 @@ export default class SupersetClientClass {
     const host = inputHost ?? this.host;
     const cleanHost = host.slice(-1) === '/' ? host.slice(0, -1) : host; // no 
backslash
 
-    return `${this.protocol}//${cleanHost}/${
+    return `${this.protocol}//${cleanHost}${this.appRoot}/${
       endpoint[0] === '/' ? endpoint.slice(1) : endpoint
     }`;
   }
diff --git 
a/superset-frontend/packages/superset-ui-core/src/connection/constants.ts 
b/superset-frontend/packages/superset-ui-core/src/connection/constants.ts
index 98a8c38574..2dbdeca177 100644
--- a/superset-frontend/packages/superset-ui-core/src/connection/constants.ts
+++ b/superset-frontend/packages/superset-ui-core/src/connection/constants.ts
@@ -19,7 +19,7 @@
 
 import { FetchRetryOptions } from './types';
 
-export const DEFAULT_BASE_URL = 'http://localhost';
+export const DEFAULT_APP_ROOT = '';
 
 // HTTP status codes
 export const HTTP_STATUS_OK = 200;
diff --git 
a/superset-frontend/packages/superset-ui-core/src/connection/types.ts 
b/superset-frontend/packages/superset-ui-core/src/connection/types.ts
index a63ffd8b68..e8e6c97771 100644
--- a/superset-frontend/packages/superset-ui-core/src/connection/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/connection/types.ts
@@ -132,9 +132,9 @@ export type CsrfPromise = Promise<string | undefined>;
 export type Protocol = 'http:' | 'https:';
 
 export interface ClientConfig {
-  baseUrl?: string;
   host?: Host;
   protocol?: Protocol;
+  appRoot?: string;
   credentials?: Credentials;
   csrfToken?: CsrfToken;
   guestToken?: string;
diff --git 
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
 
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
index 39f148f7be..f7534e2c5c 100644
--- 
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
+++ 
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
@@ -31,7 +31,8 @@ describe('SupersetClientClass', () => {
   describe('new SupersetClientClass()', () => {
     it('fallback protocol to https when setting only host', () => {
       const client = new SupersetClientClass({ host: 'TEST-HOST' });
-      expect(client.baseUrl).toEqual('https://test-host');
+      expect(client.protocol).toEqual('https:');
+      expect(client.host).toEqual('test-host');
     });
   });
 
@@ -72,6 +73,15 @@ describe('SupersetClientClass', () => {
       );
     });
 
+    it('constructs a valid url if url, endpoint, and host are all empty and 
appRoot is defined', () => {
+      client = new SupersetClientClass({
+        protocol: 'https:',
+        host: 'config_host',
+        appRoot: '/prefix',
+      });
+      expect(client.getUrl()).toBe('https://config_host/prefix/');
+    });
+
     it('does not throw if url, endpoint, and host are all empty', () => {
       client = new SupersetClientClass({ protocol: 'https:', host: '' });
       expect(client.getUrl()).toBe('https://localhost/');
diff --git 
a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts 
b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
index 286ef35cc0..d7fcf1c04c 100644
--- 
a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
+++ 
b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts
@@ -35,13 +35,13 @@ describe('makeApi()', () => {
     expect(api.requestType).toEqual('search');
   });
 
-  it('should allow custom client', async () => {
+  it('should allow custom path', async () => {
     expect.assertions(2);
     const api = makeApi({
       method: 'GET',
       endpoint: '/test-custom-client',
     });
-    const client = new SupersetClientClass({ baseUrl: 'http://foo/' });
+    const client = new SupersetClientClass({ appRoot: '/foo' });
     const mockResponse = { yes: 'ok' };
     const mockRequest = jest.fn(() =>
       Promise.resolve(
diff --git a/superset-frontend/spec/helpers/setupSupersetClient.js 
b/superset-frontend/spec/helpers/setupSupersetClient.js
index c65f684266..fc08d9e21d 100644
--- a/superset-frontend/spec/helpers/setupSupersetClient.js
+++ b/superset-frontend/spec/helpers/setupSupersetClient.js
@@ -26,5 +26,5 @@ export default function setupSupersetClient() {
   // including CSRF authentication and initialization
   global.FormData = window.FormData; // used by SupersetClient
   fetchMock.get('glob:*/api/v1/security/csrf_token/*', { result: '1234' });
-  SupersetClient.configure({ protocol: 'http', host: 'localhost' }).init();
+  SupersetClient.configure({ protocol: 'http:', host: 'localhost' }).init();
 }
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx 
b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
index 8d9a36da55..d9579071c3 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx
@@ -36,6 +36,7 @@ import * as Actions from 'src/SqlLab/actions/sqlLab';
 import { EmptyState } from 'src/components/EmptyState';
 import getBootstrapData from 'src/utils/getBootstrapData';
 import { locationContext } from 'src/pages/SqlLab/LocationContext';
+import { navigateWithState } from 'src/utils/navigationUtils';
 import { Icons } from 'src/components/Icons';
 import SqlEditor from '../SqlEditor';
 import SqlEditorTabHeader from '../SqlEditorTabHeader';
@@ -148,7 +149,7 @@ class TabbedSqlEditors extends 
PureComponent<TabbedSqlEditorsProps> {
       this.newQueryEditor();
 
       if (isNewQuery) {
-        window.history.replaceState({}, document.title, SQL_LAB_URL);
+        navigateWithState(SQL_LAB_URL, {}, { replace: true });
       }
     } else {
       const qe = this.activeQueryEditor();
@@ -171,7 +172,7 @@ class TabbedSqlEditors extends 
PureComponent<TabbedSqlEditorsProps> {
   popNewTab(urlParams: Record<string, string>) {
     // Clean the url in browser history
     const updatedUrl = `${URI(SQL_LAB_URL).query(urlParams)}`;
-    window.history.replaceState({}, document.title, updatedUrl);
+    navigateWithState(updatedUrl, {}, { replace: true });
   }
 
   activeQueryEditor() {
diff --git a/superset-frontend/src/components/Chart/chartAction.js 
b/superset-frontend/src/components/Chart/chartAction.js
index 2398eafe36..a12ae44e2b 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -41,6 +41,7 @@ import { Logger, LOG_ACTIONS_LOAD_CHART } from 
'src/logger/LogUtils';
 import { allowCrossDomain as domainShardingEnabled } from 
'src/utils/hostNamesConfig';
 import { updateDataMask } from 'src/dataMask/actions';
 import { waitForAsyncData } from 'src/middleware/asyncEvent';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { safeStringify } from 'src/utils/safeStringify';
 import { extendedDayjs } from 'src/utils/dates';
 
@@ -551,7 +552,7 @@ export function redirectSQLLab(formData, history) {
             },
           });
         } else {
-          SupersetClient.postForm(redirectUrl, {
+          SupersetClient.postForm(ensureAppRoot(redirectUrl), {
             form_data: safeStringify(payload),
           });
         }
diff --git a/superset-frontend/src/components/FacePile/index.tsx 
b/superset-frontend/src/components/FacePile/index.tsx
index b4c12c48f4..38cc1de474 100644
--- a/superset-frontend/src/components/FacePile/index.tsx
+++ b/superset-frontend/src/components/FacePile/index.tsx
@@ -25,6 +25,7 @@ import {
 import getOwnerName from 'src/utils/getOwnerName';
 import { Tooltip } from 'src/components/Tooltip';
 import { Avatar, AvatarGroup } from 'src/components/Avatar';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { getRandomColor } from './utils';
 
 interface FacePileProps {
@@ -43,7 +44,7 @@ export default function FacePile({ users, maxCount = 4 }: 
FacePileProps) {
         const uniqueKey = `${id}-${first_name}-${last_name}`;
         const color = getRandomColor(uniqueKey, colorList);
         const avatarUrl = isFeatureEnabled(FeatureFlag.SlackEnableAvatars)
-          ? `/api/v1/user/${id}/avatar.png`
+          ? ensureAppRoot(`/api/v1/user/${id}/avatar.png`)
           : undefined;
         return (
           <Tooltip key={name} title={name} placement="top">
diff --git a/superset-frontend/src/components/Tags/Tag.test.tsx 
b/superset-frontend/src/components/Tags/Tag.test.tsx
index 0f25a78e2d..79c3d27ad4 100644
--- a/superset-frontend/src/components/Tags/Tag.test.tsx
+++ b/superset-frontend/src/components/Tags/Tag.test.tsx
@@ -28,13 +28,16 @@ const mockedProps: TagType = {
   onClick: undefined,
 };
 
+const setup = (props: TagType = mockedProps) =>
+  render(<Tag {...props} />, { useRouter: true });
+
 test('should render', () => {
-  const { container } = render(<Tag {...mockedProps} />);
+  const { container } = setup();
   expect(container).toBeInTheDocument();
 });
 
 test('should render shortname properly', () => {
-  const { container } = render(<Tag {...mockedProps} />);
+  const { container } = setup();
   expect(container).toBeInTheDocument();
   expect(screen.getByTestId('tag')).toBeInTheDocument();
   expect(screen.getByTestId('tag')).toHaveTextContent(mockedProps.name);
@@ -45,7 +48,7 @@ test('should render longname properly', () => {
     ...mockedProps,
     name: 'very-long-tag-name-that-truncates',
   };
-  const { container } = render(<Tag {...longNameProps} />);
+  const { container } = setup(longNameProps);
   expect(container).toBeInTheDocument();
   expect(screen.getByTestId('tag')).toBeInTheDocument();
   expect(screen.getByTestId('tag')).toHaveTextContent(
diff --git a/superset-frontend/src/components/Tags/Tag.tsx 
b/superset-frontend/src/components/Tags/Tag.tsx
index 6bb3223c67..bc15470714 100644
--- a/superset-frontend/src/components/Tags/Tag.tsx
+++ b/superset-frontend/src/components/Tags/Tag.tsx
@@ -18,6 +18,7 @@
  */
 
 import { styled } from '@superset-ui/core';
+import { Link } from 'react-router-dom';
 import TagType from 'src/types/TagType';
 import { Tag as AntdTag } from 'antd-v5';
 import { useMemo } from 'react';
@@ -80,13 +81,13 @@ const Tag = ({
           >
             {' '}
             {id ? (
-              <a
-                href={`/superset/all_entities/?id=${id}`}
+              <Link
+                to={`/superset/all_entities/?id=${id}`}
                 target="_blank"
                 rel="noreferrer"
               >
                 {children || tagDisplay}
-              </a>
+              </Link>
             ) : (
               children || tagDisplay
             )}
diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx 
b/superset-frontend/src/components/Tags/TagsList.test.tsx
index 35819ade91..e28160a91f 100644
--- a/superset-frontend/src/components/Tags/TagsList.test.tsx
+++ b/superset-frontend/src/components/Tags/TagsList.test.tsx
@@ -53,13 +53,16 @@ const getElementsByClassName = (className: string) =>
 
 const findAllTags = () => waitFor(() => getElementsByClassName('.ant-tag'));
 
+const setup = (props: TagsListProps = mockedProps) =>
+  render(<TagsList {...props} />, { useRouter: true });
+
 test('should render', () => {
-  const { container } = render(<TagsList {...mockedProps} />);
+  const { container } = setup();
   expect(container).toBeInTheDocument();
 });
 
 test('should render 5 elements', async () => {
-  render(<TagsList {...mockedProps} />);
+  setup();
   const tagsListItems = await findAllTags();
   expect(tagsListItems).toHaveLength(5);
   expect(tagsListItems[0]).toHaveTextContent(testTags[0].name);
@@ -70,7 +73,7 @@ test('should render 5 elements', async () => {
 });
 
 test('should render 3 elements when maxTags is set to 3', async () => {
-  render(<TagsList {...mockedProps} maxTags={3} />);
+  setup({ ...mockedProps, maxTags: 3 });
   const tagsListItems = await findAllTags();
   expect(tagsListItems).toHaveLength(3);
   expect(tagsListItems[2]).toHaveTextContent('+3...');
diff --git a/superset-frontend/src/constants.ts 
b/superset-frontend/src/constants.ts
index c4fd0e94f9..98abb6c2b1 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -143,6 +143,8 @@ export const SLOW_DEBOUNCE = 500;
 export const NULL_DISPLAY = t('N/A');
 
 export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
+  application_root: '/',
+  static_assets_prefix: '',
   flash_messages: [],
   conf: {},
   locale: 'en',
diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js 
b/superset-frontend/src/dashboard/actions/dashboardState.js
index 33111c75d5..51bdb95784 100644
--- a/superset-frontend/src/dashboard/actions/dashboardState.js
+++ b/superset-frontend/src/dashboard/actions/dashboardState.js
@@ -57,6 +57,7 @@ import { safeStringify } from 'src/utils/safeStringify';
 import { logEvent } from 'src/logger/actions';
 import { LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA } from 
'src/logger/LogUtils';
 import { isEqual } from 'lodash';
+import { navigateWithState } from 'src/utils/navigationUtils';
 import { UPDATE_COMPONENTS_PARENTS_LIST } from './dashboardLayout';
 import {
   saveChartConfiguration,
@@ -402,11 +403,9 @@ export function saveDashboardRequest(data, id, saveType) {
       }
       dispatch(saveDashboardFinished());
       // redirect to the new slug or id
-      window.history.pushState(
-        { event: 'dashboard_properties_changed' },
-        '',
-        `/superset/dashboard/${slug || id}/`,
-      );
+      navigateWithState(`/superset/dashboard/${slug || id}/`, {
+        event: 'dashboard_properties_changed',
+      });
 
       dispatch(addSuccessToast(t('This dashboard was saved successfully.')));
       dispatch(setOverrideConfirm(undefined));
diff --git 
a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx 
b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
index 42852fef4d..75941c6c7c 100644
--- a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
+++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
@@ -34,9 +34,12 @@ import ImageLoader from 
'src/components/ListViewCard/ImageLoader';
 import { usePluginContext } from 'src/components/DynamicPlugins';
 import { Tooltip } from 'src/components/Tooltip';
 import { GenericLink } from 'src/components/GenericLink/GenericLink';
+import { assetUrl } from 'src/utils/assetUrl';
 import { Theme } from '@emotion/react';
 
-const FALLBACK_THUMBNAIL_URL = '/static/assets/images/chart-card-fallback.svg';
+const FALLBACK_THUMBNAIL_URL = assetUrl(
+  '/static/assets/images/chart-card-fallback.svg',
+);
 
 const TruncatedTextWithTooltip = ({
   children,
diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx 
b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
index 3ab498145d..4a619dc735 100644
--- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx
+++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx
@@ -23,6 +23,7 @@ import classNames from 'classnames';
 import { addAlpha, css, styled, t } from '@superset-ui/core';
 import { EmptyState } from 'src/components/EmptyState';
 import { Icons } from 'src/components/Icons';
+import { navigateTo } from 'src/utils/navigationUtils';
 import { componentShape } from '../util/propShapes';
 import DashboardComponent from '../containers/DashboardComponent';
 import { Droppable } from './dnd/DragDroppable';
@@ -224,11 +225,9 @@ class DashboardGrid extends PureComponent {
           </>
         }
         buttonAction={() => {
-          window.open(
-            `/chart/add?dashboard_id=${dashboardId}`,
-            '_blank',
-            'noopener noreferrer',
-          );
+          navigateTo(`/chart/add?dashboard_id=${dashboardId}`, {
+            newWindow: true,
+          });
         }}
         image="chart.svg"
       />
@@ -251,11 +250,9 @@ class DashboardGrid extends PureComponent {
           </>
         }
         buttonAction={() => {
-          window.open(
-            `/chart/add?dashboard_id=${dashboardId}`,
-            '_blank',
-            'noopener noreferrer',
-          );
+          navigateTo(`/chart/add?dashboard_id=${dashboardId}`, {
+            newWindow: true,
+          });
         }}
         image="chart.svg"
       />
diff --git a/superset-frontend/src/dashboard/components/SaveModal.tsx 
b/superset-frontend/src/dashboard/components/SaveModal.tsx
index f701892210..2126cff46e 100644
--- a/superset-frontend/src/dashboard/components/SaveModal.tsx
+++ b/superset-frontend/src/dashboard/components/SaveModal.tsx
@@ -30,6 +30,7 @@ import {
   SAVE_TYPE_OVERWRITE,
   SAVE_TYPE_NEWDASHBOARD,
 } from 'src/dashboard/util/constants';
+import { navigateTo } from 'src/utils/navigationUtils';
 
 type SaveType = typeof SAVE_TYPE_OVERWRITE | typeof SAVE_TYPE_NEWDASHBOARD;
 
@@ -154,7 +155,7 @@ class SaveModal extends PureComponent<SaveModalProps, 
SaveModalState> {
     } else {
       this.onSave(data, dashboardId, saveType).then((resp: JsonResponse) => {
         if (saveType === SAVE_TYPE_NEWDASHBOARD && resp.json?.result?.id) {
-          window.location.href = `/superset/dashboard/${resp.json.result.id}/`;
+          navigateTo(`/superset/dashboard/${resp.json.result.id}/`);
         }
       });
       this.modal?.current?.close?.();
diff --git a/superset-frontend/src/dashboard/components/SliceAdder.tsx 
b/superset-frontend/src/dashboard/components/SliceAdder.tsx
index d861c8ff32..3e8d310c36 100644
--- a/superset-frontend/src/dashboard/components/SliceAdder.tsx
+++ b/superset-frontend/src/dashboard/components/SliceAdder.tsx
@@ -47,6 +47,7 @@ import { InfoTooltipWithTrigger } from 
'@superset-ui/chart-controls';
 import { Dispatch } from 'redux';
 import { Slice } from 'src/dashboard/types';
 import { withTheme, Theme } from '@emotion/react';
+import { navigateTo } from 'src/utils/navigationUtils';
 import AddSliceCard from './AddSliceCard';
 import AddSliceDragPreview from './dnd/AddSliceDragPreview';
 import { DragDroppable } from './dnd/DragDroppable';
@@ -364,11 +365,9 @@ class SliceAdder extends Component<SliceAdderProps, 
SliceAdderState> {
             buttonStyle="link"
             buttonSize="xsmall"
             onClick={() =>
-              window.open(
-                `/chart/add?dashboard_id=${this.props.dashboardId}`,
-                '_blank',
-                'noopener noreferrer',
-              )
+              navigateTo(`/chart/add?dashboard_id=${this.props.dashboardId}`, {
+                newWindow: true,
+              })
             }
           >
             <Icons.PlusOutlined
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
index f92fd8b60c..4083755b8d 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
@@ -45,6 +45,7 @@ import { useImmer } from 'use-immer';
 import { isEmpty, isEqual, debounce } from 'lodash';
 import { getInitialDataMask } from 'src/dataMask/reducer';
 import { URL_PARAMS } from 'src/constants';
+import { applicationRoot } from 'src/utils/getBootstrapData';
 import { getUrlParam } from 'src/utils/urlUtils';
 import { useTabId } from 'src/hooks/useTabId';
 import { logEvent } from 'src/logger/actions';
@@ -120,7 +121,16 @@ const publishDataMask = debounce(
     // replace params only when current page is /superset/dashboard
     // this prevents a race condition between updating filters and navigating 
to Explore
     if (window.location.pathname.includes('/superset/dashboard')) {
-      history.location.pathname = window.location.pathname;
+      // The history API is part of React router and understands that a 
basename may exist.
+      // Internally it treats all paths as if they are relative to the root 
and appends
+      // it when necessary. We strip any prefix so that history.replace adds 
it back and doesn't
+      // double it up.
+      const appRoot = applicationRoot();
+      let replacement_pathname = window.location.pathname;
+      if (appRoot !== '/' && replacement_pathname.startsWith(appRoot)) {
+        replacement_pathname = replacement_pathname.substring(appRoot.length);
+      }
+      history.location.pathname = replacement_pathname;
       history.replace({
         search: newParams.toString(),
       });
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/keyValue.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/keyValue.tsx
index 27a4d3b6ce..4de0292261 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/keyValue.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/keyValue.tsx
@@ -24,7 +24,7 @@ const assembleEndpoint = (
   key?: string | null,
   tabId?: string,
 ) => {
-  let endpoint = `api/v1/dashboard/${dashId}/filter_state`;
+  let endpoint = `/api/v1/dashboard/${dashId}/filter_state`;
   if (key) {
     endpoint = endpoint.concat(`/${key}`);
   }
diff --git a/superset-frontend/src/embedded/index.tsx 
b/superset-frontend/src/embedded/index.tsx
index 4263de9609..deddab121e 100644
--- a/superset-frontend/src/embedded/index.tsx
+++ b/superset-frontend/src/embedded/index.tsx
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import 'src/public-path';
+
 import { lazy, Suspense } from 'react';
 import ReactDOM from 'react-dom';
 import { BrowserRouter as Router, Route } from 'react-router-dom';
diff --git 
a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx 
b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
index e7f80d4e06..96c239b9cf 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx
@@ -50,6 +50,7 @@ import {
   LOG_ACTIONS_MOUNT_EXPLORER,
   LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS,
 } from 'src/logger/LogUtils';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { getUrlParam } from 'src/utils/urlUtils';
 import cx from 'classnames';
 import * as chartActions from 'src/components/Chart/chartAction';
@@ -215,7 +216,7 @@ const updateHistory = debounce(
         stateModifier = 'pushState';
       }
       // avoid race condition in case user changes route before explore 
updates the url
-      if (window.location.pathname.startsWith('/explore')) {
+      if (window.location.pathname.startsWith(ensureAppRoot('/explore'))) {
         const url = mountExploreUrl(
           standalone ? URL_PARAMS.standalone.name : null,
           {
diff --git a/superset-frontend/src/explore/exploreUtils/getChartDataUri.test.ts 
b/superset-frontend/src/explore/exploreUtils/getChartDataUri.test.ts
index 797b5def3d..c4f0e82bdc 100644
--- a/superset-frontend/src/explore/exploreUtils/getChartDataUri.test.ts
+++ b/superset-frontend/src/explore/exploreUtils/getChartDataUri.test.ts
@@ -16,58 +16,67 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { getChartDataUri } from '.';
 
-test('Get ChartUri when allowDomainSharding:false', () => {
-  expect(
-    getChartDataUri({
-      path: '/path',
-      qs: 'same-string',
-      allowDomainSharding: false,
-    }),
-  ).toEqual({
-    _deferred_build: true,
-    _parts: {
-      duplicateQueryParameters: false,
-      escapeQuerySpace: true,
-      fragment: null,
-      hostname: 'localhost',
-      password: null,
-      path: '/path',
-      port: '',
-      preventInvalidHostname: false,
-      protocol: 'http',
-      query: 'same-string',
-      urn: null,
-      username: null,
-    },
-    _string: '',
+jest.mock('src/utils/pathUtils');
+
+describe('Get ChartUri', () => {
+  (ensureAppRoot as jest.Mock).mockImplementation(
+    (path: string) => `/prefix${path}`,
+  );
+
+  it('Get ChartUri when allowDomainSharding:false', () => {
+    expect(
+      getChartDataUri({
+        path: '/path',
+        qs: 'same-string',
+        allowDomainSharding: false,
+      }),
+    ).toEqual({
+      _deferred_build: true,
+      _parts: {
+        duplicateQueryParameters: false,
+        escapeQuerySpace: true,
+        fragment: null,
+        hostname: 'localhost',
+        password: null,
+        path: '/prefix/path',
+        port: '',
+        preventInvalidHostname: false,
+        protocol: 'http',
+        query: 'same-string',
+        urn: null,
+        username: null,
+      },
+      _string: '',
+    });
   });
-});
 
-test('Get ChartUri when allowDomainSharding:true', () => {
-  expect(
-    getChartDataUri({
-      path: '/path-allowDomainSharding-true',
-      qs: 'same-string-allowDomainSharding-true',
-      allowDomainSharding: true,
-    }),
-  ).toEqual({
-    _deferred_build: true,
-    _parts: {
-      duplicateQueryParameters: false,
-      escapeQuerySpace: true,
-      fragment: null,
-      hostname: undefined,
-      password: null,
-      path: '/path-allowDomainSharding-true',
-      port: '',
-      preventInvalidHostname: false,
-      protocol: 'http',
-      query: 'same-string-allowDomainSharding-true',
-      urn: null,
-      username: null,
-    },
-    _string: '',
+  it('Get ChartUri when allowDomainSharding:true', () => {
+    expect(
+      getChartDataUri({
+        path: '/path-allowDomainSharding-true',
+        qs: 'same-string-allowDomainSharding-true',
+        allowDomainSharding: true,
+      }),
+    ).toEqual({
+      _deferred_build: true,
+      _parts: {
+        duplicateQueryParameters: false,
+        escapeQuerySpace: true,
+        fragment: null,
+        hostname: undefined,
+        password: null,
+        path: '/prefix/path-allowDomainSharding-true',
+        port: '',
+        preventInvalidHostname: false,
+        protocol: 'http',
+        query: 'same-string-allowDomainSharding-true',
+        urn: null,
+        username: null,
+      },
+      _string: '',
+    });
   });
 });
diff --git a/superset-frontend/src/explore/exploreUtils/index.js 
b/superset-frontend/src/explore/exploreUtils/index.js
index ed4d03c6a2..e26f693626 100644
--- a/superset-frontend/src/explore/exploreUtils/index.js
+++ b/superset-frontend/src/explore/exploreUtils/index.js
@@ -30,6 +30,7 @@ import {
 import { availableDomains } from 'src/utils/hostNamesConfig';
 import { safeStringify } from 'src/utils/safeStringify';
 import { optionLabel } from 'src/utils/common';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { URL_PARAMS } from 'src/constants';
 import {
   DISABLE_INPUT_OPERATORS,
@@ -69,7 +70,7 @@ export function getAnnotationJsonUrl(slice_id, force) {
 
   const uri = URI(window.location.search);
   return uri
-    .pathname('/api/v1/chart/data')
+    .pathname(ensureAppRoot('/api/v1/chart/data'))
     .search({
       form_data: safeStringify({ slice_id }),
       force,
@@ -84,9 +85,9 @@ export function getURIDirectory(endpointType = 'base') {
       endpointType,
     )
   ) {
-    return '/superset/explore_json/';
+    return ensureAppRoot('/superset/explore_json/');
   }
-  return '/explore/';
+  return ensureAppRoot('/explore/');
 }
 
 export function mountExploreUrl(endpointType, extraSearch = {}, force = false) 
{
@@ -113,7 +114,7 @@ export function getChartDataUri({ path, qs, 
allowDomainSharding = false }) {
     protocol: window.location.protocol.slice(0, -1),
     hostname: getHostName(allowDomainSharding),
     port: window.location.port ? window.location.port : '',
-    path,
+    path: ensureAppRoot(path),
   });
   if (qs) {
     uri = uri.search(qs);
@@ -143,7 +144,10 @@ export function getExploreUrl({
   // eslint-disable-next-line no-param-reassign
   delete formData.label_colors;
 
-  let uri = getChartDataUri({ path: '/', allowDomainSharding });
+  let uri = getChartDataUri({
+    path: '/',
+    allowDomainSharding,
+  });
   if (curUrl) {
     uri = URI(URI(curUrl).search());
   }
@@ -257,7 +261,7 @@ export const exportChart = ({
     });
     payload = formData;
   } else {
-    url = '/api/v1/chart/data';
+    url = ensureAppRoot('/api/v1/chart/data');
     payload = buildV1ChartDataPayload({
       formData,
       force,
diff --git 
a/superset-frontend/src/features/allEntities/AllEntitiesTable.test.tsx 
b/superset-frontend/src/features/allEntities/AllEntitiesTable.test.tsx
index 5eb6fb341f..7143c90887 100644
--- a/superset-frontend/src/features/allEntities/AllEntitiesTable.test.tsx
+++ b/superset-frontend/src/features/allEntities/AllEntitiesTable.test.tsx
@@ -98,6 +98,7 @@ describe('AllEntitiesTable', () => {
         setShowTagModal={mockSetShowTagModal}
         objects={mockObjects}
       />,
+      { useRouter: true },
     );
 
     expect(
@@ -114,6 +115,7 @@ describe('AllEntitiesTable', () => {
         setShowTagModal={mockSetShowTagModal}
         objects={mockObjectsWithTags}
       />,
+      { useRouter: true },
     );
 
     expect(screen.getByText('Sales Dashboard')).toBeInTheDocument();
diff --git a/superset-frontend/src/features/charts/ChartCard.tsx 
b/superset-frontend/src/features/charts/ChartCard.tsx
index b662670133..56a2beea54 100644
--- a/superset-frontend/src/features/charts/ChartCard.tsx
+++ b/superset-frontend/src/features/charts/ChartCard.tsx
@@ -30,6 +30,7 @@ import FaveStar from 'src/components/FaveStar';
 import FacePile from 'src/components/FacePile';
 import { handleChartDelete, CardStyles } from 'src/views/CRUD/utils';
 import Button from 'src/components/Button';
+import { assetUrl } from 'src/utils/assetUrl';
 
 interface ChartCardProps {
   chart: Chart;
@@ -168,7 +169,9 @@ export default function ChartCard({
         }
         url={bulkSelectEnabled ? undefined : chart.url}
         imgURL={chart.thumbnail_url || ''}
-        imgFallbackURL="/static/assets/images/chart-card-fallback.svg"
+        imgFallbackURL={assetUrl(
+          '/static/assets/images/chart-card-fallback.svg',
+        )}
         description={t('Modified %s', chart.changed_on_delta_humanized)}
         coverLeft={<FacePile users={chart.owners || []} />}
         coverRight={
diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx 
b/superset-frontend/src/features/dashboards/DashboardCard.tsx
index fb5bae9c11..d8d50f5a7f 100644
--- a/superset-frontend/src/features/dashboards/DashboardCard.tsx
+++ b/superset-frontend/src/features/dashboards/DashboardCard.tsx
@@ -34,6 +34,7 @@ import FacePile from 'src/components/FacePile';
 import FaveStar from 'src/components/FaveStar';
 import { Dashboard } from 'src/views/CRUD/types';
 import { Button } from 'src/components';
+import { assetUrl } from 'src/utils/assetUrl';
 
 interface DashboardCardProps {
   isChart?: boolean;
@@ -159,7 +160,9 @@ function DashboardCard({
         url={bulkSelectEnabled ? undefined : dashboard.url}
         linkComponent={Link}
         imgURL={dashboard.thumbnail_url}
-        imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
+        imgFallbackURL={assetUrl(
+          '/static/assets/images/dashboard-card-fallback.svg',
+        )}
         description={t('Modified %s', dashboard.changed_on_delta_humanized)}
         coverLeft={<FacePile users={dashboard.owners || []} />}
         actions={
diff --git a/superset-frontend/src/features/home/ChartTable.tsx 
b/superset-frontend/src/features/home/ChartTable.tsx
index 66222d6755..e4a326b8c6 100644
--- a/superset-frontend/src/features/home/ChartTable.tsx
+++ b/superset-frontend/src/features/home/ChartTable.tsx
@@ -45,6 +45,7 @@ import handleResourceExport from 'src/utils/export';
 import Loading from 'src/components/Loading';
 import ErrorBoundary from 'src/components/ErrorBoundary';
 import { Icons } from 'src/components/Icons';
+import { navigateTo } from 'src/utils/navigationUtils';
 import EmptyState from './EmptyState';
 import { WelcomeTable } from './types';
 import SubMenu from './SubMenu';
@@ -198,7 +199,7 @@ function ChartTable({
             ),
             buttonStyle: 'tertiary',
             onClick: () => {
-              window.location.assign('/chart/add');
+              navigateTo('/chart/add', { assign: true });
             },
           },
           {
diff --git a/superset-frontend/src/features/home/DashboardTable.tsx 
b/superset-frontend/src/features/home/DashboardTable.tsx
index 7dfab69652..f2d5f68c18 100644
--- a/superset-frontend/src/features/home/DashboardTable.tsx
+++ b/superset-frontend/src/features/home/DashboardTable.tsx
@@ -41,6 +41,7 @@ import DeleteModal from 'src/components/DeleteModal';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
 import DashboardCard from 'src/features/dashboards/DashboardCard';
 import { Icons } from 'src/components/Icons';
+import { navigateTo } from 'src/utils/navigationUtils';
 import EmptyState from './EmptyState';
 import SubMenu from './SubMenu';
 import { WelcomeTable } from './types';
@@ -197,7 +198,7 @@ function DashboardTable({
             ),
             buttonStyle: 'tertiary',
             onClick: () => {
-              window.location.assign('/dashboard/new');
+              navigateTo('/dashboard/new', { assign: true });
             },
           },
           {
diff --git a/superset-frontend/src/features/home/EmptyState.tsx 
b/superset-frontend/src/features/home/EmptyState.tsx
index 7a2ba4d62b..6d180e7819 100644
--- a/superset-frontend/src/features/home/EmptyState.tsx
+++ b/superset-frontend/src/features/home/EmptyState.tsx
@@ -20,6 +20,7 @@ import Button from 'src/components/Button';
 import { EmptyState as EmptyStateComponent } from 'src/components/EmptyState';
 import { TableTab } from 'src/views/CRUD/types';
 import { styled, t } from '@superset-ui/core';
+import { navigateTo } from 'src/utils/navigationUtils';
 import { WelcomeTable } from './types';
 
 const EmptyContainer = styled.div`
@@ -84,7 +85,7 @@ export default function EmptyState({
       <Button
         buttonStyle="default"
         onClick={() => {
-          window.location.href = url;
+          navigateTo(url);
         }}
       >
         {isFavorite
diff --git a/superset-frontend/src/features/home/Menu.tsx 
b/superset-frontend/src/features/home/Menu.tsx
index 3a15f4ff5e..20d441a7cb 100644
--- a/superset-frontend/src/features/home/Menu.tsx
+++ b/superset-frontend/src/features/home/Menu.tsx
@@ -19,6 +19,7 @@
 import { useState, useEffect } from 'react';
 import { styled, css } from '@superset-ui/core';
 import { debounce } from 'lodash';
+import { assetUrl } from 'src/utils/assetUrl';
 import { getUrlParam } from 'src/utils/urlUtils';
 import { Row, Col, Grid } from 'src/components';
 import { MainNav, MenuMode } from 'src/components/Menu';
@@ -268,11 +269,11 @@ export function Menu({
           >
             {isFrontendRoute(window.location.pathname) ? (
               <GenericLink className="navbar-brand" to={brand.path}>
-                <img src={brand.icon} alt={brand.alt} />
+                <img src={assetUrl(brand.icon)} alt={brand.alt} />
               </GenericLink>
             ) : (
               <a className="navbar-brand" href={brand.path} tabIndex={-1}>
-                <img src={brand.icon} alt={brand.alt} />
+                <img src={assetUrl(brand.icon)} alt={brand.alt} />
               </a>
             )}
           </Tooltip>
diff --git a/superset-frontend/src/features/home/RightMenu.tsx 
b/superset-frontend/src/features/home/RightMenu.tsx
index 8d35c6a64a..773952677a 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -39,6 +39,7 @@ import { Menu } from 'src/components/Menu';
 import { Tooltip } from 'src/components/Tooltip';
 import { Icons } from 'src/components/Icons';
 import Label from 'src/components/Label';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import { findPermission } from 'src/utils/findPermission';
 import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
 import {
@@ -325,7 +326,11 @@ const RightMenu = ({
       </Menu.Item>
     ) : (
       <Menu.Item key={item.name} css={styledChildMenu}>
-        {item.url ? <a href={item.url}> {item.label} </a> : item.label}
+        {item.url ? (
+          <a href={ensureAppRoot(item.url)}> {item.label} </a>
+        ) : (
+          item.label
+        )}
       </Menu.Item>
     );
 
@@ -473,7 +478,7 @@ const RightMenu = ({
                         {menu.label}
                       </Link>
                     ) : (
-                      <a href={menu.url}>
+                      <a href={ensureAppRoot(menu.url || '')}>
                         <i
                           data-test={`menu-item-${menu.label}`}
                           className={`fa ${menu.icon}`}
@@ -509,7 +514,7 @@ const RightMenu = ({
                       {isFrontendRoute(child.url) ? (
                         <Link to={child.url || ''}>{menuItemDisplay}</Link>
                       ) : (
-                        <a href={child.url}>{menuItemDisplay}</a>
+                        <a href={child.url || ''}>{menuItemDisplay}</a>
                       )}
                     </Menu.Item>
                   );
diff --git a/superset-frontend/src/features/home/SavedQueries.tsx 
b/superset-frontend/src/features/home/SavedQueries.tsx
index 65c34a8bfc..adfb2ab4c6 100644
--- a/superset-frontend/src/features/home/SavedQueries.tsx
+++ b/superset-frontend/src/features/home/SavedQueries.tsx
@@ -39,6 +39,9 @@ import {
   PAGE_SIZE,
   shortenSQL,
 } from 'src/views/CRUD/utils';
+import { assetUrl } from 'src/utils/assetUrl';
+import { ensureAppRoot } from 'src/utils/pathUtils';
+import { navigateTo } from 'src/utils/navigationUtils';
 import { Button } from 'src/components';
 import SubMenu from './SubMenu';
 import EmptyState from './EmptyState';
@@ -290,7 +293,7 @@ const SavedQueries = ({
             name: t('View All »'),
             buttonStyle: 'link',
             onClick: () => {
-              window.location.href = '/savedqueryview/list';
+              navigateTo('/savedqueryview/list');
             },
           },
         ]}
@@ -301,9 +304,11 @@ const SavedQueries = ({
             <CardStyles key={q.id}>
               <ListViewCard
                 imgURL=""
-                url={`/sqllab?savedQueryId=${q.id}`}
+                url={ensureAppRoot(`/sqllab?savedQueryId=${q.id}`)}
                 title={q.label}
-                imgFallbackURL="/static/assets/images/empty-query.svg"
+                imgFallbackURL={assetUrl(
+                  '/static/assets/images/empty-query.svg',
+                )}
                 description={t('Modified %s', q.changed_on_delta_humanized)}
                 cover={
                   q?.sql?.length && showThumbnails && featureFlag ? (
diff --git a/superset-frontend/src/features/tags/TagCard.tsx 
b/superset-frontend/src/features/tags/TagCard.tsx
index 2da403bc72..b84f82cef4 100644
--- a/superset-frontend/src/features/tags/TagCard.tsx
+++ b/superset-frontend/src/features/tags/TagCard.tsx
@@ -27,6 +27,7 @@ import { Icons } from 'src/components/Icons';
 import { Tag } from 'src/views/CRUD/types';
 import { deleteTags } from 'src/features/tags/tags';
 import { Button } from 'src/components';
+import { assetUrl } from 'src/utils/assetUrl';
 
 interface TagCardProps {
   tag: Tag;
@@ -99,7 +100,9 @@ function TagCard({
         }
         url={undefined}
         linkComponent={Link}
-        imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg"
+        imgFallbackURL={assetUrl(
+          '/static/assets/images/dashboard-card-fallback.svg',
+        )}
         description={t('Modified %s', tag.changed_on_delta_humanized)}
         actions={
           <ListViewCard.Actions
diff --git a/superset-frontend/src/views/index.tsx 
b/superset-frontend/src/globals.d.ts
similarity index 87%
copy from superset-frontend/src/views/index.tsx
copy to superset-frontend/src/globals.d.ts
index ac3efe193a..e42c2f8eec 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/globals.d.ts
@@ -16,7 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import ReactDOM from 'react-dom';
-import App from './App';
-
-ReactDOM.render(<App />, document.getElementById('app'));
+declare var __webpack_public_path__: string;
diff --git a/superset-frontend/src/middleware/loggerMiddleware.js 
b/superset-frontend/src/middleware/loggerMiddleware.js
index c008d027b3..d579c8d60a 100644
--- a/superset-frontend/src/middleware/loggerMiddleware.js
+++ b/superset-frontend/src/middleware/loggerMiddleware.js
@@ -28,6 +28,7 @@ import {
   LOG_ACTIONS_SPA_NAVIGATION,
 } from '../logger/LogUtils';
 import DebouncedMessageQueue from '../utils/DebouncedMessageQueue';
+import { ensureAppRoot } from '../utils/pathUtils';
 
 const LOG_ENDPOINT = '/superset/log/?explode=events';
 const sendBeacon = events => {
@@ -51,7 +52,7 @@ const sendBeacon = events => {
       // if we have a guest token, we need to send it for auth via the form
       formData.append('guest_token', SupersetClient.getGuestToken());
     }
-    navigator.sendBeacon(endpoint, formData);
+    navigator.sendBeacon(ensureAppRoot(endpoint), formData);
   } else {
     SupersetClient.post({
       endpoint,
diff --git a/superset-frontend/src/pages/AnnotationLayerList/index.tsx 
b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
index 253d33c0d3..9ecb9be860 100644
--- a/superset-frontend/src/pages/AnnotationLayerList/index.tsx
+++ b/superset-frontend/src/pages/AnnotationLayerList/index.tsx
@@ -38,6 +38,7 @@ import { AnnotationLayerObject } from 
'src/features/annotationLayers/types';
 import { ModifiedInfo } from 'src/components/AuditInfo';
 import { QueryObjectColumns } from 'src/views/CRUD/types';
 import { Icons } from 'src/components/Icons';
+import { navigateTo } from 'src/utils/navigationUtils';
 
 const PAGE_SIZE = 25;
 
@@ -290,7 +291,7 @@ function AnnotationLayersList({
   };
 
   const onLayerAdd = (id?: number) => {
-    window.location.href = `/annotationlayer/${id}/annotation`;
+    navigateTo(`/annotationlayer/${id}/annotation`);
   };
 
   const onModalHide = () => {
diff --git a/superset-frontend/src/pages/DashboardList/index.tsx 
b/superset-frontend/src/pages/DashboardList/index.tsx
index 3c9a7cc57e..a0c9e624df 100644
--- a/superset-frontend/src/pages/DashboardList/index.tsx
+++ b/superset-frontend/src/pages/DashboardList/index.tsx
@@ -71,6 +71,7 @@ import { DashboardStatus } from 
'src/features/dashboards/types';
 import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
 import { findPermission } from 'src/utils/findPermission';
 import { ModifiedInfo } from 'src/components/AuditInfo';
+import { navigateTo } from 'src/utils/navigationUtils';
 
 const PAGE_SIZE = 25;
 const PASSWORDS_NEEDED_MESSAGE = t(
@@ -698,7 +699,7 @@ function DashboardList(props: DashboardListProps) {
       ),
       buttonStyle: 'primary',
       onClick: () => {
-        window.location.assign('/dashboard/new');
+        navigateTo('/dashboard/new', { assign: true });
       },
     });
 
diff --git a/superset-frontend/src/preamble.ts 
b/superset-frontend/src/preamble.ts
index d7aebc1c27..853c60f307 100644
--- a/superset-frontend/src/preamble.ts
+++ b/superset-frontend/src/preamble.ts
@@ -33,7 +33,7 @@ import setupColors from './setup/setupColors';
 import setupFormatters from './setup/setupFormatters';
 import setupDashboardComponents from './setup/setupDashboardComponents';
 import { User } from './types/bootstrapTypes';
-import getBootstrapData from './utils/getBootstrapData';
+import getBootstrapData, { applicationRoot } from './utils/getBootstrapData';
 
 if (process.env.WEBPACK_MODE === 'development') {
   setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
@@ -54,7 +54,7 @@ if (typeof window !== 'undefined') {
 initFeatureFlags(bootstrapData.common.feature_flags);
 
 // Setup SupersetClient
-setupClient();
+setupClient({ appRoot: applicationRoot() });
 
 setupColors(
   bootstrapData.common.extra_categorical_color_schemes,
diff --git a/superset-frontend/src/views/index.tsx 
b/superset-frontend/src/public-path.ts
similarity index 79%
copy from superset-frontend/src/views/index.tsx
copy to superset-frontend/src/public-path.ts
index ac3efe193a..2066edeef6 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/public-path.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import ReactDOM from 'react-dom';
-import App from './App';
+import { staticAssetsPrefix } from './utils/getBootstrapData';
 
-ReactDOM.render(<App />, document.getElementById('app'));
+// Prefix the defined webpack public path with our configured prefix
+__webpack_public_path__ = `${staticAssetsPrefix()}${__webpack_public_path__}`;
diff --git a/superset-frontend/src/theme.ts b/superset-frontend/src/theme.ts
index 20a874c063..0c89191169 100644
--- a/superset-frontend/src/theme.ts
+++ b/superset-frontend/src/theme.ts
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import 'src/public-path';
+
 import './assets/stylesheets/superset.less';
 
 // Importing Antd under its own stylesheet to prevent unintentional theming
diff --git a/superset-frontend/src/types/bootstrapTypes.ts 
b/superset-frontend/src/types/bootstrapTypes.ts
index d983f2378a..ff46f6ab8c 100644
--- a/superset-frontend/src/types/bootstrapTypes.ts
+++ b/superset-frontend/src/types/bootstrapTypes.ts
@@ -143,6 +143,8 @@ export interface MenuData {
 }
 
 export interface CommonBootstrapData {
+  application_root: string;
+  static_assets_prefix: string;
   flash_messages: FlashMessage[];
   conf: JsonObject;
   locale: Locale;
diff --git a/superset-frontend/src/views/index.tsx 
b/superset-frontend/src/utils/assetUrl.ts
similarity index 68%
copy from superset-frontend/src/views/index.tsx
copy to superset-frontend/src/utils/assetUrl.ts
index ac3efe193a..5e83452783 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/utils/assetUrl.ts
@@ -16,7 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import ReactDOM from 'react-dom';
-import App from './App';
+import { staticAssetsPrefix } from 'src/utils/getBootstrapData';
 
-ReactDOM.render(<App />, document.getElementById('app'));
+/**
+ * Takes a string path to a static asset and prefixes it with the defined 
static asset prefix
+ * defined in the bootstrap data
+ * @param path A string path to a resource
+ */
+export function assetUrl(path: string) {
+  return `${staticAssetsPrefix()}${path.startsWith('/') ? path : `/${path}`}`;
+}
diff --git a/superset-frontend/src/utils/export.ts 
b/superset-frontend/src/utils/export.ts
index 66ed1ce67a..2f2550824d 100644
--- a/superset-frontend/src/utils/export.ts
+++ b/superset-frontend/src/utils/export.ts
@@ -19,6 +19,7 @@
 import parseCookie from 'src/utils/parseCookie';
 import rison from 'rison';
 import { nanoid } from 'nanoid';
+import { ensureAppRoot } from './pathUtils';
 
 export default function handleResourceExport(
   resource: string,
@@ -27,9 +28,9 @@ export default function handleResourceExport(
   interval = 200,
 ): void {
   const token = nanoid();
-  const url = `/api/v1/${resource}/export/?q=${rison.encode(
-    ids,
-  )}&token=${token}`;
+  const url = ensureAppRoot(
+    `/api/v1/${resource}/export/?q=${rison.encode(ids)}&token=${token}`,
+  );
 
   // create new iframe for export
   const iframe = document.createElement('iframe');
diff --git a/superset-frontend/src/utils/getBootstrapData.ts 
b/superset-frontend/src/utils/getBootstrapData.ts
index 0877340afe..b74e61ade1 100644
--- a/superset-frontend/src/utils/getBootstrapData.ts
+++ b/superset-frontend/src/utils/getBootstrapData.ts
@@ -32,3 +32,23 @@ export default function getBootstrapData(): BootstrapData {
   // Add a fallback to ensure the returned value is always of type 
BootstrapData
   return cachedBootstrapData ?? DEFAULT_BOOTSTRAP_DATA;
 }
+
+const APPLICATION_ROOT_NO_TRAILING_SLASH =
+  getBootstrapData().common.application_root.replace(/\/$/, '');
+
+const STATIC_ASSETS_PREFIX_NO_TRAILING_SLASH =
+  getBootstrapData().common.static_assets_prefix.replace(/\/$/, '');
+
+/**
+ * @returns The configured application root
+ */
+export function applicationRoot(): string {
+  return APPLICATION_ROOT_NO_TRAILING_SLASH;
+}
+
+/**
+ * @returns The configured static assets prefix
+ */
+export function staticAssetsPrefix(): string {
+  return STATIC_ASSETS_PREFIX_NO_TRAILING_SLASH;
+}
diff --git a/superset-frontend/src/utils/getBootstrapData.ts 
b/superset-frontend/src/utils/navigationUtils.ts
similarity index 54%
copy from superset-frontend/src/utils/getBootstrapData.ts
copy to superset-frontend/src/utils/navigationUtils.ts
index 0877340afe..11606aa7e3 100644
--- a/superset-frontend/src/utils/getBootstrapData.ts
+++ b/superset-frontend/src/utils/navigationUtils.ts
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { BootstrapData } from 'src/types/bootstrapTypes';
-import { DEFAULT_BOOTSTRAP_DATA } from 'src/constants';
+import { ensureAppRoot } from './pathUtils';
 
-let cachedBootstrapData: BootstrapData | null = null;
+export const navigateTo = (
+  url: string,
+  options?: { newWindow?: boolean; assign?: boolean },
+) => {
+  if (options?.newWindow) {
+    window.open(ensureAppRoot(url), '_blank', 'noopener noreferrer');
+  } else if (options?.assign) {
+    window.location.assign(ensureAppRoot(url));
+  } else {
+    window.location.href = ensureAppRoot(url);
+  }
+};
 
-export default function getBootstrapData(): BootstrapData {
-  if (cachedBootstrapData === null) {
-    const appContainer = document.getElementById('app');
-    const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
-    cachedBootstrapData = dataBootstrap
-      ? JSON.parse(dataBootstrap)
-      : DEFAULT_BOOTSTRAP_DATA;
+export const navigateWithState = (
+  url: string,
+  state: Record<string, unknown>,
+  options?: { replace?: boolean },
+) => {
+  if (options?.replace) {
+    window.history.replaceState(state, '', ensureAppRoot(url));
+  } else {
+    window.history.pushState(state, '', ensureAppRoot(url));
   }
-  // Add a fallback to ensure the returned value is always of type 
BootstrapData
-  return cachedBootstrapData ?? DEFAULT_BOOTSTRAP_DATA;
-}
+};
diff --git a/superset-frontend/src/views/index.tsx 
b/superset-frontend/src/utils/pathUtils.ts
similarity index 66%
copy from superset-frontend/src/views/index.tsx
copy to superset-frontend/src/utils/pathUtils.ts
index ac3efe193a..db4b94d69b 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/utils/pathUtils.ts
@@ -16,7 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import ReactDOM from 'react-dom';
-import App from './App';
+import { applicationRoot } from 'src/utils/getBootstrapData';
 
-ReactDOM.render(<App />, document.getElementById('app'));
+/**
+ * Takes a string path to a resource and prefixes it with the application root 
that is
+ * defined in the application configuration. The application path is sanitized.
+ * @param path A string path to a resource
+ */
+export function ensureAppRoot(path: string): string {
+  return `${applicationRoot()}${path.startsWith('/') ? path : `/${path}`}`;
+}
diff --git a/superset-frontend/src/views/App.tsx 
b/superset-frontend/src/views/App.tsx
index 970ad95870..ee494c6a7a 100644
--- a/superset-frontend/src/views/App.tsx
+++ b/superset-frontend/src/views/App.tsx
@@ -31,7 +31,7 @@ import ErrorBoundary from 'src/components/ErrorBoundary';
 import Loading from 'src/components/Loading';
 import { Layout } from 'src/components';
 import Menu from 'src/features/home/Menu';
-import getBootstrapData from 'src/utils/getBootstrapData';
+import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
 import ToastContainer from 'src/components/MessageToasts/ToastContainer';
 import setupApp from 'src/setup/setupApp';
 import setupPlugins from 'src/setup/setupPlugins';
@@ -73,7 +73,7 @@ const LocationPathnameLogger = () => {
 };
 
 const App = () => (
-  <Router>
+  <Router basename={applicationRoot()}>
     <ScrollToTop />
     <LocationPathnameLogger />
     <RootContextProviders>
diff --git a/superset-frontend/src/views/CRUD/hooks.ts 
b/superset-frontend/src/views/CRUD/hooks.ts
index a082bd971a..f7b3c7eaf0 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -39,6 +39,7 @@ import { FetchDataConfig } from 'src/components/ListView';
 import { FilterValue } from 'src/components/ListView/types';
 import Chart, { Slice } from 'src/types/Chart';
 import copyTextToClipboard from 'src/utils/copy';
+import { ensureAppRoot } from 'src/utils/pathUtils';
 import SupersetText from 'src/utils/textUtils';
 import { DatabaseObject } from 'src/features/databases/types';
 import { FavoriteStatus, ImportResourceName } from './types';
@@ -685,7 +686,9 @@ export const copyQueryLink = (
   addSuccessToast: (arg0: string) => void,
 ) => {
   copyTextToClipboard(() =>
-    Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
+    Promise.resolve(
+      
`${window.location.origin}${ensureAppRoot(`/sqllab?savedQueryId=${id}`)}`,
+    ),
   )
     .then(() => {
       addSuccessToast(t('Link Copied!'));
diff --git a/superset-frontend/src/views/index.tsx 
b/superset-frontend/src/views/index.tsx
index ac3efe193a..79784fbc9b 100644
--- a/superset-frontend/src/views/index.tsx
+++ b/superset-frontend/src/views/index.tsx
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import 'src/public-path';
+
 import ReactDOM from 'react-dom';
 import App from './App';
 
diff --git a/superset-frontend/src/views/menu.tsx 
b/superset-frontend/src/views/menu.tsx
index 128fa9ca87..caed9eb698 100644
--- a/superset-frontend/src/views/menu.tsx
+++ b/superset-frontend/src/views/menu.tsx
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import 'src/public-path';
 
 // Menu App. Used in views that do not already include the Menu component in 
the layout.
 // eg, backend rendered views
diff --git a/superset-frontend/webpack.config.js 
b/superset-frontend/webpack.config.js
index 1bf1f72b01..94d33a3723 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -41,6 +41,11 @@ const APP_DIR = path.resolve(__dirname, './');
 // output dir
 const BUILD_DIR = path.resolve(__dirname, '../superset/static/assets');
 const ROOT_DIR = path.resolve(__dirname, '..');
+// Public path for extracted css src:urls. All assets are compiled into the 
same
+// folder. This forces the src:url in the extracted css to only contain the 
filename
+// and will therefore be relative to the .css file itself and not have to 
worry about
+// any url prefix.
+const MINI_CSS_EXTRACT_PUBLICPATH = './';
 
 const {
   mode = 'development',
@@ -50,11 +55,10 @@ const {
 } = parsedArgs;
 const isDevMode = mode !== 'production';
 const isDevServer = process.argv[1].includes('webpack-dev-server');
-const ASSET_BASE_URL = process.env.ASSET_BASE_URL || '';
 
 const output = {
   path: BUILD_DIR,
-  publicPath: `${ASSET_BASE_URL}/static/assets/`,
+  publicPath: '/static/assets/',
 };
 if (isDevMode) {
   output.filename = '[name].[contenthash:8].entry.js';
@@ -397,7 +401,14 @@ const config = {
         test: /\.css$/,
         include: [APP_DIR, /superset-ui.+\/src/],
         use: [
-          isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,
+          isDevMode
+            ? 'style-loader'
+            : {
+                loader: MiniCssExtractPlugin.loader,
+                options: {
+                  publicPath: MINI_CSS_EXTRACT_PUBLICPATH,
+                },
+              },
           {
             loader: 'css-loader',
             options: {
@@ -410,7 +421,14 @@ const config = {
         test: /\.less$/,
         include: APP_DIR,
         use: [
-          isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,
+          isDevMode
+            ? 'style-loader'
+            : {
+                loader: MiniCssExtractPlugin.loader,
+                options: {
+                  publicPath: MINI_CSS_EXTRACT_PUBLICPATH,
+                },
+              },
           {
             loader: 'css-loader',
             options: {
@@ -535,6 +553,9 @@ if (isDevMode) {
   });
 
   config.devServer = {
+    devMiddleware: {
+      writeToDisk: true,
+    },
     historyApiFallback: true,
     hot: true,
     port: devserverPort,
diff --git a/superset/app.py b/superset/app.py
index a3d02648e1..3a1196c975 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -17,16 +17,21 @@
 
 import logging
 import os
-from typing import Optional
+from typing import cast, Iterable, Optional
+from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
 
 from flask import Flask
+from werkzeug.exceptions import NotFound
 
 from superset.initialization import SupersetAppInitializer
 
 logger = logging.getLogger(__name__)
 
 
-def create_app(superset_config_module: Optional[str] = None) -> Flask:
+def create_app(
+    superset_config_module: Optional[str] = None,
+    superset_app_root: Optional[str] = None,
+) -> Flask:
     app = SupersetApp(__name__)
 
     try:
@@ -36,6 +41,20 @@ def create_app(superset_config_module: Optional[str] = None) 
-> Flask:
         )
         app.config.from_object(config_module)
 
+        # Allow application to sit on a non-root path
+        # *Please be advised that this feature is in BETA.*
+        app_root = cast(
+            str, superset_app_root or os.environ.get("SUPERSET_APP_ROOT", "/")
+        )
+        if app_root != "/":
+            app.wsgi_app = AppRootMiddleware(app.wsgi_app, app_root)
+            # If not set, manually configure options that depend on the
+            # value of app_root so things work out of the box
+            if not app.config["STATIC_ASSETS_PREFIX"]:
+                app.config["STATIC_ASSETS_PREFIX"] = app_root
+            if app.config["APPLICATION_ROOT"] == "/":
+                app.config["APPLICATION_ROOT"] = app_root
+
         app_initializer = app.config.get("APP_INITIALIZER", 
SupersetAppInitializer)(app)
         app_initializer.init_app()
 
@@ -49,3 +68,30 @@ def create_app(superset_config_module: Optional[str] = None) 
-> Flask:
 
 class SupersetApp(Flask):
     pass
+
+
+class AppRootMiddleware:
+    """A middleware that attaches the application to a fixed prefix location.
+
+    See https://wsgi.readthedocs.io/en/latest/definitions.html for definitions
+    of SCRIPT_NAME and PATH_INFO.
+    """
+
+    def __init__(
+        self,
+        wsgi_app: WSGIApplication,
+        app_root: str,
+    ):
+        self.wsgi_app = wsgi_app
+        self.app_root = app_root
+
+    def __call__(
+        self, environ: WSGIEnvironment, start_response: StartResponse
+    ) -> Iterable[bytes]:
+        original_path_info = environ.get("PATH_INFO", "")
+        if original_path_info.startswith(self.app_root):
+            environ["PATH_INFO"] = 
original_path_info.removeprefix(self.app_root)
+            environ["SCRIPT_NAME"] = self.app_root
+            return self.wsgi_app(environ, start_response)
+        else:
+            return NotFound()(environ, start_response)
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index cd3510abbf..4a35bc96a2 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -24,7 +24,7 @@ from typing import Any, Callable, TYPE_CHECKING
 
 import wtforms_json
 from deprecation import deprecated
-from flask import Flask, redirect
+from flask import Flask, redirect, url_for
 from flask_appbuilder import expose, IndexView
 from flask_babel import gettext as __
 from flask_compress import Compress
@@ -225,6 +225,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         #
         # Setup regular views
         #
+        app_root = appbuilder.app.config["APPLICATION_ROOT"]
         appbuilder.add_link(
             "Home",
             label=__("Home"),
@@ -260,7 +261,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_link(
             "Datasets",
             label=__("Datasets"),
-            href="/tablemodelview/list/",
+            href=f"{app_root}/tablemodelview/list/",
             icon="fa-table",
             category="",
             category_icon="",
@@ -323,7 +324,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_link(
             "SQL Editor",
             label=__("SQL Lab"),
-            href="/sqllab/",
+            href=f"{app_root}/sqllab/",
             category_icon="fa-flask",
             icon="fa-flask",
             category="SQL Lab",
@@ -332,7 +333,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_link(
             "Saved Queries",
             label=__("Saved Queries"),
-            href="/savedqueryview/list/",
+            href=f"{app_root}/savedqueryview/list/",
             icon="fa-save",
             category="SQL Lab",
             category_label=__("SQL"),
@@ -340,7 +341,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_link(
             "Query Search",
             label=__("Query History"),
-            href="/sqllab/history/",
+            href=f"{app_root}/sqllab/history/",
             icon="fa-search",
             category_icon="fa-flask",
             category="SQL Lab",
@@ -387,7 +388,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
             AnnotationLayerView,
             "Annotation Layers",
             label=__("Annotation Layers"),
-            href="/annotationlayer/list/",
+            href="AnnotationLayerView.list",
             icon="fa-comment",
             category_icon="",
             category="Manage",
@@ -397,7 +398,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_view(
             RowLevelSecurityView,
             "Row Level Security",
-            href="/rowlevelsecurity/list/",
+            href="RowLevelSecurityView.list",
             label=__("Row Level Security"),
             category="Security",
             category_label=__("Security"),
@@ -703,4 +704,4 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
 class SupersetIndexView(IndexView):
     @expose("/")
     def index(self) -> FlaskResponse:
-        return redirect("/superset/welcome/")
+        return redirect(url_for("Superset.welcome"))
diff --git a/superset/templates/appbuilder/navbar.html 
b/superset/templates/appbuilder/navbar.html
index dda9c2430a..ae5213b53e 100644
--- a/superset/templates/appbuilder/navbar.html
+++ b/superset/templates/appbuilder/navbar.html
@@ -34,7 +34,7 @@
           <a class="navbar-brand" href="{{ root_path }}">
             <img
               width="{{ app_icon_width }}"
-              src="{{ appbuilder.app_icon }}"
+              src="{{ assets_prefix }}{{ appbuilder.app_icon }}"
               alt="{{ appbuilder.app_name }}"
               loading="lazy"
             />
diff --git a/superset/templates/superset/base.html 
b/superset/templates/superset/base.html
index 4a8110f8c1..9cb6e7705c 100644
--- a/superset/templates/superset/base.html
+++ b/superset/templates/superset/base.html
@@ -30,18 +30,18 @@
       href="{{ "" if favicon.href.startswith("http") else assets_prefix 
}}{{favicon.href}}"
     >
   {% endfor %}
-  {{ css_bundle("theme") }}
+  {{ css_bundle(assets_prefix, "theme") }}
 {% endblock %}
 
 {% block head_js %}
   {{ super() }}
   {% include "head_custom_extra.html" %}
-  {{ js_bundle("theme") }}
+  {{ js_bundle(assets_prefix, "theme") }}
 {% endblock %}
 
 {% block tail_js %}
   {{ super() }}
-  {{ js_bundle("preamble") }}
-  {{ js_bundle('menu') }}
+  {{ js_bundle(assets_prefix, "preamble") }}
+  {{ js_bundle(assets_prefix, 'menu') }}
   {% include "tail_js_custom_extra.html" %}
 {% endblock %}
diff --git a/superset/templates/superset/basic.html 
b/superset/templates/superset/basic.html
index 49cd8aa10e..16eeeceb40 100644
--- a/superset/templates/superset/basic.html
+++ b/superset/templates/superset/basic.html
@@ -60,8 +60,8 @@
         as="style"
       />
 
-      {{ css_bundle("theme") }} {% if entry %} {{ css_bundle(entry) }} {% 
endif %}
-      {% endblock %} {{ js_bundle("theme") }}
+      {{ css_bundle(assets_prefix, "theme") }} {% if entry %} {{ 
css_bundle(assets_prefix, entry) }} {% endif %}
+      {% endblock %} {{ js_bundle(assets_prefix, "theme") }}
 
       <input
         type="hidden"
@@ -119,8 +119,8 @@
         </div>
       </div>
 
-      {% block tail_js %} {% if not standalone_mode %} {{ js_bundle('menu') }} 
{%
-      endif %} {% if entry %} {{ js_bundle(entry) }} {% endif %} {% include
+      {% block tail_js %} {% if not standalone_mode %} {{ 
js_bundle(assets_prefix, 'menu') }} {%
+      endif %} {% if entry %} {{ js_bundle(assets_prefix, entry) }} {% endif 
%} {% include
       "tail_js_custom_extra.html" %} {% endblock %}
 
       <!-- Handle deferred stylesheets -->
diff --git a/superset/templates/superset/partials/asset_bundle.html 
b/superset/templates/superset/partials/asset_bundle.html
index 41b6d7cf8a..4fe33366d8 100644
--- a/superset/templates/superset/partials/asset_bundle.html
+++ b/superset/templates/superset/partials/asset_bundle.html
@@ -1,36 +1,37 @@
 {#
-  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
+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
+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.
+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 "superset/macros.html" as macros %}
-{% macro js_bundle(filename) %}
-  {# HTML comment is needed for webpack-dev-server to replace assets
-     with development version #}
-  <!-- Bundle js {{ filename }} START -->
-  {% for entry in js_manifest(filename) %}
-    <script src="{{ entry }}" async nonce="{{ macros.get_nonce() }}"></script>
-  {% endfor %}
-  <!-- Bundle js {{ filename }} END -->
+
+{% macro js_bundle(assets_prefix, filename) %}
+{# HTML comment is needed for webpack-dev-server to replace assets
+with development version #}
+<!-- Bundle js {{ filename }} START -->
+{% for entry in js_manifest(filename) %}
+<script src="{{ assets_prefix }}{{ entry }}" async nonce="{{ 
macros.get_nonce() }}"></script>
+{% endfor %}
+<!-- Bundle js {{ filename }} END -->
 {% endmacro %}
 
-{% macro css_bundle(filename) %}
-  <!-- Bundle css {{ filename }} START -->
-  {% for entry in css_manifest(filename) %}
-    <link rel="stylesheet" type="text/css" href="{{ entry }}" />
-  {% endfor %}
-  <!-- Bundle css {{ filename }} END -->
+{% macro css_bundle(assets_prefix, filename) %}
+<!-- Bundle css {{ filename }} START -->
+{% for entry in css_manifest(filename) %}
+<link rel="stylesheet" type="text/css" href="{{ assets_prefix }}{{ entry }}" />
+{% endfor %}
+<!-- Bundle css {{ filename }} END -->
 {% endmacro %}
diff --git a/superset/templates/superset/spa.html 
b/superset/templates/superset/spa.html
index 6a0312f4f0..cf284a66be 100644
--- a/superset/templates/superset/spa.html
+++ b/superset/templates/superset/spa.html
@@ -22,6 +22,6 @@
 {% endblock %}
 
 {% block tail_js %}
-  {{ js_bundle(entry) }}
+  {{ js_bundle(assets_prefix, entry) }}
   {% include "tail_js_custom_extra.html" %}
 {% endblock %}
diff --git a/superset/utils/urls.py b/superset/utils/urls.py
index 9b186f54f3..599ef95b3a 100644
--- a/superset/utils/urls.py
+++ b/superset/utils/urls.py
@@ -32,8 +32,7 @@ def headless_url(path: str, user_friendly: bool = False) -> 
str:
 
 
 def get_url_path(view: str, user_friendly: bool = False, **kwargs: Any) -> str:
-    with current_app.test_request_context():
-        return headless_url(url_for(view, **kwargs), 
user_friendly=user_friendly)
+    return headless_url(url_for(view, **kwargs), user_friendly=user_friendly)
 
 
 def modify_url_query(url: str, **kwargs: Any) -> str:
diff --git a/superset/views/base.py b/superset/views/base.py
index 5b7ef99b02..a598994f19 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -32,6 +32,7 @@ from flask import (
     redirect,
     Response,
     session,
+    url_for,
 )
 from flask_appbuilder import BaseView, Model, ModelView
 from flask_appbuilder.actions import action
@@ -256,7 +257,8 @@ def menu_data(user: User) -> dict[str, Any]:
     return {
         "menu": appbuilder.menu.get_data(),
         "brand": {
-            "path": appbuilder.app.config["LOGO_TARGET_PATH"] or 
"/superset/welcome/",
+            "path": appbuilder.app.config["LOGO_TARGET_PATH"]
+            or url_for("Superset.welcome"),
             "icon": appbuilder.app_icon,
             "alt": appbuilder.app_name,
             "tooltip": appbuilder.app.config["LOGO_TOOLTIP"],
@@ -327,6 +329,8 @@ def cached_common_bootstrap_data(  # pylint: 
disable=unused-argument
     language = locale.language if locale else "en"
 
     bootstrap_data = {
+        "application_root": conf["APPLICATION_ROOT"],
+        "static_assets_prefix": conf["STATIC_ASSETS_PREFIX"],
         "conf": frontend_config,
         "locale": language,
         "language_pack": get_language_pack(language),
diff --git a/superset/views/core.py b/superset/views/core.py
index 8a56befe0b..54dead027b 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -23,7 +23,7 @@ from datetime import datetime
 from typing import Any, Callable, cast
 from urllib import parse
 
-from flask import abort, flash, g, redirect, request, Response
+from flask import abort, flash, g, redirect, request, Response, url_for
 from flask_appbuilder import expose
 from flask_appbuilder.security.decorators import (
     has_access,
@@ -132,11 +132,11 @@ class Superset(BaseSupersetView):
         if not slc:
             abort(404)
         form_data = parse.quote(json.dumps({"slice_id": slice_id}))
-        endpoint = f"/explore/?form_data={form_data}"
+        endpoint_params = {"form_data": f"{form_data}"}
 
         if ReservedUrlParameters.is_standalone_mode():
-            endpoint += f"&{ReservedUrlParameters.STANDALONE}=true"
-        return redirect(endpoint)
+            endpoint_params[ReservedUrlParameters.STANDALONE] = "true"
+        return redirect(url_for("ExploreView.root", **endpoint_params))
 
     def get_query_string_response(self, viz_obj: BaseViz) -> FlaskResponse:
         query = None
@@ -421,7 +421,7 @@ class Superset(BaseSupersetView):
                     )
             except (ChartNotFoundError, ExplorePermalinkGetFailedError) as ex:
                 flash(__("Error: %(msg)s", msg=ex.message), "danger")
-                return redirect("/chart/list/")
+                return redirect(url_for("SliceModelView.list"))
         elif form_data_key:
             parameters = CommandParameters(key=form_data_key)
             value = GetFormDataCommand(parameters).run()
@@ -797,7 +797,7 @@ class Superset(BaseSupersetView):
                 redirect_url = 
f"{appbuilder.get_url_for_login}?next={request.url}"
                 warn_msg = "Users must be logged in to view this dashboard."
             else:
-                redirect_url = "/dashboard/list/"
+                redirect_url = url_for("DashboardModelView.list")
                 warn_msg = utils.error_msg_from_exception(ex)
             return redirect_with_flash(
                 url=redirect_url,
@@ -840,14 +840,16 @@ class Superset(BaseSupersetView):
             value = GetDashboardPermalinkCommand(key).run()
         except DashboardPermalinkGetFailedError as ex:
             flash(__("Error: %(msg)s", msg=ex.message), "danger")
-            return redirect("/dashboard/list/")
+            return redirect(url_for("DashboardModelView.list"))
         except DashboardAccessDeniedError as ex:
             flash(__("Error: %(msg)s", msg=ex.message), "danger")
-            return redirect("/dashboard/list/")
+            return redirect(url_for("DashboardModelView.list"))
         if not value:
             return json_error_response(_("permalink state not found"), 
status=404)
         dashboard_id, state = value["dashboardId"], value.get("state", {})
-        url = f"/superset/dashboard/{dashboard_id}?permalink_key={key}"
+        url = url_for(
+            "Superset.dashboard", dashboard_id_or_slug=dashboard_id, 
permalink_key=key
+        )
         if url_params := state.get("urlParams"):
             params = parse.urlencode(url_params)
             url = f"{url}&{params}"
@@ -924,4 +926,4 @@ class Superset(BaseSupersetView):
     @expose("/sqllab/history/", methods=("GET",))
     @deprecated(new_target="/sqllab/history")
     def sqllab_history(self) -> FlaskResponse:
-        return redirect("/sqllab/history")
+        return redirect(url_for("SqllabView.history"))
diff --git a/superset/views/dashboard/views.py 
b/superset/views/dashboard/views.py
index fb70c7b396..cd30da021f 100644
--- a/superset/views/dashboard/views.py
+++ b/superset/views/dashboard/views.py
@@ -17,7 +17,7 @@
 import builtins
 from typing import Callable, Union
 
-from flask import g, redirect, Response
+from flask import g, redirect, Response, url_for
 from flask_appbuilder import expose
 from flask_appbuilder.actions import action
 from flask_appbuilder.models.sqla.interface import SQLAInterface
@@ -66,8 +66,7 @@ class DashboardModelView(DashboardMixin, SupersetModelView, 
DeleteMixin):  # pyl
     ) -> FlaskResponse:
         if not isinstance(items, list):
             items = [items]
-        ids = "".join(f"&id={d.id}" for d in items)
-        return redirect(f"/dashboard/export_dashboards_form?{ids[1:]}")
+        return redirect(url_for("DashboardModelView.download_dashboards", 
id=items))
 
 
 class Dashboard(BaseSupersetView):
@@ -86,7 +85,11 @@ class Dashboard(BaseSupersetView):
         )
         db.session.add(new_dashboard)
         db.session.commit()  # pylint: disable=consider-using-transaction
-        return redirect(f"/superset/dashboard/{new_dashboard.id}/?edit=true")
+        return redirect(
+            url_for(
+                "Superset.dashboard", dashboard_id_or_slug=new_dashboard.id, 
edit=True
+            )
+        )
 
     @expose("/<dashboard_id_or_slug>/embedded")
     @event_logger.log_this_with_extra_payload

Reply via email to