[Task 3] More fixes

This commit is contained in:
2025-11-17 09:07:47 +01:00
parent 5916cce77f
commit 157603d3d7
6 changed files with 99 additions and 62 deletions

View File

@@ -1,5 +1,6 @@
import './css/App.css'; import './css/App.css';
import '@fortawesome/fontawesome-free/css/all.css'; import '@fortawesome/fontawesome-free/css/all.css';
import './sse';
import { import {
CSV_Data, fileInfo, responseObject CSV_Data, fileInfo, responseObject
} from './types'; } from './types';
@@ -15,7 +16,6 @@ import DataTable from './components/DataTable';
import FileCard from './components/FileCard'; import FileCard from './components/FileCard';
import InfoCard from './components/InfoCard'; import InfoCard from './components/InfoCard';
import Layout from './components/Layout'; import Layout from './components/Layout';
import "./sse"
function App () { function App () {
const [ const [
@@ -35,32 +35,36 @@ function App () {
fileList, fileList,
setFileList setFileList
] = useState( null as responseObject | null ); ] = useState( null as responseObject | null );
// For the loading spinner in DataTable // 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 // Add evenbt listener for server-sent events on first render
const [active, setActive] = useState(false); const [
active,
setActive
] = useState( false );
useEffect( ()=> { useEffect( () => {
if (!active) { if ( !active ) {
document.addEventListener("sse:uploaded", async _ev => { document.addEventListener( 'sse:uploaded', () => {
await fetch( '/status', { fetch( '/status', {
'method': 'GET' 'method': 'GET'
} ) } )
.then( response => response.json() ) .then( response => response.json() )
.then( response => setFileList( response ) ) .then( response => setFileList( response ) )
.catch( error => console.log( error ) ); .catch( error => console.log( error ) );
}); } );
document.addEventListener("sse:deleted", async _ev => { document.addEventListener( 'sse:deleted', () => {
await fetch( '/status', { fetch( '/status', {
'method': 'GET' 'method': 'GET'
} ) } )
.then( response => response.json() ) .then( response => response.json() )
.then( response => setFileList( response ) ) .then( response => setFileList( response ) )
.catch( error => console.log( error ) ); .catch( error => console.log( error ) );
}); } );
setActive(true); setActive( true );
// Initial fetch of file list at first component render // Initial fetch of file list at first component render
fetch( '/status', { fetch( '/status', {
'method': 'GET' 'method': 'GET'
@@ -69,13 +73,13 @@ function App () {
.then( response => setFileList( response ) ) .then( response => setFileList( response ) )
.catch( error => console.log( error ) ); .catch( error => console.log( error ) );
} }
}) } );
const formRef = useRef( null ); const formRef = useRef( null );
// This is triggered in CSVCard // This is triggered in CSVCard
const handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => { const handleFileUpload = async ( e: React.ChangeEvent<HTMLInputElement> ): Promise<void> => {
setLoading(true) setLoading( true );
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if ( !file ) throw new Error( 'No file received' ); if ( !file ) throw new Error( 'No file received' );
@@ -90,13 +94,13 @@ function App () {
setInfo( newFileInfo ); setInfo( newFileInfo );
setData( data ); setData( data );
setLoading(false); setLoading( false );
if ( formRef.current ) { if ( formRef.current ) {
// Upload to server // Upload to server
const formData = new FormData( formRef.current ); const formData = new FormData( formRef.current );
await fetch( '/upload', { await fetch( '/upload?fname=' + file.name, {
'method': 'POST', 'method': 'POST',
'body': formData 'body': formData
} ); } );
@@ -104,7 +108,7 @@ function App () {
}; };
const handleFileChange = async ( fileName: string ) => { const handleFileChange = async ( fileName: string ) => {
setLoading(true) setLoading( true );
const response = await fetch( `/download/${ fileName }` ); const response = await fetch( `/download/${ fileName }` );
const blob = await response.blob(); const blob = await response.blob();
@@ -114,8 +118,15 @@ function App () {
const data = await convertCSVtoJSON( text ); 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 // Updating fileInfo requires more effort since blob doesn't have the metadata
setLoading(false); setLoading( false );
setData( data ); setData( data );
}; };

View File

@@ -5,6 +5,7 @@ import {
CSV_Data CSV_Data
} from '../types'; } from '../types';
const DataTable = ( props: { const DataTable = ( props: {
'data': CSV_Data, 'data': CSV_Data,
'loading': boolean 'loading': boolean
@@ -33,6 +34,7 @@ const DataTable = ( props: {
} }
}; };
if ( sortCol !== 'None' && sortType === 'asc' ) { if ( sortCol !== 'None' && sortType === 'asc' ) {
props.data.sort( ( a, b ) => { props.data.sort( ( a, b ) => {
if ( a[sortCol]! < b[sortCol]! ) return -1; if ( a[sortCol]! < b[sortCol]! ) return -1;
@@ -49,6 +51,8 @@ const DataTable = ( props: {
return 0; return 0;
} ); } );
} else {
props.data.sort();
} }
return ( return (
@@ -61,8 +65,9 @@ const DataTable = ( props: {
<thead> <thead>
<tr> <tr>
{ {
header.map( col => <ColHeader header.map( ( col, i ) => <ColHeader
col={col} col={col}
key={i}
sortingHandle={sortingHandler} sortingHandle={sortingHandler}
isSelected={col == sortCol} isSelected={col == sortCol}
sortType={sortType}></ColHeader> ) sortType={sortType}></ColHeader> )
@@ -74,7 +79,8 @@ const DataTable = ( props: {
props.data.map( ( row, i ) => ( props.data.map( ( row, i ) => (
<tr key={i}> <tr key={i}>
{ {
header.map( col => <Row key={i} col={col} content={row[col] as string}></Row> ) header.map( ( col, j ) => <Row key={`${ i }:${ j }`}
col={col} content={row[col] as string}></Row> )
} }
</tr> </tr>
) ) ) )
@@ -117,4 +123,3 @@ const Row = ( props: {
}; };
export default DataTable; export default DataTable;

View File

@@ -5,33 +5,39 @@ import {
const InfoCard = ( props: { const InfoCard = ( props: {
'info': fileInfo 'info': fileInfo
} ) => { } ) => {
let noFileMessage = <div></div>;
if ( props.info.filename === 'None' )
noFileMessage = <div id="data-info-placeholder">No file selected</div>;
return ( return (
<article> <article>
<header> <header>
<h2>Data infos</h2> <h2>Data infos</h2>
<InfoRenderer info={props.info}></InfoRenderer>
</header> </header>
<div className="info">
{noFileMessage}
<h4>Filename</h4>
<p>{props.info.filename}</p>
<h4>File type</h4>
<p>{props.info.filetype}</p>
<h4>File size</h4>
<p>{props.info.filesize}</p>
<h4>Number of rows</h4>
<p>{props.info.rowcount}</p>
</div>
</article> </article>
); );
}; };
export default InfoCard; const InfoRenderer = ( props: {
'info': fileInfo
} ) => {
if ( props.info.filename !== 'None' ) {
return <div className="info">
<h4>Filename</h4>
<p>{props.info.filename}</p>
<h4>File type</h4>
<p>{props.info.filetype}</p>
<h4>File size</h4>
<p>{props.info.filesize}</p>
<h4>Number of rows</h4>
<p>{props.info.rowcount}</p>
</div>;
} else {
return <div className="info">
<p>No file selected</p>
</div>;
}
};
export default InfoCard;

View File

@@ -1,13 +1,10 @@
import React from "react"; import '@fortawesome/fontawesome-free/css/all.css';
import ReactDOM from "react-dom/client"; import './index.css';
import "@fortawesome/fontawesome-free/css/all.css"; import '@picocss/pico/css/pico.min.css';
import App from "./App"; import App from './App';
import "@picocss/pico/css/pico.min.css"; import React from 'react';
import ReactDOM from 'react-dom/client';
import "./index.css"; ReactDOM.createRoot( document.getElementById( 'root' )! ).render( <React.StrictMode>
<App />
ReactDOM.createRoot(document.getElementById("root")!).render( </React.StrictMode>, );
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@@ -11,17 +11,35 @@ import {
} from './types'; } from './types';
const app = express(); const app = express();
const sanitizeFilePath = ( path: string ) => {
// eslint-disable-next-line no-useless-escape
return path.replace( /[\/\\].*/, '' ).replace( '..', '' );
};
// Set up file storage // Set up file storage
const storage = multer.diskStorage( { const storage = multer.diskStorage( {
'destination': './src/server/uploads', 'destination': './src/server/uploads',
'filename': ( '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 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 let fname = req.query['fname']
// request (i.e. url would be /upload?fname=<filename>) ? 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 ); fileEvent.emit( 'uploaded', fname );