Browse Source

[ui-storagebrowser] refactors PathBrowser component to construct breadcrumb from filePath (#3905)

* [ui-storagebrowser] refactors path browser component to construct breadcrumb from filePath

* fix the url decoding

* fix the fileChooserModal filepath props
Ram Prasad Agarwal 11 months ago
parent
commit
04fe0f9b68

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx

@@ -151,7 +151,7 @@ const FileChooserModal = ({
         <div className="hue-filechooser-modal__path-browser-panel">
           <span className="hue-filechooser-modal__destPath">{t('Destination Path:')}</span>
           <PathBrowser
-            breadcrumbs={filesData?.breadcrumbs}
+            filePath={destPath}
             onFilepathChange={setDestPath}
             seperator={'/'}
             showIcon={false}

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx

@@ -86,7 +86,7 @@ const StorageBrowserTabContent = ({
         >
           <span className="hue-storage-browser__filePath">{t('File Path:')}</span>
           <PathBrowser
-            breadcrumbs={filesData?.breadcrumbs}
+            filePath={filePath}
             onFilepathChange={setFilePath}
             seperator={'/'}
             showIcon={false}

+ 17 - 24
desktop/core/src/desktop/js/reactComponents/FileChooser/FileChooserModal/FileChooserModal.tsx

@@ -22,8 +22,8 @@ import HdfsIcon from '../../../components/icons/HdfsIcon';
 import S3Icon from '../../../components/icons/S3Icon';
 import AdlsIcon from '../../../components/icons/AdlsIcon';
 
-import { ApiFileSystem, FILESYSTEMS_API_URL, VIEWFILES_API_URl } from '../api';
-import { FileSystem, PathAndFileData } from '../types';
+import { ApiFileSystem, FILESYSTEMS_API_URL } from '../api';
+import { FileSystem } from '../types';
 import './FileChooserModal.scss';
 import PathBrowser from '../PathBrowser/PathBrowser';
 import useLoadData from '../../../utils/hooks/useLoadData';
@@ -72,13 +72,6 @@ const FileChooserModal: React.FC<FileProps> = ({ show, onCancel, title, okText }
     }
   }, [fileSystemsData]);
 
-  const { data: filesData, loading: loadingFiles } = useLoadData<PathAndFileData>(
-    `${VIEWFILES_API_URl}${filePath}`,
-    {
-      skip: !!filePath
-    }
-  );
-
   return (
     <Modal
       title={title}
@@ -89,7 +82,7 @@ const FileChooserModal: React.FC<FileProps> = ({ show, onCancel, title, okText }
       width={930}
       className="hue-file-chooser__modal"
     >
-      <Spin spinning={loadingFilesSystem || loadingFiles}>
+      <Spin spinning={loadingFilesSystem}>
         <Row>
           <Col span={5}>
             <Menu
@@ -101,24 +94,24 @@ const FileChooserModal: React.FC<FileProps> = ({ show, onCancel, title, okText }
             ></Menu>
           </Col>
           <Col span={19}>
-            <Spin spinning={loadingFiles}>
-              <Row className="hue-path-browser-panel" onClick={e => e.stopPropagation()}>
-                <Col span={18}>
+            <Row className="hue-path-browser-panel" onClick={e => e.stopPropagation()}>
+              <Col span={18}>
+                {filePath && (
                   <PathBrowser
-                    breadcrumbs={filesData?.breadcrumbs}
+                    filePath={filePath}
                     onFilepathChange={setFilePath}
                     seperator={'>'}
                     showIcon={true}
-                  ></PathBrowser>
-                </Col>
-                <Col span={3}>
-                  <Button className="hue-path-browser-panel__button">New Folder</Button>
-                </Col>
-                <Col span={3}>
-                  <Button className="hue-path-browser-panel__button">Upload</Button>
-                </Col>
-              </Row>
-            </Spin>
+                  />
+                )}
+              </Col>
+              <Col span={3}>
+                <Button className="hue-path-browser-panel__button">New Folder</Button>
+              </Col>
+              <Col span={3}>
+                <Button className="hue-path-browser-panel__button">Upload</Button>
+              </Col>
+            </Row>
           </Col>
         </Row>
       </Spin>

+ 28 - 69
desktop/core/src/desktop/js/reactComponents/FileChooser/PathBrowser/PathBrowser.test.tsx

@@ -21,65 +21,20 @@ import '@testing-library/jest-dom';
 
 import PathBrowser from './PathBrowser';
 
-const onFilepathChangeMock = jest.fn();
-
-const breadcrumbsTestConfig1 = [
-  {
-    url: 'abfs://',
-    label: 'abfs://'
-  },
-  {
-    url: 'abfs://test',
-    label: 'test'
-  },
-  {
-    url: 'abfs://test/test1',
-    label: 'test1'
-  }
-];
-
-const breadcrumbsTestConfig2 = [
-  {
-    url: 'abfs://',
-    label: 'abfs://'
-  },
-  {
-    url: 'abfs://test',
-    label: 'test'
-  },
-  {
-    url: 'abfs://test/test1',
-    label: 'test1'
-  },
-  {
-    url: 'abfs://test/test1/test2',
-    label: 'test2'
-  },
-  {
-    url: 'abfs://test/test1/test2/a very very very long test label',
-    label: 'a very very very long test label'
-  },
-  {
-    url: 'abfs://test/test1/test2/a very very very long test label/a very very very long test label 1',
-    label: 'a very very very long test label 1'
-  },
-  {
-    url: 'abfs://test/test1/test2/a very very very long test label/a very very very long test label 1/a very very very long test label 2',
-    label: 'a very very very long test label 2'
-  }
-];
-
 afterEach(() => {
   jest.clearAllMocks();
 });
 
 describe('Pathbrowser', () => {
+  const onFilepathChangeMock = jest.fn();
+  const mockFilePath = 'abfs://test/folder';
+  const mockLongFilePath = 'abfs://path/to/nested1/nested2/nested3/folder';
   describe('Pathbrowser breadcrumbs', () => {
-    test('renders the specified seperator to seperate the breadcrumbs', () => {
+    it('should render the specified seperator to seperate the breadcrumbs', () => {
       render(
         <PathBrowser
+          filePath={mockFilePath}
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
           seperator={'%'}
           showIcon
         />
@@ -88,23 +43,27 @@ describe('Pathbrowser', () => {
       expect(seperator).not.toBeNull();
     });
 
-    test('does not render a different seperator than specified to seperate the breadcrumbs', () => {
+    it('should not render a different seperator than specified to seperate the breadcrumbs', () => {
       render(
         <PathBrowser
+          testId="pathbroswer"
+          filePath={mockFilePath}
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
           seperator={'%'}
           showIcon
         />
       );
-      expect(screen.queryByText('/')).toBeNull();
+      screen.getAllByTestId('pathbroswer-breadcrumb-seperator').forEach(element => {
+        expect(element).toBeVisible();
+        expect(element).toHaveTextContent('%');
+      });
     });
 
-    test('renders breadcrumbs without dropdown button if there are less than or equal to 3 breadcrumbs', () => {
+    it('should render breadcrumbs without dropdown button if there are less than or equal to 3 breadcrumbs', () => {
       const rendered = render(
         <PathBrowser
+          filePath={mockFilePath}
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
           seperator={'/'}
           showIcon
         />
@@ -112,11 +71,11 @@ describe('Pathbrowser', () => {
       expect(rendered.queryByRole('button', { name: '..' })).toBeNull();
     });
 
-    test('renders breadcrumbs with dropdown button if there are more than 3 breadcrumbs', () => {
+    it('should render breadcrumbs with dropdown button if there are more than 3 breadcrumbs', () => {
       const rendered = render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig2}
+          filePath={mockLongFilePath}
           seperator={'/'}
           showIcon
         />
@@ -124,29 +83,29 @@ describe('Pathbrowser', () => {
       expect(rendered.getByRole('button', { name: '..' })).toBeVisible();
     });
 
-    test('renders dropdown on click of dropdown button', async () => {
+    it('should render dropdown on click of dropdown button', async () => {
       const user = userEvent.setup();
       render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig2}
+          filePath={mockLongFilePath}
           seperator={'/'}
           showIcon
         />
       );
       //From the given testconfig: The dropdown menu would consist of menu button with label test2. 'test2' should not be visible until the dropdown button is clicked.
-      expect(screen.queryByRole('menuitem', { name: 'test2' })).not.toBeInTheDocument();
+      expect(screen.queryByRole('menuitem', { name: 'nested3' })).not.toBeInTheDocument();
       const dropdownButton = await screen.getByRole('button', { name: '..' });
       await user.click(dropdownButton);
-      expect(screen.getByRole('menuitem', { name: 'test2' })).toBeInTheDocument();
+      expect(screen.getByRole('menuitem', { name: 'nested2' })).toBeInTheDocument();
     });
 
-    test('calls onFilepathChange on click of breadcrumb', async () => {
+    it('should calls onFilepathChange on click of breadcrumb', async () => {
       const user = userEvent.setup();
       render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
+          filePath={mockFilePath}
           seperator={'/'}
           showIcon={false}
         />
@@ -157,11 +116,11 @@ describe('Pathbrowser', () => {
       expect(onFilepathChangeMock).toHaveBeenCalled();
     });
 
-    test('renders icon in breadcrumbs only if specified', () => {
+    it('should render icon in breadcrumbs only if specified', () => {
       render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
+          filePath={mockFilePath}
           seperator={'/'}
           showIcon
         />
@@ -170,11 +129,11 @@ describe('Pathbrowser', () => {
       expect(icon).toBeVisible();
     });
 
-    test('does not render icon in breadcrumbs if showIcon is false', () => {
+    it('should not render icon in breadcrumbs if showIcon is false', () => {
       render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
+          filePath={mockFilePath}
           seperator={'/'}
           showIcon={false}
         />
@@ -185,11 +144,11 @@ describe('Pathbrowser', () => {
   });
 
   describe('Pathbrowser Input', () => {
-    test('input is hidden before toggle button is clicked', () => {
+    it('should input is hidden before toggle button is clicked', () => {
       render(
         <PathBrowser
           onFilepathChange={onFilepathChangeMock}
-          breadcrumbs={breadcrumbsTestConfig1}
+          filePath={mockFilePath}
           seperator={'/'}
           showIcon
         />

+ 124 - 130
desktop/core/src/desktop/js/reactComponents/FileChooser/PathBrowser/PathBrowser.tsx

@@ -26,8 +26,10 @@ import AdlsIcon from '../../../components/icons/AdlsIcon';
 import { BreadcrumbData } from '../types';
 import Breadcrumb from './Breadcrumb/Breadcrumb';
 import './PathBrowser.scss';
+import { getBreadcrumbs, getFileSystemAndPath } from './PathBrowser.util';
+
 interface PathBrowserProps {
-  breadcrumbs?: BreadcrumbData[];
+  filePath: string;
   onFilepathChange: (path: string) => void;
   seperator: string;
   showIcon: boolean;
@@ -39,7 +41,7 @@ const defaultProps = {
 };
 
 const PathBrowser = ({
-  breadcrumbs,
+  filePath,
   onFilepathChange,
   seperator,
   showIcon,
@@ -48,12 +50,14 @@ const PathBrowser = ({
   const [isEditMode, setIsEditMode] = useState(false);
 
   const icons = {
-    //hdfs file system begins with the first breadcrumb as "/" (ex: /user/demo)
-    '/': <HdfsIcon />,
+    hdfs: <HdfsIcon />,
     abfs: <AdlsIcon />,
     s3: <S3Icon />
   };
 
+  const { fileSystem, path } = getFileSystemAndPath(filePath);
+  const breadcrumbs = getBreadcrumbs(fileSystem, path);
+
   const useOutsideAlerter = (ref: RefObject<HTMLDivElement>) => {
     useEffect(() => {
       // Alert if clicked on outside of element
@@ -72,18 +76,8 @@ const PathBrowser = ({
     }, []);
   };
 
-  const extractFileSystem = (label: string) => {
-    const fileSystemPrefix = label.substring(0, label.length - 3);
-    //hdfs file system begins with the first breadcrumb as "/" (ex: /user/demo)
-    if (fileSystemPrefix == '') {
-      return label;
-    } else {
-      return fileSystemPrefix;
-    }
-  };
-
-  const wrapperRef = useRef<HTMLDivElement>(null);
-  useOutsideAlerter(wrapperRef);
+  const breadcrumbInputRef = useRef<HTMLDivElement>(null);
+  useOutsideAlerter(breadcrumbInputRef);
 
   const extractMenuItems = (breadcrumbMenu: BreadcrumbData[]) => {
     const menu: MenuItemType[] = breadcrumbMenu.map(breadcrumb => {
@@ -97,125 +91,125 @@ const PathBrowser = ({
   };
 
   if (breadcrumbs) {
+    if (isEditMode) {
+      return (
+        <div ref={breadcrumbInputRef}>
+          <Input
+            prefix={showIcon ? icons[fileSystem] : <span />}
+            defaultValue={filePath}
+            onPressEnter={event => {
+              onFilepathChange((event.target as HTMLInputElement).value);
+            }}
+            className="hue-path-browser__input"
+            autoFocus
+            data-testid={`${testId}-input`}
+          />
+        </div>
+      );
+    }
+
     return (
-      <>
-        {!isEditMode ? (
-          <div className="hue-path-browser" data-testid={`${testId}`}>
-            {showIcon && (
-              <div
-                className="hue-path-browser__file-system-icon"
-                data-testid={`${testId}__file-system-icon`}
-              >
-                {icons[extractFileSystem(breadcrumbs[0].label)]}
-              </div>
-            )}
-            <div className="hue-path-browser__breadcrumbs" data-testid={`${testId}-breadcrumb`}>
-              {breadcrumbs.length <= 3 ? (
-                breadcrumbs.map((item: BreadcrumbData, index: number) => {
-                  return (
-                    <React.Fragment key={item.url + index}>
-                      <Breadcrumb
-                        key={item.url}
-                        label={index === 0 ? extractFileSystem(item.label) : item.label}
-                        url={item.url}
-                        onFilepathChange={onFilepathChange}
-                      />
-                      {index != breadcrumbs.length - 1 && (
-                        <div
-                          className="hue-path-browser__breadcrumb-seperator"
-                          data-testid={`${testId}-breadcrumb-seperator`}
-                        >
-                          {seperator}
-                        </div>
-                      )}
-                    </React.Fragment>
-                  );
-                })
-              ) : (
-                <>
+      <div className="hue-path-browser" data-testid={`${testId}`}>
+        {showIcon && (
+          <div
+            className="hue-path-browser__file-system-icon"
+            data-testid={`${testId}__file-system-icon`}
+          >
+            {icons[fileSystem]}
+          </div>
+        )}
+        <div className="hue-path-browser__breadcrumbs" data-testid={`${testId}-breadcrumb`}>
+          {breadcrumbs.length <= 3 ? (
+            breadcrumbs.map((item: BreadcrumbData, index: number) => {
+              return (
+                <React.Fragment key={item.url + index}>
                   <Breadcrumb
-                    label={extractFileSystem(breadcrumbs[0].label)}
-                    url={breadcrumbs[0].url}
+                    key={item.url}
+                    label={item.label}
+                    url={item.url}
                     onFilepathChange={onFilepathChange}
-                    key={breadcrumbs[0].url}
                   />
-                  <div
-                    className="hue-path-browser__breadcrumb-seperator"
-                    data-testid={`${testId}-breadcrumb-seperator`}
-                  >
-                    {seperator}
-                  </div>
-                  <Dropdown
-                    overlayClassName="hue-path-browser__dropdown cuix antd"
-                    menu={{
-                      items: extractMenuItems(breadcrumbs.slice(1, breadcrumbs.length - 2)),
-                      className: 'hue-path-browser__dropdown-menu'
-                    }}
-                    trigger={['hover', 'click']}
-                    autoFocus
-                    data-testid={`${testId}-dropdown`}
-                  >
-                    <BorderlessButton
-                      data-event=""
-                      className="hue-path-browser__dropdown-button"
-                      data-testid={`${testId}-dropdown-btn`}
+                  {index != breadcrumbs.length - 1 && (
+                    <div
+                      className="hue-path-browser__breadcrumb-seperator"
+                      data-testid={`${testId}-breadcrumb-seperator`}
                     >
-                      ..
-                    </BorderlessButton>
-                  </Dropdown>
-                  <div
-                    className="hue-path-browser__breadcrumb-seperator"
-                    data-testid={`${testId}-breadcrumb-seperator`}
-                  >
-                    {seperator}
-                  </div>
-                  <Breadcrumb
-                    key={breadcrumbs[breadcrumbs.length - 2].url}
-                    label={breadcrumbs[breadcrumbs.length - 2].label}
-                    url={breadcrumbs[breadcrumbs.length - 2].url}
-                    onFilepathChange={onFilepathChange}
-                  />
-                  <div
-                    className="hue-path-browser__breadcrumb-seperator"
-                    data-testid={`${testId}-breadcrumb-seperator`}
-                  >
-                    {seperator}
-                  </div>
-                  <Breadcrumb
-                    key={breadcrumbs[breadcrumbs.length - 1].url}
-                    label={breadcrumbs[breadcrumbs.length - 1].label}
-                    url={breadcrumbs[breadcrumbs.length - 1].url}
-                    onFilepathChange={onFilepathChange}
-                  />
-                </>
-              )}
-            </div>
-            <BorderlessButton
-              data-event=""
-              className="hue-path-browser__toggle-breadcrumb-input-btn"
-              aria-label="hue-path-browser__toggle-breadcrumb-input-btn"
-              title="Edit path"
-              onClick={() => {
-                setIsEditMode(true);
-              }}
-              data-testid={`${testId}-toggle-input-btn`}
-            ></BorderlessButton>
-          </div>
-        ) : (
-          <div ref={wrapperRef}>
-            <Input
-              prefix={showIcon ? icons[extractFileSystem(breadcrumbs[0].label)] : <span />}
-              defaultValue={decodeURIComponent(breadcrumbs[breadcrumbs.length - 1].url)}
-              onPressEnter={customPath => {
-                onFilepathChange((customPath.target as HTMLInputElement).value);
-              }}
-              className="hue-path-browser__input"
-              autoFocus
-              data-testid={`${testId}-input`}
-            ></Input>
-          </div>
-        )}
-      </>
+                      {seperator}
+                    </div>
+                  )}
+                </React.Fragment>
+              );
+            })
+          ) : (
+            <>
+              <Breadcrumb
+                label={breadcrumbs[0].label}
+                url={breadcrumbs[0].url}
+                onFilepathChange={onFilepathChange}
+                key={breadcrumbs[0].url}
+              />
+              <div
+                className="hue-path-browser__breadcrumb-seperator"
+                data-testid={`${testId}-breadcrumb-seperator`}
+              >
+                {seperator}
+              </div>
+              <Dropdown
+                overlayClassName="hue-path-browser__dropdown cuix antd"
+                menu={{
+                  items: extractMenuItems(breadcrumbs.slice(1, breadcrumbs.length - 2)),
+                  className: 'hue-path-browser__dropdown-menu'
+                }}
+                trigger={['hover', 'click']}
+                autoFocus
+                data-testid={`${testId}-dropdown`}
+              >
+                <BorderlessButton
+                  data-event=""
+                  className="hue-path-browser__dropdown-button"
+                  data-testid={`${testId}-dropdown-btn`}
+                >
+                  ..
+                </BorderlessButton>
+              </Dropdown>
+              <div
+                className="hue-path-browser__breadcrumb-seperator"
+                data-testid={`${testId}-breadcrumb-seperator`}
+              >
+                {seperator}
+              </div>
+              <Breadcrumb
+                key={breadcrumbs[breadcrumbs.length - 2].url}
+                label={breadcrumbs[breadcrumbs.length - 2].label}
+                url={breadcrumbs[breadcrumbs.length - 2].url}
+                onFilepathChange={onFilepathChange}
+              />
+              <div
+                className="hue-path-browser__breadcrumb-seperator"
+                data-testid={`${testId}-breadcrumb-seperator`}
+              >
+                {seperator}
+              </div>
+              <Breadcrumb
+                key={breadcrumbs[breadcrumbs.length - 1].url}
+                label={breadcrumbs[breadcrumbs.length - 1].label}
+                url={breadcrumbs[breadcrumbs.length - 1].url}
+                onFilepathChange={onFilepathChange}
+              />
+            </>
+          )}
+        </div>
+        <BorderlessButton
+          data-event=""
+          className="hue-path-browser__toggle-breadcrumb-input-btn"
+          aria-label="hue-path-browser__toggle-breadcrumb-input-btn"
+          title="Edit path"
+          onClick={() => {
+            setIsEditMode(true);
+          }}
+          data-testid={`${testId}-toggle-input-btn`}
+        ></BorderlessButton>
+      </div>
     );
   }
 

+ 132 - 0
desktop/core/src/desktop/js/reactComponents/FileChooser/PathBrowser/PathBrowser.util.test.ts

@@ -0,0 +1,132 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { getBreadcrumbs, getFileSystemAndPath } from './PathBrowser.util';
+
+describe('PathBrowser utils', () => {
+  describe('getFileSystemAndPath', () => {
+    it('should return an empty filesystem and the full path if no filesystem is specified', () => {
+      const path = '/test/folder';
+      const result = getFileSystemAndPath(path);
+
+      expect(result).toEqual({
+        fileSystem: 'hdfs',
+        path: '/test/folder'
+      });
+    });
+
+    it('should correctly handle a path with a non-HDFS filesystem identifier', () => {
+      const path = 'abfs://my/storage/path';
+      const result = getFileSystemAndPath(path);
+
+      expect(result).toEqual({
+        fileSystem: 'abfs',
+        path: '/my/storage/path'
+      });
+    });
+
+    it('should correctly handle a path with special characters in the filesystem', () => {
+      const path = 's3://bucket-name/folder';
+      const result = getFileSystemAndPath(path);
+
+      expect(result).toEqual({
+        fileSystem: 's3',
+        path: '/bucket-name/folder'
+      });
+    });
+
+    it('should return an empty filesystem and path when the input is an empty string', () => {
+      const path = '';
+      const result = getFileSystemAndPath(path);
+
+      expect(result).toEqual({
+        fileSystem: 'hdfs',
+        path: ''
+      });
+    });
+  });
+
+  describe('getBreadcrumbs', () => {
+    it('should construct breadcrumbs for an HDFS path', () => {
+      const hdfsPath = '/test/folder';
+      const result = getBreadcrumbs('hdfs', hdfsPath);
+
+      expect(result).toEqual([
+        { url: '/', label: '/' },
+        { url: '/test', label: 'test' },
+        { url: '/test/folder', label: 'folder' }
+      ]);
+    });
+
+    it('should construct breadcrumbs for a non-HDFS path', () => {
+      const nonHdfsPath = '/test/folder';
+      const result = getBreadcrumbs('abfs', nonHdfsPath);
+
+      expect(result).toEqual([
+        { url: 'abfs://', label: 'abfs' },
+        { url: 'abfs://test', label: 'test' },
+        { url: 'abfs://test/folder', label: 'folder' }
+      ]);
+    });
+
+    it('should handle paths with a trailing slash correctly', () => {
+      const pathWithTrailingSlash = '/folder/with/trailing/slash/';
+      const result = getBreadcrumbs('hdfs', pathWithTrailingSlash);
+
+      expect(result).toEqual([
+        { url: '/', label: '/' },
+        { url: '/folder', label: 'folder' },
+        { url: '/folder/with', label: 'with' },
+        { url: '/folder/with/trailing', label: 'trailing' },
+        { url: '/folder/with/trailing/slash', label: 'slash' }
+      ]);
+    });
+
+    it('should handle paths that start with a leading slash with HDFS file system', () => {
+      const pathWithLeadingSlash = '/path/to/file';
+      const result = getBreadcrumbs('hdfs', pathWithLeadingSlash);
+
+      expect(result).toEqual([
+        { url: '/', label: '/' },
+        { url: '/path', label: 'path' },
+        { url: '/path/to', label: 'to' },
+        { url: '/path/to/file', label: 'file' }
+      ]);
+    });
+
+    it('should handle paths with numbers and mixed case labels', () => {
+      const mixedCasePath = '/folder/PathWith123/anotherOne';
+      const result = getBreadcrumbs('abfs', mixedCasePath);
+
+      expect(result).toEqual([
+        { url: 'abfs://', label: 'abfs' },
+        { url: 'abfs://folder', label: 'folder' },
+        { url: 'abfs://folder/PathWith123', label: 'PathWith123' },
+        { url: 'abfs://folder/PathWith123/anotherOne', label: 'anotherOne' }
+      ]);
+    });
+
+    it('should construct breadcrumbs for a file without subfolders', () => {
+      const filePath = '/file.txt';
+      const result = getBreadcrumbs('abfs', filePath);
+
+      expect(result).toEqual([
+        { url: 'abfs://', label: 'abfs' },
+        { url: 'abfs://file.txt', label: 'file.txt' }
+      ]);
+    });
+  });
+});

+ 58 - 0
desktop/core/src/desktop/js/reactComponents/FileChooser/PathBrowser/PathBrowser.util.ts

@@ -0,0 +1,58 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { BreadcrumbData } from '../types';
+
+export const getFileSystemAndPath = (
+  filePath: string
+): {
+  fileSystem: string;
+  path: string;
+} => {
+  if (filePath.includes('://')) {
+    const [fileSystem, path] = filePath.split('://');
+    return {
+      fileSystem,
+      path: `/${path}`
+    };
+  }
+  return {
+    fileSystem: 'hdfs',
+    path: filePath
+  };
+};
+
+export const getBreadcrumbs = (fileSystem: string, path: string): BreadcrumbData[] => {
+  const urlFromPath = new URL(`file:///${path}`);
+  const pathParts = urlFromPath.pathname.split('/').filter(Boolean);
+
+  const rootUrl = fileSystem === 'hdfs' ? '/' : `${fileSystem}://`;
+  const rootlabel = fileSystem === 'hdfs' ? '/' : fileSystem;
+  const rootNode = {
+    url: rootUrl,
+    label: rootlabel
+  };
+
+  return pathParts.reduce(
+    (acc, part, index) => {
+      const decodedPart = decodeURIComponent(part);
+      const currentUrl = `${acc[index].url}${index === 0 ? '' : '/'}${decodedPart}`;
+      acc.push({ url: currentUrl, label: decodedPart });
+      return acc;
+    },
+    [rootNode]
+  );
+};