Skip to content

Commit e4605d3

Browse files
authored
[ui-importer] Add configured filesystems in importer (#4131)
* [ui-importer] Add configured filesystems in importer * [ui-importer] Added Bottom slide panel,tests and improvements * [ui-importer] Updated tests and improvements * [ui-importer] Update File chooser modal with Paginated table * [ui-importer] Added tests for filechooser and improvements
1 parent 8d5eb82 commit e4605d3

File tree

15 files changed

+1086
-187
lines changed

15 files changed

+1086
-187
lines changed

desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ $option-size: 80px;
4242
margin-top: 70px;
4343
max-width: 80%;
4444
gap: $option-size;
45+
align-items: baseline;
4546
}
4647

4748
&-option {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to Cloudera, Inc. under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. Cloudera, Inc. licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
import React from 'react';
18+
import { render, screen, fireEvent } from '@testing-library/react';
19+
import '@testing-library/jest-dom';
20+
import ImporterSourceSelector from './ImporterSourceSelector';
21+
22+
import { hueWindow } from 'types/types';
23+
24+
import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
25+
import FileChooserModal from '../../storageBrowser/FileChooserModal/FileChooserModal';
26+
27+
jest.mock('../../../utils/hooks/useLoadData/useLoadData');
28+
jest.mock('../../storageBrowser/FileChooserModal/FileChooserModal', () => jest.fn(() => null));
29+
jest.mock('./LocalFileUploadOption', () => () => <div data-testid="local-upload">LocalUpload</div>);
30+
31+
const mockSetFileMetaData = jest.fn();
32+
33+
describe('ImporterSourceSelector', () => {
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
(useLoadData as jest.Mock).mockReturnValue({
37+
data: [
38+
{ name: 's3a', userHomeDirectory: '/user/test/s3' },
39+
{ name: 'hdfs', userHomeDirectory: '/user/test/hdfs' },
40+
{ name: 'abfs', userHomeDirectory: '/user/test/abfs' },
41+
{ name: 'ofs', userHomeDirectory: '/user/test/ozone' },
42+
{ name: 'gs', userHomeDirectory: '/user/test/gs' }
43+
],
44+
loading: false,
45+
error: null,
46+
reloadData: jest.fn()
47+
});
48+
// Ensure the ENABLE_DIRECT_UPLOAD is enabled
49+
(window as hueWindow).ENABLE_DIRECT_UPLOAD = true;
50+
});
51+
52+
it('should render local upload and filesystem options', () => {
53+
render(<ImporterSourceSelector setFileMetaData={mockSetFileMetaData} />);
54+
55+
expect(screen.getByText('Select a source to import from')).toBeInTheDocument();
56+
expect(screen.queryByText('LocalUpload')).toBeInTheDocument();
57+
expect(screen.getByText('Amazon S3')).toBeInTheDocument();
58+
expect(screen.getByText('HDFS')).toBeInTheDocument();
59+
expect(screen.getByText('Azure Storage')).toBeInTheDocument();
60+
expect(screen.getByText('Ozone')).toBeInTheDocument();
61+
expect(screen.getByText('Google Storage')).toBeInTheDocument();
62+
});
63+
64+
it('should not render local upload option if ENABLE_DIRECT_UPLOAD is false', () => {
65+
(window as hueWindow).ENABLE_DIRECT_UPLOAD = false;
66+
render(<ImporterSourceSelector setFileMetaData={mockSetFileMetaData} />);
67+
68+
expect(screen.getByText('Select a source to import from')).toBeInTheDocument();
69+
expect(screen.queryByText('LocalUpload')).not.toBeInTheDocument();
70+
});
71+
72+
it('should open FileChooserModal when filesystem button is clicked', () => {
73+
(FileChooserModal as jest.Mock).mockImplementation(({ showModal }) => {
74+
return showModal ? <div data-testid="file-chooser">FileChooserModal</div> : null;
75+
});
76+
77+
render(<ImporterSourceSelector setFileMetaData={mockSetFileMetaData} />);
78+
79+
const s3Button = screen.getByRole('button', { name: /Amazon S3/i }); // second button (first is local upload)
80+
fireEvent.click(s3Button);
81+
82+
expect(screen.getByTestId('file-chooser')).toBeInTheDocument();
83+
});
84+
85+
it('should handle loading state', () => {
86+
(useLoadData as jest.Mock).mockReturnValue({
87+
data: null,
88+
loading: true,
89+
error: null,
90+
reloadData: jest.fn()
91+
});
92+
93+
render(<ImporterSourceSelector setFileMetaData={mockSetFileMetaData} />);
94+
95+
expect(screen.getAllByTestId('loading-error-wrapper__spinner')).toHaveLength(2);
96+
});
97+
98+
it('should display error message on fetch failure', () => {
99+
const mockRetry = jest.fn();
100+
(useLoadData as jest.Mock).mockReturnValue({
101+
data: null,
102+
loading: false,
103+
error: true,
104+
reloadData: mockRetry
105+
});
106+
107+
render(<ImporterSourceSelector setFileMetaData={mockSetFileMetaData} />);
108+
109+
expect(screen.getByText('An error occurred while fetching the filesystem')).toBeInTheDocument();
110+
});
111+
});

desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx

Lines changed: 111 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,102 +14,139 @@
1414
// See the License for the specific language governing permissions and
1515
// limitations under the License.
1616

17-
import React, { useRef } from 'react';
17+
import React, { useState } from 'react';
1818
import Button from 'cuix/dist/components/Button';
19-
import DocumentationIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon';
19+
import S3Icon from '@cloudera/cuix-core/icons/react/S3Icon';
20+
import HDFSIcon from '@cloudera/cuix-core/icons/react/HdfsIcon';
21+
import OzoneIcon from '@cloudera/cuix-core/icons/react/OzoneIcon';
22+
import GoogleCloudIcon from '@cloudera/cuix-core/icons/react/GoogleCloudIcon';
23+
import AdlsIcon from '../../../components/icons/AdlsIcon';
24+
2025
import { hueWindow } from 'types/types';
2126

22-
import { FileMetaData, ImporterFileSource, LocalFileUploadResponse } from '../types';
27+
import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
28+
import FileChooserModal from '../../storageBrowser/FileChooserModal/FileChooserModal';
29+
import LocalFileUploadOption from './LocalFileUploadOption';
30+
import { FILESYSTEMS_API_URL } from '../../storageBrowser/api';
31+
import { FileMetaData, ImporterFileSource } from '../types';
32+
import { FileSystem } from '../../storageBrowser/types';
2333
import { i18nReact } from '../../../utils/i18nReact';
24-
import { UPLOAD_LOCAL_FILE_API_URL } from '../api';
25-
import useSaveData from '../../../utils/hooks/useSaveData/useSaveData';
26-
import huePubSub from '../../../utils/huePubSub';
34+
import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
2735

2836
import './ImporterSourceSelector.scss';
2937

38+
const getFileSystems = t => {
39+
return {
40+
s3a: {
41+
icon: <S3Icon />,
42+
title: t('Amazon S3')
43+
},
44+
hdfs: {
45+
icon: <HDFSIcon />,
46+
title: t('HDFS')
47+
},
48+
abfs: {
49+
icon: <AdlsIcon />,
50+
title: t('Azure Storage')
51+
},
52+
ofs: {
53+
icon: <OzoneIcon />,
54+
title: t('Ozone')
55+
},
56+
adls: {
57+
icon: <AdlsIcon />,
58+
title: t('Azure Storage')
59+
},
60+
gs: {
61+
icon: <GoogleCloudIcon />,
62+
title: t('Google Storage')
63+
}
64+
};
65+
};
3066
interface ImporterSourceSelectorProps {
3167
setFileMetaData: (fileMetaData: FileMetaData) => void;
3268
}
3369

