Repository: cloudera/hue
Pull request #4123: [ui-importer] show field separator and other dropdown only for CSV
Author: login: ramprasadagarwal
Created: 2025-04-22T07:11:46Z | Merged: 2025-04-24T12:56:45Z

## What changes were proposed in this pull request?

- Field Separator, Record Separator, and Quote Character drop-downs are irrelevant when the file type is Excel or JSON. Hence, showing them only when the file type is CSV.

## How was this patch tested?

- Updated the unit tests

Please review [Hue Contributing Guide](https://github.com/cloudera/hue/blob/master/CONTRIBUTING.md) before opening a pull request.

<img width="1647" alt="image" src="https://github.com/user-attachments/assets/d4ed1ecc-2954-4a28-8155-664f7f4a5282" />

<img width="1667" alt="image" src="https://github.com/user-attachments/assets/db940b81-5a5d-49a4-8eae-4b39bd926216" />




Diff:
diff --git a/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx b/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx
index 44fcbdadc46..c663bdf8e20 100644
--- a/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx
+++ b/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.test.tsx
@@ -3,15 +3,14 @@ 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 } from '../../types';
-import { separator } from '../../constants';
+import { FileFormatResponse, ImporterFileTypes } from '../../types';
 
 describe('SourceConfiguration Component', () => {
   const mockSetFileFormat = jest.fn();
   const mockFileFormat: FileFormatResponse = {
     quoteChar: '"',
     recordSeparator: '\\n',
-    type: 'csv',
+    type: ImporterFileTypes.EXCEL,
     hasHeader: true,
     fieldSeparator: ',',
     status: 0
@@ -22,27 +21,59 @@ describe('SourceConfiguration Component', () => {
   });
 
   it('should render the component', () => {
-    const { getByText, getAllByRole } = render(
+    const { getByText } = render(
       <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
     );
     expect(getByText('Configure source')).toBeInTheDocument();
-    expect(getAllByRole('combobox')).toHaveLength(5);
   });
 
-  it('calls setFileFormat on option change', async () => {
+  it('should call setFileFormat on option change', async () => {
     const { getByText, getAllByRole } = render(
       <SourceConfiguration fileFormat={mockFileFormat} setFileFormat={mockSetFileFormat} />
     );
 
     const selectElement = getAllByRole('combobox')[0];
     await userEvent.click(selectElement);
-    fireEvent.click(getByText(separator[3].label));
+    fireEvent.click(getByText('CSV'));
 
     await waitFor(() =>
       expect(mockSetFileFormat).toHaveBeenCalledWith({
         ...mockFileFormat,
-        fieldSeparator: separator[3].value
+        type: ImporterFileTypes.CSV
       })
     );
   });
+
+  it('should show fieldSepator and other downdown when fileType is CSV', () => {
+    const { getAllByRole, getByText } = render(
+      <SourceConfiguration
+        fileFormat={{ ...mockFileFormat, type: ImporterFileTypes.CSV }}
+        setFileFormat={mockSetFileFormat}
+      />
+    );
+
+    const selectElement = getAllByRole('combobox');
+
+    expect(selectElement).toHaveLength(5);
+    expect(getByText('File Type')).toBeInTheDocument();
+    expect(getByText('Has Header')).toBeInTheDocument();
+    expect(getByText('Field Separator')).toBeInTheDocument();
+    expect(getByText('Record Separator')).toBeInTheDocument();
+    expect(getByText('Quote Character')).toBeInTheDocument();
+  });
+
+  it('should not show fieldSepator and other downdown when fileType is not CSV', () => {
+    const { getAllByRole, getByText, queryByText } = render(
+      <SourceConfiguration fileFormat={mockFileFormat} 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();
+  });
 });
diff --git a/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx b/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx
index a91150937bf..f7894346e01 100644
--- a/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx
+++ b/desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx
@@ -45,6 +45,8 @@ const SourceConfiguration = ({
     [fileFormat, setFileFormat]
   );
 
+  const filteredSourceConfigs = sourceConfigs.filter(config => !config.hidden?.(fileFormat?.type));
+
   return (
     <details className="hue-importer-configuration">
       <summary className="hue-importer-configuration__summary">
@@ -52,7 +54,7 @@ const SourceConfiguration = ({
         {t('Configure source')}
       </summary>
       <div className="hue-importer-configuration-options">
-        {sourceConfigs.map(config => (
+        {filteredSourceConfigs.map(config => (
           <div key={config.name}>
             <label htmlFor={config.name}>{t(config.label)}</label>
             <Select
diff --git a/desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx b/desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx
index 5360441130e..e5248971485 100644
--- a/desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx
+++ b/desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx
@@ -19,7 +19,7 @@ import Button from 'cuix/dist/components/Button';
 import DocumentationIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon';
 import { hueWindow } from 'types/types';
 
-import { FileMetaData, LocalFileUploadResponse } from '../types';
+import { FileMetaData, ImporterFileSource, LocalFileUploadResponse } from '../types';
 import { i18nReact } from '../../../utils/i18nReact';
 import { UPLOAD_LOCAL_FILE_API_URL } from '../api';
 import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
@@ -72,7 +72,7 @@ const ImporterSourceSelector = ({ setFileMetaData }: ImporterSourceSelectorProps
           setFileMetaData({
             path: data.local_file_url,
             type: data.file_type,
-            source: 'localfile'
+            source: ImporterFileSource.LOCAL
           });
         },
         onError: error => {
diff --git a/desktop/core/src/desktop/js/apps/newimporter/constants.ts b/desktop/core/src/desktop/js/apps/newimporter/constants.ts
index 93852496499..7d756f6e3c1 100644
--- a/desktop/core/src/desktop/js/apps/newimporter/constants.ts
+++ b/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 } from './types';
+import { FileFormatResponse, ImporterFileTypes } from './types';
 
 export const separator = [
   { value: ',', label: 'Comma (,)' },
@@ -32,38 +32,45 @@ export const separator = [
 export const sourceConfigs: {
   name: keyof FileFormatResponse;
   label: string;
-  options: { label: string; value: string | boolean }[];
+  hidden?: (type?: ImporterFileTypes) => boolean;
+  options: {
+    label: string;
+    value: string | boolean;
+  }[];
 }[] = [
+  {
+    name: 'type',
+    label: 'File Type',
+    options: [
+      { value: ImporterFileTypes.CSV, label: 'CSV' },
+      { value: ImporterFileTypes.JSON, label: 'JSON' },
+      { value: ImporterFileTypes.EXCEL, label: 'Excel' }
+    ]
+  },
+  {
+    name: 'hasHeader',
+    label: 'Has Header',
+    options: [
+      { value: true, label: 'Yes' },
+      { value: false, label: 'No' }
+    ]
+  },
   {
     name: 'fieldSeparator',
     label: 'Field Separator',
+    hidden: (type?: ImporterFileTypes) => type !== ImporterFileTypes.CSV,
     options: separator
   },
   {
     name: 'recordSeparator',
     label: 'Record Separator',
+    hidden: (type?: ImporterFileTypes) => type !== ImporterFileTypes.CSV,
     options: separator
   },
   {
     name: 'quoteChar',
     label: 'Quote Character',
+    hidden: (type?: ImporterFileTypes) => type !== ImporterFileTypes.CSV,
     options: separator
-  },
-  {
-    name: 'hasHeader',
-    label: 'Has Header',
-    options: [
-      { value: true, label: 'Yes' },
-      { value: false, label: 'No' }
-    ]
-  },
-  {
-    name: 'type',
-    label: 'File Type',
-    options: [
-      { value: 'csv', label: 'CSV File' },
-      { value: 'json', label: 'JSON' },
-      { value: 'excel', label: 'Excel File' }
-    ]
   }
 ];
diff --git a/desktop/core/src/desktop/js/apps/newimporter/types.ts b/desktop/core/src/desktop/js/apps/newimporter/types.ts
index d1eb90b9b55..9d1360b9713 100644
--- a/desktop/core/src/desktop/js/apps/newimporter/types.ts
+++ b/desktop/core/src/desktop/js/apps/newimporter/types.ts
@@ -14,9 +14,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+export const enum ImporterFileTypes {
+  CSV = 'csv',
+  JSON = 'json',
+  EXCEL = 'excel'
+}
+
+export enum ImporterFileSource {
+  LOCAL = 'localfile',
+  REMOTE = 'file'
+}
+
 export interface LocalFileUploadResponse {
   local_file_url: string;
-  file_type: string;
+  file_type: ImporterFileTypes;
 }
 
 export interface FileFormatResponse {
@@ -25,13 +36,13 @@ export interface FileFormatResponse {
   quoteChar: string;
   recordSeparator: string;
   status: number;
-  type: string;
+  type: ImporterFileTypes;
 }
 
 export interface FileMetaData {
   path: string;
-  type?: string;
-  source: 'localfile' | 'file';
+  type: ImporterFileTypes;
+  source: ImporterFileSource;
 }
 
 export type GuessFieldTypesColumn = {


Reviews:
user: login: tabraiz12
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T08:55:16Z
user: login: bjornalm
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T08:40:25Z
user: login: bjornalm
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T10:31:17Z
user: login: tabraiz12
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T12:45:25Z

Review comments:
diff_hunk: @@ -52,20 +52,22 @@ const SourceConfiguration = ({
         {t('Configure source')}
       </summary>
       <div className="hue-importer-configuration-options">
-        {sourceConfigs.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))
path: desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx
user: login: tabraiz12
body: filter it outside so that this filter doesn't get triggered on every render
created_at: 2025-04-23T08:54:18Z
author_association: COLLABORATOR
diff_hunk: @@ -45,14 +45,19 @@ const SourceConfiguration = ({
     [fileFormat, setFileFormat]
   );
 
+  const filteredSourceConfigs = useMemo(
path: desktop/core/src/desktop/js/apps/newimporter/ImporterFilePreview/SourceConfiguration/SourceConfiguration.tsx
user: login: bjornalm
body: This feels like a reoccurring discussion, also adding @tabraiz12 here :-) 

If sourceConfigs were very large (hundreds/thousands of items), or the filter function itself was very complex/expensive, then useMemo would be justified. This is very basic computation on a tiny array of 5 items. 

Why am I bringing this up? Dependency tracking and managing the cache comes with a (small, but real) overhead and adds complexity to the code. When the computation itself (here, filtering 5 items) is cheaper than the overhead of memoization, you’re making things more complicated, not faster. In your specific case, the filter function is extremely fast, so you’re paying a cost (React’s memoization management) that might actually (on a microscopic level) decrease performance .

But I think more importantly, you are potentially fooling future maintainers, since at first glance they could assume that there actually is some non obvious reason for applying memoization and then spend time trying to figure out what that is. 

Bottom line; be more conservative with these hooks. Apply them only when you know you need them.

https://react.dev/reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive
https://react.dev/reference/react/useMemo#should-you-add-usememo-everywhere

created_at: 2025-04-24T08:22:02Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-22T07:12:06Z
body: <!-- add-pr-comment:add-pr-comment -->

✅ Test files were modified. Ensure that the tests cover all relevant changes. ✅
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T07:49:01Z
body: <!-- Pytest Coverage Comment: pytest-codecov-comment -->
<img alt="Python Code Coverage" src="https://img.shields.io/badge/Python Code Coverage-49%25-orange.svg" /><details><summary>Python Coverage Report • </summary><table><tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th><th>Missing</th></tr><tbody><tr><td><b>TOTAL</b></td><td><b>53689</b></td><td><b>26993</b></td><td><b>49%</b></td><td>&nbsp;</td></tr></tbody></table><i>report-only-changed-files is enabled. No files were changed during this commit :)</i></details>

## Pytest Report
| Tests | Skipped | Failures | Errors | Time |
| ----- | ------- | -------- | -------- | ------------------ |
| 1090 | 106 :zzz: | 0 :x: | 0 :fire: | 5m 54s :stopwatch: |

author_association: NONE