diff --git a/.github/workflows/bashlib.sh b/.github/workflows/bashlib.sh index 0d359e059e3..deb56ab649e 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 { 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 9f689c014b6..cd99250a9e5 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 686c9e7536c..38241613e21 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 eda6e56c452..e3250506bc0 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 b89e9f8a4b3..e55c6ac18d3 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 9778ae74459..8f3c5b024ff 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 ef4ea6419d4..9b7c4086bdc 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 506858c74fd..40c1d382720 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 0edcf77d542..b3d8c4958ed 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 2d50460fd79..1db265cfe85 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 c792a310ef8..bd56b6f76e7 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 298453a58e7..8bc17aa2bc3 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 1a8c1bca3ed..be52c08d8bc 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 0d991503178..8edd0404788 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 a844b30179c..347b54502d2 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 76653dada9c..804f35ca7b0 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 12c2d3522ac..e9e6ba39558 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 4f35bb4a20a..eb038f1d3f5 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 e92b691e1a4..5b8af481f66 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 e1a4c7b9c81..aba28794334 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 76eea9fc191..ddadae15c21 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 5c8672192a8..362f3daabbf 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 73a54a8e2c1..e78b54ddf60 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 e746489ce12..4b24c844364 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 4f0d0bb8cd3..389698dcb8d 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 c67da1afd5f..af06ad02993 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 fd040faed04..92f2e50198f 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 98a8c385742..2dbdeca1777 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 a63ffd8b68a..e8e6c97771d 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; 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 39f148f7bee..f7534e2c5c3 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 286ef35cc03..d7fcf1c04c8 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 c65f684266e..fc08d9e21d1 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 8d9a36da550..d9579071c37 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 { 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 { popNewTab(urlParams: Record) { // 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 2398eafe361..a12ae44e2b1 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 b4c12c48f47..38cc1de4741 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 ( diff --git a/superset-frontend/src/components/Tags/Tag.test.tsx b/superset-frontend/src/components/Tags/Tag.test.tsx index 0f25a78e2df..79c3d27ad4a 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(, { useRouter: true }); + test('should render', () => { - const { container } = render(); + const { container } = setup(); expect(container).toBeInTheDocument(); }); test('should render shortname properly', () => { - const { container } = render(); + 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(); + 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 6bb3223c675..bc154707143 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 ? ( - {children || tagDisplay} - + ) : ( children || tagDisplay )} diff --git a/superset-frontend/src/components/Tags/TagsList.test.tsx b/superset-frontend/src/components/Tags/TagsList.test.tsx index 35819ade912..e28160a91f5 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(, { useRouter: true }); + test('should render', () => { - const { container } = render(); + const { container } = setup(); expect(container).toBeInTheDocument(); }); test('should render 5 elements', async () => { - render(); + 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(); + 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 c4fd0e94f9c..98abb6c2b12 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 33111c75d50..51bdb95784c 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 42852fef4d9..75941c6c7c8 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 3ab498145dd..4a619dc7351 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 f701892210f..2126cff46e8 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 { } 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 d861c8ff32f..3e8d310c360 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 { 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, + }) } > { - 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 4263de96092..deddab121e9 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 e7f80d4e066..96c239b9cf4 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 797b5def3d3..c4f0e82bdc8 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'); -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: '', +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: '', + }); + }); + + 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 ed4d03c6a24..e26f693626c 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 5eb6fb341fc..7143c908879 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 b6626701337..56a2beea546 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={} coverRight={ diff --git a/superset-frontend/src/features/dashboards/DashboardCard.tsx b/superset-frontend/src/features/dashboards/DashboardCard.tsx index fb5bae9c111..d8d50f5a7f7 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={} actions={ diff --git a/superset-frontend/src/features/home/ChartTable.tsx b/superset-frontend/src/features/home/ChartTable.tsx index 66222d6755d..e4a326b8c64 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 7dfab696524..f2d5f68c18d 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 7a2ba4d62ba..6d180e7819b 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({