mirror of
https://github.com/janishutz/fundamentals-of-webengineering.git
synced 2025-11-25 05:44:24 +00:00
[Task 3] More fixes
This commit is contained in:
@@ -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'
|
||||||
@@ -68,14 +72,14 @@ function App () {
|
|||||||
.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 ) );
|
||||||
}
|
}
|
||||||
})
|
} );
|
||||||
|
|
||||||
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,8 +108,8 @@ 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();
|
||||||
const text = await blob.text();
|
const text = await blob.text();
|
||||||
@@ -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 );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>,
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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 );
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user