diff --git a/packages/api/src/controllers/uploads.js b/packages/api/src/controllers/uploads.js index d593bf104..54407d4da 100644 --- a/packages/api/src/controllers/uploads.js +++ b/packages/api/src/controllers/uploads.js @@ -2,6 +2,21 @@ const path = require('path'); const { uploadsdir } = require('../utility/directories'); const uuidv1 = require('uuid/v1'); +const extensions = [ + { + ext: '.xlsx', + type: 'excel', + }, + { + ext: '.jsonl', + type: 'jsonl', + }, + { + ext: '.csv', + type: 'csv', + }, +]; + module.exports = { upload_meta: { method: 'post', @@ -16,10 +31,21 @@ module.exports = { const uploadName = uuidv1(); const filePath = path.join(uploadsdir(), uploadName); console.log(`Uploading file ${data.name}, size=${data.size}`); + let storageType = null; + let shortName = data.name; + for (const { ext, type } of extensions) { + if (data.name.endsWith(ext)) { + storageType = type; + shortName = data.name.slice(0, -ext.length); + } + } data.mv(filePath, () => { res.json({ originalName: data.name, + shortName, + storageType, uploadName, + filePath, }); }); }, diff --git a/packages/web/src/App.js b/packages/web/src/App.js index 4dcbe3e0c..2ca0f2093 100644 --- a/packages/web/src/App.js +++ b/packages/web/src/App.js @@ -13,6 +13,7 @@ import { import { SocketProvider } from './utility/SocketProvider'; import ConnectionsPinger from './utility/ConnectionsPinger'; import { ModalLayerProvider } from './modals/showModal'; +import UploadsProvider from './utility/UploadsProvider'; function App() { return ( @@ -26,7 +27,9 @@ function App() { - + + + diff --git a/packages/web/src/DragAndDropFileTarget.js b/packages/web/src/DragAndDropFileTarget.js new file mode 100644 index 000000000..2041aa49b --- /dev/null +++ b/packages/web/src/DragAndDropFileTarget.js @@ -0,0 +1,53 @@ +import React from 'react'; +import styled from 'styled-components'; + +const TargetStyled = styled.div` + position: fixed; + display: flex; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #aaaaff; + align-items: center; + justify-content: space-around; + z-index: 1000; +`; + +const InfoBox = styled.div``; + +const IconWrapper = styled.div` + display: flex; + justify-content: space-around; + font-size: 50px; + margin-bottom: 20px; +`; + +const InfoWrapper = styled.div` + display: flex; + justify-content: space-around; + margin-top: 10px; +`; + +const TitleWrapper = styled.div` + font-size: 30px; + display: flex; + justify-content: space-around; +`; + +export default function DragAndDropFileTarget({ isDragActive, inputProps }) { + return ( + !!isDragActive && ( + + + + + + Drop the files to upload to DbGate + Supported file types: csv, MS Excel, json-lines + + + + ) + ); +} diff --git a/packages/web/src/Screen.js b/packages/web/src/Screen.js index a2ce0db8f..62d891f6a 100644 --- a/packages/web/src/Screen.js +++ b/packages/web/src/Screen.js @@ -3,7 +3,6 @@ import React from 'react'; import theme from './theme'; import styled from 'styled-components'; -import { useDropzone } from 'react-dropzone'; import TabsPanel from './TabsPanel'; import TabContent from './TabContent'; import WidgetIconPanel from './widgets/WidgetIconPanel'; @@ -13,7 +12,8 @@ import ToolBar from './widgets/Toolbar'; import StatusBar from './widgets/StatusBar'; import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter'; import { ModalLayer } from './modals/showModal'; -import resolveApi from './utility/resolveApi'; +import DragAndDropFileTarget from './DragAndDropFileTarget'; +import { useUploadsZone } from './utility/UploadsProvider'; const BodyDiv = styled.div` position: fixed; @@ -98,15 +98,6 @@ const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)` bottom: ${theme.statusBar.height}px; `; -const DragAndDropTarget = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: red; -`; - export default function Screen() { const currentWidget = useCurrentWidget(); const leftPanelWidth = useLeftPanelWidth(); @@ -117,42 +108,7 @@ export default function Screen() { const toolbarPortalRef = React.useRef(); const onSplitDown = useSplitterDrag('clientX', (diff) => setLeftPanelWidth((v) => v + diff)); - const onDrop = React.useCallback((files) => { - // Do something with the files - console.log('FILES', files); - files.forEach(async (file) => { - if (parseInt(file.size, 10) >= 4 * 1024 * 1024) { - // to big file - return; - } - - const formData = new FormData(); - formData.append('data', file); - - const fetchOptions = { - method: 'POST', - body: formData, - }; - - const apiBase = resolveApi(); - const resp = await fetch(`${apiBase}/uploads/upload`, fetchOptions); - const event = await resp.json(); - - return event; - - // const reader = new FileReader(); - - // reader.onabort = () => console.log('file reading was aborted'); - // reader.onerror = () => console.log('file reading has failed'); - // reader.onload = () => { - // // Do whatever you want with the file contents - // const binaryStr = reader.result; - // console.log(binaryStr); - // }; - // reader.readAsArrayBuffer(file); - }); - }, []); - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + const { getRootProps, getInputProps, isDragActive } = useUploadsZone(); return (
@@ -184,12 +140,7 @@ export default function Screen() { - {!!isDragActive && ( - - Drop the files here ... - {' '} - - )} +
); } diff --git a/packages/web/src/impexp/ImportExportConfigurator.js b/packages/web/src/impexp/ImportExportConfigurator.js index 58341aa60..2ef09f7a2 100644 --- a/packages/web/src/impexp/ImportExportConfigurator.js +++ b/packages/web/src/impexp/ImportExportConfigurator.js @@ -22,6 +22,7 @@ import getAsArray from '../utility/getAsArray'; import axios from '../utility/axios'; import LoadingInfo from '../widgets/LoadingInfo'; import SqlEditor from '../sqleditor/SqlEditor'; +import { useUploadsProvider } from '../utility/UploadsProvider'; const Container = styled.div` max-height: 50vh; @@ -62,6 +63,11 @@ const SqlWrapper = styled.div` width: 20vw; `; +const DragWrapper = styled.div` + padding: 10px; + background: #ddd; +`; + function getFileFilters(storageType) { const res = []; if (storageType == 'csv') res.push({ name: 'CSV files', extensions: ['csv'] }); @@ -141,7 +147,7 @@ function FilesInput() { if (electron) { return ; } - return ; + return Drag & drop imported files here; } function SourceTargetConfig({ @@ -287,12 +293,43 @@ function SourceName({ name }) { ); } -export default function ImportExportConfigurator() { +export default function ImportExportConfigurator({ uploadedFile = undefined }) { const { values, setFieldValue } = useFormikContext(); const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName }); const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId }); const { engine: sourceEngine } = sourceConnectionInfo || {}; const { sourceList } = values; + const { uploadListener, setUploadListener } = useUploadsProvider(); + + const handleUpload = React.useCallback( + (file) => { + addFilesToSourceList( + [ + { + full: file.filePath, + name: file.shortName, + }, + ], + values, + setFieldValue + ); + // setFieldValue('sourceList', [...(sourceList || []), file.originalName]); + }, + [setFieldValue, sourceList] + ); + + React.useEffect(() => { + setUploadListener(() => handleUpload); + return () => { + setUploadListener(null); + }; + }, [handleUpload]); + + React.useEffect(() => { + if (uploadedFile) { + handleUpload(uploadedFile); + } + }, []); return ( diff --git a/packages/web/src/modals/ImportExportModal.js b/packages/web/src/modals/ImportExportModal.js index 5d7ca4cec..f3d309759 100644 --- a/packages/web/src/modals/ImportExportModal.js +++ b/packages/web/src/modals/ImportExportModal.js @@ -38,7 +38,7 @@ function GenerateSctriptButton({ modalState }) { return ; } -export default function ImportExportModal({ modalState, initialValues }) { +export default function ImportExportModal({ modalState, initialValues, uploadedFile = undefined }) { const [executeNumber, setExecuteNumber] = React.useState(0); const [runnerId, setRunnerId] = React.useState(null); const archive = useCurrentArchive(); @@ -69,7 +69,7 @@ export default function ImportExportModal({ modalState, initialValues }) {
Import/Export - + diff --git a/packages/web/src/utility/UploadsProvider.js b/packages/web/src/utility/UploadsProvider.js new file mode 100644 index 000000000..83ad73201 --- /dev/null +++ b/packages/web/src/utility/UploadsProvider.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { useDropzone } from 'react-dropzone'; +import ImportExportModal from '../modals/ImportExportModal'; +import useShowModal from '../modals/showModal'; +import resolveApi from './resolveApi'; + +const UploadsContext = React.createContext(null); + +export default function UploadsProvider({ children }) { + const [uploadListener, setUploadListener] = React.useState(null); + return {children}; +} + +export function useUploadsProvider() { + return React.useContext(UploadsContext); +} + +export function useUploadsZone() { + const { uploadListener } = useUploadsProvider(); + const showModal = useShowModal(); + + const onDrop = React.useCallback( + (files) => { + files.forEach(async (file) => { + if (parseInt(file.size, 10) >= 4 * 1024 * 1024) { + // to big file + return; + } + + const formData = new FormData(); + formData.append('data', file); + + const fetchOptions = { + method: 'POST', + body: formData, + }; + + const apiBase = resolveApi(); + const resp = await fetch(`${apiBase}/uploads/upload`, fetchOptions); + const fileData = await resp.json(); + + if (uploadListener) { + uploadListener(fileData); + } else { + if (['csv', 'excel', 'jsonl'].includes(fileData.storageType)) { + showModal((modalState) => ( + + )); + } + } + + // const reader = new FileReader(); + + // reader.onabort = () => console.log('file reading was aborted'); + // reader.onerror = () => console.log('file reading has failed'); + // reader.onload = () => { + // // Do whatever you want with the file contents + // const binaryStr = reader.result; + // console.log(binaryStr); + // }; + // reader.readAsArrayBuffer(file); + }); + }, + [uploadListener] + ); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + + return { getRootProps, getInputProps, isDragActive }; +} diff --git a/packages/web/src/widgets/Toolbar.js b/packages/web/src/widgets/Toolbar.js index 7bca16900..18ffac867 100644 --- a/packages/web/src/widgets/Toolbar.js +++ b/packages/web/src/widgets/Toolbar.js @@ -8,6 +8,8 @@ import { useConfig } from '../utility/metadataLoaders'; import { useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; import { openNewTab } from '../utility/common'; import useNewFreeTable from '../freetable/useNewFreeTable'; +import ImportExportModal from '../modals/ImportExportModal'; +import useShowModal from '../modals/showModal'; const ToolbarContainer = styled.div` display: flex; @@ -22,6 +24,7 @@ export default function ToolBar({ toolbarPortalRef }) { const toolbar = config.toolbar || []; const setOpenedTabs = useSetOpenedTabs(); const openedTabs = useOpenedTabs(); + const showModal = useShowModal(); React.useEffect(() => { window['dbgate_createNewConnection'] = modalState.open; @@ -29,6 +32,21 @@ export default function ToolBar({ toolbarPortalRef }) { window['dbgate_closeAll'] = () => setOpenedTabs([]); }); + const showImport = () => { + showModal((modalState) => ( + + )); + }; + function openTabFromButton(button) { if (openedTabs.find((x) => x.tabComponent == 'InfoPageTab' && x.props && x.props.page == button.page)) { setOpenedTabs((tabs) => @@ -79,6 +97,9 @@ export default function ToolBar({ toolbarPortalRef }) { Free table editor + + Import data + );