Browse Source

[ui-importer] Public API integration (#4137)

Ram Prasad Agarwal 4 months ago
parent
commit
c613b319cd

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

@@ -113,7 +113,7 @@ describe('DestinationSettings Component', () => {
       id: 'connector2',
       displayName: 'Connector 2'
     });
-    expect(defaultProps.onChange).toHaveBeenCalledWith('engine', 'connector2');
+    expect(defaultProps.onChange).toHaveBeenCalledWith('connectorId', 'connector2');
   });
 
   it('should call onChange when database dropdown changes', async () => {

+ 50 - 15
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/DestinationSettings/DestinationSettings.tsx

@@ -50,7 +50,7 @@ const DestinationSettings = ({
   const inputConfig = [
     {
       label: t('Engine'),
-      name: 'engine',
+      name: 'connectorId',
       type: 'select',
       options: connectors.map(connector => ({
         label: connector.displayName,
@@ -61,20 +61,22 @@ const DestinationSettings = ({
       label: t('Compute'),
       name: 'compute',
       type: 'select',
-      options: computes?.map(compute => ({
-        label: compute.name,
-        value: compute.id
-      })),
+      options:
+        computes?.map(compute => ({
+          label: compute.name,
+          value: compute.id
+        })) ?? [],
       hidden: computes?.length === 1
     },
     {
       label: t('Database'),
       name: 'database',
       type: 'select',
-      options: databases?.map(database => ({
-        label: database,
-        value: database
-      }))
+      options:
+        databases?.map(database => ({
+          label: database,
+          value: database
+        })) ?? []
     },
     {
       label: t('Table Name'),
@@ -84,7 +86,7 @@ const DestinationSettings = ({
   ].filter(({ hidden }) => !hidden);
 
   const handleDropdownChange = (name: string, value: string) => {
-    if (name === 'engine') {
+    if (name === 'connectorId') {
       const selectedConnector = connectors?.find(connector => connector.id === value);
       if (selectedConnector) {
         setConnector(selectedConnector);
@@ -109,28 +111,61 @@ const DestinationSettings = ({
   };
 
   useEffect(() => {
-    if (defaultValues?.connectorId && connectors?.length) {
+    if (!connectors?.length) {
+      return;
+    }
+
+    if (defaultValues?.connectorId) {
       const selectedConnector = connectors.find(conn => conn.id === defaultValues.connectorId);
       if (selectedConnector) {
         setConnector(selectedConnector);
       }
+    } else {
+      setConnector(connectors[0]);
+      onChange('connectorId', connectors[0].id);
     }
-    if (defaultValues?.database && databases?.length) {
+  }, [connectors, defaultValues?.connectorId]);
+
+  useEffect(() => {
+    if (!databases?.length) {
+      return;
+    }
+
+    if (defaultValues?.database) {
       const selectedDatabase = databases.find(db => db === defaultValues.database);
       if (selectedDatabase) {
         setDatabase(selectedDatabase);
       }
+    } else if (!database) {
+      setDatabase(databases[0]);
+      onChange('database', databases[0]);
     }
-    if (defaultValues?.computeId && computes?.length) {
+  }, [databases, defaultValues?.database]);
+
+  useEffect(() => {
+    if (!computes?.length) {
+      return;
+    }
+
+    if (defaultValues?.computeId) {
       const selectedCompute = computes.find(comp => comp.id === defaultValues.computeId);
       if (selectedCompute) {
         setCompute(selectedCompute);
       }
+    } else if (!compute) {
+      setCompute(computes[0]);
+      onChange('computeId', computes[0].id);
+    }
+  }, [computes, defaultValues?.computeId]);
+
+  useEffect(() => {
+    if (defaultValues?.tableName && defaultValues.tableName !== tableName) {
+      setTableName(defaultValues.tableName);
     }
-  }, [defaultValues, connectors, databases, computes, setConnector, setDatabase, setCompute]);
+  }, [defaultValues?.tableName]);
 
   const selectedSettings = {
-    engine: connector?.id,
+    connectorId: connector?.id,
     compute: compute?.id,
     database: database,
     tableName: tableName

+ 82 - 22
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/ImporterFilePreview.test.tsx

@@ -16,35 +16,50 @@
 
 import React from 'react';
 import '@testing-library/jest-dom';
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, screen, waitFor, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import ImporterFilePreview from './ImporterFilePreview';
-import { FileMetaData } from '../types';
+import { FileMetaData, ImporterFileSource } from '../types';
+import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
+import { mocked } from 'jest-mock';
 
-const mockSave = jest.fn();
+const mockPreviewData = jest.fn().mockReturnValue({
+  columns: [{ name: 'Name' }, { name: 'Age' }],
+  previewData: {
+    name: ['Alice', 'Bob'],
+    age: ['30', '25']
+  }
+});
+
+jest.mock('../../../utils/hooks/useLoadData/useLoadData');
 jest.mock('../../../utils/hooks/useSaveData/useSaveData', () => ({
   __esModule: true,
   default: jest.fn(() => ({
-    data: {
-      columns: [{ name: 'Name' }, { name: 'Age' }],
-      sample: [
-        ['Alice', '30'],
-        ['Bob', '25']
-      ]
-    },
-    save: mockSave,
+    save: jest.fn(),
     loading: false
   }))
 }));
 
 describe('ImporterFilePreview', () => {
   const mockFileMetaData: FileMetaData = {
-    source: 'localfile',
-    type: 'csv',
-    path: '/path/to/file.csv'
+    source: ImporterFileSource.LOCAL,
+    path: '/path/to/file.csv',
+    fileName: 'file.csv'
   };
 
+  beforeEach(() => {
+    jest.clearAllMocks();
+    mocked(useLoadData).mockImplementation(() => ({
+      loading: false,
+      data: mockPreviewData(),
+      reloadData: jest.fn()
+    }));
+  });
+
   it('should render correctly', async () => {
-    render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+    await act(async () => {
+      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+    });
 
     await waitFor(() => {
       expect(screen.getByText('Preview')).toBeInTheDocument();
@@ -53,22 +68,67 @@ describe('ImporterFilePreview', () => {
     });
   });
 
-  it('should call guessFormat and guessFields when the component mounts', async () => {
-    render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+  it('should display data in the table after previewData is available', async () => {
+    await act(async () => {
+      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+    });
 
     await waitFor(() => {
-      expect(mockSave).toHaveBeenCalledTimes(2);
+      expect(screen.getByText('Alice')).toBeInTheDocument();
+      expect(screen.getByText('30')).toBeInTheDocument();
+      expect(screen.getByText('Bob')).toBeInTheDocument();
+      expect(screen.getByText('25')).toBeInTheDocument();
     });
   });
 
-  it('should display data in the table after previewData is available', async () => {
-    render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+  it('should open edit columns modal when button is clicked', async () => {
+    await act(async () => {
+      render(<ImporterFilePreview fileMetaData={mockFileMetaData} />);
+    });
+
+    const editColumnsButton = screen.getByText('Edit Columns');
+
+    await act(async () => {
+      await userEvent.click(editColumnsButton);
+    });
+
+    await waitFor(() => {
+      expect(screen.getByRole('dialog')).toBeInTheDocument();
+    });
+  });
+
+  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} />);
+    });
 
     await waitFor(() => {
+      expect(screen.getByText('Preview')).toBeInTheDocument();
       expect(screen.getByText('Alice')).toBeInTheDocument();
-      expect(screen.getByText('30')).toBeInTheDocument();
       expect(screen.getByText('Bob')).toBeInTheDocument();
-      expect(screen.getByText('25')).toBeInTheDocument();
     });
   });
 });

+ 70 - 45
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/ImporterFilePreview.tsx

@@ -14,20 +14,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React, { useEffect, useState } from 'react';
+import React, { useState } from 'react';
 import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
+import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import {
+  CombinedFileFormat,
   DestinationConfig,
   FileFormatResponse,
   FileMetaData,
-  GuessFieldTypesResponse,
+  FilePreviewResponse,
+  GuessHeaderResponse,
   ImporterTableData
 } from '../types';
 import { convertToAntdColumns, convertToDataSource, getDefaultTableName } from '../utils/utils';
 import { i18nReact } from '../../../utils/i18nReact';
 import { BorderlessButton, PrimaryButton } from 'cuix/dist/components/Button';
 import PaginatedTable from '../../../reactComponents/PaginatedTable/PaginatedTable';
-import { GUESS_FORMAT_URL, GUESS_FIELD_TYPES_URL, FINISH_IMPORT_URL } from '../api';
+import {
+  FILE_GUESS_METADATA,
+  FILE_GUESS_HEADER,
+  FILE_PREVIEW_URL,
+  FINISH_IMPORT_URL
+} from '../api';
 import SourceConfiguration from './SourceConfiguration/SourceConfiguration';
 import EditColumnsModal from './EditColumns/EditColumnsModal';
 import DestinationSettings from './DestinationSettings/DestinationSettings';
@@ -40,10 +48,11 @@ interface ImporterFilePreviewProps {
 
 const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
-  const [fileFormat, setFileFormat] = useState<FileFormatResponse | undefined>();
+  const [fileFormat, setFileFormat] = useState<CombinedFileFormat | undefined>();
+
   const [isEditColumnsOpen, setIsEditColumnsOpen] = useState(false);
   const [destinationConfig, setDestinationConfig] = useState<DestinationConfig>({
-    tableName: getDefaultTableName(fileMetaData.path, fileMetaData.source)
+    tableName: getDefaultTableName(fileMetaData)
   });
 
   const handleDestinationSettingsChange = (name: string, value: string) => {
@@ -53,49 +62,65 @@ const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.El
     }));
   };
 
-  const { save: guessFormat, loading: guessingFormat } = useSaveData<FileFormatResponse>(
-    GUESS_FORMAT_URL,
-    {
-      onSuccess: data => {
-        setFileFormat(data);
-      }
+  const { loading: guessingFormat } = useLoadData<FileFormatResponse>(FILE_GUESS_METADATA, {
+    params: {
+      file_path: fileMetaData.path,
+      import_type: fileMetaData.source
+    },
+    skip: !fileMetaData.path,
+    onSuccess: data => {
+      setFileFormat({
+        ...data,
+        recordSeparator: data?.recordSeparator?.includes('\n') ? '\\n' : data?.recordSeparator,
+        selectedSheetName: data?.sheetNames?.[0]
+      });
     }
-  );
-
-  const {
-    save: guessFields,
-    data: previewData,
-    loading: guessingFields
-  } = useSaveData<GuessFieldTypesResponse>(GUESS_FIELD_TYPES_URL);
+  });
 
-  const { save, loading: finalizingImport } =
-    useSaveData<GuessFieldTypesResponse>(FINISH_IMPORT_URL);
+  const { loading: guessingHeader } = useLoadData<GuessHeaderResponse>(FILE_GUESS_HEADER, {
+    params: {
+      file_path: fileMetaData.path,
+      file_type: fileFormat?.type,
+      import_type: fileMetaData.source,
+      sheet_name: fileFormat?.selectedSheetName
+    },
+    skip: !fileFormat?.type,
+    onSuccess: data => {
+      setFileFormat(prev => ({
+        ...(prev ?? {}),
+        hasHeader: data.hasHeader
+      }));
+    },
+    onError: () => {
+      setFileFormat(prev => ({
+        ...(prev ?? {}),
+        hasHeader: false
+      }));
+    }
+  });
 
-  useEffect(() => {
-    const guessFormatPayload = {
-      inputFormat: fileMetaData.source,
-      file_type: fileMetaData.type,
-      path: fileMetaData.path
-    };
-    const guessFormatormData = new FormData();
-    guessFormatormData.append('fileFormat', JSON.stringify(guessFormatPayload));
-    guessFormat(guessFormatormData);
-  }, [fileMetaData]);
-
-  useEffect(() => {
-    if (!fileFormat) {
-      return;
+  const { data: previewData, loading: loadingPreview } = useLoadData<FilePreviewResponse>(
+    FILE_PREVIEW_URL,
+    {
+      params: {
+        file_path: fileMetaData.path,
+        file_type: fileFormat?.type,
+        import_type: fileMetaData.source,
+        sql_dialect: destinationConfig.connectorId,
+        has_header: fileFormat?.hasHeader,
+        sheet_name: fileFormat?.selectedSheetName,
+        field_separator: fileFormat?.fieldSeparator,
+        quote_char: fileFormat?.quoteChar,
+        record_separator: fileFormat?.recordSeparator
+      },
+      skip:
+        !fileFormat?.type ||
+        fileFormat?.hasHeader === undefined ||
+        destinationConfig.connectorId === undefined
     }
+  );
 
-    const payload = {
-      path: fileMetaData.path,
-      format: fileFormat,
-      inputFormat: fileMetaData.source
-    };
-    const formData = new FormData();
-    formData.append('fileFormat', JSON.stringify(payload));
-    guessFields(formData);
-  }, [fileMetaData.path, fileFormat]);
+  const { save, loading: finalizingImport } = useSaveData(FINISH_IMPORT_URL);
 
   const handleFinishImport = () => {
     const source = {
@@ -120,7 +145,7 @@ const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.El
   };
 
   const columns = convertToAntdColumns(previewData?.columns ?? []);
-  const tableData = convertToDataSource(columns, previewData?.sample);
+  const tableData = convertToDataSource(previewData?.previewData ?? {});
 
   return (
     <div className="hue-importer-preview-page">
@@ -149,7 +174,7 @@ const ImporterFilePreview = ({ fileMetaData }: ImporterFilePreviewProps): JSX.El
           </BorderlessButton>
         </div>
         <PaginatedTable<ImporterTableData>
-          loading={guessingFormat || guessingFields}
+          loading={guessingFormat || loadingPreview || guessingHeader}
           data={tableData}
           columns={columns}
           rowKey="importerDataKey"

+ 152 - 6
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx

@@ -1,19 +1,34 @@
+// 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, fireEvent, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
 import SourceConfiguration from './SourceConfiguration';
-import { FileFormatResponse, ImporterFileTypes } from '../../types';
+import { CombinedFileFormat, ImporterFileTypes } from '../../types';
 
 describe('SourceConfiguration Component', () => {
   const mockSetFileFormat = jest.fn();
-  const mockFileFormat: FileFormatResponse = {
+  const mockFileFormat: CombinedFileFormat = {
     quoteChar: '"',
     recordSeparator: '\\n',
     type: ImporterFileTypes.EXCEL,
     hasHeader: true,
-    fieldSeparator: ',',
-    status: 0
+    fieldSeparator: ','
   };
 
   beforeEach(() => {
@@ -27,6 +42,17 @@ describe('SourceConfiguration Component', () => {
     expect(getByText('Configure source')).toBeInTheDocument();
   });
 
+  it('should render as collapsible details element', () => {
+    const { container, getByText } = render(
+      <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
+    );
+
+    const detailsElement = container.querySelector('details');
+    expect(detailsElement).toBeInTheDocument();
+    expect(detailsElement).toHaveClass('hue-importer-configuration');
+    expect(getByText('File Type')).not.toBeVisible();
+  });
+
   it('should call setFileFormat on option change', async () => {
     const { getByText, getAllByRole } = render(
       <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
@@ -44,7 +70,7 @@ describe('SourceConfiguration Component', () => {
     );
   });
 
-  it('should show fieldSepator and other downdown when fileType is CSV', () => {
+  it('should show fieldSeparator and other dropdown when fileType is CSV', () => {
     const { getAllByRole, getByText } = render(
       <SourceConfiguration
         fileFormat={{ ...mockFileFormat, type: ImporterFileTypes.CSV }}
@@ -62,18 +88,138 @@ describe('SourceConfiguration Component', () => {
     expect(getByText('Quote Character')).toBeInTheDocument();
   });
 
-  it('should not show fieldSepator and other downdown when fileType is not CSV', () => {
+  it('should not show fieldSeparator and other dropdown when fileType is not CSV', () => {
     const { getAllByRole, getByText, queryByText } = render(
       <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
     );
 
     const selectElement = getAllByRole('combobox');
 
+    expect(selectElement).toHaveLength(3);
+    expect(getByText('File Type')).toBeInTheDocument();
+    expect(getByText('Has Header')).toBeInTheDocument();
+    expect(queryByText('Field Separator')).not.toBeInTheDocument();
+    expect(queryByText('Record Separator')).not.toBeInTheDocument();
+    expect(queryByText('Quote Character')).not.toBeInTheDocument();
+  });
+
+  it('should show select sheet dropdown when fileType is EXCEL', () => {
+    const { getAllByRole, getByText } = render(
+      <SourceConfiguration
+        fileFormat={{ ...mockFileFormat, type: ImporterFileTypes.EXCEL }}
+        setFileFormat={mockSetFileFormat}
+      />
+    );
+
+    const selectElement = getAllByRole('combobox');
+
+    expect(selectElement).toHaveLength(3);
+    expect(getByText('Sheet Name')).toBeInTheDocument();
+  });
+
+  it('should not show select sheet dropdown when fileType is not EXCEL', () => {
+    const { getAllByRole, queryByText } = render(
+      <SourceConfiguration
+        fileFormat={{ ...mockFileFormat, type: ImporterFileTypes.CSV }}
+        setFileFormat={mockSetFileFormat}
+      />
+    );
+
+    const selectElement = getAllByRole('combobox');
+
+    expect(selectElement).toHaveLength(5);
+    expect(queryByText('Sheet Name')).not.toBeInTheDocument();
+  });
+
+  it('should not call setFileFormat when fileFormat is undefined', async () => {
+    const { getAllByRole } = render(
+      <SourceConfiguration fileFormat={undefined} setFileFormat={mockSetFileFormat} />
+    );
+
+    const selectElement = getAllByRole('combobox')[0];
+    await userEvent.click(selectElement);
+
+    const csvOption = getAllByRole('option').find(option => option.textContent === 'CSV');
+    if (csvOption) {
+      await userEvent.click(csvOption);
+    }
+
+    expect(mockSetFileFormat).not.toHaveBeenCalled();
+  });
+
+  it('should call setFileFormat with correct parameters for hasHeader change', async () => {
+    const { getByText, getAllByRole } = render(
+      <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
+    );
+
+    const hasHeaderSelect = getAllByRole('combobox')[1];
+    await userEvent.click(hasHeaderSelect);
+
+    const noOption = getByText('No');
+    await userEvent.click(noOption);
+
+    await waitFor(() =>
+      expect(mockSetFileFormat).toHaveBeenCalledWith({
+        ...mockFileFormat,
+        hasHeader: false
+      })
+    );
+  });
+
+  it('should call setFileFormat with correct parameters for fieldSeparator change', async () => {
+    const csvFormat = { ...mockFileFormat, type: ImporterFileTypes.CSV };
+    const { getByText, getAllByRole } = render(
+      <SourceConfiguration fileFormat={csvFormat} setFileFormat={mockSetFileFormat} />
+    );
+
+    const fieldSeparatorSelect = getAllByRole('combobox')[2];
+    await userEvent.click(fieldSeparatorSelect);
+
+    const tabOption = getByText('^Tab (\\t)');
+    await userEvent.click(tabOption);
+
+    await waitFor(() =>
+      expect(mockSetFileFormat).toHaveBeenCalledWith({
+        ...csvFormat,
+        fieldSeparator: '\\t'
+      })
+    );
+  });
+
+  it('should call setFileFormat with correct parameters for quoteChar change', async () => {
+    const csvFormat = { ...mockFileFormat, type: ImporterFileTypes.CSV };
+    const { getByText, getAllByRole } = render(
+      <SourceConfiguration fileFormat={csvFormat} setFileFormat={mockSetFileFormat} />
+    );
+
+    const quoteCharSelect = getAllByRole('combobox')[4];
+    await userEvent.click(quoteCharSelect);
+
+    const singleQuoteOption = getByText("Single Quote (')");
+    await userEvent.click(singleQuoteOption);
+
+    await waitFor(() =>
+      expect(mockSetFileFormat).toHaveBeenCalledWith({
+        ...csvFormat,
+        quoteChar: "'"
+      })
+    );
+  });
+
+  it('should show JSON type dropdown when fileType is JSON', () => {
+    const jsonFormat = { ...mockFileFormat, type: ImporterFileTypes.JSON };
+    const { getAllByRole, getByText, queryByText } = render(
+      <SourceConfiguration fileFormat={jsonFormat} setFileFormat={mockSetFileFormat} />
+    );
+
+    const selectElement = getAllByRole('combobox');
+
     expect(selectElement).toHaveLength(2);
     expect(getByText('File Type')).toBeInTheDocument();
     expect(getByText('Has Header')).toBeInTheDocument();
     expect(queryByText('Field Separator')).not.toBeInTheDocument();
     expect(queryByText('Record Separator')).not.toBeInTheDocument();
     expect(queryByText('Quote Character')).not.toBeInTheDocument();
+    expect(queryByText('Sheet Name')).not.toBeInTheDocument();
   });
 });

+ 26 - 29
desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx

@@ -14,12 +14,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import React, { useCallback } from 'react';
+import React from 'react';
 import Select from 'cuix/dist/components/Select/Select';
 import ConfigureIcon from '@cloudera/cuix-core/icons/react/ConfigureIcon';
 import { i18nReact } from '../../../../utils/i18nReact';
 import { sourceConfigs } from '../../constants';
-import { FileFormatResponse } from '../../types';
+import { CombinedFileFormat, FileFormatResponse } from '../../types';
 
 import './SourceConfiguration.scss';
 
@@ -33,19 +33,14 @@ const SourceConfiguration = ({
 }: SourceConfigurationProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const onChange = useCallback(
-    (value: string | number | boolean, name: keyof FileFormatResponse) => {
-      if (fileFormat) {
-        setFileFormat({
-          ...fileFormat,
-          [name]: value
-        });
-      }
-    },
-    [fileFormat, setFileFormat]
-  );
-
-  const filteredSourceConfigs = sourceConfigs.filter(config => !config.hidden?.(fileFormat?.type));
+  const onChange = (value: string | number | boolean, name: keyof CombinedFileFormat) => {
+    if (fileFormat) {
+      setFileFormat({
+        ...fileFormat,
+        [name]: value
+      });
+    }
+  };
 
   return (
     <details className="hue-importer-configuration">
@@ -54,20 +49,22 @@ const SourceConfiguration = ({
         {t('Configure source')}
       </summary>
       <div className="hue-importer-configuration-options">
-        {filteredSourceConfigs.map(config => (
-          <div key={config.name}>
-            <label htmlFor={config.name}>{t(config.label)}</label>
-            <Select
-              bordered={true}
-              className="hue-importer-configuration__dropdown"
-              id={config.name}
-              options={config.options}
-              onChange={value => onChange(value, config.name)}
-              value={fileFormat?.[config.name]}
-              getPopupContainer={triggerNode => triggerNode.parentElement}
-            />
-          </div>
-        ))}
+        {sourceConfigs
+          .filter(config => !config.hidden?.(fileFormat?.type))
+          .map(config => (
+            <div key={config.name}>
+              <label htmlFor={config.name}>{t(config.label)}</label>
+              <Select
+                bordered={true}
+                className="hue-importer-configuration__dropdown"
+                id={config.name}
+                options={config.options}
+                onChange={value => onChange(value, config.name)}
+                value={fileFormat?.[config.name]}
+                getPopupContainer={triggerNode => triggerNode.parentElement}
+              />
+            </div>
+          ))}
       </div>
     </details>
   );

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

@@ -70,8 +70,8 @@ const ImporterSourceSelector = ({ setFileMetaData }: ImporterSourceSelectorProps
       upload(payload, {
         onSuccess: data => {
           setFileMetaData({
-            path: data.local_file_url,
-            type: data.file_type,
+            path: data.file_path,
+            fileName: file.name,
             source: ImporterFileSource.LOCAL
           });
         },

+ 4 - 3
desktop/core/src/desktop/js/apps/newimporter/api.ts

@@ -14,7 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export const UPLOAD_LOCAL_FILE_API_URL = '/indexer/api/indexer/upload_local_file';
-export const GUESS_FORMAT_URL = '/indexer/api/indexer/guess_format';
-export const GUESS_FIELD_TYPES_URL = '/indexer/api/indexer/guess_field_types';
+export const UPLOAD_LOCAL_FILE_API_URL = '/api/v1/importer/upload/file';
+export const FILE_GUESS_METADATA = '/api/v1/importer/file/guess_metadata';
+export const FILE_GUESS_HEADER = '/api/v1/importer/file/guess_header';
+export const FILE_PREVIEW_URL = '/api/v1/importer/file/preview';
 export const FINISH_IMPORT_URL = '/indexer/api/importer/submit';

+ 8 - 2
desktop/core/src/desktop/js/apps/newimporter/constants.ts

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { FileFormatResponse, ImporterFileTypes } from './types';
+import { CombinedFileFormat, ImporterFileTypes } from './types';
 
 export const separator = [
   { value: ',', label: 'Comma (,)' },
@@ -30,7 +30,7 @@ export const separator = [
 ];
 
 export const sourceConfigs: {
-  name: keyof FileFormatResponse;
+  name: keyof CombinedFileFormat;
   label: string;
   hidden?: (type?: ImporterFileTypes) => boolean;
   options: {
@@ -72,5 +72,11 @@ export const sourceConfigs: {
     label: 'Quote Character',
     hidden: (type?: ImporterFileTypes) => type !== ImporterFileTypes.CSV,
     options: separator
+  },
+  {
+    name: 'selectedSheetName',
+    label: 'Sheet Name',
+    hidden: (type?: ImporterFileTypes) => type !== ImporterFileTypes.EXCEL,
+    options: []
   }
 ];

+ 25 - 28
desktop/core/src/desktop/js/apps/newimporter/types.ts

@@ -21,52 +21,49 @@ export enum ImporterFileTypes {
 }
 
 export enum ImporterFileSource {
-  LOCAL = 'localfile',
-  REMOTE = 'file'
+  LOCAL = 'local',
+  REMOTE = 'remote'
 }
 
 export interface LocalFileUploadResponse {
-  local_file_url: string;
-  file_type: ImporterFileTypes;
+  file_path: string;
 }
 
 export interface FileFormatResponse {
-  fieldSeparator: string;
-  hasHeader: boolean;
-  quoteChar: string;
-  recordSeparator: string;
-  status: number;
-  type: ImporterFileTypes;
+  type?: ImporterFileTypes;
+  fieldSeparator?: string;
+  quoteChar?: string;
+  recordSeparator?: string;
+  sheetNames?: string[];
+}
+
+export interface GuessHeaderResponse {
+  hasHeader?: boolean;
+}
+
+export interface CombinedFileFormat extends FileFormatResponse, GuessHeaderResponse {
+  selectedSheetName?: string;
 }
 
 export interface FileMetaData {
   path: string;
-  type: ImporterFileTypes;
+  fileName?: string;
   source: ImporterFileSource;
 }
 
-export type GuessFieldTypesColumn = {
+export type FilePreviewTableColumn = {
   importerDataKey?: string; // key for identifying unique data row
   name: string;
   type?: string;
-  unique?: boolean;
-  keep?: boolean;
-  required?: boolean;
-  multiValued?: boolean;
-  showProperties?: boolean;
-  level?: number;
-  length?: number;
-  keyType?: string;
-  isPartition?: boolean;
-  partitionValue?: string;
-  comment?: string;
-  scale?: number;
-  precision?: number;
 };
 
-export interface GuessFieldTypesResponse {
-  columns: GuessFieldTypesColumn[];
-  sample: string[][];
+export interface FilePreviewTableData {
+  [key: string]: (string | number)[];
+}
+
+export interface FilePreviewResponse {
+  columns: FilePreviewTableColumn[];
+  previewData: FilePreviewTableData;
 }
 
 export interface ImporterTableData {

+ 226 - 39
desktop/core/src/desktop/js/apps/newimporter/utils/utils.test.ts

@@ -14,17 +14,64 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { ColumnProps } from 'cuix/dist/components/Table';
 import { convertToAntdColumns, convertToDataSource, getDefaultTableName } from './utils';
-import { ImporterFileSource, GuessFieldTypesColumn, ImporterTableData } from '../types';
+import { ImporterFileSource, FilePreviewTableColumn, FilePreviewTableData } from '../types';
 
 describe('convertToAntdColumns', () => {
-  it('should return an empty array when no input is provided', () => {
-    expect(convertToAntdColumns()).toEqual([]);
+  it('should correctly convert FilePreviewTableColumn[] to ColumnProps[]', () => {
+    const input: FilePreviewTableColumn[] = [{ name: 'name' }, { name: 'age' }];
+    const expectedOutput = [
+      { title: 'name', dataIndex: 'name', key: 'name', width: '100px' },
+      { title: 'age', dataIndex: 'age', key: 'age', width: '100px' }
+    ];
+
+    expect(convertToAntdColumns(input)).toEqual(expectedOutput);
   });
 
-  it('should correctly convert GuessFieldTypesColumn[] to ColumnProps[]', () => {
-    const input: GuessFieldTypesColumn[] = [{ name: 'name' }, { name: 'age' }];
+  it('should handle single column input', () => {
+    const input: FilePreviewTableColumn[] = [{ name: 'singleColumn' }];
+    const expectedOutput = [
+      { title: 'singleColumn', dataIndex: 'singleColumn', key: 'singleColumn', width: '100px' }
+    ];
+
+    expect(convertToAntdColumns(input)).toEqual(expectedOutput);
+  });
+
+  it('should handle columns with special characters', () => {
+    const input: FilePreviewTableColumn[] = [
+      { name: 'first-name' },
+      { name: 'email_address' },
+      { name: 'phone number' }
+    ];
+    const expectedOutput = [
+      { title: 'first-name', dataIndex: 'firstName', key: 'first-name', width: '100px' },
+      { title: 'email_address', dataIndex: 'emailAddress', key: 'email_address', width: '100px' },
+      { title: 'phone number', dataIndex: 'phoneNumber', key: 'phone number', width: '100px' }
+    ];
+
+    expect(convertToAntdColumns(input)).toEqual(expectedOutput);
+  });
+
+  it('should handle columns with numbers and special characters', () => {
+    const input: FilePreviewTableColumn[] = [
+      { name: 'column1' },
+      { name: '2nd_column' },
+      { name: 'column-3' }
+    ];
+    const expectedOutput = [
+      { title: 'column1', dataIndex: 'column1', key: 'column1', width: '100px' },
+      { title: '2nd_column', dataIndex: '2NdColumn', key: '2nd_column', width: '100px' },
+      { title: 'column-3', dataIndex: 'column3', key: 'column-3', width: '100px' }
+    ];
+
+    expect(convertToAntdColumns(input)).toEqual(expectedOutput);
+  });
+
+  it('should handle columns with type property', () => {
+    const input: FilePreviewTableColumn[] = [
+      { name: 'name', type: 'string' },
+      { name: 'age', type: 'number' }
+    ];
     const expectedOutput = [
       { title: 'name', dataIndex: 'name', key: 'name', width: '100px' },
       { title: 'age', dataIndex: 'age', key: 'age', width: '100px' }
@@ -35,70 +82,210 @@ describe('convertToAntdColumns', () => {
 });
 
 describe('convertToDataSource', () => {
-  const columns: ColumnProps<ImporterTableData>[] = [
-    { title: 'Name', dataIndex: 'name', key: 'name', width: '100px' },
-    { title: 'Age', dataIndex: 'age', key: 'age', width: '100px' }
-  ];
-
   it('should return an empty array when no apiResponse is provided', () => {
-    expect(convertToDataSource(columns)).toEqual([]);
+    expect(convertToDataSource({})).toEqual([]);
   });
 
-  it('should correctly convert apiResponse to GuessFieldTypesColumn[]', () => {
-    const apiResponse: string[][] = [
-      ['Alice', '30'],
-      ['Bob', '25']
+  it('should correctly convert apiResponse to FilePreviewTableColumn[]', () => {
+    const apiResponse: FilePreviewTableData = {
+      name: ['Alice', 'Bob'],
+      age: ['30', '25']
+    };
+
+    const expectedOutput = [
+      { importerDataKey: 'importer-row__0', name: 'Alice', age: '30' },
+      { importerDataKey: 'importer-row__1', name: 'Bob', age: '25' }
     ];
 
+    expect(convertToDataSource(apiResponse)).toEqual(expectedOutput);
+  });
+
+  it('should handle uneven array lengths with null values', () => {
+    const apiResponse: FilePreviewTableData = {
+      name: ['Alice', 'Bob', 'Charlie'],
+      age: ['30', '25'],
+      city: ['New York']
+    };
+
     const expectedOutput = [
-      { importerDataKey: 'Alice__0', name: 'Alice', age: '30' },
-      { importerDataKey: 'Bob__1', name: 'Bob', age: '25' }
+      { importerDataKey: 'importer-row__0', name: 'Alice', age: '30', city: 'New York' },
+      { importerDataKey: 'importer-row__1', name: 'Bob', age: '25', city: null },
+      { importerDataKey: 'importer-row__2', name: 'Charlie', age: null, city: null }
     ];
 
-    expect(convertToDataSource(columns, apiResponse)).toEqual(expectedOutput);
+    expect(convertToDataSource(apiResponse)).toEqual(expectedOutput);
+  });
+
+  it('should handle empty arrays in apiResponse', () => {
+    const apiResponse: FilePreviewTableData = {
+      name: [],
+      age: []
+    };
+
+    expect(convertToDataSource(apiResponse)).toEqual([]);
   });
 });
 
 describe('getDefaultTableName', () => {
-  it('should extract file name from LOCALFILE path using pattern', () => {
-    const filePath = '/user/data/:myDocument;v1.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.LOCAL);
+  it('should extract file name from LOCAL file metadata with fileName provided', () => {
+    const fileMetaData = {
+      path: '/user/data/myDocument.csv',
+      fileName: 'myDocument.csv',
+      source: ImporterFileSource.LOCAL
+    };
+    const result = getDefaultTableName(fileMetaData);
     expect(result).toBe('myDocument');
   });
 
-  it('should return empty string if LOCALFILE pattern does not match', () => {
-    const filePath = '/user/data/myDocument.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.LOCAL);
+  it('should return empty string if LOCAL file has no fileName property', () => {
+    const fileMetaData = {
+      path: '/user/data/myDocument.csv',
+      source: ImporterFileSource.LOCAL
+    };
+    const result = getDefaultTableName(fileMetaData);
     expect(result).toBe('');
   });
 
+  it('should handle LOCAL file with complex filename', () => {
+    const fileMetaData = {
+      path: '/user/data/complex-file_name.with.dots.xlsx',
+      fileName: 'complex-file_name.with.dots.xlsx',
+      source: ImporterFileSource.LOCAL
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('complex_file_name_with_dots');
+  });
+
+  it('should handle LOCAL file with special characters in filename', () => {
+    const fileMetaData = {
+      path: '/user/data/file@2023#test.csv',
+      fileName: 'file@2023#test.csv',
+      source: ImporterFileSource.LOCAL
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('file_2023_test');
+  });
+
+  it('should handle LOCAL file with spaces in filename', () => {
+    const fileMetaData = {
+      path: '/user/data/my file name.csv',
+      fileName: 'my file name.csv',
+      source: ImporterFileSource.LOCAL
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('my_file_name');
+  });
+
   it('should extract file name from REMOTE path as last part of path', () => {
-    const filePath = 'https://demo.gethue.com/hue/test-file.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.REMOTE);
-    expect(result).toBe('test-file');
+    const fileMetaData = {
+      path: '/user/data/test-file.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('test_file');
   });
 
   it('should handle file names with multiple dots correctly', () => {
-    const filePath = 'https://demo.gethue.com/hue/test.file.name.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.REMOTE);
+    const fileMetaData = {
+      path: '/user/data/test.file.name.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
     expect(result).toBe('test_file_name');
   });
 
   it('should handle file names with no extension correctly', () => {
-    const filePath = 'https://demo.gethue.com/hue/test-file';
-    const result = getDefaultTableName(filePath, ImporterFileSource.REMOTE);
-    expect(result).toBe('test-file');
+    const fileMetaData = {
+      path: '/user/data/test-file',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('test_file');
   });
 
   it('should handle file names with special characters correctly', () => {
-    const filePath = 'https://demo.gethue.com/hue/test-file@2023.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.REMOTE);
-    expect(result).toBe('test-file@2023');
+    const fileMetaData = {
+      path: '/user/data/test-file@2023.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('test_file_2023');
   });
 
   it('should handle file names with spaces correctly', () => {
-    const filePath = 'https://demo.gethue.com/hue/test file.csv';
-    const result = getDefaultTableName(filePath, ImporterFileSource.REMOTE);
-    expect(result).toBe('test file');
+    const fileMetaData = {
+      path: '/user/data/test file.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('test_file');
+  });
+
+  it('should handle REMOTE files with only extension', () => {
+    const fileMetaData = {
+      path: '/user/data/.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('');
+  });
+
+  it('should handle REMOTE files with multiple extensions', () => {
+    const fileMetaData = {
+      path: '/user/data/file.backup.tar.gz',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('file_backup_tar');
+  });
+
+  it('should handle REMOTE files with numbers and underscores', () => {
+    const fileMetaData = {
+      path: '/user/data/data_file_2023_v1.xlsx',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('data_file_2023_v1');
+  });
+
+  it('should handle S3 URLs', () => {
+    const fileMetaData = {
+      path: 's3://bucket-name/folder/data-file.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('data_file');
+  });
+
+  it('should handle Azure blob URLs', () => {
+    const fileMetaData = {
+      path: 'abfs://container/data.file.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('data_file');
+  });
+
+  it('should return empty string for empty file paths', () => {
+    const localFileMetaData = {
+      path: '',
+      fileName: '',
+      source: ImporterFileSource.LOCAL
+    };
+    const remoteFileMetaData = {
+      path: '',
+      source: ImporterFileSource.REMOTE
+    };
+    expect(getDefaultTableName(localFileMetaData)).toBe('');
+    expect(getDefaultTableName(remoteFileMetaData)).toBe('');
+  });
+
+  it('should handle single character file names', () => {
+    const fileMetaData = {
+      path: '/user/data/a.csv',
+      source: ImporterFileSource.REMOTE
+    };
+    const result = getDefaultTableName(fileMetaData);
+    expect(result).toBe('a');
   });
 });

+ 39 - 30
desktop/core/src/desktop/js/apps/newimporter/utils/utils.ts

@@ -15,57 +15,66 @@
 // limitations under the License.
 
 import { type ColumnProps } from 'cuix/dist/components/Table';
-import { ImporterFileSource, GuessFieldTypesColumn, ImporterTableData } from '../types';
+import {
+  ImporterFileSource,
+  FilePreviewTableColumn,
+  ImporterTableData,
+  FilePreviewResponse,
+  FileMetaData
+} from '../types';
 import { getLastDirOrFileNameFromPath } from '../../../reactComponents/PathBrowser/PathBrowser.util';
+import { toCamelCase } from '../../../utils/string/changeCasing';
 
 export const convertToAntdColumns = (
-  input?: GuessFieldTypesColumn[]
+  input?: FilePreviewTableColumn[]
 ): ColumnProps<ImporterTableData>[] => {
   if (!input) {
     return [];
   }
   return input?.map(item => ({
     title: item.name,
-    dataIndex: item.name,
+    dataIndex: toCamelCase(item.name),
     key: item.name,
     width: '100px'
   }));
 };
 
 export const convertToDataSource = (
-  columns: ColumnProps<ImporterTableData>[],
-  apiResponse?: string[][]
+  inputData: FilePreviewResponse['previewData']
 ): ImporterTableData[] => {
-  if (!apiResponse) {
-    return [];
-  }
-  return apiResponse?.map((rowData, index) => {
-    const row = {
-      importerDataKey: `${rowData[0]}__${index}` // this ensure the key is unique
+  const maxLength = Math.max(...Object.values(inputData).map(arr => arr.length));
+
+  const data = Array.from({ length: maxLength }, (_, index) => {
+    const row: ImporterTableData = {
+      importerDataKey: `importer-row__${index}`
     };
-    columns.forEach((column, index) => {
-      if (column.key) {
-        row[column.key] = rowData[index];
-      }
+    Object.keys(inputData).forEach(key => {
+      row[key] = inputData[key][index] ?? null;
     });
     return row;
   });
+
+  return data;
 };
 
-export const getDefaultTableName = (filePath: string, fileSource: ImporterFileSource): string => {
-  // For local files, the file name is extracted from the path
-  // Example: /**/**/**:fileName;**.fileExtension
-  if (fileSource === ImporterFileSource.LOCAL) {
-    const match = filePath.match(/:(.*?);/);
-    return match?.[1] ?? '';
-  }
+const sanitizeTableName = (name: string): string => {
+  return name
+    .replace(/[^a-zA-Z0-9]/g, '_') // replace non-alphanumeric characters with underscores
+    .replace(/_+/g, '_') // replace multiple underscores with a single underscore
+    .replace(/^_+|_+$/g, ''); // remove leading and trailing underscores
+};
 
-  // For Remote, remove extension and replace '.' with '_'
-  // Example: file.name.fileExtension -> file_name
-  const fileName = getLastDirOrFileNameFromPath(filePath);
-  if (fileName.split('.').length === 1) {
-    // If there is no extension, return the file name as is
-    return fileName;
-  }
-  return fileName.split('.').slice(0, -1).join('_');
+const getLastDirOrFileNameWithoutExtension = (fileName: string): string => {
+  return fileName.split('.').length > 1 ? fileName.split('.').slice(0, -1).join('.') : fileName;
+};
+
+export const getDefaultTableName = (fileMetaData: FileMetaData): string => {
+  const rawFileName =
+    fileMetaData.source === ImporterFileSource.LOCAL
+      ? (fileMetaData.fileName ?? '')
+      : getLastDirOrFileNameFromPath(fileMetaData.path);
+
+  const fileNameWithoutExtension = getLastDirOrFileNameWithoutExtension(rawFileName);
+
+  return sanitizeTableName(fileNameWithoutExtension);
 };