This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 6ec164e6a9 feat: Use SPA navigation from datasets list to Explore
(#20890)
6ec164e6a9 is described below
commit 6ec164e6a901fae1c88b03227e627fd8a4619400
Author: Kamil Gabryjelski <[email protected]>
AuthorDate: Mon Aug 1 16:45:07 2022 +0200
feat: Use SPA navigation from datasets list to Explore (#20890)
* feat: Use SPA navigation from datasets list to Explore
* fix lint
* Fix lint
* Fix type
* Make external links work without protocols
* add comment
---
.../components/GenericLink/GenericLink.test.tsx | 59 ++++++++++++++++++++++
.../src/components/GenericLink/GenericLink.tsx | 52 +++++++++++++++++++
superset-frontend/src/utils/urlUtils.test.ts | 54 ++++++++++++++++++++
superset-frontend/src/utils/urlUtils.ts | 33 +++++++++++-
.../src/views/CRUD/data/dataset/DatasetList.tsx | 7 ++-
5 files changed, 203 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/src/components/GenericLink/GenericLink.test.tsx
b/superset-frontend/src/components/GenericLink/GenericLink.test.tsx
new file mode 100644
index 0000000000..c8f2ba5f5f
--- /dev/null
+++ b/superset-frontend/src/components/GenericLink/GenericLink.test.tsx
@@ -0,0 +1,59 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import { GenericLink } from './GenericLink';
+
+test('renders', () => {
+ render(<GenericLink to="/explore">Link to Explore</GenericLink>, {
+ useRouter: true,
+ });
+ expect(screen.getByText('Link to Explore')).toBeVisible();
+});
+
+test('navigates to internal URL', () => {
+ render(<GenericLink to="/explore">Link to Explore</GenericLink>, {
+ useRouter: true,
+ });
+ const internalLink = screen.getByTestId('internal-link');
+ expect(internalLink).toHaveAttribute('href', '/explore');
+});
+
+test('navigates to external URL', () => {
+ render(
+ <GenericLink to="https://superset.apache.org/">
+ Link to external website
+ </GenericLink>,
+ { useRouter: true },
+ );
+ const externalLink = screen.getByTestId('external-link');
+ expect(externalLink).toHaveAttribute('href', 'https://superset.apache.org/');
+});
+
+test('navigates to external URL without host', () => {
+ render(
+ <GenericLink to="superset.apache.org/">
+ Link to external website
+ </GenericLink>,
+ { useRouter: true },
+ );
+ const externalLink = screen.getByTestId('external-link');
+ expect(externalLink).toHaveAttribute('href', '//superset.apache.org/');
+});
diff --git a/superset-frontend/src/components/GenericLink/GenericLink.tsx
b/superset-frontend/src/components/GenericLink/GenericLink.tsx
new file mode 100644
index 0000000000..2bc111d1b6
--- /dev/null
+++ b/superset-frontend/src/components/GenericLink/GenericLink.tsx
@@ -0,0 +1,52 @@
+/**
+ * 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 { Link, LinkProps } from 'react-router-dom';
+import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
+
+export const GenericLink = <S,>({
+ to,
+ component,
+ replace,
+ innerRef,
+ children,
+ ...rest
+}: React.PropsWithoutRef<LinkProps<S>> &
+ React.RefAttributes<HTMLAnchorElement>) => {
+ if (typeof to === 'string' && isUrlExternal(to)) {
+ return (
+ <a data-test="external-link" href={parseUrl(to)} {...rest}>
+ {children}
+ </a>
+ );
+ }
+ return (
+ <Link
+ data-test="internal-link"
+ to={to}
+ component={component}
+ replace={replace}
+ innerRef={innerRef}
+ {...rest}
+ >
+ {children}
+ </Link>
+ );
+};
diff --git a/superset-frontend/src/utils/urlUtils.test.ts
b/superset-frontend/src/utils/urlUtils.test.ts
new file mode 100644
index 0000000000..5f413ac590
--- /dev/null
+++ b/superset-frontend/src/utils/urlUtils.test.ts
@@ -0,0 +1,54 @@
+/**
+ * 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 { isUrlExternal, parseUrl } from './urlUtils';
+
+test('isUrlExternal', () => {
+ expect(isUrlExternal('http://google.com')).toBeTruthy();
+ expect(isUrlExternal('https://google.com')).toBeTruthy();
+ expect(isUrlExternal('//google.com')).toBeTruthy();
+ expect(isUrlExternal('google.com')).toBeTruthy();
+ expect(isUrlExternal('www.google.com')).toBeTruthy();
+ expect(isUrlExternal('mailto:[email protected]')).toBeTruthy();
+
+ // treat all urls starting with protocol or hostname as external
+ // such urls are not handled well by react-router Link component
+ expect(isUrlExternal('http://localhost:8888/port')).toBeTruthy();
+ expect(isUrlExternal('https://localhost/secure')).toBeTruthy();
+ expect(isUrlExternal('http://localhost/about')).toBeTruthy();
+ expect(isUrlExternal('HTTP://localhost/about')).toBeTruthy();
+ expect(isUrlExternal('//localhost/about')).toBeTruthy();
+ expect(isUrlExternal('localhost/about')).toBeTruthy();
+
+ expect(isUrlExternal('/about')).toBeFalsy();
+ expect(isUrlExternal('#anchor')).toBeFalsy();
+});
+
+test('parseUrl', () => {
+ expect(parseUrl('http://google.com')).toEqual('http://google.com');
+ expect(parseUrl('//google.com')).toEqual('//google.com');
+ expect(parseUrl('mailto:[email protected]')).toEqual(
+ 'mailto:[email protected]',
+ );
+ expect(parseUrl('google.com')).toEqual('//google.com');
+ expect(parseUrl('www.google.com')).toEqual('//www.google.com');
+
+ expect(parseUrl('/about')).toEqual('/about');
+ expect(parseUrl('#anchor')).toEqual('#anchor');
+});
diff --git a/superset-frontend/src/utils/urlUtils.ts
b/superset-frontend/src/utils/urlUtils.ts
index 2aac5f3e28..24442a2f48 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -16,7 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { JsonObject, QueryFormData, SupersetClient } from '@superset-ui/core';
+import {
+ isDefined,
+ JsonObject,
+ QueryFormData,
+ SupersetClient,
+} from '@superset-ui/core';
import rison from 'rison';
import { isEmpty } from 'lodash';
import {
@@ -175,3 +180,29 @@ export function getDashboardPermalink({
anchor,
});
}
+
+const externalUrlRegex =
+ /^([^:/?#]+:)?(?:(\/\/)?([^/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/;
+
+// group 1 matches protocol
+// group 2 matches '//'
+// group 3 matches hostname
+export function isUrlExternal(url: string) {
+ const match = url.match(externalUrlRegex) || [];
+ return (
+ (typeof match[1] === 'string' && match[1].length > 0) ||
+ match[2] === '//' ||
+ (typeof match[3] === 'string' && match[3].length > 0)
+ );
+}
+
+export function parseUrl(url: string) {
+ const match = url.match(externalUrlRegex) || [];
+ // if url is external but start with protocol or '//',
+ // it can't be used correctly with <a> element
+ // in such case, add '//' prefix
+ if (isUrlExternal(url) && !isDefined(match[1]) && !url.startsWith('//')) {
+ return `//${url}`;
+ }
+ return url;
+}
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index c5b182871f..107b072f25 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -60,6 +60,7 @@ import ImportModelsModal from
'src/components/ImportModal/index';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
+import { GenericLink } from 'src/components/GenericLink/GenericLink';
import AddDatasetModal from './AddDatasetModal';
import {
@@ -289,7 +290,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
},
},
}: any) => {
- const titleLink = <a href={exploreURL}>{datasetTitle}</a>;
+ const titleLink = (
+ // exploreUrl can be a link to Explore or an external link
+ // in the first case use SPA routing, else use HTML anchor
+ <GenericLink to={exploreURL}>{datasetTitle}</GenericLink>
+ );
try {
const parsedExtra = JSON.parse(extra);
return (