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