diff --git a/task_3_react/src/client/App.tsx b/task_3_react/src/client/App.tsx index 9fa0715..62d0d54 100644 --- a/task_3_react/src/client/App.tsx +++ b/task_3_react/src/client/App.tsx @@ -1,5 +1,6 @@ import './css/App.css'; import '@fortawesome/fontawesome-free/css/all.css'; +import './sse'; import { CSV_Data, fileInfo, responseObject } from './types'; @@ -15,7 +16,6 @@ import DataTable from './components/DataTable'; import FileCard from './components/FileCard'; import InfoCard from './components/InfoCard'; import Layout from './components/Layout'; -import "./sse" function App () { const [ @@ -35,32 +35,36 @@ function App () { fileList, setFileList ] = useState( null as responseObject | null ); - // For the loading spinner in DataTable - const [loading, setLoading] = useState(false); - + const [ + loading, + setLoading + ] = useState( false ); // Add evenbt listener for server-sent events on first render - const [active, setActive] = useState(false); + const [ + active, + setActive + ] = useState( false ); - useEffect( ()=> { - if (!active) { - document.addEventListener("sse:uploaded", async _ev => { - await fetch( '/status', { + useEffect( () => { + if ( !active ) { + document.addEventListener( 'sse:uploaded', () => { + fetch( '/status', { 'method': 'GET' } ) .then( response => response.json() ) .then( response => setFileList( response ) ) .catch( error => console.log( error ) ); - }); - document.addEventListener("sse:deleted", async _ev => { - await fetch( '/status', { + } ); + document.addEventListener( 'sse:deleted', () => { + fetch( '/status', { 'method': 'GET' } ) .then( response => response.json() ) .then( response => setFileList( response ) ) .catch( error => console.log( error ) ); - }); - setActive(true); + } ); + setActive( true ); // Initial fetch of file list at first component render fetch( '/status', { 'method': 'GET' @@ -68,14 +72,14 @@ function App () { .then( response => response.json() ) .then( response => setFileList( response ) ) .catch( error => console.log( error ) ); - } - }) + } + } ); const formRef = useRef( null ); // This is triggered in CSVCard const handleFileUpload = async ( e: React.ChangeEvent ): Promise => { - setLoading(true) + setLoading( true ); const file = e.target.files?.[0]; if ( !file ) throw new Error( 'No file received' ); @@ -90,13 +94,13 @@ function App () { setInfo( newFileInfo ); setData( data ); - setLoading(false); + setLoading( false ); if ( formRef.current ) { // Upload to server const formData = new FormData( formRef.current ); - await fetch( '/upload', { + await fetch( '/upload?fname=' + file.name, { 'method': 'POST', 'body': formData } ); @@ -104,8 +108,8 @@ function App () { }; const handleFileChange = async ( fileName: string ) => { - setLoading(true) - + setLoading( true ); + const response = await fetch( `/download/${ fileName }` ); const blob = await response.blob(); const text = await blob.text(); @@ -114,8 +118,15 @@ function App () { const data = await convertCSVtoJSON( text ); + setInfo( { + 'filesize': blob.size + 'B', + 'filetype': response.headers.get( 'Content-Type' ) ?? 'text/csv', + 'filename': fileName, + 'rowcount': data.length + } ); + // Updating fileInfo requires more effort since blob doesn't have the metadata - setLoading(false); + setLoading( false ); setData( data ); }; diff --git a/task_3_react/src/client/components/DataTable.tsx b/task_3_react/src/client/components/DataTable.tsx index 665a3a2..ca26b35 100644 --- a/task_3_react/src/client/components/DataTable.tsx +++ b/task_3_react/src/client/components/DataTable.tsx @@ -5,6 +5,7 @@ import { CSV_Data } from '../types'; + const DataTable = ( props: { 'data': CSV_Data, 'loading': boolean @@ -33,6 +34,7 @@ const DataTable = ( props: { } }; + if ( sortCol !== 'None' && sortType === 'asc' ) { props.data.sort( ( a, b ) => { if ( a[sortCol]! < b[sortCol]! ) return -1; @@ -49,6 +51,8 @@ const DataTable = ( props: { return 0; } ); + } else { + props.data.sort(); } return ( @@ -61,8 +65,9 @@ const DataTable = ( props: { { - header.map( col => ) @@ -74,7 +79,8 @@ const DataTable = ( props: { props.data.map( ( row, i ) => ( { - header.map( col => ) + header.map( ( col, j ) => ) } ) ) @@ -117,4 +123,3 @@ const Row = ( props: { }; export default DataTable; - diff --git a/task_3_react/src/client/components/InfoCard.tsx b/task_3_react/src/client/components/InfoCard.tsx index 8b3fc2d..9ea8e12 100644 --- a/task_3_react/src/client/components/InfoCard.tsx +++ b/task_3_react/src/client/components/InfoCard.tsx @@ -5,33 +5,39 @@ import { const InfoCard = ( props: { 'info': fileInfo } ) => { - let noFileMessage =
; - - if ( props.info.filename === 'None' ) - noFileMessage =
No file selected
; - return (

Data infos

+
-
- {noFileMessage} -

Filename

-

{props.info.filename}

- -

File type

-

{props.info.filetype}

- -

File size

-

{props.info.filesize}

- -

Number of rows

-

{props.info.rowcount}

-
); }; -export default InfoCard; +const InfoRenderer = ( props: { + 'info': fileInfo +} ) => { + if ( props.info.filename !== 'None' ) { + return
+

Filename

+

{props.info.filename}

+

File type

+

{props.info.filetype}

+ +

File size

+

{props.info.filesize}

+ +

Number of rows

+

{props.info.rowcount}

+
; + } else { + return
+

No file selected

+
; + } +}; + + +export default InfoCard; diff --git a/task_3_react/src/client/main.tsx b/task_3_react/src/client/main.tsx index 4a35729..adff04f 100644 --- a/task_3_react/src/client/main.tsx +++ b/task_3_react/src/client/main.tsx @@ -1,13 +1,10 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "@fortawesome/fontawesome-free/css/all.css"; -import App from "./App"; -import "@picocss/pico/css/pico.min.css"; +import '@fortawesome/fontawesome-free/css/all.css'; +import './index.css'; +import '@picocss/pico/css/pico.min.css'; +import App from './App'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; -import "./index.css"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); +ReactDOM.createRoot( document.getElementById( 'root' )! ).render( + +, ); diff --git a/task_3_react/src/server/main.ts b/task_3_react/src/server/main.ts index f5aebf9..3d1e7f6 100644 --- a/task_3_react/src/server/main.ts +++ b/task_3_react/src/server/main.ts @@ -11,17 +11,35 @@ import { } from './types'; const app = express(); + +const sanitizeFilePath = ( path: string ) => { + // eslint-disable-next-line no-useless-escape + return path.replace( /[\/\\].*/, '' ).replace( '..', '' ); +}; + // Set up file storage const storage = multer.diskStorage( { 'destination': './src/server/uploads', 'filename': ( - _req, file, cb + req, file, cb ) => { - // Suggested in Multer's readme + // Suggested in Multer's readme const uniqueSuffix = Date.now() + '-' + Math.round( Math.random() * 1E3 ); - const fname = file.fieldname + '-' + uniqueSuffix; - // TODO: We could consider allowing the filename to be overwritten using a query param on the - // request (i.e. url would be /upload?fname=) + + let fname = req.query['fname'] + ? sanitizeFilePath( String( req.query['fname'] ) ) + : file.fieldname; + + const index = fname.lastIndexOf( '.' ); + + let fext = ''; + + if ( index > -1 ) { + fname = fname.slice( 0, index ); + fext = fname.substring( index ); + } + + fname += '-' + uniqueSuffix + fext; fileEvent.emit( 'uploaded', fname ); diff --git a/task_3_react/src/server/uploads/dataFile-1763362295232-475 b/task_3_react/src/server/uploads/Kantiball23-1763364363520-185 similarity index 100% rename from task_3_react/src/server/uploads/dataFile-1763362295232-475 rename to task_3_react/src/server/uploads/Kantiball23-1763364363520-185