mirror of https://github.com/apache/superset.git
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:
parent
158ac302d8
commit
21cf12a480
|
@ -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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 },
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
|
|
|
@ -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')],
|
||||
},
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -22,6 +22,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ js_bundle("crudViews") }}
|
||||
{{ js_bundle("spa") }}
|
||||
{% include "tail_js_custom_extra.html" %}
|
||||
{% endblock %}
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue