Jelajahi Sumber

[ui-importer] Add settings tab (#4212)

Ram Prasad Agarwal 3 bulan lalu
induk
melakukan
06cd0048ee

+ 31 - 14
desktop/core/src/desktop/js/apps/newimporter/FileImportTabs/FileImportTabs.tsx

@@ -26,22 +26,37 @@ 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';
+import SettingsTab from '../SettingsTab/SettingsTab';
+import { ImporterSettings, StoreLocation, TableFormat } from '../types';
 
 interface FileImportTabsProps {
   fileMetaData: FileMetaData;
-  onTabChange?: (activeKey: string) => void;
-  defaultActiveKey?: string;
 }
 
-const FileImportTabs = ({
-  fileMetaData,
-  defaultActiveKey = 'preview'
-}: FileImportTabsProps): JSX.Element => {
+const FileImportTabs = ({ fileMetaData }: FileImportTabsProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
   const [destinationConfig, setDestinationConfig] = useState<DestinationConfig>({
     tableName: getDefaultTableName(fileMetaData)
   });
+  const [settings, setSettings] = useState<ImporterSettings>({
+    storeLocation: StoreLocation.MANAGED,
+    isTransactional: false,
+    isInsertOnly: false,
+    externalLocation: '',
+    importData: true,
+    isIcebergTable: false,
+    isCopyFile: false,
+    description: '',
+    tableFormat: TableFormat.TEXT,
+    primaryKeys: [],
+    createEmptyTable: false,
+    useExternalLocation: false,
+    customCharDelimiters: false,
+    fieldDelimiter: '',
+    arrayMapDelimiter: '',
+    structDelimiter: ''
+  });
 
   const handleDestinationConfigChange = (name: string, value: string) => {
     setDestinationConfig(prevConfig => ({
@@ -88,12 +103,18 @@ const FileImportTabs = ({
     },
     {
       label: t('Settings'),
-      key: 'settings'
-      // children: <SettingsTab />
+      key: 'settings',
+      children: (
+        <SettingsTab
+          fileMetaData={fileMetaData}
+          settings={settings}
+          onSettingsChange={setSettings}
+        />
+      )
     },
     {
       label: t('Partitions'),
-      key: 'partitions'
+      key: 'partition'
       // children: <PartitionsTab />
     }
   ];
@@ -111,11 +132,7 @@ const FileImportTabs = ({
           </PrimaryButton>
         </div>
       </div>
-      <Tabs
-        defaultActiveKey={defaultActiveKey}
-        items={tabItems}
-        className="hue-file-import-tabs__tabs"
-      />
+      <Tabs items={tabItems} className="hue-file-import-tabs__tabs" />
     </div>
   );
 };

+ 59 - 0
desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTab.scss

@@ -0,0 +1,59 @@
+// 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-settings-tab {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    padding: vars.$cdl-spacing-s;
+    background-color: vars.$cdl-white;
+
+    &__form {
+      display: flex;
+      flex-direction: column;
+      gap: vars.$cdl-spacing-m;
+    }
+
+    &__section {
+      display: flex;
+      flex-direction: column;
+      gap: vars.$cdl-spacing-xs;
+
+      &__title {
+        font-size: vars.$font-size-lg;
+        font-weight: vars.$cdl-text-default-bold-weight;
+        color: vars.$cdl-gray-900;
+      }
+
+      &__single-row {
+        display: flex;
+        flex-direction: row;
+        flex: 1;
+        width: 100%;
+        gap: vars.$cdl-spacing-m;
+      }
+
+      &__fields {
+        display: flex;
+        flex-direction: column;
+        gap: vars.$cdl-spacing-xs;
+      }
+    }
+  }
+}

+ 276 - 0
desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTab.test.tsx

@@ -0,0 +1,276 @@
+// 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 } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import SettingsTab from './SettingsTab';
+import {
+  FileMetaData,
+  ImporterFileSource,
+  ImporterSettings,
+  StoreLocation,
+  TableFormat
+} from '../types';
+
+describe('SettingsTab', () => {
+  const mockOnSettingsChange = jest.fn();
+
+  const defaultFileMetaData: FileMetaData = {
+    path: '/test/path/file.csv',
+    fileName: 'file.csv',
+    source: ImporterFileSource.REMOTE
+  };
+
+  const defaultSettings: ImporterSettings = {
+    storeLocation: StoreLocation.MANAGED,
+    isTransactional: false,
+    isInsertOnly: false,
+    externalLocation: '',
+    importData: true,
+    isIcebergTable: false,
+    isCopyFile: false,
+    description: '',
+    tableFormat: TableFormat.TEXT,
+    primaryKeys: [],
+    createEmptyTable: false,
+    useExternalLocation: false,
+    customCharDelimiters: false,
+    fieldDelimiter: 'new_line',
+    arrayMapDelimiter: 'comma',
+    structDelimiter: 'tab'
+  };
+
+  const defaultProps = {
+    fileMetaData: defaultFileMetaData,
+    settings: defaultSettings,
+    onSettingsChange: mockOnSettingsChange
+  };
+
+  beforeEach(() => {
+    mockOnSettingsChange.mockClear();
+  });
+
+  it('should render the settings tab with all sections', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(document.querySelector('.hue-importer-settings-tab')).toBeInTheDocument();
+    expect(document.querySelector('.hue-importer-settings-tab__form')).toBeInTheDocument();
+
+    expect(screen.getByText('Properties')).toBeInTheDocument();
+    expect(screen.getByText('Character Delimiters')).toBeInTheDocument();
+  });
+
+  it('should call onSettingsChange when description changes', async () => {
+    const user = userEvent.setup();
+    render(<SettingsTab {...defaultProps} />);
+
+    const descriptionInput = screen.getByLabelText('Description');
+    await user.type(descriptionInput, 'Test description');
+
+    expect(mockOnSettingsChange).toHaveBeenCalledWith({
+      ...defaultSettings,
+      description: 'Test description'
+    });
+  });
+
+  it('should call onSettingsChange when checkbox is toggled', async () => {
+    const user = userEvent.setup();
+    render(<SettingsTab {...defaultProps} />);
+
+    const createEmptyTableCheckbox = screen.getByRole('checkbox', {
+      name: /create empty table/i
+    });
+    await user.click(createEmptyTableCheckbox);
+
+    expect(mockOnSettingsChange).toHaveBeenCalledWith({
+      ...defaultSettings,
+      createEmptyTable: true
+    });
+  });
+
+  it('should call onSettingsChange when external location checkbox is toggled', async () => {
+    const user = userEvent.setup();
+    render(<SettingsTab {...defaultProps} />);
+
+    const useExternalLocationCheckbox = screen.getByRole('checkbox', {
+      name: /use external location instead of default/i
+    });
+    await user.click(useExternalLocationCheckbox);
+
+    expect(mockOnSettingsChange).toHaveBeenCalledWith({
+      ...defaultSettings,
+      useExternalLocation: true
+    });
+  });
+
+  it('should call onSettingsChange when custom char delimiters checkbox is toggled', async () => {
+    const user = userEvent.setup();
+    render(<SettingsTab {...defaultProps} />);
+
+    const customCharDelimitersCheckbox = screen.getByRole('checkbox', {
+      name: /custom char delimiters/i
+    });
+    await user.click(customCharDelimitersCheckbox);
+
+    expect(mockOnSettingsChange).toHaveBeenCalledWith({
+      ...defaultSettings,
+      customCharDelimiters: true
+    });
+  });
+
+  it('should hide transactional table checkbox for Kudu tables', () => {
+    const settingsWithKudu = {
+      ...defaultSettings,
+      tableFormat: TableFormat.KUDU
+    };
+
+    render(<SettingsTab {...defaultProps} settings={settingsWithKudu} />);
+
+    expect(screen.queryByRole('checkbox', { name: /transactional table/i })).toBeNull();
+  });
+
+  it('should hide transactional table checkbox for remote files', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.queryByRole('checkbox', { name: /transactional table/i })).toBeNull();
+  });
+
+  it('should show transactional table checkbox for local files', () => {
+    const localFileMetaData = {
+      ...defaultFileMetaData,
+      source: ImporterFileSource.LOCAL
+    };
+
+    render(<SettingsTab {...defaultProps} fileMetaData={localFileMetaData} />);
+
+    expect(screen.getByRole('checkbox', { name: /transactional table/i })).toBeInTheDocument();
+  });
+
+  it('should show insert only checkbox when transactional is enabled', () => {
+    const transactionalSettings = {
+      ...defaultSettings,
+      isTransactional: true
+    };
+
+    render(<SettingsTab {...defaultProps} settings={transactionalSettings} />);
+
+    expect(screen.getByRole('checkbox', { name: /insert only/i })).toBeInTheDocument();
+  });
+
+  it('should hide insert only checkbox when transactional is disabled', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.queryByRole('checkbox', { name: /insert only/i })).toBeNull();
+  });
+
+  it('should show iceberg table checkbox for remote tables when iceberg is enabled', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.getByRole('checkbox', { name: /iceberg table/i })).toBeInTheDocument();
+  });
+
+  it('should hide iceberg table checkbox for local files', () => {
+    const localFileMetaData = {
+      ...defaultFileMetaData,
+      source: ImporterFileSource.LOCAL
+    };
+
+    render(<SettingsTab {...defaultProps} fileMetaData={localFileMetaData} />);
+
+    expect(screen.queryByRole('checkbox', { name: /iceberg table/i })).toBeNull();
+  });
+
+  it('should show copy file checkbox for external remote tables', () => {
+    const externalSettings = {
+      ...defaultSettings,
+      storeLocation: StoreLocation.EXTERNAL
+    };
+
+    render(<SettingsTab {...defaultProps} settings={externalSettings} />);
+
+    expect(screen.getByRole('checkbox', { name: /copy file/i })).toBeInTheDocument();
+  });
+
+  it('should hide copy file checkbox for managed tables', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.queryByRole('checkbox', { name: /copy file/i })).toBeNull();
+  });
+
+  it('should hide copy file checkbox when transactional is enabled', () => {
+    const transactionalSettings = {
+      ...defaultSettings,
+      isTransactional: true
+    };
+
+    render(<SettingsTab {...defaultProps} settings={transactionalSettings} />);
+
+    expect(screen.queryByRole('checkbox', { name: /copy file/i })).toBeNull();
+  });
+
+  it('should hide copy file checkbox when importData is false', () => {
+    const noImportSettings = {
+      ...defaultSettings,
+      importData: false
+    };
+
+    render(<SettingsTab {...defaultProps} settings={noImportSettings} />);
+
+    expect(screen.queryByRole('checkbox', { name: /copy file/i })).toBeNull();
+  });
+
+  it('should show external location input when useExternalLocation is enabled', () => {
+    const externalLocationSettings = {
+      ...defaultSettings,
+      useExternalLocation: true
+    };
+
+    render(<SettingsTab {...defaultProps} settings={externalLocationSettings} />);
+
+    const externalLocationInput = screen.getByPlaceholderText('External location');
+    expect(externalLocationInput).toBeInTheDocument();
+  });
+
+  it('should hide external location input when useExternalLocation is disabled', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.queryByPlaceholderText('External location')).not.toBeInTheDocument();
+  });
+
+  it('should show delimiter fields when customCharDelimiters is enabled', () => {
+    const customDelimiterSettings = {
+      ...defaultSettings,
+      customCharDelimiters: true
+    };
+
+    render(<SettingsTab {...defaultProps} settings={customDelimiterSettings} />);
+
+    expect(screen.getByRole('combobox', { name: /field/i })).toBeInTheDocument();
+    expect(screen.getByRole('combobox', { name: /array map/i })).toBeInTheDocument();
+    expect(screen.getByRole('combobox', { name: /struct/i })).toBeInTheDocument();
+  });
+
+  it('should hide delimiter fields when customCharDelimiters is disabled', () => {
+    render(<SettingsTab {...defaultProps} />);
+
+    expect(screen.queryByRole('combobox', { name: /field/i })).not.toBeInTheDocument();
+    expect(screen.queryByRole('combobox', { name: /array map/i })).not.toBeInTheDocument();
+    expect(screen.queryByRole('combobox', { name: /struct/i })).not.toBeInTheDocument();
+  });
+});

