diff --git a/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts index 51fd2ce46bb39..f43076bbbfba9 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/advanced_analytics.test.ts @@ -22,7 +22,7 @@ describe('Advanced analytics', () => { cy.intercept('POST', '/superset/explore_json/**').as('postJson'); cy.intercept('GET', '/superset/explore_json/**').as('getJson'); cy.intercept('PUT', '/api/v1/explore/**').as('putExplore'); - cy.intercept('GET', '/superset/explore/**').as('getExplore'); + cy.intercept('GET', '/explore/**').as('getExplore'); }); it('Create custom time compare', () => { diff --git a/superset-frontend/cypress-base/cypress/support/index.ts b/superset-frontend/cypress-base/cypress/support/index.ts index 9d77f98accad9..7ededd67a5fee 100644 --- a/superset-frontend/cypress-base/cypress/support/index.ts +++ b/superset-frontend/cypress-base/cypress/support/index.ts @@ -19,7 +19,7 @@ import '@cypress/code-coverage/support'; import '@applitools/eyes-cypress/commands'; -const BASE_EXPLORE_URL = '/superset/explore/?form_data='; +const BASE_EXPLORE_URL = '/explore/?form_data='; const TokenName = Cypress.env('TOKEN_NAME'); require('cy-verify-downloads').addCustomCommand(); @@ -90,7 +90,7 @@ Cypress.Commands.add( }, }).then(response => { const formDataKey = response.body.key; - const url = `/superset/explore/?form_data_key=${formDataKey}`; + const url = `/explore/?form_data_key=${formDataKey}`; cy.visit(url); }); }, diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx index bc58e925e43d5..91e6b28dd11bb 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx @@ -138,6 +138,6 @@ test('formats Explore url', async () => { datasource, vizType: 'table', }); - const formattedUrl = '/superset/explore/?viz_type=table&datasource=1'; + const formattedUrl = '/explore/?viz_type=table&datasource=1'; expect(wrapper.instance().exploreUrl()).toBe(formattedUrl); }); diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx index 71981fdef9a11..d4c6bfc78aa0f 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx @@ -226,7 +226,7 @@ export default class AddSliceContainer extends React.PureComponent< exploreUrl() { const dashboardId = getUrlParam(URL_PARAMS.dashboardId); - let url = `/superset/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`; + let url = `/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`; if (!isNullish(dashboardId)) { url += `&dashboard_id=${dashboardId}`; } diff --git a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx index ffee7f6ee465a..1fca2f95e1c3c 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/SliceHeader.test.tsx @@ -115,7 +115,7 @@ const createProps = (overrides: any = {}) => ({ sliceCanEdit: false, slice: { slice_id: 312, - slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20312%7D', + slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%20312%7D', slice_name: 'Vaccine Candidates per Phase', form_data: { adhoc_filters: [], diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index 9e212018ad422..234029407fc46 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -48,7 +48,7 @@ const createProps = (viz_type = 'sunburst') => ({ onExploreChart: jest.fn(), slice: { slice_id: 371, - slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20371%7D', + slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%20371%7D', slice_name: 'Vaccine Candidates per Country & Stage', slice_description: 'Table of vaccine candidates for 100 countries', form_data: { diff --git a/superset-frontend/src/dashboard/util/filterboxMigrationHelper.test.ts b/superset-frontend/src/dashboard/util/filterboxMigrationHelper.test.ts index 82ed0dd8ac401..f59f36541e35f 100644 --- a/superset-frontend/src/dashboard/util/filterboxMigrationHelper.test.ts +++ b/superset-frontend/src/dashboard/util/filterboxMigrationHelper.test.ts @@ -61,7 +61,7 @@ const regionFilter = { }, modified: '', slice_name: 'Region Filter', - slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%2032%7D', + slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%2032%7D', slice_id: 32, }; const chart1 = { @@ -88,7 +88,7 @@ const chart1 = { }, modified: "", slice_name: "World's Population", - slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%2033%7D', + slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%2033%7D', slice_id: 33, }; const chartData = [regionFilter, chart1]; diff --git a/superset-frontend/src/explore/App.jsx b/superset-frontend/src/explore/App.jsx deleted file mode 100644 index 995cf3d9e91c8..0000000000000 --- a/superset-frontend/src/explore/App.jsx +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import { hot } from 'react-hot-loader/root'; -import { Provider } from 'react-redux'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { ThemeProvider } from '@superset-ui/core'; -import { GlobalStyles } from 'src/GlobalStyles'; -import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; -import ToastContainer from 'src/components/MessageToasts/ToastContainer'; -import setupApp from 'src/setup/setupApp'; -import setupPlugins from 'src/setup/setupPlugins'; -import { theme } from 'src/preamble'; -import { ExplorePage } from './ExplorePage'; -import './main.less'; -import '../assets/stylesheets/reactable-pagination.less'; - -setupApp(); -setupPlugins(); - -const App = ({ store }) => ( - - - - - - - - - - - -); - -export default hot(App); diff --git a/superset-frontend/src/explore/ExplorePage.tsx b/superset-frontend/src/explore/ExplorePage.tsx index 50982924eedff..8ae31cb88335a 100644 --- a/superset-frontend/src/explore/ExplorePage.tsx +++ b/superset-frontend/src/explore/ExplorePage.tsx @@ -38,7 +38,7 @@ const fetchExploreData = () => { })(exploreUrlParams); }; -export const ExplorePage = () => { +const ExplorePage = () => { const [isLoaded, setIsLoaded] = useState(false); const dispatch = useDispatch(); @@ -66,3 +66,5 @@ export const ExplorePage = () => { } return ; }; + +export default ExplorePage; diff --git a/superset-frontend/src/explore/actions/saveModalActions.js b/superset-frontend/src/explore/actions/saveModalActions.js index 6e87fe52b5f17..91abb04e180d9 100644 --- a/superset-frontend/src/explore/actions/saveModalActions.js +++ b/superset-frontend/src/explore/actions/saveModalActions.js @@ -62,7 +62,7 @@ export function removeSaveModalAlert() { export function saveSlice(formData, requestParams) { return dispatch => { - const url = getExploreUrl({ + let url = getExploreUrl({ formData, endpointType: 'base', force: false, @@ -70,6 +70,9 @@ export function saveSlice(formData, requestParams) { requestParams, }); + // TODO: This will be removed in the next PR that will change the logic to save a slice + url = url.replace('/explore', '/superset/explore'); + // Save the query context so we can re-generate the data from Python // for alerts and reports const queryContext = buildV1ChartDataPayload({ diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx index 540a444ab0473..1664afbb0d438 100644 --- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx +++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx @@ -88,7 +88,7 @@ const createProps = () => ({ ], slice_id: 318, slice_name: 'Age distribution of respondents', - slice_url: '/superset/explore/?form_data=%7B%22slice_id%22%3A%20318%7D', + slice_url: '/explore/?form_data=%7B%22slice_id%22%3A%20318%7D', }, slice_name: 'Age distribution of respondents', actions: { diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index d6f3637f625d0..d05811f7a0581 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -81,7 +81,7 @@ fetchMock.get('glob:*/api/v1/explore/form_data*', {}); fetchMock.get('glob:*/favstar/slice*', { count: 0 }); const renderWithRouter = (withKey?: boolean) => { - const path = '/superset/explore/'; + const path = '/explore/'; const search = withKey ? `?form_data_key=${key}&dataset_id=1` : ''; return render( diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index 0155e3c37822b..cc374c1cf9b96 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -85,6 +85,7 @@ const ExploreContainer = styled.div` display: flex; flex-direction: column; height: 100%; + min-height: 0; `; const ExplorePanelContainer = styled.div` diff --git a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx index b385099699cb4..52c20dcd84bd2 100644 --- a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx +++ b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx @@ -51,7 +51,7 @@ describe('exploreUtils', () => { force: false, curUrl: 'http://superset.com', }); - compareURI(URI(url), URI('/superset/explore/')); + compareURI(URI(url), URI('/explore/')); }); it('generates proper json url', () => { const url = getExploreUrl({ @@ -95,7 +95,7 @@ describe('exploreUtils', () => { }); compareURI( URI(url), - URI('/superset/explore/').search({ + URI('/explore/').search({ standalone: DashboardStandaloneMode.HIDE_NAV, }), ); diff --git a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts index 7457b462b0314..c0f2721a49b98 100644 --- a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts @@ -33,7 +33,7 @@ const createParams = () => ({ test('Get ExploreUrl with default params', () => { const params = createParams(); - expect(getExploreUrl(params)).toBe('http://localhost/superset/explore/'); + expect(getExploreUrl(params)).toBe('http://localhost/explore/'); }); test('Get ExploreUrl with endpointType:full', () => { diff --git a/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts index 8d5a8ee09c16e..5039d3421fb19 100644 --- a/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts @@ -19,7 +19,7 @@ import { getParsedExploreURLParams } from './getParsedExploreURLParams'; -const EXPLORE_BASE_URL = 'http://localhost:9000/superset/explore/'; +const EXPLORE_BASE_URL = 'http://localhost:9000/explore/'; const setupLocation = (newUrl: string) => { delete (window as any).location; // @ts-ignore diff --git a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts index c8d43f413f699..1d6ad40045ae7 100644 --- a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts @@ -25,6 +25,6 @@ test('Cases in which the "explore_json" will be returned', () => { }); test('Cases in which the "explore" will be returned', () => { - expect(getURIDirectory('any-string')).toBe('/superset/explore/'); - expect(getURIDirectory()).toBe('/superset/explore/'); + expect(getURIDirectory('any-string')).toBe('/explore/'); + expect(getURIDirectory()).toBe('/explore/'); }); diff --git a/superset-frontend/src/explore/exploreUtils/index.js b/superset-frontend/src/explore/exploreUtils/index.js index 506de032e5d73..0e6a79ab91c9d 100644 --- a/superset-frontend/src/explore/exploreUtils/index.js +++ b/superset-frontend/src/explore/exploreUtils/index.js @@ -87,7 +87,7 @@ export function getURIDirectory(endpointType = 'base') { ) { return '/superset/explore_json/'; } - return '/superset/explore/'; + return '/explore/'; } export function mountExploreUrl(endpointType, extraSearch = {}, force = false) { diff --git a/superset-frontend/src/explore/index.jsx b/superset-frontend/src/explore/index.jsx deleted file mode 100644 index 0af6b02747e5f..0000000000000 --- a/superset-frontend/src/explore/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import shortid from 'shortid'; -import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages'; -import logger from 'src/middleware/loggerMiddleware'; -import { initFeatureFlags } from 'src/featureFlags'; -import { initEnhancer } from 'src/reduxUtils'; -import rootReducer from './reducers/index'; -import App from './App'; - -const exploreViewContainer = document.getElementById('app'); -const bootstrapData = JSON.parse( - exploreViewContainer.getAttribute('data-bootstrap'), -); - -const user = { ...bootstrapData.user }; -const common = { ...bootstrapData.common }; -initFeatureFlags(common.feature_flags); -const store = createStore( - rootReducer, - { - user, - common, - impressionId: shortid.generate(), - messageToasts: getToastsFromPyFlashMessages(common?.flash_messages || []), - }, - compose(applyMiddleware(thunk, logger), initEnhancer(false)), -); - -ReactDOM.render(, document.getElementById('app')); diff --git a/superset-frontend/src/explore/reducers/index.js b/superset-frontend/src/explore/reducers/index.js deleted file mode 100644 index 96a1698281f8d..0000000000000 --- a/superset-frontend/src/explore/reducers/index.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { combineReducers } from 'redux'; -import shortid from 'shortid'; - -import { bootstrapData } from 'src/preamble'; -import reports from 'src/reports/reducers/reports'; -import charts from 'src/components/Chart/chartReducer'; -import dataMask from 'src/dataMask/reducer'; -import messageToasts from 'src/components/MessageToasts/reducers'; -import datasources from './datasourcesReducer'; -import saveModal from './saveModalReducer'; -import explore from './exploreReducer'; - -// noopReducer, userReducer temporarily copied from src/views/store.ts -// TODO: when SPA work is done, we'll be able to reuse those instead of copying - -const noopReducer = - initialState => - (state = initialState) => - state; - -const userReducer = (user = bootstrapData.user || {}, action) => { - if (action.type === 'USER_LOADED') { - return action.user; - } - return user; -}; - -export default combineReducers({ - charts, - saveModal, - dataMask, - datasources, - explore, - messageToasts, - reports, - impressionId: noopReducer(shortid.generate()), - user: userReducer, - common: noopReducer(bootstrapData.common || {}), -}); diff --git a/superset-frontend/src/preamble.ts b/superset-frontend/src/preamble.ts index 4ca1849768d77..f6ae99b0845ac 100644 --- a/superset-frontend/src/preamble.ts +++ b/superset-frontend/src/preamble.ts @@ -28,6 +28,7 @@ import setupExtensions from './setup/setupExtensions'; import setupFormatters from './setup/setupFormatters'; import setupDashboardComponents from './setup/setupDasboardComponents'; import { BootstrapUser, User } from './types/bootstrapTypes'; +import { initFeatureFlags } from './featureFlags'; if (process.env.WEBPACK_MODE === 'development') { setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); @@ -44,6 +45,7 @@ export let bootstrapData: { dashboard_id: string; }; } = {}; + // Configure translation if (typeof window !== 'undefined') { const root = document.getElementById('app'); @@ -61,6 +63,9 @@ if (typeof window !== 'undefined') { configure(); } +// Configure feature flags +initFeatureFlags(bootstrapData?.common?.feature_flags); + // Setup SupersetClient setupClient(); diff --git a/superset-frontend/src/utils/getDatasourceUid.ts b/superset-frontend/src/utils/getDatasourceUid.ts index da074ce5eccff..b438d18ba7dc0 100644 --- a/superset-frontend/src/utils/getDatasourceUid.ts +++ b/superset-frontend/src/utils/getDatasourceUid.ts @@ -19,4 +19,4 @@ import { Dataset } from '@superset-ui/chart-controls'; export const getDatasourceUid = (datasource: Dataset) => - datasource.uid ?? `${datasource.id}__${datasource.type}`; + datasource.uid ?? `${datasource.id ?? 'None'}__${datasource.type}`; diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx index 04ca777c3d7a7..a142d5fc7a91e 100644 --- a/superset-frontend/src/views/App.tsx +++ b/superset-frontend/src/views/App.tsx @@ -25,25 +25,25 @@ import { useLocation, } from 'react-router-dom'; import { GlobalStyles } from 'src/GlobalStyles'; -import { initFeatureFlags } from 'src/featureFlags'; import ErrorBoundary from 'src/components/ErrorBoundary'; import Loading from 'src/components/Loading'; import Menu from 'src/views/components/Menu'; import { bootstrapData } from 'src/preamble'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import setupApp from 'src/setup/setupApp'; +import setupPlugins from 'src/setup/setupPlugins'; import { routes, isFrontendRoute } from 'src/views/routes'; import { Logger } from 'src/logger/LogUtils'; import { RootContextProviders } from './RootContextProviders'; setupApp(); +setupPlugins(); const user = { ...bootstrapData.user }; const menu = { ...bootstrapData.common.menu_data, }; let lastLocationPathname: string; -initFeatureFlags(bootstrapData.common.feature_flags); const LocationPathnameLogger = () => { const location = useLocation(); diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index 69812bd641559..4b4f4569283e0 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -18,6 +18,7 @@ */ import React from 'react'; import { t, useTheme } from '@superset-ui/core'; +import { Link, useHistory } from 'react-router-dom'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import Icons from 'src/components/Icons'; @@ -64,6 +65,7 @@ export default function ChartCard({ userId, handleBulkChartExport, }: ChartCardProps) { + const history = useHistory(); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); const canExport = @@ -136,7 +138,7 @@ export default function ChartCard({ { if (!bulkSelectEnabled && chart.url) { - window.location.href = chart.url; + history.push(chart.url); } }} > @@ -158,6 +160,7 @@ export default function ChartCard({ coverRight={ } + linkComponent={Link} actions={ { diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.test.jsx b/superset-frontend/src/views/CRUD/chart/ChartList.test.jsx index fd9aee16ab06c..8bfe9951f7765 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.test.jsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.test.jsx @@ -189,7 +189,7 @@ describe('RTL', () => { , - { useRedux: true }, + { useRedux: true, useRouter: true }, ); }); diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 637e9b6ea72c0..c2ffcf028483f 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -41,6 +41,7 @@ import handleResourceExport from 'src/utils/export'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import SubMenu, { SubMenuProps } from 'src/views/components/SubMenu'; import FaveStar from 'src/components/FaveStar'; +import { Link } from 'react-router-dom'; import ListView, { Filter, FilterOperator, @@ -270,7 +271,7 @@ function ChartList(props: ChartListProps) { }, }: any) => ( - + {certifiedBy && ( <> )} {sliceName} - + {description && ( )} diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx index 540c033419009..d4f4fb4a8be23 100644 --- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useState } from 'react'; import moment from 'moment'; import { styled, t } from '@superset-ui/core'; import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers'; - +import { Link } from 'react-router-dom'; import ListViewCard from 'src/components/ListViewCard'; import SubMenu from 'src/views/components/SubMenu'; import { ActivityData, LoadingCards } from 'src/views/CRUD/welcome/Welcome'; @@ -189,20 +189,17 @@ export default function ActivityTable({ const url = getEntityUrl(entity); const lastActionOn = getEntityLastActionOn(entity); return ( - { - window.location.href = url; - }} - key={url} - > - } - url={url} - title={getEntityTitle(entity)} - description={lastActionOn} - avatar={getEntityIcon(entity)} - actions={null} - /> + + + } + url={url} + title={getEntityTitle(entity)} + description={lastActionOn} + avatar={getEntityIcon(entity)} + actions={null} + /> + ); }, diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx index 525860ecef066..e0d9d144bff9b 100644 --- a/superset-frontend/src/views/routes.tsx +++ b/superset-frontend/src/views/routes.tsx @@ -81,6 +81,9 @@ const ExecutionLog = lazy( /* webpackChunkName: "ExecutionLog" */ 'src/views/CRUD/alert/ExecutionLog' ), ); +const ExplorePage = lazy( + () => import(/* webpackChunkName: "ExplorePage" */ 'src/explore/ExplorePage'), +); const QueryList = lazy( () => import( @@ -168,6 +171,14 @@ export const routes: Routes = [ isReportEnabled: true, }, }, + { + path: '/explore/', + Component: ExplorePage, + }, + { + path: '/superset/explore/p', + Component: ExplorePage, + }, ]; const frontEndRoutes = routes diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts index d363ed2cd99fa..37da4b5b7cc8b 100644 --- a/superset-frontend/src/views/store.ts +++ b/superset-frontend/src/views/store.ts @@ -27,15 +27,26 @@ import dashboardInfo from 'src/dashboard/reducers/dashboardInfo'; import dashboardState from 'src/dashboard/reducers/dashboardState'; import dashboardFilters from 'src/dashboard/reducers/dashboardFilters'; import nativeFilters from 'src/dashboard/reducers/nativeFilters'; -import datasources from 'src/dashboard/reducers/datasources'; +import dashboardDatasources from 'src/dashboard/reducers/datasources'; import sliceEntities from 'src/dashboard/reducers/sliceEntities'; import dashboardLayout from 'src/dashboard/reducers/undoableDashboardLayout'; import logger from 'src/middleware/loggerMiddleware'; +import saveModal from 'src/explore/reducers/saveModalReducer'; +import explore from 'src/explore/reducers/exploreReducer'; +import exploreDatasources from 'src/explore/reducers/datasourcesReducer'; +import { DatasourcesState } from 'src/dashboard/types'; +import { + DatasourcesActionPayload, + DatasourcesAction, +} from 'src/dashboard/actions/datasources'; import shortid from 'shortid'; import { BootstrapUser, UserWithPermissionsAndRoles, } from 'src/types/bootstrapTypes'; +import { AnyDatasourcesAction } from 'src/explore/actions/datasourcesActions'; +import { HydrateExplore } from 'src/explore/actions/hydrateExplore'; +import { Dataset } from '@superset-ui/chart-controls'; // Some reducers don't do anything, and redux is just used to reference the initial "state". // This may change later, as the client application takes on more responsibilities. @@ -47,20 +58,6 @@ const noopReducer = const container = document.getElementById('app'); const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}'); -// reducers used only in the dashboard page -const dashboardReducers = { - charts, - datasources, - dashboardInfo, - dashboardFilters, - dataMask, - nativeFilters, - dashboardState, - dashboardLayout, - sliceEntities, - reports, -}; - export const USER_LOADED = 'USER_LOADED'; export type UserLoadedAction = { @@ -78,13 +75,44 @@ const userReducer = ( return user; }; +// TODO: This reducer is a combination of the Dashboard and Explore reducers. +// The correct way of handling this is to unify the actions and reducers from both +// modules in shared files. This involves a big refactor to unify the parameter types +// and move files around. We should tackle this in a specific PR. +const CombinedDatasourceReducers = ( + datasources: DatasourcesState | undefined | { [key: string]: Dataset }, + action: DatasourcesActionPayload | AnyDatasourcesAction | HydrateExplore, +) => { + if (action.type === DatasourcesAction.SET_DATASOURCES) { + return dashboardDatasources( + datasources as DatasourcesState | undefined, + action as DatasourcesActionPayload, + ); + } + return exploreDatasources( + datasources as { [key: string]: Dataset }, + action as AnyDatasourcesAction | HydrateExplore, + ); +}; + // exported for tests export const rootReducer = combineReducers({ messageToasts: messageToastReducer, common: noopReducer(bootstrap.common || {}), user: userReducer, impressionId: noopReducer(shortid.generate()), - ...dashboardReducers, + charts, + datasources: CombinedDatasourceReducers, + dashboardInfo, + dashboardFilters, + dataMask, + nativeFilters, + dashboardState, + dashboardLayout, + sliceEntities, + reports, + saveModal, + explore, }); export const store = createStore( diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index 6fff90105d3b1..450453a946c55 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -210,7 +210,6 @@ const config = { spa: addPreamble('/src/views/index.tsx'), embedded: addPreamble('/src/embedded/index.tsx'), addSlice: addPreamble('/src/addSlice/index.tsx'), - explore: addPreamble('/src/explore/index.jsx'), sqllab: addPreamble('/src/SqlLab/index.tsx'), profile: addPreamble('/src/profile/index.tsx'), showSavedQuery: [path.join(APP_DIR, '/src/showSavedQuery/index.jsx')], diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index 7809dab4ae9c5..db16ce063c66b 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -209,7 +209,7 @@ def url(self) -> str: def explore_url(self) -> str: if self.default_endpoint: return self.default_endpoint - return f"/superset/explore/{self.type}/{self.id}/" + return f"/explore/?dataset_type={self.type}&dataset_id={self.id}" @property def column_formats(self) -> Dict[str, Optional[str]]: diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 8242a677729d9..588d9b66bfd11 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -171,6 +171,7 @@ def init_views(self) -> None: ) from superset.views.datasource.views import Datasource from superset.views.dynamic_plugins import DynamicPluginsView + from superset.views.explore import ExplorePermalinkView, ExploreView from superset.views.key_value import KV from superset.views.log.api import LogRestApi from superset.views.log.views import LogModelView @@ -289,6 +290,8 @@ def init_views(self) -> None: appbuilder.add_view_no_menu(DashboardModelViewAsync) appbuilder.add_view_no_menu(Datasource) appbuilder.add_view_no_menu(EmbeddedView) + appbuilder.add_view_no_menu(ExploreView) + appbuilder.add_view_no_menu(ExplorePermalinkView) appbuilder.add_view_no_menu(KV) appbuilder.add_view_no_menu(R) appbuilder.add_view_no_menu(SavedQueryView) diff --git a/superset/models/slice.py b/superset/models/slice.py index 841539bc66573..de0f3df59684f 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -286,14 +286,14 @@ def get_query_context(self) -> Optional[QueryContext]: def get_explore_url( self, - base_url: str = "/superset/explore", + base_url: str = "/explore", overrides: Optional[Dict[str, Any]] = None, ) -> str: overrides = overrides or {} form_data = {"slice_id": self.id} form_data.update(overrides) params = parse.quote(json.dumps(form_data)) - return f"{base_url}/?form_data={params}" + return f"{base_url}/?slice_id={self.id}&form_data={params}" @property def slice_url(self) -> str: @@ -335,7 +335,8 @@ def icons(self) -> str: @property def url(self) -> str: - return f"/superset/explore/?form_data=%7B%22slice_id%22%3A%20{self.id}%7D" + form_data = f"%7B%22slice_id%22%3A%20{self.id}%7D" + return f"/explore/?slice_id={self.id}&form_data={form_data}" def get_query_context_factory(self) -> QueryContextFactory: if self.query_context_factory is None: diff --git a/superset/views/core.py b/superset/views/core.py index ffaa4204c4b3b..79517f347be4a 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -742,7 +742,6 @@ def import_dashboards(self) -> FlaskResponse: @event_logger.log_this @expose("/explore///", methods=["GET", "POST"]) @expose("/explore/", methods=["GET", "POST"]) - @expose("/explore/p//", methods=["GET"]) # pylint: disable=too-many-locals,too-many-branches,too-many-statements def explore( self, diff --git a/superset/views/explore.py b/superset/views/explore.py new file mode 100644 index 0000000000000..8becaaba70354 --- /dev/null +++ b/superset/views/explore.py @@ -0,0 +1,49 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from flask_appbuilder import permission_name +from flask_appbuilder.api import expose +from flask_appbuilder.security.decorators import has_access + +from superset import event_logger +from superset.superset_typing import FlaskResponse + +from .base import BaseSupersetView + + +class ExploreView(BaseSupersetView): + route_base = "/explore" + class_permission_name = "Explore" + + @expose("/") + @has_access + @permission_name("read") + @event_logger.log_this + def root(self) -> FlaskResponse: + return super().render_app_template() + + +class ExplorePermalinkView(BaseSupersetView): + route_base = "/superset" + class_permission_name = "Explore" + + @expose("/explore/p//") + @has_access + @permission_name("read") + @event_logger.log_this + # pylint: disable=unused-argument + def permalink(self, key: str) -> FlaskResponse: + return super().render_app_template() diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 58943246c545b..276231f1ff550 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -879,7 +879,7 @@ def test_slice_id_is_always_logged_correctly_on_web_request(self): self.login("admin") slc = db.session.query(Slice).filter_by(slice_name="Girls").one() qry = db.session.query(models.Log).filter_by(slice_id=slc.id) - self.get_resp(slc.slice_url, {"form_data": json.dumps(slc.form_data)}) + self.get_resp(slc.slice_url) self.assertEqual(1, qry.count()) def create_sample_csvfile(self, filename: str, content: List[str]) -> None: