diff --git a/src/components/DataImport/ArtefactUploadView.jsx b/src/components/DataImport/ArtefactUploadView.jsx new file mode 100644 index 0000000..e41d223 --- /dev/null +++ b/src/components/DataImport/ArtefactUploadView.jsx @@ -0,0 +1,224 @@ +import { Button, Flex, Box, Text } from "@chakra-ui/react"; +import { useEffect, useRef, useState } from "react"; +import { HiUpload } from "react-icons/hi"; +import server from "../../networking"; +import ToastWizard from "../toastWizard"; +import FileItem from "./FileItem"; +import { IoArrowBackCircleSharp } from "react-icons/io5"; +import PendingBatchCard from "./PendingBatchCard"; +import hp1 from "../../assets/hp1.png"; + +function ArtefactUploadView({ batch, setBatchID, onClose, fetchBatches, onBackToPending }) { + const batchID = batch?.id; + const fileInputRef = useRef(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [selectedFiles, setSelectedFiles] = useState([]); + const [fileStatuses, setFileStatuses] = useState({}); + + const handleButtonClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (event) => { + const files = Array.from(event.target.files); + if (files.length > 0) { + setSelectedFiles((prev) => [...prev, ...files]); + // Clear statuses for newly added files + const newStatuses = { ...fileStatuses }; + files.forEach((file) => { + delete newStatuses[file.name]; + }); + setFileStatuses(newStatuses); + } + }; + + const removeFile = (fileToRemove) => { + setSelectedFiles((prev) => prev.filter((file) => file !== fileToRemove)); + setFileStatuses((prev) => { + const updated = { ...prev }; + delete updated[fileToRemove.name]; + return updated; + }); + }; + + const clearAll = () => { + setSelectedFiles([]); + setFileStatuses({}); + }; + + const hasPreviousStatus = selectedFiles.some(file => fileStatuses[file.name]); + + const isDisabled = + selectedFiles.length === 0 || + isSubmitting || + hasPreviousStatus; + + const handleUpload = async () => { + if (selectedFiles.length === 0 || isSubmitting) { + ToastWizard.standard("warning", "No Files Selected", "Please choose at least one file before uploading."); + return; + } + + const formData = new FormData(); + selectedFiles.forEach((file) => { + formData.append("file", file); + }); + + if (batchID) { + formData.append("batchID", batchID); + } + + setIsSubmitting(true); + + try { + const res = await server.post("/dataImport/upload", formData, { + headers: { "Content-Type": "multipart/form-data" }, + transformRequest: formData => formData + }); + + const data = res.data; + const updates = data.raw?.updates || {}; + const returnedBatchID = data.raw?.batchID; + + if (returnedBatchID) { + setBatchID(returnedBatchID); + } + setFileStatuses(updates); + fetchBatches(); + + ToastWizard.standard( + "success", + "Upload Complete", + `Uploaded files to Batch ${returnedBatchID || ''}. Ready for processing.` + ); + } catch (err) { + console.log(`Non-success response in file upload to batch ${batchID || 'New Batch'}`, err) + const updates = err?.response?.data?.raw?.updates; + if (updates) { + setFileStatuses(updates); + } else { + // If no detailed errors, mark all files as failed + const fallbackStatus = {}; + selectedFiles.forEach((file) => { + fallbackStatus[file.name] = "ERROR: Failed to upload."; + }); + setFileStatuses(fallbackStatus); + } + + ToastWizard.standard("error", "Upload Failed", "Some or all files failed to upload."); + } finally { + setIsSubmitting(false); + } + }; + + useEffect(() => { + return () => { + clearAll(); + } + }, []); + + return ( + <> + + + + + + + + + + + + + + { + selectedFiles.length > 0 && ( + + + + {selectedFiles.length} file(s) selected + + + + + + {selectedFiles.map((file, index) => ( + + ))} + + + ) + } + + { + hasPreviousStatus && ( + + Click "Remove All" to upload new files. + + ) + } + + + + + + + + ); +} + +export default ArtefactUploadView; \ No newline at end of file diff --git a/src/components/DataImport/DialogUpload.jsx b/src/components/DataImport/DialogUpload.jsx deleted file mode 100644 index ff7c556..0000000 --- a/src/components/DataImport/DialogUpload.jsx +++ /dev/null @@ -1,256 +0,0 @@ -import { Button, FileUpload, VStack, Text, Box, CloseButton, Dialog, Portal, Badge } from "@chakra-ui/react"; -import { HiUpload } from "react-icons/hi"; -import { useState, useRef } from "react"; -import server from "../../networking"; -import ToastWizard from "../../components/toastWizard"; -import { FaPlus } from "react-icons/fa"; - -function DialogUpload() { - const [files, setFiles] = useState([]); - const [fileStatuses, setFileStatuses] = useState({}); - const [isSubmitting, setIsSubmitting] = useState(false); - const [batchID, setBatchID] = useState(null); - const [isConfirmed, setIsConfirmed] = useState(false); - const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); - const inputRef = useRef(); - const isUploading = useRef(false); - - // Handle file upload - const handleFileChange = async (e) => { - if (isUploading.current) return; - isUploading.current = true; - - const selected = Array.from(e.target.files); - setFiles(selected); - setFileStatuses({}); - if (selected.length === 0) return; - - const formData = new FormData(); - selected.forEach((file) => formData.append("file", file)); - - setIsSubmitting(true); - try { - const res = await server.post("/dataImport/upload", formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - - const data = res.data; - const updates = data.raw?.updates || {}; - const newBatchID = data.raw?.batchID; - - setFileStatuses(updates); - setBatchID(newBatchID); - setIsConfirmed(false); - - ToastWizard.standard("success", "Upload Complete", `Batch ID: ${newBatchID}. Ready for confirmation.`); - } catch (err) { - const updates = err?.response?.data?.raw?.updates; - if (updates) { - setFileStatuses(updates); - } - ToastWizard.standard("error", "Upload Failed", "Failed to upload files."); - } finally { - setIsSubmitting(false); - isUploading.current = false; - } - }; - - const handleConfirmBatch = async () => { - if (!batchID) return; - - setIsSubmitting(true); - try { - const res = await server.post("/dataImport/confirm", { batchID }); - - if (res.status === 200 || res.data.raw.success !== false) { - setIsConfirmed(true); - ToastWizard.standard("success", "Processing Started", `Batch ${batchID} is now being processed.`); - setIsConfirmDialogOpen(false); - - } else { - ToastWizard.standard("error", "Failed to Confirm", res.data.message || "Unknown error."); - } - } catch (err) { - const errorMsg = err?.response?.data?.message || "Failed to confirm batch."; - ToastWizard.standard("error", "Confirm Error", errorMsg); - - } finally { - setIsSubmitting(false); - } - }; - - return ( - - - - - - - - - - - Upload Files - - - - - - - - - - - - - {/* Custom file list with status and remove button */} - {files.length > 0 && ( - - {files.map((file) => { - const status = fileStatuses[file.name]; - return ( - - - - {file.name} - - - {(file.size / 1024).toFixed(1)} KB - - - - {status && ( - - {status.startsWith("ERROR") && ( - - Error - - )} - - {status} - - - { - setFiles((prev) => - prev.filter((f) => f.name !== file.name) - ); - setFileStatuses((prev) => { - const updated = { ...prev }; - delete updated[file.name]; - return updated; - }); - }} - /> - - )} - - ); - })} - - )} - - - - - - - {/* Only show Confirm button after upload */} - {batchID && !isConfirmed && ( - - )} - - {isConfirmed && ( - - Processing started for Batch {batchID} - - )} - - - - - - setIsConfirmDialogOpen(open)}> - - - - - Confirm Processing - - - Are you sure you want to process batch{" "} - - {batchID} - - ? This will start processing the artefacts in the batch. - - - - - - - - - - - - ); -} - -export default DialogUpload; \ No newline at end of file diff --git a/src/components/DataImport/FileItem.jsx b/src/components/DataImport/FileItem.jsx new file mode 100644 index 0000000..1946c35 --- /dev/null +++ b/src/components/DataImport/FileItem.jsx @@ -0,0 +1,45 @@ +import { Flex, Box, Text, CloseButton } from "@chakra-ui/react"; + +function FileItem({ file, statusMsg, onRemove }) { + const isError = statusMsg && statusMsg.startsWith("ERROR"); + const isSuccess = statusMsg && statusMsg.startsWith("SUCCESS"); + + return ( + + + + {file.name} + + {(file.size / 1024).toFixed(1)} KB + + + onRemove(file)} + aria-label={`Remove ${file.name}`} + /> + + {statusMsg && ( + + {statusMsg.replace(/^(SUCCESS|ERROR):\s*/, "")} + + )} + + ); +} + +export default FileItem; \ No newline at end of file diff --git a/src/components/DataImport/ImportMenuDialog.jsx b/src/components/DataImport/ImportMenuDialog.jsx new file mode 100644 index 0000000..65be9a4 --- /dev/null +++ b/src/components/DataImport/ImportMenuDialog.jsx @@ -0,0 +1,99 @@ +import { Button, Dialog, Portal, Flex, Box, Text, VStack, CloseButton } from "@chakra-ui/react"; +import ArtefactUploadView from "./ArtefactUploadView"; +import hp1 from "../../assets/hp1.png"; +import PendingBatchCard from "./PendingBatchCard"; +import { useState } from "react"; + +function ImportMenuDialog({ pendingBatches, fetchBatches }) { + const [isOpen, setIsOpen] = useState(false); + const [view, setView] = useState('pendingList'); + const [targetBatchID, setTargetBatchID] = useState(null); + + const handleClose = () => { + setIsOpen(false); + setTimeout(() => { + setView('pendingList'); + setTargetBatchID(null); + }, 0); + } + + return ( + setIsOpen(e.open)} + unmountOnExit + > + + + + + + + + + Upload Artefacts + + + {view === 'uploadExisting' && + b.id === targetBatchID) || null} + setBatchID={(id) => setTargetBatchID(id)} + onClose={handleClose} + fetchBatches={fetchBatches} + onBackToPending={() => setView("pendingList")} />} + + {view === "pendingList" && ( + + + + + + + + + + Pending Batches + + {pendingBatches.length === 0 ? ( + + No pending batches. + + ) : ( + + {pendingBatches.map((batch) => ( + { + setTargetBatchID(batch.id); + setView("uploadExisting"); + }} + /> + ))} + + )} + + + )} + + + + + + + + + + + ) +} + +export default ImportMenuDialog; \ No newline at end of file diff --git a/src/components/DataImport/PendingBatchCard.jsx b/src/components/DataImport/PendingBatchCard.jsx new file mode 100644 index 0000000..52a2e24 --- /dev/null +++ b/src/components/DataImport/PendingBatchCard.jsx @@ -0,0 +1,43 @@ +import { Box, Flex, Image, Text } from "@chakra-ui/react"; + +const PendingBatchCard = ({ batchName, artefactCount, timestamp, thumbnail, onClick }) => { + return ( + + + {batchName} + + + {batchName} + + + {artefactCount != null + ? `${artefactCount} Artefact${artefactCount === 1 ? '' : 's'}` + : "No artefacts"} + + + {timestamp} + + + + + ); +}; + +export default PendingBatchCard; \ No newline at end of file diff --git a/src/pages/DataImport.jsx b/src/pages/DataImport.jsx index 8cfca43..93b0c7f 100644 --- a/src/pages/DataImport.jsx +++ b/src/pages/DataImport.jsx @@ -6,7 +6,7 @@ import { useSelector } from "react-redux"; import ToastWizard from '../components/toastWizard' import server, { JSONResponse } from "../networking"; import BatchCard from "../components/DataImport/BatchCard"; -import DialogUpload from "../components/DataImport/DialogUpload"; +import ImportMenuDialog from "../components/DataImport/ImportMenuDialog.jsx"; import CentredSpinner from "../components/centredSpinner.jsx"; const stageCollection = createListCollection({ @@ -59,6 +59,9 @@ function DataImport() { const [loading, setLoading] = useState(true); const [selectedStages, setSelectedStages] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); + const [batchNumberMap, setBatchNumberMap] = useState({}); + const [pendingBatches, setPendingBatches] = useState([]); + const { loaded } = useSelector(state => state.auth); @@ -113,7 +116,23 @@ function DataImport() { }; }); - setBatches(batchList); + const sorted = batchList.sort((a, b) => new Date(a.created) - new Date(b.created)); + const batchNumberMap = {}; + sorted.forEach((batch, i) => { + batchNumberMap[batch.id] = i + 1; + }); + + const pendingBatches = sorted + .filter(b => b.stage === "upload_pending") + .map(b => ({ + ...b, + created: formatTimestamp(b.created), + artefactCount: b.artefactSummary?.total ?? 0, + })); + + setBatches(sorted); + setBatchNumberMap(batchNumberMap); + setPendingBatches(pendingBatches); } else { throw new Error("Unexpected response format"); @@ -145,11 +164,12 @@ function DataImport() { const stageGroups = useMemo(() => { return stages .filter(stage => selectedStages.length === 0 || selectedStages.includes(stage)) - .map(stage => { - const filtered = batches.filter(batch => batch.stage === stage); - return { stage, label: stageLabels[stage], batches: filtered }; - }); - }, [batches, selectedStages]); + .map(stage => ({ + stage, + label: stageLabels[stage], + batches: batches.filter(batch => batch.stage === stage), + })); + }, [batches, selectedStages, stageLabels, stages]); if (loading) { return ; @@ -188,7 +208,10 @@ function DataImport() { {/* Upload Button */} - + {/* Second Row: Stage Filter + Upload Button */} @@ -249,7 +272,7 @@ function DataImport() { filtered.map((batch, idx) => (