fix(viz-gallery): respect denylist in viz gallery (#22658)

This commit is contained in:
Ville Brofeldt 2023-01-10 19:08:30 +02:00 committed by GitHub
parent 1e3746be21
commit 08f45ef207
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 111 additions and 123 deletions

View File

@ -31,13 +31,13 @@ import explore from 'src/explore/reducers/exploreReducer';
import sqlLab from 'src/SqlLab/reducers/sqlLab'; import sqlLab from 'src/SqlLab/reducers/sqlLab';
import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage'; import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
import reports from 'src/reports/reducers/reports'; import reports from 'src/reports/reducers/reports';
import getBootstrapData from 'src/utils/getBootstrapData';
const impressionId = (state = '') => state; const impressionId = (state = '') => state;
const container = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}'); const common = { ...bootstrapData.common };
const common = { ...bootstrap.common }; const user = { ...bootstrapData.user };
const user = { ...bootstrap.user };
const noopReducer = const noopReducer =
(initialState: unknown) => (initialState: unknown) =>

View File

@ -25,5 +25,5 @@ configureTestingLibrary({
testIdAttribute: 'data-test', testIdAttribute: 'data-test',
}); });
document.body.innerHTML = '<div id="app" data-bootstrap="{}"></div>'; document.body.innerHTML = '<div id="app" data-bootstrap=""></div>';
expect.extend(matchers); expect.extend(matchers);

View File

@ -30,6 +30,7 @@ import {
FeatureFlag, FeatureFlag,
} from 'src/featureFlags'; } from 'src/featureFlags';
import setupExtensions from 'src/setup/setupExtensions'; import setupExtensions from 'src/setup/setupExtensions';
import getBootstrapData from 'src/utils/getBootstrapData';
import getInitialState from './reducers/getInitialState'; import getInitialState from './reducers/getInitialState';
import rootReducer from './reducers/index'; import rootReducer from './reducers/index';
import { initEnhancer } from '../reduxUtils'; import { initEnhancer } from '../reduxUtils';
@ -48,8 +49,7 @@ import { theme } from '../preamble';
setupApp(); setupApp();
setupExtensions(); setupExtensions();
const appContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap'));
initFeatureFlags(bootstrapData.common.feature_flags); initFeatureFlags(bootstrapData.common.feature_flags);

View File

