diff --git a/superset-frontend/spec/fixtures/mockStore.js b/superset-frontend/spec/fixtures/mockStore.js index 66434c5158a..9f8a065964d 100644 --- a/superset-frontend/spec/fixtures/mockStore.js +++ b/superset-frontend/spec/fixtures/mockStore.js @@ -19,7 +19,7 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import rootReducer from 'src/dashboard/reducers/index'; +import { rootReducer } from 'src/views/store'; import mockState from './mockState'; import { diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx index 2991e45319e..2b78aaf104a 100644 --- a/superset-frontend/src/components/ListViewCard/index.tsx +++ b/superset-frontend/src/components/ListViewCard/index.tsx @@ -97,13 +97,15 @@ const TitleContainer = styled.div` } `; -const TitleLink = styled.a` - color: ${({ theme }) => theme.colors.grayscale.dark1} !important; - overflow: hidden; - text-overflow: ellipsis; +const TitleLink = styled.span` + & a { + color: ${({ theme }) => theme.colors.grayscale.dark1} !important; + overflow: hidden; + text-overflow: ellipsis; - & + .title-right { - margin-left: ${({ theme }) => theme.gridUnit * 2}px; + & + .title-right { + margin-left: ${({ theme }) => theme.gridUnit * 2}px; + } } `; @@ -137,9 +139,19 @@ const SkeletonActions = styled(Skeleton.Button)` `; const paragraphConfig = { rows: 1, width: 150 }; + +interface LinkProps { + to: string; +} + +const AnchorLink: React.FC = ({ to, children }) => ( + {children} +); + interface CardProps { title?: React.ReactNode; url?: string; + linkComponent?: React.ComponentType; imgURL?: string; imgFallbackURL?: string; imgPosition?: BackgroundPosition; @@ -157,6 +169,7 @@ interface CardProps { function ListViewCard({ title, url, + linkComponent, titleRight, imgURL, imgFallbackURL, @@ -169,13 +182,14 @@ function ListViewCard({ imgPosition = 'top', cover, }: CardProps) { + const Link = url && linkComponent ? linkComponent : AnchorLink; return ( - +
-
+ {!loading && coverLeft && ( {coverLeft} @@ -225,7 +239,9 @@ function ListViewCard({ title={ - {title} + + {title} + {titleRight &&
{titleRight}
}
diff --git a/superset-frontend/src/components/Menu/Menu.tsx b/superset-frontend/src/components/Menu/Menu.tsx index 04a2cd086e1..0503f9f6020 100644 --- a/superset-frontend/src/components/Menu/Menu.tsx +++ b/superset-frontend/src/components/Menu/Menu.tsx @@ -17,11 +17,14 @@ * under the License. */ import React, { useState } from 'react'; -import { t, styled } from '@superset-ui/core'; -import { Nav, Navbar, NavItem } from 'react-bootstrap'; -import NavDropdown from 'src/components/NavDropdown'; -import { Menu as DropdownMenu } from 'src/common/components'; import { Link } from 'react-router-dom'; +import { Nav, Navbar, NavItem } from 'react-bootstrap'; +import { t, styled } from '@superset-ui/core'; + +import { Menu as DropdownMenu } from 'src/common/components'; +import NavDropdown from 'src/components/NavDropdown'; +import { getUrlParam } from 'src/utils/urlUtils'; + import MenuObject, { MenuObjectProps, MenuObjectChildProps, @@ -159,6 +162,10 @@ export function Menu({ }: MenuProps) { const [dropdownOpen, setDropdownOpen] = useState(false); + // would useQueryParam here but not all apps provide a router context + const standalone = getUrlParam('standalone', 'boolean'); + if (standalone) return <>; + return ( diff --git a/superset-frontend/src/dashboard/App.jsx b/superset-frontend/src/dashboard/App.jsx deleted file mode 100644 index 43d00f5a579..00000000000 --- a/superset-frontend/src/dashboard/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 { hot } from 'react-hot-loader/root'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from '@superset-ui/core'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; -import setupApp from '../setup/setupApp'; -import setupPlugins from '../setup/setupPlugins'; -import DashboardPage from './containers/DashboardPage'; -import { theme } from '../preamble'; - -setupApp(); -setupPlugins(); - -const App = ({ store }) => { - const dashboardIdOrSlug = window.location.pathname.split('/')[3]; - return ( - - - - - - - - - - ); -}; - -export default hot(App); diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx index 6e391e0b00c..580df62b3e0 100644 --- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx +++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx @@ -18,8 +18,8 @@ */ import React, { useEffect, useState, FC } from 'react'; import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; import Loading from 'src/components/Loading'; -import ErrorBoundary from 'src/components/ErrorBoundary'; import { useDashboard, useDashboardCharts, @@ -28,20 +28,24 @@ import { import { ResourceStatus } from 'src/common/hooks/apiResources/apiResources'; import { usePrevious } from 'src/common/hooks/usePrevious'; import { hydrateDashboard } from 'src/dashboard/actions/hydrate'; -import DashboardContainer from 'src/dashboard/containers/Dashboard'; +import injectCustomCss from 'src/dashboard/util/injectCustomCss'; -interface DashboardRouteProps { - dashboardIdOrSlug: string; -} +const DashboardContainer = React.lazy( + () => + import( + /* webpackChunkName: "DashboardContainer" */ + /* webpackPreload: true */ + 'src/dashboard/containers/Dashboard' + ), +); -const DashboardPage: FC = ({ - dashboardIdOrSlug, // eventually get from react router -}) => { +const DashboardPage: FC = () => { const dispatch = useDispatch(); + const { idOrSlug } = useParams<{ idOrSlug: string }>(); const [isLoaded, setLoaded] = useState(false); - const dashboardResource = useDashboard(dashboardIdOrSlug); - const chartsResource = useDashboardCharts(dashboardIdOrSlug); - const datasetsResource = useDashboardDatasets(dashboardIdOrSlug); + const dashboardResource = useDashboard(idOrSlug); + const chartsResource = useDashboardCharts(idOrSlug); + const datasetsResource = useDashboardDatasets(idOrSlug); const isLoading = [dashboardResource, chartsResource, datasetsResource].some( resource => resource.status === ResourceStatus.LOADING, ); @@ -49,6 +53,13 @@ const DashboardPage: FC = ({ const error = [dashboardResource, chartsResource, datasetsResource].find( resource => resource.status === ResourceStatus.ERROR, )?.error; + + useEffect(() => { + if (dashboardResource.result) { + document.title = dashboardResource.result.dashboard_title; + } + }, [dashboardResource.result]); + useEffect(() => { if ( wasLoading && @@ -63,6 +74,7 @@ const DashboardPage: FC = ({ datasetsResource.result, ), ); + injectCustomCss(dashboardResource.result.css); setLoaded(true); } }, [ @@ -79,12 +91,4 @@ const DashboardPage: FC = ({ return ; }; -const DashboardPageWithErrorBoundary = ({ - dashboardIdOrSlug, -}: DashboardRouteProps) => ( - - - -); - -export default DashboardPageWithErrorBoundary; +export default DashboardPage; diff --git a/superset-frontend/src/dashboard/index.jsx b/superset-frontend/src/dashboard/index.jsx deleted file mode 100644 index 1a287c072ec..00000000000 --- a/superset-frontend/src/dashboard/index.jsx +++ /dev/null @@ -1,45 +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 thunk from 'redux-thunk'; -import { createStore, applyMiddleware, compose } from 'redux'; -import { initFeatureFlags } from 'src/featureFlags'; -import { initEnhancer } from '../reduxUtils'; -import rootReducer from './reducers/index'; -import logger from '../middleware/loggerMiddleware'; -import App from './App'; - -const appContainer = document.getElementById('app'); -const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); -initFeatureFlags(bootstrapData.common.feature_flags); - -const initialState = { - user: bootstrapData.user, - common: bootstrapData.common, - datasources: bootstrapData.datasources, -}; - -const store = createStore( - rootReducer, - initialState, - compose(applyMiddleware(thunk, logger), initEnhancer(false)), -); - -ReactDOM.render(, document.getElementById('app')); diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js deleted file mode 100644 index 28804a72090..00000000000 --- a/superset-frontend/src/dashboard/reducers/index.js +++ /dev/null @@ -1,48 +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 charts from 'src/chart/chartReducer'; -import dataMask from 'src/dataMask/reducer'; -import dashboardInfo from './dashboardInfo'; -import dashboardState from './dashboardState'; -import dashboardFilters from './dashboardFilters'; -import nativeFilters from './nativeFilters'; -import datasources from './datasources'; -import sliceEntities from './sliceEntities'; -import dashboardLayout from './undoableDashboardLayout'; -import messageToasts from '../../messageToasts/reducers'; - -const impressionId = (state = '') => state; - -export default combineReducers({ - user: (state = null) => state, - common: (state = null) => state, - charts, - datasources, - dashboardInfo, - dashboardFilters, - dataMask, - nativeFilters, - dashboardState, - dashboardLayout, - impressionId, - messageToasts, - sliceEntities, -}); diff --git a/superset-frontend/src/utils/urlUtils.ts b/superset-frontend/src/utils/urlUtils.ts index 266555ca6d3..5e1ee9b55a3 100644 --- a/superset-frontend/src/utils/urlUtils.ts +++ b/superset-frontend/src/utils/urlUtils.ts @@ -40,7 +40,11 @@ export function getUrlParam(paramName: string, type: UrlParamType): unknown { return Number(urlParam); } return null; - // TODO: process other types when needed + case 'boolean': + if (!urlParam) { + return null; + } + return urlParam !== 'false' && urlParam !== '0'; default: return urlParam; } diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx index babc03f3d1f..e36dd803983 100644 --- a/superset-frontend/src/views/App.tsx +++ b/superset-frontend/src/views/App.tsx @@ -20,6 +20,8 @@ import React, { Suspense } from 'react'; import { hot } from 'react-hot-loader/root'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import { QueryParamProvider } from 'use-query-params'; import { initFeatureFlags } from 'src/featureFlags'; import { ThemeProvider } from '@superset-ui/core'; @@ -45,37 +47,43 @@ const menu = { ...bootstrap.common.menu_data }; const common = { ...bootstrap.common }; initFeatureFlags(bootstrap.common.feature_flags); -const App = () => ( - - - - +const RootContextProviders: React.FC = ({ children }) => ( + + + + - - - {routes.map( - ({ path, Component, props = {}, Fallback = Loading }) => ( - - }> - - - - - - ), - )} - - + {children} - - - - + + + + +); + +const App = () => ( + + + + + {routes.map(({ path, Component, props = {}, Fallback = Loading }) => ( + + }> + + + + + + ))} + + + + ); export default hot(App); diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx index cabce7167f8..93de8ead611 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { Link, useHistory } from 'react-router-dom'; import { t } from '@superset-ui/core'; import { handleDashboardDelete, @@ -64,6 +65,7 @@ function DashboardCard({ saveFavoriteStatus, showThumbnails, }: DashboardCardProps) { + const history = useHistory(); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); const canExport = hasPerm('can_read'); @@ -139,7 +141,7 @@ function DashboardCard({ { if (!bulkSelectEnabled) { - window.location.href = dashboard.url; + history.push(dashboard.url); } }} > @@ -155,6 +157,7 @@ function DashboardCard({ ) : null } url={bulkSelectEnabled ? undefined : dashboard.url} + linkComponent={Link} imgURL={dashboard.thumbnail_url} imgFallbackURL="/static/assets/images/dashboard-card-fallback.svg" description={t( diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.test.jsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.test.jsx index 8bb9a48b11d..230ccc5e48e 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.test.jsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.test.jsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; @@ -104,9 +105,11 @@ describe('DashboardList', () => { const mockedProps = {}; const wrapper = mount( - - - , + + + + + , ); beforeAll(async () => { @@ -182,9 +185,11 @@ describe('RTL', () => { const mounted = act(async () => { const mockedProps = {}; render( - - - , + + + + + , { useRedux: true }, ); }); diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index 840bfd6a476..65fb349d9b6 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -18,6 +18,7 @@ */ import { styled, SupersetClient, t } from '@superset-ui/core'; import React, { useState, useMemo } from 'react'; +import { Link } from 'react-router-dom'; import rison from 'rison'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { @@ -210,7 +211,7 @@ function DashboardList(props: DashboardListProps) { row: { original: { url, dashboard_title: dashboardTitle }, }, - }: any) => {dashboardTitle}, + }: any) => {dashboardTitle}, Header: t('Title'), accessor: 'dashboard_title', }, diff --git a/superset-frontend/src/views/routes.test.tsx b/superset-frontend/src/views/routes.test.tsx index 130e85fe9d4..e00c64f4af9 100644 --- a/superset-frontend/src/views/routes.test.tsx +++ b/superset-frontend/src/views/routes.test.tsx @@ -19,6 +19,11 @@ import { isFrontendRoute, routes } from './routes'; +jest.mock('src/featureFlags', () => ({ + ...jest.requireActual('src/featureFlags'), + isFeatureEnabled: jest.fn().mockReturnValue(true), +})); + describe('isFrontendRoute', () => { it('returns true if a route matches', () => { routes.forEach(r => { diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx index f7de0c2bc8b..ba92d46b61e 100644 --- a/superset-frontend/src/views/routes.tsx +++ b/superset-frontend/src/views/routes.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import React, { lazy } from 'react'; // not lazy loaded since this is the home page. @@ -57,6 +58,12 @@ const DashboardList = lazy( /* webpackChunkName: "DashboardList" */ 'src/views/CRUD/dashboard/DashboardList' ), ); +const DashboardPage = lazy( + () => + import( + /* webpackChunkName: "DashboardPage" */ 'src/dashboard/containers/DashboardPage' + ), +); const DatabaseList = lazy( () => import( @@ -104,6 +111,10 @@ export const routes: Routes = [ path: '/dashboard/list/', Component: DashboardList, }, + { + path: '/superset/dashboard/:idOrSlug/', + Component: DashboardPage, + }, { path: '/chart/list/', Component: ChartList, @@ -160,7 +171,7 @@ export const routes: Routes = [ }, ]; -export const frontEndRoutes = routes +const frontEndRoutes = routes .map(r => r.path) .reduce( (acc, curr) => ({ @@ -171,6 +182,7 @@ export const frontEndRoutes = routes ); export function isFrontendRoute(path?: string) { + if (!isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS)) return false; if (path) { const basePath = path.split(/[?#]/)[0]; // strip out query params and link bookmarks return !!frontEndRoutes[basePath]; diff --git a/superset-frontend/src/views/store.ts b/superset-frontend/src/views/store.ts index c36d7d6af4a..3b488f9beaa 100644 --- a/superset-frontend/src/views/store.ts +++ b/superset-frontend/src/views/store.ts @@ -20,16 +20,49 @@ import { applyMiddleware, combineReducers, compose, createStore } from 'redux'; import thunk from 'redux-thunk'; import messageToastReducer from 'src/messageToasts/reducers'; import { initEnhancer } from 'src/reduxUtils'; +import charts from 'src/chart/chartReducer'; +import dataMask from 'src/dataMask/reducer'; +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 sliceEntities from 'src/dashboard/reducers/sliceEntities'; +import dashboardLayout from 'src/dashboard/reducers/undoableDashboardLayout'; + +// 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. +const noopReducer = (initialState: STATE) => ( + state: STATE = initialState, +) => state; const container = document.getElementById('app'); const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}'); -const common = { ...bootstrap.common }; + +// reducers used only in the dashboard page +const dashboardReducers = { + charts, + datasources, + dashboardInfo, + dashboardFilters, + dataMask, + nativeFilters, + dashboardState, + dashboardLayout, + sliceEntities, +}; + +// exported for tests +export const rootReducer = combineReducers({ + messageToasts: messageToastReducer, + common: noopReducer(bootstrap.common || {}), + user: noopReducer(bootstrap.user || {}), + impressionId: noopReducer(''), + ...dashboardReducers, +}); export const store = createStore( - combineReducers({ - messageToasts: messageToastReducer, - common: () => common, - }), + rootReducer, {}, compose(applyMiddleware(thunk), initEnhancer(false)), ); diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index a044ab2cd3b..d4cc8787950 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -202,14 +202,13 @@ const config = { fs: 'empty', }, entry: { - theme: path.join(APP_DIR, '/src/theme.ts'), preamble: PREAMBLE, + theme: path.join(APP_DIR, '/src/theme.ts'), + menu: addPreamble('src/views/menu.tsx'), + spa: addPreamble('/src/views/index.tsx'), addSlice: addPreamble('/src/addSlice/index.tsx'), explore: addPreamble('/src/explore/index.jsx'), - dashboard: addPreamble('/src/dashboard/index.jsx'), sqllab: addPreamble('/src/SqlLab/index.tsx'), - crudViews: addPreamble('/src/views/index.tsx'), - menu: addPreamble('src/views/menu.tsx'), profile: addPreamble('/src/profile/index.tsx'), showSavedQuery: [path.join(APP_DIR, '/src/showSavedQuery/index.jsx')], }, diff --git a/superset/templates/superset/dashboard.html b/superset/templates/superset/dashboard.html deleted file mode 100644 index ed481a2a149..00000000000 --- a/superset/templates/superset/dashboard.html +++ /dev/null @@ -1,32 +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. -#} -{% extends "superset/basic.html" %} - -{% block head_css %} - {{ super() }} - {% if custom_css %} - - {% endif %} -{% endblock %} - -{% block body %} -
-{% endblock %} diff --git a/superset/templates/superset/dashboard_v1_deprecated.html b/superset/templates/superset/dashboard_v1_deprecated.html deleted file mode 100644 index 1b8c0e2fba6..00000000000 --- a/superset/templates/superset/dashboard_v1_deprecated.html +++ /dev/null @@ -1,28 +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. -#} -{% extends "superset/basic.html" %} - -{% block body %} -
-
-{% endblock %} diff --git a/superset/templates/superset/crud_views.html b/superset/templates/superset/spa.html similarity index 96% rename from superset/templates/superset/crud_views.html rename to superset/templates/superset/spa.html index 862b791a5eb..1e38cb2e75b 100644 --- a/superset/templates/superset/crud_views.html +++ b/superset/templates/superset/spa.html @@ -22,6 +22,6 @@ {% endblock %} {% block tail_js %} - {{ js_bundle("crudViews") }} + {{ js_bundle("spa") }} {% include "tail_js_custom_extra.html" %} {% endblock %} diff --git a/superset/views/base.py b/superset/views/base.py index 72736f876b8..394190e0069 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -272,12 +272,12 @@ class BaseSupersetView(BaseView): def render_app_template(self) -> FlaskResponse: payload = { - "user": bootstrap_user_data(g.user), + "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), } return self.render_template( - "superset/crud_views.html", - entry="crudViews", + "superset/spa.html", + entry="spa", bootstrap_data=json.dumps( payload, default=utils.pessimistic_json_iso_dttm_ser ), @@ -434,12 +434,12 @@ class SupersetModelView(ModelView): def render_app_template(self) -> FlaskResponse: payload = { - "user": bootstrap_user_data(g.user), + "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), } return self.render_template( - "superset/crud_views.html", - entry="crudViews", + "superset/spa.html", + entry="spa", bootstrap_data=json.dumps( payload, default=utils.pessimistic_json_iso_dttm_ser ), diff --git a/superset/views/core.py b/superset/views/core.py index 8cbf3dfd7dd..e9cdff19ef9 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1849,7 +1849,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods dash_edit_perm = check_ownership( dashboard, raise_if_false=False ) and security_manager.can_access("can_save_dash", "Superset") - standalone_mode = ReservedUrlParameters.is_standalone_mode() edit_mode = ( request.args.get(utils.ReservedUrlParameters.EDIT_MODE.value) == "true" ) @@ -1867,11 +1866,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods } return self.render_template( - "superset/dashboard.html", - entry="dashboard", - standalone_mode=standalone_mode, - title=dashboard.dashboard_title, - custom_css=dashboard.css, + "superset/spa.html", + entry="spa", bootstrap_data=json.dumps( bootstrap_data, default=utils.pessimistic_json_iso_dttm_ser ), @@ -2798,13 +2794,13 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods return self.dashboard(dashboard_id_or_slug=str(welcome_dashboard_id)) payload = { - "user": bootstrap_user_data(g.user), + "user": bootstrap_user_data(g.user, include_perms=True), "common": common_bootstrap_payload(), } return self.render_template( - "superset/crud_views.html", - entry="crudViews", + "superset/spa.html", + entry="spa", bootstrap_data=json.dumps( payload, default=utils.pessimistic_json_iso_dttm_ser ), diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index f88d3a4aa30..b15e4b1306f 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -413,7 +413,8 @@ class TestDashboard(SupersetTestCase): db.session.merge(dash) db.session.commit() - assert "Births" in self.get_resp("/superset/dashboard/births/") + # this asserts a non-4xx response + self.get_resp("/superset/dashboard/births/") # Cleanup self.revoke_public_access_to_table(table) diff --git a/tests/dashboards/security/base_case.py b/tests/dashboards/security/base_case.py index ab24734ce7d..5c8e82151dd 100644 --- a/tests/dashboards/security/base_case.py +++ b/tests/dashboards/security/base_case.py @@ -26,14 +26,6 @@ class BaseTestDashboardSecurity(DashboardTestCase): def tearDown(self) -> None: self.clean_created_objects() - def assert_dashboard_view_response( - self, response: Response, dashboard_to_access: Dashboard - ) -> None: - self.assert200(response) - assert escape(dashboard_to_access.dashboard_title) in response.data.decode( - "utf-8" - ) - def assert_dashboard_api_response( self, response: Response, dashboard_to_access: Dashboard ) -> None: diff --git a/tests/dashboards/security/security_dataset_tests.py b/tests/dashboards/security/security_dataset_tests.py index c7233ebd868..b22e1fee143 100644 --- a/tests/dashboards/security/security_dataset_tests.py +++ b/tests/dashboards/security/security_dataset_tests.py @@ -76,15 +76,12 @@ class TestDashboardDatasetSecurity(DashboardTestCase): # act responses_by_url = { - url: self.client.get(url).data.decode("utf-8") - for url in dashboard_title_by_url.keys() + url: self.client.get(url) for url in dashboard_title_by_url.keys() } # assert for dashboard_url, get_dashboard_response in responses_by_url.items(): - assert ( - escape(dashboard_title_by_url[dashboard_url]) in get_dashboard_response - ) + self.assert200(get_dashboard_response) def test_get_dashboards__users_are_dashboards_owners(self): # arrange diff --git a/tests/dashboards/security/security_rbac_tests.py b/tests/dashboards/security/security_rbac_tests.py index 42cab6c7d99..bb8666845f2 100644 --- a/tests/dashboards/security/security_rbac_tests.py +++ b/tests/dashboards/security/security_rbac_tests.py @@ -49,7 +49,7 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity): response = self.get_dashboard_view_response(dashboard_to_access) # assert - self.assert_dashboard_view_response(response, dashboard_to_access) + self.assert200(response) def test_get_dashboard_view__owner_can_access(self): # arrange @@ -67,7 +67,7 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity): response = self.get_dashboard_view_response(dashboard_to_access) # assert - self.assert_dashboard_view_response(response, dashboard_to_access) + self.assert200(response) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_get_dashboard_view__user_can_not_access_without_permission(self): @@ -133,7 +133,7 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity): response = self.get_dashboard_view_response(dashboard_to_access) # assert - self.assert_dashboard_view_response(response, dashboard_to_access) + self.assert200(response) request_payload = get_query_context("birth_names") rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data") @@ -184,7 +184,7 @@ class TestDashboardRoleBasedSecurity(BaseTestDashboardSecurity): response = self.get_dashboard_view_response(dashboard_to_access) # assert - self.assert_dashboard_view_response(response, dashboard_to_access) + self.assert200(response) # post revoke_access_to_dashboard(dashboard_to_access, "Public")