Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions superset-frontend/src/SqlLab/components/ResultSet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,16 @@ const ResultSet = ({

all_columns: results.columns.map(column => column.column_name),
});
const url = mountExploreUrl(null, {
[URL_PARAMS.formDataKey.name]: key,
});
const force = false;
const includeAppRoot = openInNewWindow;
const url = mountExploreUrl(
null,
{
[URL_PARAMS.formDataKey.name]: key,
},
force,
includeAppRoot,
);
if (openInNewWindow) {
window.open(url, '_blank', 'noreferrer');
} else {
Expand Down
25 changes: 14 additions & 11 deletions superset-frontend/src/explore/exploreUtils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,24 @@ export function getAnnotationJsonUrl(slice_id, force) {
.toString();
}

export function getURIDirectory(endpointType = 'base') {
export function getURIDirectory(endpointType = 'base', includeAppRoot = true) {
// Building the directory part of the URI
if (
['full', 'json', 'csv', 'query', 'results', 'samples'].includes(
endpointType,
)
) {
return ensureAppRoot('/superset/explore_json/');
}
return ensureAppRoot('/explore/');
const uri = ['full', 'json', 'csv', 'query', 'results', 'samples'].includes(
endpointType,
)
? '/superset/explore_json/'
: '/explore/';
return includeAppRoot ? ensureAppRoot(uri) : uri;
}

export function mountExploreUrl(endpointType, extraSearch = {}, force = false) {
export function mountExploreUrl(
endpointType,
extraSearch = {},
force = false,
includeAppRoot = true,
) {
const uri = new URI('/');
const directory = getURIDirectory(endpointType);
const directory = getURIDirectory(endpointType, includeAppRoot);
const search = uri.search(true);
Object.keys(extraSearch).forEach(key => {
search[key] = extraSearch[key];
Expand Down
45 changes: 28 additions & 17 deletions superset-frontend/src/features/home/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { render, screen, userEvent } from 'spec/helpers/testing-library';
import setupCodeOverrides from 'src/setup/setupCodeOverrides';
import { getExtensionsRegistry } from '@superset-ui/core';
import { Menu } from './Menu';
import * as getBootstrapData from 'src/utils/getBootstrapData';

const dropdownItems = [
{
Expand Down Expand Up @@ -238,6 +239,10 @@ const notanonProps = {
};

const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
const staticAssetsPrefixMock = jest.spyOn(
getBootstrapData,
'staticAssetsPrefix',
);

fetchMock.get(
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))',
Expand All @@ -247,6 +252,8 @@ fetchMock.get(
beforeEach(() => {
// setup a DOM element as a render target
useSelectorMock.mockClear();
// By default use empty static assets prefix
staticAssetsPrefixMock.mockReturnValue('');
});

test('should render', async () => {
Expand All @@ -272,23 +279,27 @@ test('should render the navigation', async () => {
expect(await screen.findByRole('navigation')).toBeInTheDocument();
});

test('should render the brand', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
const {
data: {
brand: { alt, icon },
},
} = mockedProps;
render(<Menu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
expect(await screen.findByAltText(alt)).toBeInTheDocument();
const image = screen.getByAltText(alt);
expect(image).toHaveAttribute('src', icon);
});
test.each(['', '/myapp'])(
'should render the brand, including app_root "%s"',
async app_root => {
staticAssetsPrefixMock.mockReturnValue(app_root);
useSelectorMock.mockReturnValue({ roles: user.roles });
const {
data: {
brand: { alt, icon },
},
} = mockedProps;
render(<Menu {...mockedProps} />, {
useRedux: true,
useQueryParams: true,
useRouter: true,
useTheme: true,
});
expect(await screen.findByAltText(alt)).toBeInTheDocument();
const image = screen.getByAltText(alt);
expect(image).toHaveAttribute('src', `${app_root}${icon}`);
},
);

test('should render the environment tag', async () => {
useSelectorMock.mockReturnValue({ roles: user.roles });
Expand Down
20 changes: 15 additions & 5 deletions superset-frontend/src/features/home/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
import { useState, useEffect } from 'react';
import { styled, css, useTheme } from '@apache-superset/core/ui';
import { ensureStaticPrefix } from 'src/utils/assetUrl';
import { ensureAppRoot } from 'src/utils/pathUtils';
import { getUrlParam } from 'src/utils/urlUtils';
import { MainNav, MenuItem } from '@superset-ui/core/components/Menu';
import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components';
Expand Down Expand Up @@ -287,10 +289,10 @@ export function Menu({
if (theme.brandLogoUrl) {
link = (
<StyledBrandWrapper margin={theme.brandLogoMargin}>
<StyledBrandLink href={theme.brandLogoHref}>
<StyledBrandLink href={ensureAppRoot(theme.brandLogoHref)}>
<StyledImage
preview={false}
src={theme.brandLogoUrl}
src={ensureStaticPrefix(theme.brandLogoUrl)}
alt={theme.brandLogoAlt || 'Apache Superset'}
height={theme.brandLogoHeight}
/>
Expand All @@ -303,17 +305,25 @@ export function Menu({
// Kept as is for backwards compatibility with the old theme system / superset_config.py
link = (
<GenericLink className="navbar-brand" to={brand.path}>
<StyledImage preview={false} src={brand.icon} alt={brand.alt} />
<StyledImage
preview={false}
src={ensureStaticPrefix(brand.icon)}
alt={brand.alt}
/>
</GenericLink>
);
} else {
link = (
<Typography.Link
className="navbar-brand"
href={brand.path}
href={ensureAppRoot(brand.path)}
tabIndex={-1}
>
<StyledImage preview={false} src={brand.icon} alt={brand.alt} />
<StyledImage
preview={false}
src={ensureStaticPrefix(brand.icon)}
alt={brand.alt}
/>
</Typography.Link>
);
}
Expand Down
2 changes: 1 addition & 1 deletion superset-frontend/src/features/home/RightMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ const RightMenu = ({
userItems.push({
key: 'info',
label: (
<Typography.Link href={navbarRight.user_info_url}>
<Typography.Link href={ensureAppRoot(navbarRight.user_info_url)}>
{t('Info')}
</Typography.Link>
),
Expand Down
48 changes: 48 additions & 0 deletions superset-frontend/src/utils/assetUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* 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 * as getBootstrapData from 'src/utils/getBootstrapData';
import { assetUrl, ensureStaticPrefix } from './assetUrl';

const staticAssetsPrefixMock = jest.spyOn(
getBootstrapData,
'staticAssetsPrefix',
);
const resourcePath = '/endpoint/img.png';
const absoluteResourcePath = `https://cdn.domain.com/static${resourcePath}`;

beforeEach(() => {
staticAssetsPrefixMock.mockReturnValue('');
});

describe('assetUrl should prepend static asset prefix for relative paths', () => {
it.each(['', '/myapp'])("'%s' for relative path", app_root => {
staticAssetsPrefixMock.mockReturnValue(app_root);
expect(assetUrl(resourcePath)).toBe(`${app_root}${resourcePath}`);
expect(assetUrl(absoluteResourcePath)).toBe(
`${app_root}/${absoluteResourcePath}`,
);
});
});

describe('assetUrl should ignore static asset prefix for absolute URLs', () => {
it.each(['', '/myapp'])("'%s' for absolute url", app_root => {
staticAssetsPrefixMock.mockReturnValue(app_root);
expect(ensureStaticPrefix(absoluteResourcePath)).toBe(absoluteResourcePath);
});
});
13 changes: 12 additions & 1 deletion superset-frontend/src/utils/assetUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ import { staticAssetsPrefix } from 'src/utils/getBootstrapData';
* defined in the bootstrap data
* @param path A string path to a resource
*/
export function assetUrl(path: string) {
export function assetUrl(path: string): string {
return `${staticAssetsPrefix()}${path.startsWith('/') ? path : `/${path}`}`;
}

/**
* Returns the path prepended with the staticAssetsPrefix if the string is a relative path else it returns
* the string as is.
* @param url_or_path A url or relative path to a resource
*/
export function ensureStaticPrefix(url_or_path: string): string {
if (url_or_path.startsWith('/')) return assetUrl(url_or_path);

return url_or_path;
}
25 changes: 0 additions & 25 deletions superset/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
PermissionViewModelView,
ViewMenuModelView,
)
from flask_appbuilder.widgets import ListWidget
from flask_babel import lazy_gettext as _
from flask_login import AnonymousUserMixin, LoginManager
from jwt.api_jwt import _jwt_global_obj
Expand Down Expand Up @@ -109,27 +108,6 @@ class DatabaseCatalogSchema(NamedTuple):
schema: str


class SupersetSecurityListWidget(ListWidget): # pylint: disable=too-few-public-methods
"""
Redeclaring to avoid circular imports
"""

template = "superset/fab_overrides/list.html"


class SupersetRoleListWidget(ListWidget): # pylint: disable=too-few-public-methods
"""
Role model view from FAB already uses a custom list widget override
So we override the override
"""

template = "superset/fab_overrides/list_role.html"

def __init__(self, **kwargs: Any) -> None:
kwargs["appbuilder"] = current_app.appbuilder
super().__init__(**kwargs)


class SupersetRoleApi(RoleApi):
"""
Overriding the RoleApi to be able to delete roles with permissions
Expand Down Expand Up @@ -170,9 +148,6 @@ def pre_delete(self, item: Model) -> None:
item.roles = []


PermissionViewModelView.list_widget = SupersetSecurityListWidget
PermissionModelView.list_widget = SupersetSecurityListWidget

# Limiting routes on FAB model views
PermissionViewModelView.include_route_methods = {RouteMethod.LIST}
PermissionModelView.include_route_methods = {RouteMethod.LIST}
Expand Down
6 changes: 0 additions & 6 deletions superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
from flask_appbuilder.forms import DynamicForm
from flask_appbuilder.models.sqla.filters import BaseFilter
from flask_appbuilder.security.sqla.models import User
from flask_appbuilder.widgets import ListWidget
from flask_babel import get_locale, gettext as __
from flask_jwt_extended.exceptions import NoAuthorizationError
from flask_wtf.form import FlaskForm
Expand Down Expand Up @@ -589,13 +588,8 @@ def get_spa_template_context(
}


class SupersetListWidget(ListWidget): # pylint: disable=too-few-public-methods
template = "superset/fab_overrides/list.html"


class SupersetModelView(ModelView):
page_size = 100
list_widget = SupersetListWidget

def render_app_template(self) -> FlaskResponse:
context = get_spa_template_context()
Expand Down
Loading