@ -25,11 +25,9 @@ import * as chrono from 'chrono-node';
import ModalTrigger, { ModalTriggerRef } from 'src/components/ModalTrigger'; import ModalTrigger, { ModalTriggerRef } from 'src/components/ModalTrigger';
import { Form, FormItem } from 'src/components/Form'; import { Form, FormItem } from 'src/components/Form';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import getBootstrapData from 'src/utils/getBootstrapData';
const appContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapData = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES; const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
const validators = { const validators = {

View File

@ -74,6 +74,7 @@ import {
} from 'src/utils/localStorageHelpers'; } from 'src/utils/localStorageHelpers';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { EmptyStateBig } from 'src/components/EmptyState'; import { EmptyStateBig } from 'src/components/EmptyState';
import getBootstrapData from 'src/utils/getBootstrapData';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import TemplateParamsEditor from '../TemplateParamsEditor'; import TemplateParamsEditor from '../TemplateParamsEditor';
import SouthPane from '../SouthPane'; import SouthPane from '../SouthPane';
@ -86,10 +87,7 @@ import AceEditorWrapper from '../AceEditorWrapper';
import RunQueryActionButton from '../RunQueryActionButton'; import RunQueryActionButton from '../RunQueryActionButton';
import QueryLimitSelect from '../QueryLimitSelect'; import QueryLimitSelect from '../QueryLimitSelect';
const appContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapData = JSON.parse(
appContainer.getAttribute('data-bootstrap') || '{}',
);
const validatorMap = const validatorMap =
bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {}; bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES; const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;

View File

@ -28,6 +28,7 @@ import { Tooltip } from 'src/components/Tooltip';
import { detectOS } from 'src/utils/common'; import { detectOS } from 'src/utils/common';
import * as Actions from 'src/SqlLab/actions/sqlLab'; import * as Actions from 'src/SqlLab/actions/sqlLab';
import { EmptyStateBig } from 'src/components/EmptyState'; import { EmptyStateBig } from 'src/components/EmptyState';
import getBootstrapData from 'src/utils/getBootstrapData';
import SqlEditor from '../SqlEditor'; import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader'; import SqlEditorTabHeader from '../SqlEditorTabHeader';
@ -106,13 +107,10 @@ class TabbedSqlEditors extends React.PureComponent {
} }
// merge post form data with GET search params // merge post form data with GET search params
// Hack: this data should be comming from getInitialState // Hack: this data should be coming from getInitialState
// but for some reason this data isn't being passed properly through // but for some reason this data isn't being passed properly through
// the reducer. // the reducer.
const appContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapData = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);
const query = { const query = {
...bootstrapData.requested_query, ...bootstrapData.requested_query,
...URI(window.location).search(true), ...URI(window.location).search(true),

View File

@ -84,7 +84,7 @@ async function getWrapper(user = mockUser) {
return wrapper; return wrapper;
} }
test('renders a select and a VizTypeControl', async () => { test('renders a select and a VizTypeGallery', async () => {
const wrapper = await getWrapper(); const wrapper = await getWrapper();
expect(wrapper.find(AsyncSelect)).toExist(); expect(wrapper.find(AsyncSelect)).toExist();
expect(wrapper.find(VizTypeGallery)).toExist(); expect(wrapper.find(VizTypeGallery)).toExist();

View File

@ -39,6 +39,7 @@ import VizTypeGallery, {
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery'; } from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
import { findPermission } from 'src/utils/findPermission'; import { findPermission } from 'src/utils/findPermission';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import getBootstrapData from 'src/utils/getBootstrapData';
type Dataset = { type Dataset = {
id: number; id: number;
@ -62,6 +63,9 @@ export type AddSliceContainerState = {
const ESTIMATED_NAV_HEIGHT = 56; const ESTIMATED_NAV_HEIGHT = 56;
const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250; const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250;
const bootstrapData = getBootstrapData();
const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || [];
const StyledContainer = styled.div` const StyledContainer = styled.div`
${({ theme }) => ` ${({ theme }) => `
flex: 1 1 auto; flex: 1 1 auto;
@ -395,6 +399,7 @@ export class AddSliceContainer extends React.PureComponent<
description={ description={
<StyledStepDescription> <StyledStepDescription>
<VizTypeGallery <VizTypeGallery
denyList={denyList}
className="viz-gallery" className="viz-gallery"
onChange={this.changeVizType} onChange={this.changeVizType}
onDoubleClick={this.onVizTypeDoubleClick} onDoubleClick={this.onVizTypeDoubleClick}

View File

@ -22,6 +22,7 @@ import { isFeatureEnabled, t, FeatureFlag } from '@superset-ui/core';
import { PluginContext } from 'src/components/DynamicPlugins'; import { PluginContext } from 'src/components/DynamicPlugins';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import getBootstrapData from 'src/utils/getBootstrapData';
import getChartIdsFromLayout from '../util/getChartIdsFromLayout'; import getChartIdsFromLayout from '../util/getChartIdsFromLayout';
import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId'; import getLayoutComponentFromChartId from '../util/getLayoutComponentFromChartId';
import DashboardBuilder from './DashboardBuilder/DashboardBuilder'; import DashboardBuilder from './DashboardBuilder/DashboardBuilder';
@ -97,8 +98,7 @@ class Dashboard extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
const appContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapData = appContainer?.getAttribute('data-bootstrap') || '{}';
const { dashboardState, layout } = this.props; const { dashboardState, layout } = this.props;
const eventData = { const eventData = {
is_soft_navigation: Logger.timeOriginOffset > 0, is_soft_navigation: Logger.timeOriginOffset > 0,

View File

@ -16,10 +16,12 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import getBootstrapData from 'src/utils/getBootstrapData';
import { store } from '../views/store'; import { store } from '../views/store';
import { bootstrapData } from '../preamble';
import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils'; import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils';
const bootstrapData = getBootstrapData();
type Size = { type Size = {
width: number; width: number;
height: number; height: number;

View File

@ -21,7 +21,7 @@ import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom';
import { makeApi, t, logging } from '@superset-ui/core'; import { makeApi, t, logging } from '@superset-ui/core';
import Switchboard from '@superset-ui/switchboard'; import Switchboard from '@superset-ui/switchboard';
import { bootstrapData } from 'src/preamble'; import getBootstrapData from 'src/utils/getBootstrapData';
import setupClient from 'src/setup/setupClient'; import setupClient from 'src/setup/setupClient';
import { RootContextProviders } from 'src/views/RootContextProviders'; import { RootContextProviders } from 'src/views/RootContextProviders';
import { store, USER_LOADED } from 'src/views/store'; import { store, USER_LOADED } from 'src/views/store';
@ -33,6 +33,7 @@ import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { embeddedApi } from './api'; import { embeddedApi } from './api';
const debugMode = process.env.WEBPACK_MODE === 'development'; const debugMode = process.env.WEBPACK_MODE === 'development';
const bootstrapData = getBootstrapData();
function log(...info: unknown[]) { function log(...info: unknown[]) {
if (debugMode) { if (debugMode) {

View File

@ -40,7 +40,7 @@ import {
} from '@superset-ui/plugin-chart-echarts'; } from '@superset-ui/plugin-chart-echarts';
import TableChartPlugin from '@superset-ui/plugin-chart-table'; import TableChartPlugin from '@superset-ui/plugin-chart-table';
import { LineChartPlugin } from '@superset-ui/preset-chart-xy'; import { LineChartPlugin } from '@superset-ui/preset-chart-xy';
import TimeTableChartPlugin from '../../../../visualizations/TimeTable'; import TimeTableChartPlugin from 'src/visualizations/TimeTable';
import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index'; import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index';
jest.useFakeTimers(); jest.useFakeTimers();

View File

@ -50,6 +50,7 @@ interface VizTypeGalleryProps {
onDoubleClick: () => void; onDoubleClick: () => void;
selectedViz: string | null; selectedViz: string | null;
className?: string; className?: string;
denyList: string[];
} }
type VizEntry = { type VizEntry = {
@ -506,6 +507,7 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) {
const chartMetadata: VizEntry[] = useMemo(() => { const chartMetadata: VizEntry[] = useMemo(() => {
const result = Object.entries(mountedPluginMetadata) const result = Object.entries(mountedPluginMetadata)
.map(([key, value]) => ({ key, value })) .map(([key, value]) => ({ key, value }))
.filter(({ key }) => !props.denyList.includes(key))
.filter( .filter(
({ value }) => ({ value }) =>
nativeFilterGate(value.behaviors || []) && !value.deprecated, nativeFilterGate(value.behaviors || []) && !value.deprecated,

View File

@ -27,6 +27,7 @@ import {
import { usePluginContext } from 'src/components/DynamicPlugins'; import { usePluginContext } from 'src/components/DynamicPlugins';
import Modal from 'src/components/Modal'; import Modal from 'src/components/Modal';
import { noOp } from 'src/utils/common'; import { noOp } from 'src/utils/common';
import getBootstrapData from 'src/utils/getBootstrapData';
import VizTypeGallery, { import VizTypeGallery, {
MAX_ADVISABLE_VIZ_GALLERY_WIDTH, MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
} from './VizTypeGallery'; } from './VizTypeGallery';
@ -41,6 +42,8 @@ interface VizTypeControlProps {
isModalOpenInit?: boolean; isModalOpenInit?: boolean;
} }
const bootstrapData = getBootstrapData();
const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || [];
const metadataRegistry = getChartMetadataRegistry(); const metadataRegistry = getChartMetadataRegistry();
export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control';
@ -141,6 +144,7 @@ const VizTypeControl = ({
selectedViz={selectedViz} selectedViz={selectedViz}
onChange={setSelectedViz} onChange={setSelectedViz}
onDoubleClick={onSubmit} onDoubleClick={onSubmit}
denyList={denyList}
/> />
</UnpaddedModal> </UnpaddedModal>
</> </>

View File

@ -23,6 +23,7 @@ import {
logging, logging,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { SupersetError } from 'src/components/ErrorMessage/types'; import { SupersetError } from 'src/components/ErrorMessage/types';
import getBootstrapData from 'src/utils/getBootstrapData';
import { FeatureFlag, isFeatureEnabled } from '../featureFlags'; import { FeatureFlag, isFeatureEnabled } from '../featureFlags';
import { import {
getClientErrorObject, getClientErrorObject,
@ -235,21 +236,7 @@ export const init = (appConfig?: AppConfig) => {
retriesByJobId = {}; retriesByJobId = {};
lastReceivedEventId = null; lastReceivedEventId = null;
if (appConfig) { config = appConfig || getBootstrapData().config;
config = appConfig;
} else {
// load bootstrap data from DOM
const appContainer = document.getElementById('app');
if (appContainer) {
const bootstrapData = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);
config = bootstrapData?.common?.conf;
} else {
config = {};
logging.warn('asyncEvent: app config data not found');
}
}
transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING;
pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500;

View File

@ -26,47 +26,34 @@ import setupClient from './setup/setupClient';
import setupColors from './setup/setupColors'; import setupColors from './setup/setupColors';
import setupFormatters from './setup/setupFormatters'; import setupFormatters from './setup/setupFormatters';
import setupDashboardComponents from './setup/setupDasboardComponents'; import setupDashboardComponents from './setup/setupDasboardComponents';
import { BootstrapData, User } from './types/bootstrapTypes'; import { User } from './types/bootstrapTypes';
import { initFeatureFlags } from './featureFlags'; import { initFeatureFlags } from './featureFlags';
import { DEFAULT_COMMON_BOOTSTRAP_DATA } from './constants'; import getBootstrapData from './utils/getBootstrapData';
if (process.env.WEBPACK_MODE === 'development') { if (process.env.WEBPACK_MODE === 'development') {
setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false }); setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
} }
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports
export let bootstrapData: BootstrapData = { const bootstrapData = getBootstrapData();
common: {
...DEFAULT_COMMON_BOOTSTRAP_DATA,
},
};
// Configure translation // Configure translation
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const root = document.getElementById('app'); configure({ languagePack: bootstrapData.common.language_pack });
bootstrapData = root moment.locale(bootstrapData.common.locale);
? JSON.parse(root.getAttribute('data-bootstrap') || '{}')
: {};
if (bootstrapData?.common?.language_pack) {
const languagePack = bootstrapData.common.language_pack;
configure({ languagePack });
moment.locale(bootstrapData.common.locale);
} else {
configure();
}
} else { } else {
configure(); configure();
} }
// Configure feature flags // Configure feature flags
initFeatureFlags(bootstrapData?.common?.feature_flags); initFeatureFlags(bootstrapData.common.feature_flags);
// Setup SupersetClient // Setup SupersetClient
setupClient(); setupClient();
setupColors( setupColors(
bootstrapData?.common?.extra_categorical_color_schemes, bootstrapData.common.extra_categorical_color_schemes,
bootstrapData?.common?.extra_sequential_color_schemes, bootstrapData.common.extra_sequential_color_schemes,
); );
// Setup number formatters // Setup number formatters
@ -76,7 +63,7 @@ setupDashboardComponents();
export const theme = merge( export const theme = merge(
supersetTheme, supersetTheme,
bootstrapData?.common?.theme_overrides ?? {}, bootstrapData.common.theme_overrides ?? {},
); );
const getMe = makeApi<void, User>({ const getMe = makeApi<void, User>({

View File

@ -30,14 +30,12 @@ import setupApp from 'src/setup/setupApp';
import setupExtensions from 'src/setup/setupExtensions'; import setupExtensions from 'src/setup/setupExtensions';
import { theme } from 'src/preamble'; import { theme } from 'src/preamble';
import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import getBootstrapData from 'src/utils/getBootstrapData';
setupApp(); setupApp();
setupExtensions(); setupExtensions();
const profileViewContainer = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrap = JSON.parse(
profileViewContainer?.getAttribute('data-bootstrap') ?? '{}',
);
const store = createStore( const store = createStore(
combineReducers({ combineReducers({
@ -51,7 +49,7 @@ const Application = () => (
<Provider store={store}> <Provider store={store}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalStyles /> <GlobalStyles />
<App user={bootstrap.user} /> <App user={bootstrapData.user} />
<ToastContainer /> <ToastContainer />
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>

View File

@ -20,7 +20,7 @@ import React from 'react';
import { t, styled } from '@superset-ui/core'; import { t, styled } from '@superset-ui/core';
import { Row, Col } from 'src/components'; import { Row, Col } from 'src/components';
import Tabs from 'src/components/Tabs'; import Tabs from 'src/components/Tabs';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { BootstrapUser } from 'src/types/bootstrapTypes';
import Favorites from './Favorites'; import Favorites from './Favorites';
import UserInfo from './UserInfo'; import UserInfo from './UserInfo';
import Security from './Security'; import Security from './Security';
@ -28,7 +28,7 @@ import RecentActivity from './RecentActivity';
import CreatedContent from './CreatedContent'; import CreatedContent from './CreatedContent';
interface AppProps { interface AppProps {
user: UserWithPermissionsAndRoles; user: BootstrapUser;
} }
const StyledTabPane = styled(Tabs.TabPane)` const StyledTabPane = styled(Tabs.TabPane)`

View File

@ -22,13 +22,13 @@ import { t } from '@superset-ui/core';
import TableLoader from 'src/components/TableLoader'; import TableLoader from 'src/components/TableLoader';
import { import {
User, BootstrapUser,
DashboardResponse,
ChartResponse, ChartResponse,
DashboardResponse,
} from 'src/types/bootstrapTypes'; } from 'src/types/bootstrapTypes';
interface CreatedContentProps { interface CreatedContentProps {
user: User; user: BootstrapUser;
} }
class CreatedContent extends React.PureComponent<CreatedContentProps> { class CreatedContent extends React.PureComponent<CreatedContentProps> {

View File

@ -20,13 +20,12 @@ import React from 'react';
import rison from 'rison'; import rison from 'rison';
import moment from 'moment'; import moment from 'moment';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { DashboardResponse, BootstrapUser } from 'src/types/bootstrapTypes';
import TableLoader from '../../components/TableLoader'; import TableLoader from '../../components/TableLoader';
import { Slice } from '../types'; import { Slice } from '../types';
import { User, DashboardResponse } from '../../types/bootstrapTypes';
interface FavoritesProps { interface FavoritesProps {
user: User; user: BootstrapUser;
} }
export default class Favorites extends React.PureComponent<FavoritesProps> { export default class Favorites extends React.PureComponent<FavoritesProps> {
@ -40,7 +39,7 @@ export default class Favorites extends React.PureComponent<FavoritesProps> {
})); }));
return ( return (
<TableLoader <TableLoader
dataEndpoint={`/superset/fave_slices/${this.props.user.userId}/`} dataEndpoint={`/superset/fave_slices/${this.props.user?.userId}/`}
className="table-condensed" className="table-condensed"
columns={['slice', 'creator', 'favorited']} columns={['slice', 'creator', 'favorited']}
mutator={mutator} mutator={mutator}

View File

@ -21,10 +21,10 @@ import moment from 'moment';
import TableLoader from '../../components/TableLoader'; import TableLoader from '../../components/TableLoader';
import { Activity } from '../types'; import { Activity } from '../types';
import { User } from '../../types/bootstrapTypes'; import { BootstrapUser } from '../../types/bootstrapTypes';
interface RecentActivityProps { interface RecentActivityProps {
user: User; user: BootstrapUser;
} }
export default function RecentActivity({ user }: RecentActivityProps) { export default function RecentActivity({ user }: RecentActivityProps) {
@ -45,7 +45,7 @@ export default function RecentActivity({ user }: RecentActivityProps) {
className="table-condensed" className="table-condensed"
mutator={mutator} mutator={mutator}
sortable sortable
dataEndpoint={`/superset/recent_activity/${user.userId}/?limit=${rowLimit}`} dataEndpoint={`/superset/recent_activity/${user?.userId}/?limit=${rowLimit}`}
/> />
</div> </div>
); );

View File

@ -21,10 +21,10 @@ import Badge from 'src/components/Badge';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { UserWithPermissionsAndRoles } from '../../types/bootstrapTypes'; import { BootstrapUser } from 'src/types/bootstrapTypes';
interface SecurityProps { interface SecurityProps {
user: UserWithPermissionsAndRoles; user: BootstrapUser;
} }
export default function Security({ user }: SecurityProps) { export default function Security({ user }: SecurityProps) {
@ -32,15 +32,16 @@ export default function Security({ user }: SecurityProps) {
<div> <div>
<div className="roles"> <div className="roles">
<h4> <h4>
{t('Roles')} <Badge count={Object.keys(user.roles).length} showZero /> {t('Roles')}{' '}
<Badge count={Object.keys(user?.roles || {}).length} showZero />
</h4> </h4>
{Object.keys(user.roles).map(role => ( {Object.keys(user?.roles || {}).map(role => (
<Label key={role}>{role}</Label> <Label key={role}>{role}</Label>
))} ))}
<hr /> <hr />
</div> </div>
<div className="databases"> <div className="databases">
{user.permissions.database_access && ( {user?.permissions.database_access && (
<div> <div>
<h4> <h4>
{t('Databases')}{' '} {t('Databases')}{' '}
@ -54,7 +55,7 @@ export default function Security({ user }: SecurityProps) {
)} )}
</div> </div>
<div className="datasources"> <div className="datasources">
{user.permissions.datasource_access && ( {user?.permissions.datasource_access && (
<div> <div>
<h4> <h4>
{t('Datasets')}{' '} {t('Datasets')}{' '}

View File

@ -20,10 +20,10 @@ import React from 'react';
import Gravatar from 'react-gravatar'; import Gravatar from 'react-gravatar';
import moment from 'moment'; import moment from 'moment';
import { t, styled } from '@superset-ui/core'; import { t, styled } from '@superset-ui/core';
import { UserWithPermissionsAndRoles } from '../../types/bootstrapTypes'; import { BootstrapUser } from 'src/types/bootstrapTypes';
interface UserInfoProps { interface UserInfoProps {
user: UserWithPermissionsAndRoles; user: BootstrapUser;
} }
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -37,7 +37,7 @@ export default function UserInfo({ user }: UserInfoProps) {
<StyledContainer> <StyledContainer>
<a href="https://en.gravatar.com/"> <a href="https://en.gravatar.com/">
<Gravatar <Gravatar
email={user.email} email={user?.email}
width="100%" width="100%"
height="" height=""
size={220} size={220}
@ -51,29 +51,30 @@ export default function UserInfo({ user }: UserInfoProps) {
<div className="header"> <div className="header">
<h3> <h3>
<strong> <strong>
{user.firstName} {user.lastName} {user?.firstName} {user?.lastName}
</strong> </strong>
</h3> </h3>
<h4 className="username"> <h4 className="username">
<i className="fa fa-user-o" /> {user.username} <i className="fa fa-user-o" /> {user?.username}
</h4> </h4>
</div> </div>
<hr /> <hr />
<p> <p>
<i className="fa fa-clock-o" data-test="clock-icon-test" />{' '} <i className="fa fa-clock-o" data-test="clock-icon-test" />{' '}
{t('joined')} {moment(user.createdOn, 'YYYYMMDD').fromNow()} {t('joined')} {moment(user?.createdOn, 'YYYYMMDD').fromNow()}
</p> </p>
<p className="email"> <p className="email">
<i className="fa fa-envelope-o" /> {user.email} <i className="fa fa-envelope-o" /> {user?.email}
</p> </p>
<p className="roles"> <p className="roles">
<i className="fa fa-lock" /> {Object.keys(user.roles).join(', ')} <i className="fa fa-lock" />{' '}
{Object.keys(user?.roles || {}).join(', ')}
</p> </p>
<p> <p>
<i className="fa fa-key" /> <i className="fa fa-key" />
&nbsp; &nbsp;
<span className="text-muted">{t('id:')}</span>&nbsp; <span className="text-muted">{t('id:')}</span>&nbsp;
<span className="user-id">{user.userId}</span> <span className="user-id">{user?.userId}</span>
</p> </p>
</div> </div>
</StyledContainer> </StyledContainer>

View File

@ -21,11 +21,10 @@ import ReactDom from 'react-dom';
import Form from 'react-jsonschema-form'; import Form from 'react-jsonschema-form';
import { interpolate } from 'src/showSavedQuery/utils'; import { interpolate } from 'src/showSavedQuery/utils';
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import getBootstrapData from 'src/utils/getBootstrapData';
const scheduleInfoContainer = document.getElementById('schedule-info'); const scheduleInfoContainer = document.getElementById('schedule-info');
const bootstrapData = JSON.parse( const bootstrapData = getBootstrapData();
scheduleInfoContainer.getAttribute('data-bootstrap'),
);
const config = bootstrapData.common.conf.SCHEDULED_QUERIES; const config = bootstrapData.common.conf.SCHEDULED_QUERIES;
const { query } = bootstrapData.common; const { query } = bootstrapData.common;
const scheduleInfo = query.extra_json.schedule_info; const scheduleInfo = query.extra_json.schedule_info;

View File

@ -18,9 +18,10 @@
*/ */
import { BootstrapData } from 'src/types/bootstrapTypes'; import { BootstrapData } from 'src/types/bootstrapTypes';
import { DEFAULT_BOOTSTRAP_DATA } from 'src/constants';
export default function getBootstrapData(): BootstrapData { export default function getBootstrapData(): BootstrapData {
const appContainer = document.getElementById('app'); const appContainer = document.getElementById('app');
const dataBootstrap = appContainer?.getAttribute('data-bootstrap'); const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
return dataBootstrap ? JSON.parse(dataBootstrap) : {}; return dataBootstrap ? JSON.parse(dataBootstrap) : DEFAULT_BOOTSTRAP_DATA;
} }

View File

@ -21,6 +21,7 @@ import {
isFeatureEnabled, isFeatureEnabled,
FeatureFlag, FeatureFlag,
} from 'src/featureFlags'; } from 'src/featureFlags';
import getBootstrapData from './getBootstrapData';
function getDomainsConfig() { function getDomainsConfig() {
const appContainer = document.getElementById('app'); const appContainer = document.getElementById('app');
@ -38,11 +39,11 @@ function getDomainsConfig() {
return Array.from(availableDomains); return Array.from(availableDomains);
} }
const bootstrapData = JSON.parse(appContainer.getAttribute('data-bootstrap')); const bootstrapData = getBootstrapData();
// this module is a little special, it may be loaded before index.jsx, // this module is a little special, it may be loaded before index.jsx,
// where window.featureFlags get initialized // where window.featureFlags get initialized
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
initFeatureFlags(bootstrapData?.common?.feature_flags); initFeatureFlags(bootstrapData.common.feature_flags);
if ( if (
isFeatureEnabled(FeatureFlag.ALLOW_DASHBOARD_DOMAIN_SHARDING) && isFeatureEnabled(FeatureFlag.ALLOW_DASHBOARD_DOMAIN_SHARDING) &&

View File

@ -29,7 +29,7 @@ import { GlobalStyles } from 'src/GlobalStyles';
import ErrorBoundary from 'src/components/ErrorBoundary'; import ErrorBoundary from 'src/components/ErrorBoundary';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import Menu from 'src/views/components/Menu'; import Menu from 'src/views/components/Menu';
import { bootstrapData } from 'src/preamble'; import getBootstrapData from 'src/utils/getBootstrapData';
import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import setupApp from 'src/setup/setupApp'; import setupApp from 'src/setup/setupApp';
import setupPlugins from 'src/setup/setupPlugins'; import setupPlugins from 'src/setup/setupPlugins';
@ -46,10 +46,8 @@ setupApp();
setupPlugins(); setupPlugins();
setupExtensions(); setupExtensions();
const user = { ...bootstrapData.user }; const bootstrapData = getBootstrapData();
const menu = {
...bootstrapData.common.menu_data,
};
let lastLocationPathname: string; let lastLocationPathname: string;
const boundActions = bindActionCreators({ logEvent }, store.dispatch); const boundActions = bindActionCreators({ logEvent }, store.dispatch);
@ -78,13 +76,16 @@ const App = () => (
<LocationPathnameLogger /> <LocationPathnameLogger />
<RootContextProviders> <RootContextProviders>
<GlobalStyles /> <GlobalStyles />
<Menu data={menu} isFrontendRoute={isFrontendRoute} /> <Menu
data={bootstrapData.common.menu_data}
isFrontendRoute={isFrontendRoute}
/>
<Switch> <Switch>
{routes.map(({ path, Component, props = {}, Fallback = Loading }) => ( {routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
<Route path={path} key={path}> <Route path={path} key={path}>
<Suspense fallback={<Fallback />}> <Suspense fallback={<Fallback />}>
<ErrorBoundary> <ErrorBoundary>
<Component user={user} {...props} /> <Component user={bootstrapData.user} {...props} />
</ErrorBoundary> </ErrorBoundary>
</Suspense> </Suspense>
</Route> </Route>

View File

@ -65,7 +65,7 @@ import setupPlugins from 'src/setup/setupPlugins';
import InfoTooltip from 'src/components/InfoTooltip'; import InfoTooltip from 'src/components/InfoTooltip';
import CertifiedBadge from 'src/components/CertifiedBadge'; import CertifiedBadge from 'src/components/CertifiedBadge';
import { GenericLink } from 'src/components/GenericLink/GenericLink'; import { GenericLink } from 'src/components/GenericLink/GenericLink';
import { bootstrapData } from 'src/preamble'; import getBootstrapData from 'src/utils/getBootstrapData';
import Owner from 'src/types/Owner'; import Owner from 'src/types/Owner';
import ChartCard from './ChartCard'; import ChartCard from './ChartCard';
@ -157,6 +157,8 @@ const Actions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base}; color: ${({ theme }) => theme.colors.grayscale.base};
`; `;
const bootstrapData = getBootstrapData();
function ChartList(props: ChartListProps) { function ChartList(props: ChartListProps) {
const { const {
addDangerToast, addDangerToast,
@ -224,7 +226,7 @@ function ChartList(props: ChartListProps) {
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const enableBroadUserAccess = const enableBroadUserAccess =
bootstrapData?.common?.conf?.ENABLE_BROAD_ACTIVITY_ACCESS; bootstrapData.common.conf.ENABLE_BROAD_ACTIVITY_ACCESS;
const handleBulkChartExport = (chartsToExport: Chart[]) => { const handleBulkChartExport = (chartsToExport: Chart[]) => {
const ids = chartsToExport.map(({ id }) => id); const ids = chartsToExport.map(({ id }) => id);
handleResourceExport('chart', ids, () => { handleResourceExport('chart', ids, () => {

View File

@ -49,7 +49,7 @@ import ImportModelsModal from 'src/components/ImportModal/index';
import Dashboard from 'src/dashboard/containers/Dashboard'; import Dashboard from 'src/dashboard/containers/Dashboard';
import CertifiedBadge from 'src/components/CertifiedBadge'; import CertifiedBadge from 'src/components/CertifiedBadge';
import { bootstrapData } from 'src/preamble'; import getBootstrapData from 'src/utils/getBootstrapData';
import DashboardCard from './DashboardCard'; import DashboardCard from './DashboardCard';
import { DashboardStatus } from './types'; import { DashboardStatus } from './types';
@ -95,6 +95,8 @@ const Actions = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base}; color: ${({ theme }) => theme.colors.grayscale.base};
`; `;
const bootstrapData = getBootstrapData();
function DashboardList(props: DashboardListProps) { function DashboardList(props: DashboardListProps) {
const { const {
addDangerToast, addDangerToast,

View File

@ -24,14 +24,14 @@ import { Provider as ReduxProvider } from 'react-redux';
import { QueryParamProvider } from 'use-query-params'; import { QueryParamProvider } from 'use-query-params';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import getBootstrapData from 'src/utils/getBootstrapData';
import { store } from './store'; import { store } from './store';
import FlashProvider from '../components/FlashProvider'; import FlashProvider from '../components/FlashProvider';
import { bootstrapData, theme } from '../preamble'; import { theme } from '../preamble';
import { EmbeddedUiConfigProvider } from '../components/UiConfigContext'; import { EmbeddedUiConfigProvider } from '../components/UiConfigContext';
import { DynamicPluginProvider } from '../components/DynamicPlugins'; import { DynamicPluginProvider } from '../components/DynamicPlugins';
const common = { ...bootstrapData.common }; const { common } = getBootstrapData();
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();

View File

@ -26,6 +26,7 @@ import createCache from '@emotion/cache';
import { ThemeProvider } from '@superset-ui/core'; import { ThemeProvider } from '@superset-ui/core';
import Menu from 'src/views/components/Menu'; import Menu from 'src/views/components/Menu';
import { theme } from 'src/preamble'; import { theme } from 'src/preamble';
import getBootstrapData from 'src/utils/getBootstrapData';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { setupStore } from './store'; import { setupStore } from './store';
@ -33,10 +34,8 @@ import { setupStore } from './store';
// Disable connecting to redux debugger so that the React app injected // Disable connecting to redux debugger so that the React app injected
// Below the menu like SqlLab or Explore can connect its redux store to the debugger // Below the menu like SqlLab or Explore can connect its redux store to the debugger
const store = setupStore(true); const store = setupStore(true);
const container = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrapJson = container?.getAttribute('data-bootstrap') ?? '{}'; const menu = { ...bootstrapData.common.menu_data };
const bootstrap = JSON.parse(bootstrapJson);
const menu = { ...bootstrap.common.menu_data };
const emotionCache = createCache({ const emotionCache = createCache({
key: 'menu', key: 'menu',

View File

@ -48,10 +48,12 @@ import {
import shortid from 'shortid'; import shortid from 'shortid';
import { import {
BootstrapUser, BootstrapUser,
UndefinedUser,
UserWithPermissionsAndRoles, UserWithPermissionsAndRoles,
} from 'src/types/bootstrapTypes'; } from 'src/types/bootstrapTypes';
import { AnyDatasourcesAction } from 'src/explore/actions/datasourcesActions'; import { AnyDatasourcesAction } from 'src/explore/actions/datasourcesActions';
import { HydrateExplore } from 'src/explore/actions/hydrateExplore'; import { HydrateExplore } from 'src/explore/actions/hydrateExplore';
import getBootstrapData from 'src/utils/getBootstrapData';
import { Dataset } from '@superset-ui/chart-controls'; import { Dataset } from '@superset-ui/chart-controls';
// Some reducers don't do anything, and redux is just used to reference the initial "state". // Some reducers don't do anything, and redux is just used to reference the initial "state".
@ -61,8 +63,7 @@ const noopReducer =
(state: STATE = initialState) => (state: STATE = initialState) =>
state; state;
const container = document.getElementById('app'); const bootstrapData = getBootstrapData();
const bootstrap = JSON.parse(container?.getAttribute('data-bootstrap') ?? '{}');
export const USER_LOADED = 'USER_LOADED'; export const USER_LOADED = 'USER_LOADED';
@ -72,9 +73,9 @@ export type UserLoadedAction = {
}; };
const userReducer = ( const userReducer = (
user: BootstrapUser = bootstrap.user || {}, user = bootstrapData.user || {},
action: UserLoadedAction, action: UserLoadedAction,
): BootstrapUser => { ): BootstrapUser | UndefinedUser => {
if (action.type === USER_LOADED) { if (action.type === USER_LOADED) {
return action.user; return action.user;
} }
@ -104,7 +105,7 @@ const CombinedDatasourceReducers = (
// exported for tests // exported for tests
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
messageToasts: messageToastReducer, messageToasts: messageToastReducer,
common: noopReducer(bootstrap.common || {}), common: noopReducer(bootstrapData.common),
user: userReducer, user: userReducer,
impressionId: noopReducer(shortid.generate()), impressionId: noopReducer(shortid.generate()),
charts, charts,

View File

@ -115,6 +115,7 @@ FRONTEND_CONF_KEYS = (
"HTML_SANITIZATION", "HTML_SANITIZATION",
"HTML_SANITIZATION_SCHEMA_EXTENSIONS", "HTML_SANITIZATION_SCHEMA_EXTENSIONS",
"WELCOME_PAGE_LAST_TAB", "WELCOME_PAGE_LAST_TAB",
"VIZ_TYPE_DENYLIST",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)