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:
parent
1a806687fc
commit
662bab1e6d
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}`]}>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const ExploreContainer = styled.div`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const ExplorePanelContainer = styled.div`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
@ -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 || {}),
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ describe('RTL', () => {
|
|||
<QueryParamProvider>
|
||||
<ChartList {...mockedProps} user={mockUser} />
|
||||
</QueryParamProvider>,
|
||||
{ useRedux: true },
|
||||
{ useRedux: true, useRouter: true },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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')],
|
||||
|
|
|
|||
|
|
@ -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]]:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue