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 (

Reply via email to