+ 83 - 0
desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTab.tsx

@@ -0,0 +1,83 @@
+// 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 { Form } from 'antd';
+
+import { FileMetaData, ImporterFileSource, ImporterSettings, SettingsContext } from '../types';
+import { ADVANCED_SETTINGS_CONFIG, SettingsFieldConfig } from './SettingsTabConfig';
+import FormInput from '../../../reactComponents/FormInput/FormInput';
+import { i18nReact } from '../../../utils/i18nReact';
+
+import './SettingsTab.scss';
+
+interface SettingsTabProps {
+  fileMetaData: FileMetaData;
+  settings: ImporterSettings;
+  onSettingsChange: (settings: ImporterSettings) => void;
+}
+
+const SettingsTab = ({
+  fileMetaData,
+  settings,
+  onSettingsChange
+}: SettingsTabProps): JSX.Element => {
+  const { t } = i18nReact.useTranslation();
+
+  const handleChange = (fieldId: string, value: string | boolean | string[]) => {
+    onSettingsChange({
+      ...settings,
+      [fieldId]: value
+    });
+  };
+
+  // Create visibility context
+  const context: SettingsContext = {
+    ...settings,
+    isRemoteTable: fileMetaData.source === ImporterFileSource.REMOTE,
+    isIcebergEnabled: true // TODO: Get from config
+  };
+
+  const renderSection = (fields: SettingsFieldConfig[], title?: string, isSingleRow?: boolean) => (
+    <div className="hue-importer-settings-tab__section">
+      {title && <div className="hue-importer-settings-tab__section__title">{title}</div>}
+      <div
+        className={`${isSingleRow ? 'hue-importer-settings-tab__section__single-row' : 'hue-importer-settings-tab__section__fields'}`}
+      >
+        {fields
+          .filter(field => !field.isHidden?.(context))
+          .map(field => (
+            <FormInput<string> key={field.name} field={field} onChange={handleChange} />
+          ))}
+      </div>
+    </div>
+  );
+
+  return (
+    <div className="hue-importer-settings-tab">
+      <Form layout="vertical" className="hue-importer-settings-tab__form">
+        {renderSection(ADVANCED_SETTINGS_CONFIG.description)}
+        {renderSection(ADVANCED_SETTINGS_CONFIG.properties, t('Properties'))}
+        <div>
+          {renderSection(ADVANCED_SETTINGS_CONFIG.characterDelimiters, t('Character Delimiters'))}
+          {renderSection(ADVANCED_SETTINGS_CONFIG.delimiters, undefined, true)}
+        </div>
+      </Form>
+    </div>
+  );
+};
+
+export default SettingsTab;

