feat: Renders Explore in SPA (#20572)

* feat: Renders Explore in SPA

* Adds permalink support

* Replaces navigation from Welcome page

* Fix initializing feature flags

* Remove redundant import

* Adds saveSlice workaround

* Fixes paths

* Fixes lint error

* Fixes tests

* Fix url to explore from Datasets view

* Fix explore page height

Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
Michael S. Molina 2022-07-05 16:58:09 -03:00 committed by GitHub
parent 1a806687fc
commit 662bab1e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 167 additions and 224 deletions

View File

@ -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', () => {

View File

@ -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);
});
},

View File

@ -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);
});

View File

@ -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}`;
}

View File

@ -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: [],

View File

@ -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: {

View File

@ -61,7 +61,7 @@ const regionFilter = {
},
modified: '<bound method AuditMixinNullable.modified of Region Filter>',
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: "<bound method AuditMixinNullable.modified of World's Population>",
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];

View File

@ -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 }) => (
<Provider store={store}>
<DndProvider backend={HTML5Backend}>
<ThemeProvider theme={theme}>
<GlobalStyles />
<DynamicPluginProvider>
<ExplorePage />
<ToastContainer />
</DynamicPluginProvider>
</ThemeProvider>
</DndProvider>
</Provider>
);
export default hot(App);

View File

@ -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 <ExploreViewContainer />;
};
export default ExplorePage;

View File

@ -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({

View File

@ -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: {

View File

@ -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(
<MemoryRouter initialEntries={[`${path}${search}`]}>

View File

@ -85,6 +85,7 @@ const ExploreContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
`;
const ExplorePanelContainer = styled.div`

View File

@ -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,
}),
);

View File

@ -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', () => {

View File

@ -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

View File

@ -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/');
});

View File

@ -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) {

View File

@ -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(<App store={store} />, document.getElementById('app'));

View File

@ -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 || {}),
});

View File

@ -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();

View File

@ -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}`;

View File

@ -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();

View File

@ -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({
<CardStyles
onClick={() => {
if (!bulkSelectEnabled && chart.url) {
window.location.href = chart.url;
history.push(chart.url);
}
}}
>
@ -158,6 +160,7 @@ export default function ChartCard({
coverRight={
<Label type="secondary">{chart.datasource_name_text}</Label>
}
linkComponent={Link}
actions={
<ListViewCard.Actions
onClick={e => {

View File

@ -189,7 +189,7 @@ describe('RTL', () => {
<QueryParamProvider>
<ChartList {...mockedProps} user={mockUser} />
</QueryParamProvider>,
{ useRedux: true },
{ useRedux: true, useRouter: true },
);
});

View File

@ -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) => (
<FlexRowContainer>
<a href={url} data-test={`${sliceName}-list-chart-title`}>
<Link to={url} data-test={`${sliceName}-list-chart-title`}>
{certifiedBy && (
<>
<CertifiedBadge
@ -280,7 +281,7 @@ function ChartList(props: ChartListProps) {
</>
)}
{sliceName}
</a>
</Link>
{description && (
<InfoTooltip tooltip={description} viewBox="0 -1 24 24" />
)}

View File

@ -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 (
<CardStyles
onClick={() => {
window.location.href = url;
}}
key={url}
>
<ListViewCard
cover={<></>}
url={url}
title={getEntityTitle(entity)}
description={lastActionOn}
avatar={getEntityIcon(entity)}
actions={null}
/>
<CardStyles key={url}>
<Link to={url}>
<ListViewCard
cover={<></>}
url={url}
title={getEntityTitle(entity)}
description={lastActionOn}
avatar={getEntityIcon(entity)}
actions={null}
/>
</Link>
</CardStyles>
);
},

View File

@ -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

View File

@ -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(

View File

@ -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')],

View File

@ -209,7 +209,7 @@ class BaseDatasource(
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]]:

View File

@ -171,6 +171,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
)
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 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
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)

View File

@ -286,14 +286,14 @@ class Slice( # pylint: disable=too-many-public-methods
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 @@ class Slice( # pylint: disable=too-many-public-methods
@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:

View File

@ -742,7 +742,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
@event_logger.log_this
@expose("/explore/<datasource_type>/<int:datasource_id>/", methods=["GET", "POST"])
@expose("/explore/", methods=["GET", "POST"])
@expose("/explore/p/<key>/", methods=["GET"])
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
def explore(
self,

49
superset/views/explore.py Normal file
View File

@ -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/<key>/")
@has_access
@permission_name("read")
@event_logger.log_this
# pylint: disable=unused-argument
def permalink(self, key: str) -> FlaskResponse:
return super().render_app_template()

View File

@ -879,7 +879,7 @@ class TestCore(SupersetTestCase):
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: