// 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, { useCallback, useMemo, useRef, useState } from 'react'; import Modal from 'cuix/dist/components/Modal'; import { Input, InputRef, Tooltip } from 'antd'; import { ColumnProps } from 'cuix/dist/components/Table'; import classNames from 'classnames'; import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon'; import FileIcon from '@cloudera/cuix-core/icons/react/DocumentationIcon'; import { i18nReact } from '../../../utils/i18nReact'; import useDebounce from '../../../utils/useDebounce'; import useLoadData from '../../../utils/hooks/useLoadData/useLoadData'; import useSaveData from '../../../utils/hooks/useSaveData/useSaveData'; import huePubSub from '../../../utils/huePubSub'; import { useHuePubSub } from '../../../utils/hooks/useHuePubSub/useHuePubSub'; import { FileStatus } from '../../../utils/hooks/useFileUpload/types'; import UUID from '../../../utils/string/UUID'; import { DEFAULT_POLLING_TIME } from '../../../utils/constants/storageBrowser'; import { BrowserViewType, ListDirectory } from '../types'; import { LIST_DIRECTORY_API_URL, CREATE_DIRECTORY_API_URL } from '../api'; import PathBrowser from '../../../reactComponents/PathBrowser/PathBrowser'; import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper'; import DragAndDrop from '../../../reactComponents/DragAndDrop/DragAndDrop'; import BottomSlidePanel from '../../../reactComponents/BottomSlidePanel/BottomSlidePanel'; import PaginatedTable from '../../../reactComponents/PaginatedTable/PaginatedTable'; import { FILE_UPLOAD_START_EVENT, FILE_UPLOAD_SUCCESS_EVENT } from '../../../reactComponents/FileUploadQueue/event'; import './FileChooserModal.scss'; interface FileChooserModalProps { onClose: () => void; onSubmit: (destinationPath: string) => Promise; showModal: boolean; title: string; sourcePath: string; submitText?: string; cancelText?: string; isFileSelectionAllowed?: boolean; isUploadEnabled?: boolean; } interface FileChooserTableData { name: string; path: string; type: string; } const FileChooserModal = ({ showModal, onClose, onSubmit, title, sourcePath, isFileSelectionAllowed = false, isUploadEnabled = false, ...i18n }: FileChooserModalProps): JSX.Element => { const { t } = i18nReact.useTranslation(); const { cancelText = t('Cancel'), submitText = t('Submit') } = i18n; const [destPath, setDestPath] = useState(sourcePath); const [submitLoading, setSubmitLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [showCreateFolderModal, setShowCreateFolderModal] = useState(false); const [polling, setPolling] = useState(false); const uploadRef = useRef(null); const createFolderInputRef = useRef(null); const [createFolderValue, setCreateFolderValue] = useState(''); const { data: filesData, loading, error, reloadData } = useLoadData(LIST_DIRECTORY_API_URL, { params: { path: destPath, pagesize: '1000', filter: searchTerm }, skip: destPath === '' || destPath === undefined || !showModal, pollInterval: polling ? DEFAULT_POLLING_TIME : undefined }); const tableData: FileChooserTableData[] = useMemo(() => { if (!filesData?.files) { return []; } return filesData?.files?.map(file => ({ name: file.path.split('/').pop() ?? '', path: file.path, type: file.type })); }, [filesData, loading, error]); const handleSearch = useCallback( useDebounce(searchTerm => { setSearchTerm(encodeURIComponent(searchTerm)); }), [setSearchTerm] ); const getColumns = (file: FileChooserTableData) => { const columns: ColumnProps[] = []; for (const key of Object.keys(file)) { const column: ColumnProps = { dataIndex: key, key: `${key}` }; if (key === 'name') { column.render = (_, record: FileChooserTableData) => ( {record.type === BrowserViewType.dir ? : } {record.name} ); } columns.push(column); } return columns.filter(col => col.dataIndex !== 'type' && col.dataIndex !== 'path'); }; const onRowClicked = (record: FileChooserTableData) => { return { onClick: () => { if (record.type === BrowserViewType.dir) { setDestPath(record.path); } if (isFileSelectionAllowed && record.type === BrowserViewType.file) { onSubmit(record.path); onClose(); } } }; }; const onFilesDrop = (newFiles: File[]) => { const newUploadItems = newFiles.map(file => { return { file, filePath: destPath, uuid: UUID(), status: FileStatus.Pending }; }); setPolling(true); huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: newUploadItems }); }; useHuePubSub({ topic: FILE_UPLOAD_SUCCESS_EVENT, callback: () => { setPolling(false); } }); const handleUploadClick = () => { if (!uploadRef || !uploadRef.current) { return; } uploadRef.current.click(); }; const handleFileUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files as ArrayLike); onFilesDrop(files); }; const { save: createFolder, loading: createFolderLoading, error: createFolderError } = useSaveData(CREATE_DIRECTORY_API_URL, { options: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload }); const handleCreate = () => { createFolder( { path: destPath, name: createFolderValue }, { onSuccess: () => { setShowCreateFolderModal(false); setDestPath(prev => `${prev}/${createFolderValue}`); } } ); }; const locale = { emptyText: t('Folder is empty') }; const handleOk = async () => { setSubmitLoading(true); await onSubmit(destPath); setSubmitLoading(false); onClose(); }; const errorConfig = [ { enabled: !!error, message: t('An error occurred while fetching the filesystem'), action: t('Retry'), onClick: reloadData } ]; const createFolderErrorConfig = [ { enabled: !!createFolderError, message: createFolderError } ]; const TableContent = ( loading={loading && !polling} data={tableData} isDynamicHeight rowKey={r => `${r.path}__${r.type}__${r.name}`} locale={locale} columns={getColumns(tableData[0] ?? {})} rowClassName={record => record.type === BrowserViewType.file && !isFileSelectionAllowed ? classNames('hue-filechooser-modal__table-row', 'disabled-row') : 'hue-filechooser-modal__table-row' } onRowClick={onRowClicked} showHeader={false} /> ); return ( <> {showModal && ( setShowCreateFolderModal(true)} cancelText={cancelText} onCancel={onClose} >
{ handleSearch(event.target.value); }} /> {isUploadEnabled ? ( {TableContent} ) : ( TableContent )}
{showCreateFolderModal && ( { setShowCreateFolderModal(false); setCreateFolderValue(''); }} onPrimaryClick={handleCreate} > {/* TODO: Refactor CreateAndUpload to reuse */} handleCreate()} ref={createFolderInputRef} onChange={e => { setCreateFolderValue(e.target.value); }} /> )}
)} ); }; export default FileChooserModal;