+ 164 - 0
desktop/core/src/desktop/js/apps/newimporter/SettingsTab/SettingsTabConfig.ts

@@ -0,0 +1,164 @@
+// 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 { FieldConfig, FieldOption, FieldType } from '../../../reactComponents/FormInput/FormInput';
+import { SettingsContext, StoreLocation, TableFormat } from '../types';
+
+export const TABLE_FORMAT_OPTIONS: FieldOption[] = [
+  { value: TableFormat.TEXT, label: 'Text' },
+  { value: TableFormat.PARQUET, label: 'Parquet' },
+  { value: TableFormat.AVRO, label: 'Avro' },
+  { value: TableFormat.ORC, label: 'ORC' },
+  { value: TableFormat.JSON, label: 'JSON' },
+  { value: TableFormat.KUDU, label: 'Kudu' },
+  { value: TableFormat.CSV, label: 'CSV' }
+];
+
+export const DELIMITER_OPTIONS: FieldOption[] = [
+  { value: 'new_line', label: 'New line' },
+  { value: 'comma', label: 'Comma' },
+  { value: 'tab', label: 'Tab' },
+  { value: 'semicolon', label: 'Semicolon' },
+  { value: 'pipe', label: 'Pipe' },
+  { value: 'custom', label: 'Custom' }
+];
+
+export const EXTERNAL_LOCATION_OPTIONS: FieldOption[] = [
+  { value: 'hdfs', label: 'HDFS' },
+  { value: 's3', label: 'S3' },
+  { value: 'abfs', label: 'ABFS' },
+  { value: 'custom', label: 'Custom' }
+];
+
+export interface SettingsFieldConfig extends FieldConfig {
+  isHidden?: (context: SettingsContext) => boolean;
+}
+
+export const ADVANCED_SETTINGS_CONFIG: Record<string, SettingsFieldConfig[]> = {
+  description: [
+    {
+      name: 'description',
+      type: FieldType.INPUT,
+      label: 'Description',
+      placeholder: "A table to store customer data imported from the marketing team's CRM.",
+      tooltip:
+        "This description will be used as the comment for the new database table. It helps other developers understand the table's purpose."
+    }
+  ],
+  properties: [
+    {
+      name: 'tableFormat',
+      type: FieldType.SELECT,
+      label: 'Format',
+      placeholder: 'Choose an option',
+      options: TABLE_FORMAT_OPTIONS,
+      tooltip: 'Format of the table'
+    },
+    {
+      name: 'createEmptyTable',
+      type: FieldType.CHECKBOX,
+      label: 'Create empty table',
+      tooltip: 'This will only create the table and not import any data'
+    },
+    {
+      name: 'isTransactional',
+      type: FieldType.CHECKBOX,
+      label: 'Transactional table',
+      tooltip: 'Transactional table',
+      isHidden: (context: SettingsContext) =>
+        context.tableFormat === TableFormat.KUDU ||
+        context.isRemoteTable ||
+        context.isIcebergTable ||
+        context.isCopyFile
+    },
+    {
+      name: 'isInsertOnly',
+      type: FieldType.CHECKBOX,
+      label: 'Insert only',
+      tooltip:
+        'Table will be created with insert only mode, when disabled, the table will be created with insert, delete and update mode',
+      isHidden: (context: SettingsContext) => !context.isTransactional
+    },
+    {
+      name: 'isIcebergTable',
+      type: FieldType.CHECKBOX,
+      label: 'Iceberg table',
+      isHidden: (context: SettingsContext) => !context.isIcebergEnabled || !context.isRemoteTable
+    },
+    {
+      name: 'isCopyFile',
+      type: FieldType.CHECKBOX,
+      label: 'Copy file',
+      tooltip:
+        'Choosing this option will copy the file instead of moving it to the new location, and ensuring the original file remains unchanged.',
+      isHidden: (context: SettingsContext) =>
+        context.storeLocation === StoreLocation.MANAGED ||
+        context.isTransactional ||
+        !context.isRemoteTable ||
+        context.importData === false
+    },
+    {
+      name: 'useExternalLocation',
+      type: FieldType.CHECKBOX,
+      label: 'Use external location instead of default',
+      tooltip: 'Use external location instead of default'
+    },
+    {
+      name: 'externalLocation',
+      type: FieldType.INPUT,
+      placeholder: 'External location',
+      isHidden: (context: SettingsContext) => !context.useExternalLocation
+    }
+  ],
+
+  characterDelimiters: [
+    {
+      name: 'customCharDelimiters',
+      type: FieldType.CHECKBOX,
+      label: 'Custom char delimiters',
+      tooltip: 'Custom char delimiters'
+    }
+  ],
+  delimiters: [
+    {
+      name: 'fieldDelimiter',
+      type: FieldType.SELECT,
+      label: 'Field',
+      placeholder: 'Choose an option',
+      options: DELIMITER_OPTIONS,
+      tooltip: 'Field delimiter',
+      isHidden: (context: SettingsContext) => !context.customCharDelimiters
+    },
+    {
+      name: 'arrayMapDelimiter',
+      type: FieldType.SELECT,
+      label: 'Array Map',
+      placeholder: 'Choose an option',
+      options: DELIMITER_OPTIONS,
+      tooltip: 'Array map delimiter',
+      isHidden: (context: SettingsContext) => !context.customCharDelimiters
+    },
+    {
+      name: 'structDelimiter',
+      type: FieldType.SELECT,
+      label: 'Struct',
+      placeholder: 'Choose an option',
+      options: DELIMITER_OPTIONS,
+      tooltip: 'Struct delimiter',
+      isHidden: (context: SettingsContext) => !context.customCharDelimiters
+    }
+  ]
+};

