Explorar el Código

[ui-importer] add tabs layout (#4203)

Ram Prasad Agarwal hace 3 meses
padre
commit
cedbf39338
Se han modificado 17 ficheros con 522 adiciones y 146 borrados
  1. 18 19
      desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.scss
  2. 134 0
      desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.test.tsx
  3. 123 0
      desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.tsx
  4. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.scss
  5. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.test.tsx
  6. 1 1
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.tsx
  7. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/EditColumns/EditColumns.scss
  8. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/EditColumns/EditColumnsModal.tsx
  9. 45 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.scss
  10. 45 47
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.test.tsx
  11. 14 59
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.tsx
  12. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.scss
  13. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.test.tsx
  14. 0 0
      desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.tsx
  15. 120 0
      desktop/core/src/desktop/js/apps/newimporter/ImporterPage.test.tsx
  16. 2 2
      desktop/core/src/desktop/js/apps/newimporter/ImporterPage.tsx
  17. 20 18
      desktop/core/src/desktop/js/utils/hooks/useDataCatalog/useDataCatalog.test.tsx

+ 18 - 19
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/ImporterFilePreview.scss → desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.scss

@@ -17,12 +17,13 @@
 @use 'variables' as vars;
 
 .antd.cuix {
-  .hue-importer-preview-page {
+  .hue-file-import-tabs {
     display: flex;
     flex-direction: column;
-    gap: 16px;
-    padding: 8px 24px 24px;
     height: 100%;
+    overflow: auto;
+    gap: vars.$cdl-spacing-s;
+    padding: vars.$cdl-spacing-s vars.$cdl-spacing-m vars.$cdl-spacing-s vars.$cdl-spacing-m;
 
     &__header {
       display: flex;
@@ -39,7 +40,7 @@
       &__actions {
         display: flex;
         align-items: center;
-        gap: 24px;
+        gap: vars.$cdl-spacing-m;
 
         .action {
           margin-left: 10px;
@@ -47,23 +48,21 @@
       }
     }
 
-    &__metadata {
-      background-color: white;
-      padding: 16px;
-      font-size: 12px;
-      color: #5a656d;
-    }
-
-    &__main-section {
+    &__tabs {
       display: flex;
-      flex-direction: column;
-      flex: 1;
-      background-color: white;
-    }
+      height: 100%;
 
-    &__header-section {
-      display: flex;
-      justify-content: space-between;
+      .ant-tabs-content-holder {
+        display: flex;
+        flex: 1;
+
+        .ant-tabs-content,
+        .ant-tabs-tabpane-active {
+          display: flex;
+          flex-direction: column;
+          flex: 1;
+        }
+      }
     }
   }
 }

+ 134 - 0
desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.test.tsx

@@ -0,0 +1,134 @@
+// 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 React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import FileImportTabs from './FileImportTabs';
+import { FileMetaData, ImporterFileSource } from '../types';
+import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
+import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
+
+jest.mock('../../../utils/hooks/useSaveData/useSaveData');
+const mockedUseSaveData = jest.mocked(useSaveData);
+
+jest.mock('../../../utils/hooks/useLoadData/useLoadData');
+const mockedUseLoadData = jest.mocked(useLoadData);
+
+jest.mock('../../../utils/hooks/useDataCatalog/useDataCatalog', () => ({
+  useDataCatalog: jest.fn(() => ({
+    loading: false,
+    databases: ['default'],
+    database: 'default',
+    connectors: [{ id: 'hive', displayName: 'Hive' }],
+    connector: { id: 'hive', displayName: 'Hive' },
+    computes: [{ id: 'compute1', name: 'Compute 1' }],
+    compute: { id: 'compute1', name: 'Compute 1' },
+    setCompute: jest.fn(),
+    setConnector: jest.fn(),
+    setDatabase: jest.fn()
+  }))
+}));
+
+describe('FileImportTabs', () => {
+  const mockFileMetaData: FileMetaData = {
+    path: '/test/path/file.csv',
+    fileName: 'file.csv',
+    source: ImporterFileSource.LOCAL
+  };
+
+  const defaultProps = {
+    fileMetaData: mockFileMetaData
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    mockedUseSaveData.mockReturnValue({
+      save: jest.fn(),
+      loading: false
+    });
+
+    mockedUseLoadData.mockReturnValue({
+      data: undefined,
+      loading: false,
+      error: undefined,
+      reloadData: jest.fn()
+    });
+  });
+
+  it('renders the component with header and tabs', async () => {
+    render(<FileImportTabs {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('file.csv')).toBeInTheDocument();
+      expect(screen.getByText('Cancel')).toBeInTheDocument();
+      expect(screen.getByText('Finish Import')).toBeInTheDocument();
+      expect(screen.getByText('Preview')).toBeInTheDocument();
+      expect(screen.getByText('Settings')).toBeInTheDocument();
+      expect(screen.getByText('Partitions')).toBeInTheDocument();
+    });
+  });
+
+  it('renders FilePreviewTab in the Preview tab by default', async () => {
+    render(<FileImportTabs {...defaultProps} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Preview')).toBeInTheDocument();
+      expect(screen.getByLabelText('Engine')).toBeInTheDocument();
+    });
+  });
+
+  it('starts with custom default active tab', async () => {
+    render(<FileImportTabs {...defaultProps} defaultActiveKey="settings" />);
+
+    await waitFor(() => {
+      expect(screen.getByText('Settings')).toBeInTheDocument();
+    });
+  });
+
+  it('has finish import button that calls save function', async () => {
+    const mockSave = jest.fn();
+    mockedUseSaveData.mockReturnValue({
+      save: mockSave,
+      loading: false
+    });
+
+    render(<FileImportTabs {...defaultProps} />);
+
+    const finishButton = screen.getByText('Finish Import');
+    fireEvent.click(finishButton);
+
+    await waitFor(() => {
+      expect(mockSave).toHaveBeenCalledWith(expect.any(FormData));
+    });
+  });
+
+  it('shows loading state on finish import button', async () => {
+    mockedUseSaveData.mockReturnValue({
+      save: jest.fn(),
+      loading: true
+    });
+
+    render(<FileImportTabs {...defaultProps} />);
+
+    const finishButton = screen.getByText('Finish Import');
+    await waitFor(() => {
+      expect(finishButton.closest('button')).toHaveClass('ant-btn-loading');
+    });
+  });
+});

+ 123 - 0
desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.tsx

@@ -0,0 +1,123 @@
+// 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 React, { useState } from 'react';
+import { Tabs } from 'antd';
+import { i18nReact } from '../../../utils/i18nReact';
+import { DestinationConfig, FileMetaData } from '../types';
+
+import './FileImportTabs.scss';
+import FilePreviewTab from '../FilePreviewTab/FilePreviewTab';
+import BorderlessButton from 'cuix/dist/components/Button/BorderlessButton';
+import { PrimaryButton } from 'cuix/dist/components/Button';
+import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
+import { FINISH_IMPORT_URL } from '../api';
+import { getDefaultTableName } from '../utils/utils';
+
+interface FileImportTabsProps {
+  fileMetaData: FileMetaData;
+  onTabChange?: (activeKey: string) => void;
+  defaultActiveKey?: string;
+}
+
+const FileImportTabs = ({
+  fileMetaData,
+  defaultActiveKey = 'preview'
+}: FileImportTabsProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const [destinationConfig, setDestinationConfig] = useState<DestinationConfig>({
+    tableName: getDefaultTableName(fileMetaData)
+  });
+
+  const handleDestinationConfigChange = (name: string, value: string) => {
+    setDestinationConfig(prevConfig => ({
+      ...prevConfig,
+      [name]: value
+    }));
+  };
+
+  const { save: finishImport, loading: finalizingImport } = useSaveData(FINISH_IMPORT_URL);
+
+  const handleFinishImport = () => {
+    const source = {
+      inputFormat: fileMetaData.source,
+      path: fileMetaData.path,
+      // format: fileFormat,
+      sourceType: destinationConfig.connectorId
+    };
+    const destination = {
+      outputFormat: 'table',
+      nonDefaultLocation: fileMetaData.path,
+      name: `${destinationConfig.database}.${destinationConfig.tableName}`,
+      sourceType: destinationConfig.connectorId
+      // columns: previewData?.columns
+    };
+
+    const formData = new FormData();
+    formData.append('source', JSON.stringify(source));
+    formData.append('destination', JSON.stringify(destination));
+
+    finishImport(formData);
+  };
+
+  const tabItems = [
+    {
+      label: t('Preview'),
+      key: 'preview',
+      children: (
+        <FilePreviewTab
+          fileMetaData={fileMetaData}
+          destinationConfig={destinationConfig}
+          onDestinationConfigChange={handleDestinationConfigChange}
+        />
+      )
+    },
+    {
+      label: t('Settings'),
+      key: 'settings'
+      // children: <SettingsTab />
+    },
+    {
+      label: t('Partitions'),
+      key: 'partitions'
+      // children: <PartitionsTab />
+    }
+  ];
+
+  return (
+    <div className="hue-file-import-tabs">
+      <div className="hue-file-import-tabs__header">
+        <div className="hue-file-import-tabs__header__title">{fileMetaData?.fileName}</div>
+        <div className="hue-file-import-tabs__header__actions">
+          <BorderlessButton data-testid="hue-importer-preview-page__header__actions__cancel">
+            {t('Cancel')}
+          </BorderlessButton>
+          <PrimaryButton loading={finalizingImport} onClick={handleFinishImport}>
+            {t('Finish Import')}
+          </PrimaryButton>
+        </div>
+      </div>
+      <Tabs
+        defaultActiveKey={defaultActiveKey}
+        items={tabItems}
+        className="hue-file-import-tabs__tabs"
+      />
+    </div>
+  );
+};
+
+export default FileImportTabs;

+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/DestinationSettings/DestinationSettings.scss → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.scss


+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/DestinationSettings/DestinationSettings.test.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.test.tsx


+ 1 - 1
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/DestinationSettings/DestinationSettings.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/DestinationSettings/DestinationSettings.tsx

@@ -115,7 +115,7 @@ const DestinationSettings = ({
   };
 
   const validateTableName = (name: string) => {
-    const tableExists = tables.some(table => table.name.toLowerCase() === name.toLowerCase());
+    const tableExists = tables?.some(table => table.name.toLowerCase() === name.toLowerCase());
     if (tableExists) {
       setError(prev => ({ ...prev, tableName: t('Table name already exists in the database') }));
     } else {

+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/EditColumns/EditColumns.scss → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/EditColumns/EditColumns.scss


+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/EditColumns/EditColumnsModal.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/EditColumns/EditColumnsModal.tsx


+ 45 - 0
desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.scss

@@ -0,0 +1,45 @@
+// 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.
+
+@use 'variables' as vars;
+
+.antd.cuix {
+  .hue-importer-preview-page {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    height: 100%;
+
+    &__metadata {
+      background-color: vars.$cdl-white;
+      padding: 16px;
+      font-size: 12px;
+      color: vars.$cdl-gray-700;
+    }
+
+    &__main-section {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      background-color: vars.$cdl-white;
+    }
+
+    &__header-section {
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+}

+ 45 - 47
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/ImporterFilePreview.test.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.test.tsx

@@ -16,9 +16,9 @@
 
 import React from 'react';
 import '@testing-library/jest-dom';
-import { render, screen, waitFor, act } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import ImporterFilePreview from './ImporterFilePreview';
+import FilePreviewTab from './FilePreviewTab';
 import { FileMetaData, ImporterFileSource } from '../types';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import { mocked } from 'jest-mock';
@@ -40,13 +40,22 @@ jest.mock('../../../utils/hooks/useSaveData/useSaveData', () => ({
   }))
 }));
 
-describe('ImporterFilePreview', () => {
+describe('FilePreviewTab', () => {
   const mockFileMetaData: FileMetaData = {
     source: ImporterFileSource.LOCAL,
     path: '/path/to/file.csv',
     fileName: 'file.csv'
   };
 
+  const mockDestinationConfig = {
+    tableName: 'test_table',
+    connectorId: 'hive',
+    database: 'default',
+    computeId: 'compute1'
+  };
+
+  const mockOnDestinationConfigChange = jest.fn();
+
   beforeEach(() => {
     jest.clearAllMocks();
     mocked(useLoadData).mockImplementation(() => ({
@@ -57,21 +66,29 @@ describe('ImporterFilePreview', () => {
   });
 
   it('should render correctly', async () => {
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
+    render(
+      <FilePreviewTab
+        fileMetaData={mockFileMetaData}
+        destinationConfig={mockDestinationConfig}
+        onDestinationConfigChange={mockOnDestinationConfigChange}
+      />
+    );
 
     await waitFor(() => {
-      expect(screen.getByText('Preview')).toBeInTheDocument();
-      expect(screen.getByText('Cancel')).toBeInTheDocument();
-      expect(screen.getByText('Finish Import')).toBeInTheDocument();
+      expect(screen.getByText('Engine')).toBeInTheDocument();
+      expect(screen.getByText('Database')).toBeInTheDocument();
+      expect(screen.getByText('Table Name')).toBeInTheDocument();
     });
   });
 
   it('should display data in the table after previewData is available', async () => {
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
+    render(
+      <FilePreviewTab
+        fileMetaData={mockFileMetaData}
+        destinationConfig={mockDestinationConfig}
+        onDestinationConfigChange={mockOnDestinationConfigChange}
+      />
+    );
 
     await waitFor(() => {
       expect(screen.getByText('Alice')).toBeInTheDocument();
@@ -82,15 +99,17 @@ describe('ImporterFilePreview', () => {
   });
 
   it('should open edit columns modal when button is clicked', async () => {
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
+    render(
+      <FilePreviewTab
+        fileMetaData={mockFileMetaData}
+        destinationConfig={mockDestinationConfig}
+        onDestinationConfigChange={mockOnDestinationConfigChange}
+      />
+    );
 
     const editColumnsButton = screen.getByText('Edit Columns');
 
-    await act(async () => {
-      await userEvent.click(editColumnsButton);
-    });
+    await userEvent.click(editColumnsButton);
 
     await waitFor(() => {
       expect(screen.getByRole('dialog')).toBeInTheDocument();
@@ -98,37 +117,16 @@ describe('ImporterFilePreview', () => {
   });
 
   it('should display source configuration', async () => {
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
-
-    expect(screen.getByText('Configure source')).toBeInTheDocument();
-  });
-
-  it('should display cancel button', async () => {
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
-
-    const cancelButton = screen.getByRole('button', { name: 'Cancel' });
-    expect(cancelButton).toBeInTheDocument();
-  });
-
-  it('should handle complete file format workflow', async () => {
-    mocked(useLoadData).mockImplementation(() => ({
-      loading: false,
-      data: mockPreviewData(),
-      reloadData: jest.fn()
-    }));
-
-    await act(async () => {
-      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
-    });
+    render(
+      <FilePreviewTab
+        fileMetaData={mockFileMetaData}
+        destinationConfig={mockDestinationConfig}
+        onDestinationConfigChange={mockOnDestinationConfigChange}
+      />
+    );
 
     await waitFor(() => {
-      expect(screen.getByText('Preview')).toBeInTheDocument();
-      expect(screen.getByText('Alice')).toBeInTheDocument();
-      expect(screen.getByText('Bob')).toBeInTheDocument();
+      expect(screen.getByText('Configure source')).toBeInTheDocument();
     });
   });
 });

+ 14 - 59
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/ImporterFilePreview.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/FilePreviewTab.tsx

@@ -15,7 +15,6 @@
 // limitations under the License.
 
 import React, { useState } from 'react';
-import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import {
   CombinedFileFormat,
@@ -26,41 +25,32 @@ import {
   GuessHeaderResponse,
   ImporterTableData
 } from '../types';
-import { convertToAntdColumns, convertToDataSource, getDefaultTableName } from '../utils/utils';
+import { convertToAntdColumns, convertToDataSource } from '../utils/utils';
 import { i18nReact } from '../../../utils/i18nReact';
-import { BorderlessButton, PrimaryButton } from 'cuix/dist/components/Button';
+import { BorderlessButton } from 'cuix/dist/components/Button';
 import PaginatedTable from '../../../reactComponents/PaginatedTable/PaginatedTable';
-import {
-  FILE_GUESS_METADATA,
-  FILE_GUESS_HEADER,
-  FILE_PREVIEW_URL,
-  FINISH_IMPORT_URL
-} from '../api';
+import { FILE_GUESS_METADATA, FILE_GUESS_HEADER, FILE_PREVIEW_URL } from '../api';
 import SourceConfiguration from './SourceConfiguration/SourceConfiguration';
 import EditColumnsModal from './EditColumns/EditColumnsModal';
 import DestinationSettings from './DestinationSettings/DestinationSettings';
 
-import './ImporterFilePreview.scss';
+import './FilePreviewTab.scss';
 
-interface ImporterFilePreviewProps {
+interface FilePreviewTabProps {
   fileMetaData: FileMetaData;
+  destinationConfig: DestinationConfig;
+  onDestinationConfigChange: (name: string, value: string) => void;
 }
 
-const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.Element => {
+const FilePreviewTab = ({
+  fileMetaData,
+  destinationConfig,
+  onDestinationConfigChange
+}: FilePreviewTabProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
   const [fileFormat, setFileFormat] = useState<CombinedFileFormat | undefined>();
 
   const [isEditColumnsOpen, setIsEditColumnsOpen] = useState(false);
-  const [destinationConfig, setDestinationConfig] = useState<DestinationConfig>({
-    tableName: getDefaultTableName(fileMetaData)
-  });
-
-  const handleDestinationSettingsChange = (name: string, value: string) => {
-    setDestinationConfig(prevConfig => ({
-      ...prevConfig,
-      [name]: value
-    }));
-  };
 
   const { loading: guessingFormat } = useLoadData<FileFormatResponse>(FILE_GUESS_METADATA, {
     params: {
@@ -120,50 +110,15 @@ const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.El
     }
   );
 
-  const { save, loading: finalizingImport } = useSaveData(FINISH_IMPORT_URL);
-
-  const handleFinishImport = () => {
-    const source = {
-      inputFormat: fileMetaData.source,
-      path: fileMetaData.path,
-      format: fileFormat,
-      sourceType: destinationConfig.connectorId
-    };
-    const destination = {
-      outputFormat: 'table',
-      nonDefaultLocation: fileMetaData.path,
-      name: `${destinationConfig.database}.${destinationConfig.tableName}`,
-      sourceType: destinationConfig.connectorId,
-      columns: previewData?.columns
-    };
-
-    const formData = new FormData();
-    formData.append('source', JSON.stringify(source));
-    formData.append('destination', JSON.stringify(destination));
-
-    save(formData);
-  };
-
   const columns = convertToAntdColumns(previewData?.columns ?? []);
   const tableData = convertToDataSource(previewData?.previewData ?? {});
 
   return (
     <div className="hue-importer-preview-page">
-      <div className="hue-importer-preview-page__header">
-        <div className="hue-importer-preview-page__header__title">{t('Preview')}</div>
-        <div className="hue-importer-preview-page__header__actions">
-          <BorderlessButton data-testid="hue-importer-preview-page__header__actions__cancel">
-            {t('Cancel')}
-          </BorderlessButton>
-          <PrimaryButton loading={finalizingImport} onClick={handleFinishImport}>
-            {t('Finish Import')}
-          </PrimaryButton>
-        </div>
-      </div>
       <div className="hue-importer-preview-page__metadata">
         <DestinationSettings
           defaultValues={destinationConfig}
-          onChange={handleDestinationSettingsChange}
+          onChange={onDestinationConfigChange}
         />
       </div>
       <div className="hue-importer-preview-page__main-section">
@@ -187,4 +142,4 @@ const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.El
   );
 };
 
-export default ImporterFilePreview;
+export default FilePreviewTab;

+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.scss → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.scss


+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.test.tsx


+ 0 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx → desktop/core/src/desktop/js/apps/newimporter/FilePreviewTab/SourceConfiguration/SourceConfiguration.tsx


+ 120 - 0
desktop/core/src/desktop/js/apps/newimporter/ImporterPage.test.tsx

@@ -0,0 +1,120 @@
+// 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 React from 'react';
+import { fireEvent, render, screen, act, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+
+import ImporterPage from './ImporterPage';
+import useSaveData from '../../utils/hooks/useSaveData/useSaveData';
+import useLoadData from '../../utils/hooks/useLoadData/useLoadData';
+
+jest.mock('../../utils/hooks/useSaveData/useSaveData');
+const mockedUseSaveData = jest.mocked(useSaveData);
+
+jest.mock('../../utils/hooks/useLoadData/useLoadData');
+const mockedUseLoadData = jest.mocked(useLoadData);
+
+jest.mock('../../utils/hooks/useDataCatalog/useDataCatalog', () => ({
+  useDataCatalog: jest.fn(() => ({
+    loading: false,
+    databases: ['default'],
+    database: 'default',
+    connectors: [{ id: 'hive', displayName: 'Hive' }],
+    connector: { id: 'hive', displayName: 'Hive' },
+    computes: [{ id: 'compute1', name: 'Compute 1' }],
+    compute: { id: 'compute1', name: 'Compute 1' },
+    setCompute: jest.fn(),
+    setConnector: jest.fn(),
+    setDatabase: jest.fn()
+  }))
+}));
+
+describe('ImporterPage', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+
+    mockedUseSaveData.mockReturnValue({
+      save: jest.fn(),
+      loading: false
+    });
+
+    mockedUseLoadData.mockReturnValue({
+      data: undefined,
+      loading: false,
+      error: undefined,
+      reloadData: jest.fn()
+    });
+
+    (window as Window & { ENABLE_DIRECT_UPLOAD?: boolean }).ENABLE_DIRECT_UPLOAD = true;
+  });
+
+  afterEach(() => {
+    delete (window as Window & { ENABLE_DIRECT_UPLOAD?: boolean }).ENABLE_DIRECT_UPLOAD;
+  });
+
+  it('renders the page with header', () => {
+    render(<ImporterPage />);
+
+    expect(screen.getByText('Importer')).toBeInTheDocument();
+  });
+
+  it('shows ImporterSourceSelector when no file is selected', () => {
+    render(<ImporterPage />);
+
+    expect(screen.getByText('Select a source to import from')).toBeInTheDocument();
+    expect(screen.getByText('Upload from File')).toBeInTheDocument();
+  });
+
+  it('shows FileImportTabs when file is selected', async () => {
+    const mockSave = jest.fn();
+    mockedUseSaveData.mockReturnValue({
+      save: mockSave,
+      loading: false
+    });
+
+    render(<ImporterPage />);
+
+    const file = new File(['test content'], 'test.csv', { type: 'text/csv' });
+
+    const fileInput = screen
+      .getByRole('button')
+      .parentElement?.querySelector('input[type="file"]') as HTMLInputElement;
+
+    expect(fileInput).toBeInTheDocument();
+
+    act(() => {
+      Object.defineProperty(fileInput, 'files', {
+        value: [file],
+        writable: false
+      });
+
+      fireEvent.change(fileInput);
+
+      const saveCall = mockSave.mock.calls[0];
+      if (saveCall && saveCall[1] && saveCall[1].onSuccess) {
+        saveCall[1].onSuccess({
+          file_path: '/tmp/test.csv'
+        });
+      }
+    });
+
+    await waitFor(() => {
+      expect(screen.getByText('test.csv')).toBeInTheDocument();
+      expect(screen.getByText('Preview')).toBeInTheDocument();
+    });
+  });
+});

+ 2 - 2
desktop/core/src/desktop/js/apps/newimporter/ImporterPage.tsx

@@ -20,8 +20,8 @@ import DataInIcon from '@cloudera/cuix-core/icons/react/DataInIcon';
 import { i18nReact } from '../../utils/i18nReact';
 import { FileMetaData } from './types';
 import CommonHeader from '../../reactComponents/CommonHeader/CommonHeader';
+import FileImportTabs from './FileImportTabs/FileImportTabs';
 import ImporterSourceSelector from './ImporterSourceSelector/ImporterSourceSelector';
-import ImporterFilePreview from './ImporterFilePreview/ImporterFilePreview';
 
 import './ImporterPage.scss';
 
@@ -36,7 +36,7 @@ const ImporterPage = (): JSX.Element => {
         {!fileMetaData?.path ? (
           <ImporterSourceSelector setFileMetaData={setFileMetaData} />
         ) : (
-          <ImporterFilePreview fileMetaData={fileMetaData} />
+          <FileImportTabs fileMetaData={fileMetaData} />
         )}
       </div>
     </div>

+ 20 - 18
desktop/core/src/desktop/js/utils/hooks/useDataCatalog/useDataCatalog.test.tsx

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { renderHook, waitFor } from '@testing-library/react';
+import { renderHook, waitFor, act } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import { useDataCatalog } from './useDataCatalog';
 import { filterEditorConnectors } from '../../../config/hueConfig';
@@ -174,24 +174,26 @@ describe('useDataCatalog', () => {
     const { result } = renderHook(() => useDataCatalog());
     await waitFor(() => expect(result.current.connectors.length).toBeGreaterThan(0));
 
-    result.current.setConnector({
-      id: 'c2',
-      displayName: 'Connector 2',
-      buttonName: 'Button',
-      page: '',
-      tooltip: '',
-      type: '',
-      dialect: '',
-      optimizer: ''
-    });
-    result.current.setNamespace({
-      computes: [],
-      id: 'namespace2',
-      name: 'Namespace 2',
-      status: 'active'
+    act(() => {
+      result.current.setConnector({
+        id: 'c2',
+        displayName: 'Connector 2',
+        buttonName: 'Button',
+        page: '',
+        tooltip: '',
+        type: '',
+        dialect: '',
+        optimizer: ''
+      });
+      result.current.setNamespace({
+        computes: [],
+        id: 'namespace2',
+        name: 'Namespace 2',
+        status: 'active'
+      });
+      result.current.setCompute({ id: 'compute2', name: 'Compute 2', type: 'spark' });
+      result.current.setDatabase('db2');
     });
-    result.current.setCompute({ id: 'compute2', name: 'Compute 2', type: 'spark' });
-    result.current.setDatabase('db2');
 
     await waitFor(() => {
       expect(result.current.connector).toEqual({