chore(dashboard): Integrate dashboard app into the SPA bundle (#14356)

* chore(dashboard): Integrate dashboard app into the SPA bundle

* fix url params

* change variable name

* change title correctly

* custom css

* lint

* remove unused file

* remove content assertions from dashboard tests

* fix case with missing bootstrap data

* fix: respect crud views flag

* crud views -> spa

* remove unused dashboard templates

* fix: remove unused variable

* fix: missed a spot with the crudViews -> spa

* router link to dashboard from dashboard list page

* link using the router when in card mode

* lint

* fix tests, add memory router

* remove  dashboard app files

* split up the bundle a little more

* use webpack preload
This commit is contained in:
David Aaron Suddjian 2021-05-04 08:51:17 -07:00 committed by GitHub
parent 158ac302d8
commit 21cf12a480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 194 additions and 316 deletions

View File

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

View File

@ -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<LinkProps> = ({ to, children }) => (
<a href={to}>{children}</a>
);
interface CardProps {
title?: React.ReactNode;
url?: string;
linkComponent?: React.ComponentType<LinkProps>;
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 (
<StyledCard
data-test="styled-card"
cover={
cover || (
<Cover>
<a href={url}>
<Link to={url!}>
<div className="gradient-container">
<ImageLoader
src={imgURL || ''}
@ -184,7 +198,7 @@ function ListViewCard({
position={imgPosition}
/>
</div>
</a>
</Link>
<CoverFooter className="cover-footer">
{!loading && coverLeft && (
<CoverFooterLeft>{coverLeft}</CoverFooterLeft>
@ -225,7 +239,9 @@ function ListViewCard({
title={
<TitleContainer>
<Tooltip title={title}>
<TitleLink href={url}>{title}</TitleLink>
<TitleLink>
<Link to={url!}>{title}</Link>
</TitleLink>
</Tooltip>
{titleRight && <div className="title-right"> {titleRight}</div>}
<div className="card-actions" data-test="card-actions">

View File

@ -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 (
<StyledHeader className="top" id="main-menu">
<Navbar inverse fluid staticTop role="navigation">

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

View File

@ -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<DashboardRouteProps> = ({
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<DashboardRouteProps> = ({
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<DashboardRouteProps> = ({
datasetsResource.result,
),
);
injectCustomCss(dashboardResource.result.css);
setLoaded(true);
}
}, [
@ -79,12 +91,4 @@ const DashboardPage: FC<DashboardRouteProps> = ({
return <DashboardContainer />;
};
const DashboardPageWithErrorBoundary = ({
dashboardIdOrSlug,
}: DashboardRouteProps) => (
<ErrorBoundary>
<DashboardPage dashboardIdOrSlug={dashboardIdOrSlug} />
</ErrorBoundary>
);
export default DashboardPageWithErrorBoundary;
export default DashboardPage;

View File

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

View File

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

View File

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

View File

@ -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 = () => (
<ReduxProvider store={store}>
<ThemeProvider theme={theme}>
<FlashProvider messages={common.flash_messages}>
<Router>
const RootContextProviders: React.FC = ({ children }) => (
<ThemeProvider theme={theme}>
<ReduxProvider store={store}>
<DndProvider backend={HTML5Backend}>
<FlashProvider messages={common.flash_messages}>
<DynamicPluginProvider>
<QueryParamProvider
ReactRouterRoute={Route}
stringifyOptions={{ encode: false }}
>
<Menu data={menu} isFrontendRoute={isFrontendRoute} />
<Switch>
{routes.map(
({ path, Component, props = {}, Fallback = Loading }) => (
<Route path={path} key={path}>
<Suspense fallback={<Fallback />}>
<ErrorBoundary>
<Component user={user} {...props} />
</ErrorBoundary>
</Suspense>
</Route>
),
)}
</Switch>
<ToastPresenter />
{children}
</QueryParamProvider>
</DynamicPluginProvider>
</Router>
</FlashProvider>
</ThemeProvider>
</ReduxProvider>
</FlashProvider>
</DndProvider>
</ReduxProvider>
</ThemeProvider>
);
const App = () => (
<Router>
<RootContextProviders>
<Menu data={menu} isFrontendRoute={isFrontendRoute} />
<Switch>
{routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
<Route path={path} key={path}>
<Suspense fallback={<Fallback />}>
<ErrorBoundary>
<Component user={user} {...props} />
</ErrorBoundary>
</Suspense>
</Route>
))}
</Switch>
<ToastPresenter />
</RootContextProviders>
</Router>
);
export default hot(App);

View File

@ -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({
<CardStyles
onClick={() => {
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(

View File

@ -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(
<Provider store={store}>
<DashboardList {...mockedProps} user={mockUser} />
</Provider>,
<MemoryRouter>
<Provider store={store}>
<DashboardList {...mockedProps} user={mockUser} />
</Provider>
</MemoryRouter>,
);
beforeAll(async () => {
@ -182,9 +185,11 @@ describe('RTL', () => {
const mounted = act(async () => {
const mockedProps = {};
render(
<QueryParamProvider>
<DashboardList {...mockedProps} user={mockUser} />
</QueryParamProvider>,
<MemoryRouter>
<QueryParamProvider>
<DashboardList {...mockedProps} user={mockUser} />
</QueryParamProvider>
</MemoryRouter>,
{ useRedux: true },
);
});

View File

@ -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) => <a href={url}>{dashboardTitle}</a>,
}: any) => <Link to={url}>{dashboardTitle}</Link>,
Header: t('Title'),
accessor: 'dashboard_title',
},

View File

@ -19,6 +19,11 @@
import { isFrontendRoute, routes } from './routes';
jest.mock('src/featureFlags', () => ({
...jest.requireActual<object>('src/featureFlags'),
isFeatureEnabled: jest.fn().mockReturnValue(true),
}));
describe('isFrontendRoute', () => {
it('returns true if a route matches', () => {
routes.forEach(r => {

View File

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

View File

@ -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 = <STATE = unknown>(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)),
);

View File

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

View File

@ -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 %}
<style class="CssEditor-css" type="text/css">
{{ custom_css }}
</style>
{% endif %}
{% endblock %}
{% block body %}
<div id="app" data-bootstrap="{{ bootstrap_data }}" />
{% endblock %}

View File

@ -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 %}
<div
id="app"
class="dashboard container-fluid"
data-bootstrap="{{ bootstrap_data }}"
>
</div>
{% endblock %}

View File

@ -22,6 +22,6 @@
{% endblock %}
{% block tail_js %}
{{ js_bundle("crudViews") }}
{{ js_bundle("spa") }}
{% include "tail_js_custom_extra.html" %}
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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