+ 41 - 0
desktop/core/src/desktop/js/apps/newimporter/types.ts

@@ -25,6 +25,21 @@ export enum ImporterFileSource {
   REMOTE = 'remote'
 }
 
+export enum TableFormat {
+  TEXT = 'text',
+  PARQUET = 'parquet',
+  AVRO = 'avro',
+  ORC = 'orc',
+  JSON = 'json',
+  KUDU = 'kudu',
+  CSV = 'csv'
+}
+
+export enum StoreLocation {
+  MANAGED = 'managed',
+  EXTERNAL = 'external'
+}
+
 export interface LocalFileUploadResponse {
   file_path: string;
 }
@@ -77,3 +92,29 @@ export interface DestinationConfig {
   connectorId?: string;
   computeId?: string;
 }
+
+// TODO: Verify the fields once Backend is ready
+export interface ImporterSettings {
+  storeLocation: StoreLocation;
+  isTransactional: boolean;
+  isInsertOnly: boolean;
+  externalLocation: string;
+  importData: boolean;
+  isIcebergTable: boolean;
+  isCopyFile: boolean;
+  description: string;
+  tableFormat: string;
+  primaryKeys: string[];
+  createEmptyTable: boolean;
+  useExternalLocation: boolean;
+  customCharDelimiters: boolean;
+  fieldDelimiter: string;
+  arrayMapDelimiter: string;
+  structDelimiter: string;
+}
+
+// TODO: verify if ImporterSettings can be used as context
+export interface SettingsContext extends ImporterSettings {
+  isRemoteTable: boolean;
+  isIcebergEnabled: boolean;
+}