3470
const ImporterSourceSelector = ({ setFileMetaData }: ImporterSourceSelectorProps): JSX.Element => {
71+
const [selectedUserHomeDirectory, setSelectedUserHomeDirectory] = useState<string | undefined>(
72+
undefined
73+
);
74+
const [uploadError, setUploadError] = useState<string | undefined>(undefined);
3575
const { t } = i18nReact.useTranslation();
36-
const uploadRef = useRef<HTMLInputElement>(null);
37-
38-
const { save: upload } = useSaveData<LocalFileUploadResponse>(UPLOAD_LOCAL_FILE_API_URL);
76+
const fileSystems = getFileSystems(t);
3977

40-
const handleUploadClick = () => {
41-
if (!uploadRef || !uploadRef.current) {
42-
return;
43-
}
44-
uploadRef.current.click();
45-
};
78+
const {
79+
data: fileSystemsData,
80+
loading,
81+
error,
82+
reloadData
83+
} = useLoadData<FileSystem[]>(FILESYSTEMS_API_URL);
4684

47-
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
48-
const files = e.target.files;
49-
if (!files) {
50-
return;
85+
const errorConfig = [
86+
{
87+
enabled: !!error,
88+
message: t('An error occurred while fetching the filesystem'),
89+
action: t('Retry'),
90+
onClick: reloadData
91+
},
92+
{
93+
enabled: !!uploadError,
94+
message: uploadError
5195
}
96+
];
5297

53-
const file = files[0];
54-
55-
const payload = new FormData();
56-
payload.append('file', file);
57-
58-
const file_size = file.size;
59-
if (file_size === 0) {
60-
huePubSub.publish('hue.global.warning', {
61-
message: t('This file is empty, please select another file.')
62-
});
63-
} else if (file_size > 200 * 1000) {
64-
huePubSub.publish('hue.global.warning', {
65-
message: t(
66-
'File size exceeds the supported size (200 KB). Please use the S3, ABFS or HDFS browser to upload files.'
67-
)
68-
});
69-
} else {
70-
upload(payload, {
71-
onSuccess: data => {
72-
setFileMetaData({
73-
path: data.file_path,
74-
fileName: file.name,
75-
source: ImporterFileSource.LOCAL
76-
});
77-
},
78-
onError: error => {
79-
huePubSub.publish('hue.error', error);
80-
}
81-
});
82-
}
98+
const handleFileSelection = async (destinationPath: string) => {
99+
setFileMetaData({
100+
path: destinationPath,
101+
source: ImporterFileSource.REMOTE
102+
});
83103
};
84104

85105
return (
86-
<div className="hue-importer__source-selector cuix antd">
87-
<div className="hue-importer__source-selector-title">
88-
{t('Select a source to import from')}
89-
</div>
90-
<div className="hue-importer__source-selector-options">
91-
{(window as hueWindow).ENABLE_DIRECT_UPLOAD && (
92-
<div className="hue-importer__source-selector-option">
93-
<Button
94-
className="hue-importer__source-selector-option-button"
95-
size="large"
96-
icon={<DocumentationIcon />}
97-
onClick={handleUploadClick}
98-
></Button>
99-
<span className="hue-importer__source-selector-option-btn-title">
100-
{t('Upload from File')}
101-
</span>
102-
<input
103-
ref={uploadRef}
104-
type="file"
105-
className="hue-importer__source-selector-option-upload"
106-
onChange={handleFileUpload}
107-
accept=".csv, .xlsx, .xls"
106+
<LoadingErrorWrapper loading={loading} errors={errorConfig}>
107+
<div className="hue-importer__source-selector cuix antd">
108+
<div className="hue-importer__source-selector-title">
109+
{t('Select a source to import from')}
110+
</div>
111+
<div className="hue-importer__source-selector-options">
112+
{(window as hueWindow).ENABLE_DIRECT_UPLOAD && (
113+
<LocalFileUploadOption
114+
setFileMetaData={setFileMetaData}
115+
setUploadError={setUploadError}
108116
/>
109-
</div>
110-
)}
117+
)}
118+
{fileSystemsData?.map(filesystem => (
119+
<div className="hue-importer__source-selector-option" key={filesystem.name}>
120+
<Button
121+
className="hue-importer__source-selector-option-button"
122+
aria-label={t(fileSystems[filesystem.name].title)}
123+
size="large"
124+
icon={fileSystems[filesystem.name].icon}
125+
onClick={() => {
126+
setSelectedUserHomeDirectory(filesystem.userHomeDirectory);
127+
}}
128+
></Button>
129+
<span className="hue-importer__source-selector-option-btn-title">
130+
{t(fileSystems[filesystem.name].title)}
131+
</span>
132+
</div>
133+
))}
134+
</div>
111135
</div>
112-
</div>
136+
{selectedUserHomeDirectory && (
137+
<FileChooserModal
138+
onClose={() => {
139+
setSelectedUserHomeDirectory(undefined);
140+
}}
141+
onSubmit={handleFileSelection}
142+
showModal={true}
143+
title={t('Import file')}
144+
sourcePath={selectedUserHomeDirectory}
145+
isFileSelectionAllowed={true}
146+
isUploadEnabled={true}
147+
/>
148+
)}
149+
</LoadingErrorWrapper>
113150
);
114151
};
115152

0 commit comments

Comments
 (0)