+ 49 - 0
desktop/core/src/desktop/js/reactComponents/FormInput/FormInput.scss

@@ -0,0 +1,49 @@
+// 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-form-input {
+    width: 100%;
+    margin-bottom: 0;
+
+    &__checkbox {
+      display: flex;
+      align-items: center;
+      margin-bottom: 0;
+      gap: vars.$cdl-spacing-xs;
+
+      input {
+        width: fit-content;
+
+        &:focus {
+          box-shadow: none;
+        }
+      }
+    }
+
+    &__select {
+      border: 1px solid vars.$cdl-gray-600;
+      border-radius: vars.$border-radius-base;
+      width: 100%;
+    }
+  }
+
+  .hue-form-input__tooltip-icon {
+    margin-left: vars.$cdl-spacing-tiny;
+  }
+}

+ 21 - 45
desktop/core/src/desktop/js/reactComponents/FormInput/FormInput.test.tsx

@@ -18,18 +18,13 @@ import React from 'react';
 import { render, screen, fireEvent } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import '@testing-library/jest-dom';
-import FormInput, { FieldType, FieldConfig } from './FormInput';
-
-interface MockContext {
-  hideField: boolean;
-}
+import FormInput, { FieldType, BaseFieldConfig } from './FormInput';
 
 describe('FormInput Component', () => {
   const mockOnChange = jest.fn();
   const defaultProps = {
     loading: false,
-    onChange: mockOnChange,
-    className: 'test-class'
+    onChange: mockOnChange
   };
 
   beforeEach(() => {
@@ -37,11 +32,12 @@ describe('FormInput Component', () => {
   });
 
   describe('INPUT field type', () => {
-    const inputField: FieldConfig = {
+    const inputField: BaseFieldConfig = {
       name: 'testInput',
       type: FieldType.INPUT,
       label: 'Test Input',
-      placeholder: 'Enter text'
+      placeholder: 'Enter text',
+      className: 'test-class'
     };
 
     it('should render input field with correct props', () => {
@@ -69,7 +65,7 @@ describe('FormInput Component', () => {
     });
 
     it('should render tooltip icon when tooltip is provided', () => {
-      const inputWithTooltip: FieldConfig = {
+      const inputWithTooltip: BaseFieldConfig = {
         name: 'testInputTooltip',
         type: FieldType.INPUT,
         label: 'Input with Tooltip',
@@ -82,49 +78,35 @@ describe('FormInput Component', () => {
     });
 
     it('should handle visibility conditions - render when not hidden', () => {
-      const visibilityField: FieldConfig<MockContext> = {
+      const visibilityField: BaseFieldConfig = {
         name: 'conditionalField',
         type: FieldType.INPUT,
         label: 'Conditional Field',
-        isHidden: (context?: MockContext) => context?.hideField === true
+        hidden: false
       };
 
-      render(
-        <FormInput
-          field={visibilityField}
-          context={{ hideField: false }}
-          value=""
-          {...defaultProps}
-        />
-      );
+      render(<FormInput field={visibilityField} value="" {...defaultProps} />);
 
-      expect(screen.getByDisplayValue('')).toBeInTheDocument();
-      expect(screen.getByText('Conditional Field')).toBeInTheDocument();
+      expect(screen.queryByDisplayValue('')).toBeInTheDocument();
+      expect(screen.queryByRole('textbox', { name: /conditional field/i })).toBeInTheDocument();
     });
 
     it('should handle visibility conditions - not render when hidden', () => {
-      const visibilityField: FieldConfig<MockContext> = {
+      const visibilityField: BaseFieldConfig = {
         name: 'conditionalField',
         type: FieldType.INPUT,
         label: 'Conditional Field',
-        isHidden: (context?: MockContext) => context?.hideField === true
+        hidden: true
       };
 
-      render(
-        <FormInput
-          field={visibilityField}
-          context={{ hideField: true }}
-          value=""
-          {...defaultProps}
-        />
-      );
+      render(<FormInput field={visibilityField} value="" {...defaultProps} />);
 
-      expect(screen.queryByText('Conditional Field')).not.toBeInTheDocument();
+      expect(screen.queryByRole('textbox', { name: /conditional field/i })).not.toBeInTheDocument();
     });
   });
 
   describe('SELECT field type', () => {
-    const selectField: FieldConfig = {
+    const selectField: BaseFieldConfig = {
       name: 'testSelect',
       type: FieldType.SELECT,
       label: 'Test Select',
@@ -147,13 +129,7 @@ describe('FormInput Component', () => {
       const user = userEvent.setup();
       const onChangeSpy = jest.fn();
       render(
-        <FormInput
-          field={selectField}
-          value="option1"
-          onChange={onChangeSpy}
-          loading={false}
-          className="test-class"
-        />
+        <FormInput field={selectField} value="option1" onChange={onChangeSpy} loading={false} />
       );
 
       const select = screen.getByRole('combobox');
@@ -169,7 +145,7 @@ describe('FormInput Component', () => {
     });
 
     it('should render tooltip icon when tooltip is provided', () => {
-      const selectWithTooltip: FieldConfig = {
+      const selectWithTooltip: BaseFieldConfig = {
         name: 'testSelectTooltip',
         type: FieldType.SELECT,
         label: 'Select with Tooltip',
@@ -184,7 +160,7 @@ describe('FormInput Component', () => {
   });
 
   describe('CHECKBOX field type', () => {
-    const checkboxField: FieldConfig = {
+    const checkboxField: BaseFieldConfig = {
       name: 'testCheckbox',
       type: FieldType.CHECKBOX,
       label: 'Test Checkbox',
@@ -233,7 +209,7 @@ describe('FormInput Component', () => {
   });
 
   describe('RADIO field type', () => {
-    const radioField: FieldConfig = {
+    const radioField: BaseFieldConfig = {
       name: 'testRadio',
       type: FieldType.RADIO,
       label: 'Test Radio',
@@ -275,7 +251,7 @@ describe('FormInput Component', () => {
     });
 
     it('should render tooltip icon when tooltip is provided', () => {
-      const radioWithTooltip: FieldConfig = {
+      const radioWithTooltip: BaseFieldConfig = {
         name: 'testRadioTooltip',
         type: FieldType.RADIO,
         label: 'Radio with Tooltip',

+ 35 - 24
desktop/core/src/desktop/js/reactComponents/FormInput/FormInput.tsx

@@ -19,11 +19,13 @@ import Input from 'cuix/dist/components/Input';
 import Select from 'cuix/dist/components/Select';
 import { InfoCircleOutlined } from '@ant-design/icons';
 import { i18nReact } from '../../utils/i18nReact';
-import { Form, Radio, Tooltip } from 'antd';
+import { Form, Radio, Tooltip, Input as AntdInput } from 'antd';
+import './FormInput.scss';
 
 export enum FieldType {
   CHECKBOX = 'checkbox',
   INPUT = 'input',
+  TEXTAREA = 'textarea',
   SELECT = 'select',
   RADIO = 'radio'
 }
@@ -33,44 +35,41 @@ export interface FieldOption {
   label: string;
 }
 
-export interface FieldConfig<U = unknown> {
+export interface FieldConfig {
   name: string;
   type: FieldType;
   label?: string;
   placeholder?: string;
   tooltip?: string;
   options?: FieldOption[];
-  isHidden?: (context?: U) => boolean;
+  hidden?: boolean;
   style?: React.CSSProperties;
   nested?: boolean;
   parentField?: string;
+  className?: string;
 }
 
-interface FormInputProps<T = string, U = unknown> {
-  field: FieldConfig<U>;
-  context?: U;
+interface FormInputProps<T = string> {
+  field: FieldConfig;
   defaultValue?: T;
-  loading: boolean;
+  loading?: boolean;
   value?: T;
-  className?: string;
   onChange: (fieldId: string, value: T) => void;
   error?: boolean;
 }
 
-const FormInput = <T, U = unknown>({
+const FormInput = <T,>({
   field,
-  context,
   defaultValue,
   loading,
   value,
   onChange,
-  className,
   error
-}: FormInputProps<T, U>): JSX.Element => {
+}: FormInputProps<T>): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
   // Check if field should be hidden
-  if (field.isHidden && field.isHidden(context)) {
+  if (field.hidden) {
     return <></>;
   }
 
@@ -79,7 +78,7 @@ const FormInput = <T, U = unknown>({
       {field.label && t(field.label)}
       {field.tooltip && (
         <Tooltip title={t(field.tooltip)}>
-          <InfoCircleOutlined style={{ marginLeft: 4 }} />
+          <InfoCircleOutlined className="hue-form-input__tooltip-icon" />
         </Tooltip>
       )}
     </>
@@ -89,20 +88,18 @@ const FormInput = <T, U = unknown>({
     switch (field.type) {
       case FieldType.CHECKBOX:
         return (
-          <>
+          <div className="hue-form-input__checkbox">
             <Input
               type="checkbox"
               checked={value as boolean}
               onChange={e => onChange(field.name, e.target.checked as T)}
               defaultChecked={value === undefined ? (defaultValue as boolean) : undefined}
-              className={className}
+              className={field.className}
               status={error ? 'error' : undefined}
               id={field.name}
             />
-            <label htmlFor={field.name} style={{ marginLeft: 8 }}>
-              {renderLabel()}
-            </label>
-          </>
+            <label htmlFor={field.name}>{renderLabel()}</label>
+          </div>
         );
 
       case FieldType.INPUT:
@@ -111,9 +108,22 @@ const FormInput = <T, U = unknown>({
             value={value as string}
             onChange={e => onChange(field.name, e.target.value as T)}
             placeholder={field.placeholder ? t(field.placeholder) : undefined}
-            className={className}
+            className={field.className}
+            status={error ? 'error' : undefined}
+            id={field.name}
+          />
+        );
+
+      case FieldType.TEXTAREA:
+        return (
+          <AntdInput.TextArea
+            value={value as string}
+            onChange={e => onChange(field.name, e.target.value as T)}
+            placeholder={field.placeholder ? t(field.placeholder) : undefined}
+            className={field.className}
             status={error ? 'error' : undefined}
             id={field.name}
+            rows={3}
           />
         );
 
@@ -131,7 +141,7 @@ const FormInput = <T, U = unknown>({
             bordered
             loading={loading}
             defaultValue={defaultValue as string}
-            className={className}
+            className={`hue-form-input__select ${field.className}`}
             status={error ? 'error' : undefined}
             id={field.name}
           />
@@ -143,7 +153,7 @@ const FormInput = <T, U = unknown>({
             value={value as string}
             defaultValue={defaultValue as string}
             onChange={e => onChange(field.name, e.target.value as T)}
-            className={className}
+            className={field.className}
             id={field.name}
           >
             {field.options?.map(option => (
@@ -161,7 +171,8 @@ const FormInput = <T, U = unknown>({
 
   return (
     <Form.Item
-      label={field.type === FieldType.CHECKBOX ? undefined : renderLabel()}
+      className="hue-form-input"
+      label={field.type === FieldType.CHECKBOX || !field.label ? undefined : renderLabel()}
       htmlFor={field.name}
     >
       {renderField()}