diff --git a/Dockerfile-local b/Dockerfile-local index be4e450b..5f48aa85 100644 --- a/Dockerfile-local +++ b/Dockerfile-local @@ -36,6 +36,6 @@ EXPOSE 5055 # Command to run Gunicorn #CMD ["poetry", "run", "gunicorn", "-w", "1", "-b", "0.0.0.0:5055", "mdvtools.dbutils.mdv_server_app:app"] -CMD ["poetry", "run", "python", "-m", "mdvtools.dbutils.mdv_server_app"] +CMD ["poetry", "run", "python", "-u", "-m", "mdvtools.dbutils.mdv_server_app"] diff --git a/docker-local.yml b/docker-local.yml index 184d9698..fa627d5c 100644 --- a/docker-local.yml +++ b/docker-local.yml @@ -2,17 +2,23 @@ services: mdv_app: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile-local #image: jayeshire/mdv-frontend:latest ports: - "5055:5055" volumes: + - ./python:/app/python - mdv-data:/app/mdv - ./secrets/db_user:/run/secrets/db_user:ro - ./secrets/db_password:/run/secrets/db_password:ro - ./secrets/db_name:/run/secrets/db_name:ro + environment: + - FLASK_ENV=development + - FLASK_DEBUG=1 + - PYTHONUNBUFFERED=1 depends_on: - mdv_db + restart: always mdv_db: @@ -26,6 +32,7 @@ services: POSTGRES_USER_FILE: /run/secrets/db_user POSTGRES_PASSWORD_FILE: /run/secrets/db_password POSTGRES_DB_FILE: /run/secrets/db_name + restart: always volumes: diff --git a/package.json b/package.json index b9bd420b..f8c6c08e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "python-setup": "python -m venv venv && source venv/bin/activate && cd python && poetry install --with dev && npm run build-flask-vite", "mdv_desktop": "source venv/bin/activate && python -m mdvtools.mdv_desktop", "docker_build": "docker-compose build && docker-compose -f docker-compose-dev.yml up -d --force-recreate", - "docker_dev": "docker-compose -f docker-compose-dev.yml up -d --force-recreate", + "docker_dev": "docker-compose -f docker-local.yml up -d --force-recreate", + "docker_purge": "docker rm -f $(docker ps -a -q) && docker rmi -f $(docker images -q) && docker volume rm $(docker volume ls -q)", "docker_stack_rm": "docker stack rm mdv_stack", "docker_createandpush": "docker build -t my_mdv_app_image:latest . && docker tag my_mdv_app_image:latest jayeshire/my_mdv_app_image:latest && docker push jayeshire/my_mdv_app_image:latest", "docker_stack_deploy": "docker stack deploy -c docker-compose-stack.yml mdv_stack", diff --git a/python/mdvtools/dbutils/test/__init__.py b/python/mdvtools/dbutils/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/mdvtools/dbutils/test/integration/__init__.py b/python/mdvtools/dbutils/test/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/mdvtools/dbutils/test/unit/__init__.py b/python/mdvtools/dbutils/test/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/mdvtools/dbutils/test/unit/test_dbservice.py b/python/mdvtools/dbutils/test/unit/test_dbservice.py index b94b7478..f09e34c9 100644 --- a/python/mdvtools/dbutils/test/unit/test_dbservice.py +++ b/python/mdvtools/dbutils/test/unit/test_dbservice.py @@ -1,12 +1,12 @@ import unittest from unittest.mock import patch, MagicMock -from project_service import ProjectService, FileService +from mdvtools.dbutils.dbservice import ProjectService, FileService from mdvtools.dbutils.dbmodels import Project, File from datetime import datetime class TestProjectService(unittest.TestCase): - @patch('project_service.Project.query') + @patch('mdvtools.dbutils.dbmodels.Project.query') def test_get_active_projects_success(self, mock_query): mock_query.filter_by.return_value.all.return_value = [MagicMock(), MagicMock()] result = ProjectService.get_active_projects() diff --git a/python/mdvtools/mdvproject.py b/python/mdvtools/mdvproject.py index c2e87e84..d0fd4e57 100644 --- a/python/mdvtools/mdvproject.py +++ b/python/mdvtools/mdvproject.py @@ -20,6 +20,7 @@ from .charts.view import View import time import copy +from mdvtools.dbutils.dbservice import ProjectService, FileService DataSourceName = str # NewType("DataSourceName", str) ColumnName = str # NewType("ColumnName", str) @@ -259,7 +260,7 @@ def add_images_to_datasource( print(f"Added image set {name} to {ds} datasource") self.set_datasource_metadata(ds_metadata) - def add_or_update_image_datasource(self, tiff_metadata, datasource_name): + def add_or_update_image_datasource(self, tiff_metadata, datasource_name, file, project_id): """Add or update an image datasource in datasources.json""" try: # Load current datasources @@ -272,36 +273,92 @@ def add_or_update_image_datasource(self, tiff_metadata, datasource_name): # Update the existing datasource and check the result update_success = self.update_datasource(datasource, tiff_metadata) if not update_success: + error_message = "update_datasource failed" print(f"Failed to update datasource '{datasource_name}'.") return False else: # Create a new datasource # Uncomment and implement the following line if needed # creation_success = self.create_new_datasource(tiff_metadata, datasource_name) - print(f"Datasource '{datasource_name}' does not exist and creation is not implemented yet.") + print(f"Datasource '{datasource_name}' does not exist") + return False + + # Upload the TIFF file only if the datasource update was successful + upload_success = self.upload_image_file(file, project_id) + if not upload_success: + print(f"Failed to upload TIFF file for datasource '{datasource_name}'.") return False + # If both update and upload succeed, return True + #file_path = '' + #db_update_success = FileService.add_or_update_file_in_project( + # file_name=file.filename, + # file_path=file.filename, # Assuming 'file_path' is the filename in this example, adjust as necessary + # project_id=project_id + #) return True except Exception as e: print(f"Error updating or adding datasource '{datasource_name}': {e}") return False + + def upload_image_file(self, file, project_id): + """Upload the TIFF file to the imagefolder, saving it with the original filename.""" + try: + # Define the target folder inside imagefolder (e.g., /images/avivator) + target_folder = os.path.join(self.imagefolder, 'avivator') + print(target_folder) + # Ensure the target folder exists + if not os.path.exists(target_folder): + os.makedirs(target_folder) + + # Get the original filename from the file + original_filename = file.filename # This will give you the name of the uploaded file + + # Create the full file path inside /images/avivator + file_path = os.path.join(target_folder, original_filename) + + # Save the file to the /images/avivator folder + file.save(file_path) + print(f"File uploaded successfully to {file_path}") + + # Update the database with the file information + db_file = FileService.add_or_update_file_in_project( + file_name=original_filename, + file_path=file_path, # The full path where the file was saved + project_id=project_id + ) + + # Check if the file was successfully added or updated in the database + if db_file is None: + raise ValueError(f"Failed to add file '{original_filename}' to the database.") + else: + print(f"Added file to DB: {db_file}") + + return True + except Exception as e: + print(f"Error uploading file: {e}") + return False def update_datasource(self, datasource, tiff_metadata): """Update an existing datasource with new image metadata.""" try: - # Extract image metadata from tiff_metadata - width = tiff_metadata['OME']['Image']['Pixels']['SizeX'] - height = tiff_metadata['OME']['Image']['Pixels']['SizeY'] - scale = tiff_metadata['OME']['Image']['Pixels']['PhysicalSizeX'] - scale_unit = tiff_metadata['OME']['Image']['Pixels'].get('PhysicalSizeUnit', 'µm') # Default to µm if not present - + print("*****1") + + # Corrected path to access size and scale information + pixels_data = tiff_metadata['Pixels'] + width = pixels_data['SizeX'] + height = pixels_data['SizeY'] + scale = pixels_data['PhysicalSizeX'] + scale_unit = pixels_data.get('PhysicalSizeXUnit', 'µm') # Default to µm if not present + + print("*****2") # Ensure datasource has a 'regions' field if "regions" not in datasource: datasource["regions"] = {"all_regions": {}} # Determine region name - region_name = tiff_metadata.get('name', 'unknown') # Default to 'unknown' if not present in metadata + region_name = tiff_metadata.get('Name', 'unknown') # Use 'Name' from metadata or 'unknown' # Define new region with metadata new_region = { @@ -324,25 +381,27 @@ def update_datasource(self, datasource, tiff_metadata): } image_metadata = { - 'path': tiff_metadata['path'] + 'path': tiff_metadata.get('path', '') # Use the 'path' key if it exists, or default to '' } # Update or add the region in the datasource datasource["regions"]["all_regions"][region_name] = new_region datasource['size'] = len(datasource['regions']['all_regions']) + print("*****3") # Save the updated datasource self.set_datasource_metadata(datasource) - + print("*****4") # Update views and images - self.add_viv_viewer(region_name, [{'name': 'DAPI'}]) - self.add_viv_images(region_name, image_metadata, link_images=True) + #self.add_viv_viewer(region_name, [{'name': 'DAPI'}]) + #self.add_viv_images(region_name, image_metadata, link_images=True) print(f"Datasource '{datasource.get('name', 'unknown')}' updated successfully.") return True except Exception as e: print(f"Error updating datasource '{datasource.get('name', 'unknown')}': {e}") return False + diff --git a/python/mdvtools/server.py b/python/mdvtools/server.py index 18c9efab..31c044c6 100644 --- a/python/mdvtools/server.py +++ b/python/mdvtools/server.py @@ -248,24 +248,37 @@ def save_data(): @project_bp.route("/add_or_update_image_datasource", methods=["POST"]) def add_or_update_image_datasource(): try: - # Extract data from the request - data = request.json - if not data: - return "Request must contain JSON data with tiffMetadata & datasourceName", 400 - tiff_metadata = data.get('tiffMetadata') - datasource_name = data.get('datasourceName') - - if not tiff_metadata or not datasource_name: - return "Request must contain JSON data with tiffMetadata & datasourceName", 400 + # Check if request has a file part + if 'file' not in request.files: + return "No file part in the request", 400 + + # Get the file from the request + file = request.files['file'] + + # Get the text fields from the request form + datasource_name = request.form.get('datasourceName') # "" + tiff_metadata = request.form.get('tiffMetadata') + + # Validate the presence of required fields + if not file or not tiff_metadata: + return jsonify({"status": "error", "message": "Missing file, tiffMetadata, or datasourceName"}), 400 + + # If tiff_metadata is sent as JSON string, deserialize it + try: + tiff_metadata = json.loads(tiff_metadata) + except Exception as e: + return jsonify({"status": "error", "message": f"Invalid JSON format for tiffMetadata: {e}"}), 400 + + # Call your method to add or update the image datasource + success = project.add_or_update_image_datasource(tiff_metadata, datasource_name, file, project.id) - # Call the method in the project class to add or update image datasource - success = project.add_or_update_image_datasource(tiff_metadata, datasource_name) if success: - return "Image datasource updated successfully", 200 + return jsonify({"status": "success", "message": "Image datasource updated and file uploaded successfully"}), 200 else: return "Failed to update image datasource", 500 except Exception as e: - return str(e), 500 + return jsonify({"status": "error", "message": str(e)}), 500 + @project_bp.route("/add_datasource", methods=["POST"]) def add_datasource(): diff --git a/src/charts/dialogs/FileUploadDialog.tsx b/src/charts/dialogs/FileUploadDialog.tsx index 2e025534..d974b2d9 100644 --- a/src/charts/dialogs/FileUploadDialog.tsx +++ b/src/charts/dialogs/FileUploadDialog.tsx @@ -1,237 +1,237 @@ import type React from "react"; import { - useState, - useCallback, - useReducer, - type PropsWithChildren, - forwardRef, + useState, + useCallback, + useReducer, + type PropsWithChildren, + forwardRef, } from "react"; import { useDropzone } from "react-dropzone"; -import { observer } from "mobx-react-lite"; +import { observer } from 'mobx-react-lite'; -import axios from "axios"; +import axios from 'axios'; import { useProject } from "../../modules/ProjectContext"; -import { ColumnPreview } from "./ColumnPreview"; +import { ColumnPreview } from "./ColumnPreview" -import { - useViewerStoreApi, - useChannelsStoreApi, -} from "../../react/components/avivatorish/state"; -import { createLoader } from "../../react/components/avivatorish/utils"; -import { unstable_batchedUpdates } from "react-dom"; +import { useViewerStoreApi, useChannelsStoreApi } from '../../react/components/avivatorish/state'; +import { createLoader } from '../../react/components/avivatorish/utils'; +import { unstable_batchedUpdates } from 'react-dom'; import { TiffPreview } from "./TiffPreview"; -import { TiffMetadataTable } from "./TiffMetadataTable"; -import TiffVisualization from "./TiffVisualization"; +import { TiffMetadataTable } from "./TiffMetadataTable" +import TiffVisualization from './TiffVisualization'; import { DatasourceDropdown } from "./DatasourceDropdown"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; + // Use dynamic import for the worker const CsvWorker = new Worker(new URL("./csvWorker.ts", import.meta.url), { - type: "module", + type: "module", }); const Container = ({ children }: PropsWithChildren) => { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); }; const StatusContainer = ({ children }: PropsWithChildren) => { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); }; const SuccessContainer = ({ children }) => ( -
- {children} -
+
+ {children} +
); const SuccessHeading = ({ children }) => ( -

{children}

+

{children}

); const SuccessText = ({ children }) => ( -

- {children} -

+

+ {children} +

); const DropzoneContainer = forwardRef( - ({ isDragOver, children, ...props }: any, ref) => ( -
- {children} -
- ), + ({ isDragOver, children, ...props }: any, ref) => ( +
+ {children} +
+ ), ); const FileInputLabel = ({ children, ...props }) => ( - + ); const Spinner = () => { - return ( -
- ); + return ( +
+ ); }; const colorStyles = { - blue: { - bgColor: "bg-blue-600", - hoverColor: "hover:bg-blue-700", - darkBgColor: "dark:bg-blue-800", - darkHoverColor: "dark:bg-blue-900", - }, - red: { - bgColor: "bg-red-600", - hoverColor: "hover:bg-red-700", - darkBgColor: "dark:bg-red-800", - darkHoverColor: "dark:bg-red-900", - }, - green: { - bgColor: "bg-green-600", - hoverColor: "hover:bg-green-700", - darkBgColor: "dark:bg-green-800", - darkHoverColor: "dark:bg-green-900", - }, - gray: { - bgColor: "bg-gray-600", - hoverColor: "hover:bg-gray-700", - darkBgColor: "dark:bg-gray-800", - darkHoverColor: "dark:bg-gray-900", - }, + blue: { + bgColor: "bg-blue-600", + hoverColor: "hover:bg-blue-700", + darkBgColor: "dark:bg-blue-800", + darkHoverColor: "dark:bg-blue-900", + }, + red: { + bgColor: "bg-red-600", + hoverColor: "hover:bg-red-700", + darkBgColor: "dark:bg-red-800", + darkHoverColor: "dark:bg-red-900", + }, + green: { + bgColor: "bg-green-600", + hoverColor: "hover:bg-green-700", + darkBgColor: "dark:bg-green-800", + darkHoverColor: "dark:bg-green-900", + }, + gray: { + bgColor: "bg-gray-600", + hoverColor: "hover:bg-gray-700", + darkBgColor: "dark:bg-gray-800", + darkHoverColor: "dark:bg-gray-900", + }, }; const Button = ({ - onClick, - color = "blue", - disabled = false, - size = "px-5 py-2.5", - marginTop = "mt-2.5", - children, + onClick, + color = "blue", + disabled = false, + size = "px-5 py-2.5", + marginTop = "mt-2.5", + children, }) => { - const { bgColor, hoverColor, darkBgColor, darkHoverColor } = - colorStyles[color] || colorStyles.blue; - - return ( - - ); + const { bgColor, hoverColor, darkBgColor, darkHoverColor } = + colorStyles[color] || colorStyles.blue; + + return ( + + ); }; const ProgressBar = ({ value, max }) => ( - + ); const Message = ({ children }) => ( -

- {children} -

+

+ {children} +

); const FileSummary = ({ children }) => ( -
{children}
+
{children}
); const FileSummaryHeading = ({ children }) => ( -

{children}

+

{children}

); const FileSummaryText = ({ children }) => ( -

{children}

+

{children}

); const ErrorContainer = ({ children }) => ( -
- {children} -
+
+ {children} +
); const DynamicText = ({ text, className = "" }) => ( -
-

- {text} -

-
+
+

+ {text} +

+
); const ErrorHeading = ({ children }) => ( -

{children}

+

{children}

); const DatasourceNameInput = ({ value, onChange, isDisabled }) => ( -
- - -
+
+ + +
); // Reducer function const reducer = (state, action) => { - switch (action.type) { - case "SET_SELECTED_FILES": - return { ...state, selectedFiles: action.payload }; - case "SET_IS_UPLOADING": - return { ...state, isUploading: action.payload }; - case "SET_IS_INSERTING": - return { ...state, isInserting: action.payload }; - case "SET_SUCCESS": - return { ...state, success: action.payload }; - case "SET_ERROR": - return { ...state, error: action.payload }; - case "SET_IS_VALIDATING": - return { ...state, isValidating: action.payload }; - case "SET_VALIDATION_RESULT": - return { ...state, validationResult: action.payload }; - case "SET_FILE_TYPE": - return { ...state, fileType: action.payload }; - case "SET_TIFF_METADATA": - return { ...state, tiffMetadata: action.payload }; - default: - return state; - } + switch (action.type) { + case "SET_SELECTED_FILES": + return { ...state, selectedFiles: action.payload }; + case "SET_IS_UPLOADING": + return { ...state, isUploading: action.payload }; + case "SET_IS_INSERTING": + return { ...state, isInserting: action.payload }; + case "SET_SUCCESS": + return { ...state, success: action.payload }; + case "SET_ERROR": + return { ...state, error: action.payload }; + case "SET_IS_VALIDATING": + return { ...state, isValidating: action.payload }; + case "SET_VALIDATION_RESULT": + return { ...state, validationResult: action.payload }; + case "SET_FILE_TYPE": + return { ...state, fileType: action.payload }; + case "SET_TIFF_METADATA": + return { ...state, tiffMetadata: action.payload }; + default: + return state; + } }; // Constants @@ -240,698 +240,563 @@ const UPLOAD_DURATION = 3000; const UPLOAD_STEP = 100 / (UPLOAD_DURATION / UPLOAD_INTERVAL); interface FileUploadDialogComponentProps { - onClose: () => void; - onResize: (width: number, height: number) => void; // Add this prop + onClose: () => void; + onResize: (width: number, height: number) => void; // Add this prop } // Custom hook for file upload progress const useFileUploadProgress = () => { - const [progress, setProgress] = useState(0); - - const startProgress = useCallback(() => { - setProgress(0); - const interval = setInterval(() => { - setProgress((prevProgress) => { - const updatedProgress = prevProgress + UPLOAD_STEP; - if (updatedProgress >= 100) { - clearInterval(interval); - return 100; - } - return updatedProgress; - }); - }, UPLOAD_INTERVAL); - }, []); + const [progress, setProgress] = useState(0); + + const startProgress = useCallback(() => { + setProgress(0); + const interval = setInterval(() => { + setProgress((prevProgress) => { + const updatedProgress = prevProgress + UPLOAD_STEP; + if (updatedProgress >= 100) { + clearInterval(interval); + return 100; + } + return updatedProgress; + }); + }, UPLOAD_INTERVAL); + }, []); - const resetProgress = useCallback(() => { - setProgress(0); - }, []); + const resetProgress = useCallback(() => { + setProgress(0); + }, []); - return { progress, setProgress, startProgress, resetProgress }; + return { progress, setProgress, startProgress, resetProgress }; }; const FileUploadDialogComponent: React.FC = ({ - onClose, - onResize, + onClose, + onResize, }) => { - const { root, projectName, chartManager } = useProject(); - const [selectedOption, setSelectedOption] = useState(null); - const [updatedNamesArray, setUpdatedNamesArray] = useState([]); + //const viewerStore = vivStores.viewerStore; + + const { root, projectName, chartManager } = useProject(); + + const [selectedOption, setSelectedOption] = useState(null); + + const [updatedNamesArray, setUpdatedNamesArray] = useState([]); + + const handleSelect = (value: string) => { + setSelectedOption(value); + setDatasourceName(value); + console.log('Selected:', value); + }; + + const [datasourceName, setDatasourceName] = useState(""); + + const [showMetadata, setShowMetadata] = useState(true); + + const toggleView = () => { + setShowMetadata(prevState => !prevState); + }; + + const [csvSummary, setCsvSummary] = useState({ + datasourceName: "", + fileName: "", + fileSize: "", + rowCount: 0, + columnCount: 0, + }); + + const [tiffSummary, settiffSummary] = useState({ + fileName: "", + fileSize: "", + }); + + const [columnNames, setColumnNames] = useState([]); + const [columnTypes, setColumnTypes] = useState([]); + const [secondRowValues, setSecondRowValues] = useState([]); + + // TIFF + const channelsStore = useChannelsStoreApi(); + + const [state, dispatch] = useReducer(reducer, { + selectedFiles: [], + isUploading: false, + isInserting: false, + isValidating: false, + validationResult: null, + success: false, + error: null, + fileType: null, + tiffMetadata: null, + }); + + const viewerStore = useViewerStoreApi(); + + const { progress, resetProgress, setProgress } = useFileUploadProgress(); + + const onDrop = useCallback((acceptedFiles: File[]) => { + dispatch({ type: "SET_SELECTED_FILES", payload: acceptedFiles }); + if (acceptedFiles.length > 0) { + const file = acceptedFiles[0]; + const fileExtension = file.name.split('.').pop().toLowerCase(); + if (fileExtension === 'csv') { + const newDatasourceName = file.name; + setDatasourceName(newDatasourceName); + setCsvSummary({ + ...csvSummary, + datasourceName: newDatasourceName, + fileName: file.name, + fileSize: (file.size / (1024 * 1024)).toFixed(2) + }); + dispatch({ type: "SET_FILE_TYPE", payload: "csv" }); + } else if (fileExtension === 'tiff' || fileExtension === 'tif') { + + const dataSources = window.mdv.chartManager?.dataSources ?? []; + const namesArray = dataSources.map(dataSource => dataSource.name); + + // Update state with the new array, triggering re-render + setUpdatedNamesArray([...namesArray, "new datasource"]); + settiffSummary({ + ...tiffSummary, + fileName: file.name, + fileSize: (file.size / (1024 * 1024)).toFixed(2) + }); - const handleSelect = (value: string) => { - setSelectedOption(value); - setDatasourceName(value); - console.log("Selected:", value); - }; + dispatch({ type: "SET_FILE_TYPE", payload: "tiff" }); + handleSubmitFile(acceptedFiles); + } + } + }, [csvSummary]); + + const handleSubmitFile = useCallback(async (files: File[]) => { + let newSource; + if (files.length === 1) { + newSource = { + urlOrFile: files[0], + description: files[0].name + }; + } else { + newSource = { + urlOrFile: files, + description: 'data.zarr' + }; + } - const [datasourceName, setDatasourceName] = useState(""); + viewerStore.setState({ isChannelLoading: [true] }); + viewerStore.setState({ isViewerLoading: true }); + + try { + const newLoader = await createLoader( + newSource.urlOrFile, + () => { }, // placeholder for toggleIsOffsetsSnackbarOn + (message) => viewerStore.setState({ loaderErrorSnackbar: { on: true, message } }) + ); + + let nextMeta; + let nextLoader; + if (Array.isArray(newLoader)) { + if (newLoader.length > 1) { + nextMeta = newLoader.map(l => l.metadata); + nextLoader = newLoader.map(l => l.data); + } else { + nextMeta = newLoader[0].metadata; + nextLoader = newLoader[0].data; + } + } else { + nextMeta = newLoader.metadata; + nextLoader = newLoader.data; + } - const [showMetadata, setShowMetadata] = useState(true); + if (nextLoader) { + console.log('Metadata (in JSON-like form) for current file being viewed: ', nextMeta); - const toggleView = () => { - setShowMetadata((prevState) => !prevState); - }; + unstable_batchedUpdates(() => { + channelsStore.setState({ loader: nextLoader }); + viewerStore.setState({ metadata: nextMeta }); + }); - const [csvSummary, setCsvSummary] = useState({ - datasourceName: "", - fileName: "", - fileSize: "", - rowCount: 0, - columnCount: 0, - }); - - const [tiffSummary, settiffSummary] = useState({ - fileName: "", - fileSize: "", - }); - - const [columnNames, setColumnNames] = useState([]); - const [columnTypes, setColumnTypes] = useState([]); - const [secondRowValues, setSecondRowValues] = useState([]); - - // TIFF - const channelsStore = useChannelsStoreApi(); - - const [state, dispatch] = useReducer(reducer, { - selectedFiles: [], - isUploading: false, - isInserting: false, - isValidating: false, - validationResult: null, - success: false, - error: null, - fileType: null, - tiffMetadata: null, - }); - - const viewerStore = useViewerStoreApi(); - - const { progress, resetProgress, setProgress } = useFileUploadProgress(); - - const onDrop = useCallback( - (acceptedFiles: File[]) => { - if (acceptedFiles.length > 0) { - const file = acceptedFiles[0]; - dispatch({ - type: "SET_SELECTED_FILES", - payload: acceptedFiles, - }); - const fileExtension = file.name.split(".").pop().toLowerCase(); - - // Common logic for both CSV and TIFF files - if (fileExtension === "csv") { - const newDatasourceName = file.name; - setDatasourceName(newDatasourceName); - setCsvSummary({ - ...csvSummary, - datasourceName: newDatasourceName, - fileName: file.name, - fileSize: (file.size / (1024 * 1024)).toFixed(2), - }); - dispatch({ type: "SET_FILE_TYPE", payload: "csv" }); - } else if ( - fileExtension === "tiff" || - fileExtension === "tif" - ) { - const dataSources = - window.mdv.chartManager?.dataSources ?? []; - const namesArray = dataSources.map( - (dataSource) => dataSource.name, - ); - - // Update state with the new array, triggering re-render - setUpdatedNamesArray([...namesArray, "new datasource"]); - settiffSummary({ - ...tiffSummary, - fileName: file.name, - fileSize: (file.size / (1024 * 1024)).toFixed(2), - }); - dispatch({ type: "SET_FILE_TYPE", payload: "tiff" }); - handleSubmitFile(acceptedFiles); - } - - // Start validation after dispatching file type - dispatch({ type: "SET_IS_VALIDATING", payload: true }); - - if (fileExtension === "csv") { - CsvWorker.postMessage(file); - CsvWorker.onmessage = (event: MessageEvent) => { - const { - columnNames, - columnTypes, - secondRowValues, - rowCount, - columnCount, - error, - } = event.data; - if (error) { - dispatch({ - type: "SET_ERROR", - payload: { - message: "Validation failed.", - traceback: error, - }, - }); - dispatch({ - type: "SET_IS_VALIDATING", - payload: false, - }); - } else { - setColumnNames(columnNames); - setColumnTypes(columnTypes); - setSecondRowValues(secondRowValues); - setCsvSummary((prevCsvSummary) => ({ - ...prevCsvSummary, - rowCount, - columnCount, - })); - - const totalWidth = calculateTotalWidth( - columnNames, - columnTypes, - secondRowValues, - ); - onResize(totalWidth, 745); - dispatch({ - type: "SET_IS_VALIDATING", - payload: false, - }); - dispatch({ - type: "SET_VALIDATION_RESULT", - payload: { columnNames, columnTypes }, - }); - } - }; - } else if (fileExtension === "tiff") { - onResize(1032, 580); - dispatch({ type: "SET_IS_VALIDATING", payload: false }); - dispatch({ - type: "SET_VALIDATION_RESULT", - payload: { columnNames, columnTypes }, - }); - } - } - }, - [csvSummary, tiffSummary], + dispatch({ type: "SET_TIFF_METADATA", payload: nextMeta }); + } + } catch (error) { + console.error('Error loading file:', error); + dispatch({ + type: "SET_ERROR", + payload: { + message: "Error loading TIFF file.", + traceback: error.message + } + }); + } finally { + viewerStore.setState({ isChannelLoading: [false] }); + viewerStore.setState({ isViewerLoading: false }); + } + }, [viewerStore, channelsStore]); + + const handleDatasourceNameChange = (event) => { + const { value } = event.target; + setDatasourceName(value); // Update the state with the new value + setCsvSummary((prevCsvSummary) => ({ + ...prevCsvSummary, + datasourceName: value, // Update datasourceName in the summary object + })); + }; + + const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ + onDrop, + accept: { + 'text/csv': ['.csv'], + 'image/tiff': ['.tiff', '.tif'] + } + }); + const rejectionMessage = fileRejections.length > 0 + ? "Only CSV and TIFF files can be selected" + : "Drag and drop files here or click the button below to upload"; + + const rejectionMessageStyle = fileRejections.length > 0 ? "text-red-500" : ""; + + const getTextWidth = ( + canvas: HTMLCanvasElement, + context: CanvasRenderingContext2D, + text: string, + ) => { + context.font = getComputedStyle(document.body).fontSize + " Arial"; + return context.measureText(text).width; + }; + + // Function to calculate the maximum total width needed for the ColumnPreview component + const calculateTotalWidth = ( + columnNames: string[], + columnTypes: string[], + secondRowValues: string[], + ) => { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + + let maxColumnNameWidth = 0; + let maxColumnTypeWidth = 0; + let maxColumnSecondRowWidth = 0; + + // Calculate the maximum width of column names + maxColumnNameWidth = Math.max( + ...columnNames.map((name) => getTextWidth(canvas, context, name)), ); - const handleSubmitFile = useCallback( - async (files: File[]) => { - let newSource; - if (files.length === 1) { - newSource = { - urlOrFile: files[0], - description: files[0].name, - }; - } else { - newSource = { - urlOrFile: files, - description: "data.zarr", - }; - } - - viewerStore.setState({ isChannelLoading: [true] }); - viewerStore.setState({ isViewerLoading: true }); - - try { - const newLoader = await createLoader( - newSource.urlOrFile, - () => {}, // placeholder for toggleIsOffsetsSnackbarOn - (message) => - viewerStore.setState({ - loaderErrorSnackbar: { on: true, message }, - }), - ); - - let nextMeta; - let nextLoader; - if (Array.isArray(newLoader)) { - if (newLoader.length > 1) { - nextMeta = newLoader.map((l) => l.metadata); - nextLoader = newLoader.map((l) => l.data); - } else { - nextMeta = newLoader[0].metadata; - nextLoader = newLoader[0].data; - } - } else { - nextMeta = newLoader.metadata; - nextLoader = newLoader.data; - } - - if (nextLoader) { - console.log( - "Metadata (in JSON-like form) for current file being viewed: ", - nextMeta, - ); - - unstable_batchedUpdates(() => { - channelsStore.setState({ loader: nextLoader }); - viewerStore.setState({ metadata: nextMeta }); - }); - - dispatch({ type: "SET_TIFF_METADATA", payload: nextMeta }); - } - } catch (error) { - console.error("Error loading file:", error); - dispatch({ - type: "SET_ERROR", - payload: { - message: "Error loading TIFF file.", - traceback: error.message, - }, - }); - } finally { - viewerStore.setState({ isChannelLoading: [false] }); - viewerStore.setState({ isViewerLoading: false }); - } - }, - [viewerStore, channelsStore], + // Calculate the maximum width of column types + maxColumnTypeWidth = Math.max( + ...columnTypes.map((type) => getTextWidth(canvas, context, type)), ); - const handleDatasourceNameChange = (event) => { - const { value } = event.target; - setDatasourceName(value); // Update the state with the new value - setCsvSummary((prevCsvSummary) => ({ - ...prevCsvSummary, - datasourceName: value, // Update datasourceName in the summary object - })); - }; - - const { getRootProps, getInputProps, isDragActive, fileRejections } = - useDropzone({ - onDrop, - accept: { - "text/csv": [".csv"], - "image/tiff": [".tiff", ".tif"], - }, - }); - const rejectionMessage = - fileRejections.length > 0 - ? "Only CSV and TIFF files can be selected" - : "Drag and drop files here or click the button below to upload"; - - const rejectionMessageStyle = - fileRejections.length > 0 ? "text-red-500" : ""; - - const getTextWidth = ( - canvas: HTMLCanvasElement, - context: CanvasRenderingContext2D, - text: string, - ) => { - context.font = getComputedStyle(document.body).fontSize + " Arial"; - return context.measureText(text).width; - }; + // Calculate the maximum width of second row values + maxColumnSecondRowWidth = Math.max( + ...secondRowValues.map((value) => + getTextWidth(canvas, context, value), + ), + ); - // Function to calculate the maximum total width needed for the ColumnPreview component - const calculateTotalWidth = ( - columnNames: string[], - columnTypes: string[], - secondRowValues: string[], - ) => { - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - - let maxColumnNameWidth = 0; - let maxColumnTypeWidth = 0; - let maxColumnSecondRowWidth = 0; - - // Calculate the maximum width of column names - maxColumnNameWidth = Math.max( - ...columnNames.map((name) => getTextWidth(canvas, context, name)), - ); - - // Calculate the maximum width of column types - maxColumnTypeWidth = Math.max( - ...columnTypes.map((type) => getTextWidth(canvas, context, type)), - ); - - // Calculate the maximum width of second row values - maxColumnSecondRowWidth = Math.max( - ...secondRowValues.map((value) => - getTextWidth(canvas, context, value), - ), - ); - - // Calculate the total width needed for the ColumnPreview component - const totalWidth = - maxColumnNameWidth + - maxColumnTypeWidth + - maxColumnSecondRowWidth + - 32; // Add padding - canvas.remove(); - return Math.max(800, totalWidth); - }; + // Calculate the total width needed for the ColumnPreview component + const totalWidth = maxColumnNameWidth + maxColumnTypeWidth + maxColumnSecondRowWidth + 32; // Add padding + canvas.remove(); + return Math.max(500, totalWidth); + }; + + const handleValidateClick = () => { + const file = state.selectedFiles[0]; + if (file) { + dispatch({ type: "SET_IS_VALIDATING", payload: true }); + + const fileExtension = file.name.split('.').pop().toLowerCase(); + if (fileExtension === 'csv') { + CsvWorker.postMessage(file); + CsvWorker.onmessage = (event: MessageEvent) => { + const { + columnNames, + columnTypes, + secondRowValues, + rowCount, + columnCount, + error, + } = event.data; + if (error) { + dispatch({ + type: "SET_ERROR", + payload: { + message: "Validation failed.", + traceback: error, + }, + }); + dispatch({ type: "SET_IS_VALIDATING", payload: false }); + } else { + setColumnNames(columnNames); + setColumnTypes(columnTypes); + setSecondRowValues(secondRowValues); + setCsvSummary((prevCsvSummary) => ({ + ...prevCsvSummary, + rowCount, + columnCount, + })); + + // Calculate the total width needed for the ColumnPreview component + const totalWidth = calculateTotalWidth( + columnNames, + columnTypes, + secondRowValues, + ); + onResize(totalWidth, 730); + dispatch({ type: "SET_IS_VALIDATING", payload: false }); + dispatch({ + type: "SET_VALIDATION_RESULT", + payload: { columnNames, columnTypes }, + }); + } + }; + } else if (fileExtension === 'tiff') { + onResize(1032, 580); + dispatch({ type: "SET_IS_VALIDATING", payload: false }); + dispatch({ type: "SET_VALIDATION_RESULT", payload: { columnNames, columnTypes } }); + } + } + }; - const handleUploadClick = async () => { - console.log("Uploading file..."); + const handleUploadClick = async () => { + console.log("Uploading file..."); + if (!state.selectedFiles.length) { + dispatch({ type: "SET_ERROR", payload: "noFilesSelected" }); + return; + } - if (!state.selectedFiles.length) { - dispatch({ type: "SET_ERROR", payload: "noFilesSelected" }); - return; - } + const fileExtension = state.selectedFiles[0].name + .split(".") + .pop() + ?.toLowerCase(); + dispatch({ type: "SET_IS_UPLOADING", payload: true }); + resetProgress(); - const fileExtension = state.selectedFiles[0].name - .split(".") - .pop() - .toLowerCase(); - dispatch({ type: "SET_IS_UPLOADING", payload: true }); - resetProgress(); + const config = { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: (progressEvent) => { + const percentComplete = Math.round( + (progressEvent.loaded * 100) / progressEvent.total, + ); + setProgress(percentComplete); + }, + }; - const formData = new FormData(); - formData.append("file", state.selectedFiles[0]); - let endpoint: "add_datasource" | "upload" | null = null; + try { + let response; + if (fileExtension === "tiff") { + const formData = new FormData(); + formData.append("file", state.selectedFiles[0]); + formData.append("tiffMetadata", JSON.stringify(state.tiffMetadata)); + formData.append("datasourceName", datasourceName); - if (fileExtension === "csv") { + response = await axios.post( + `${root}/add_or_update_image_datasource`, + formData, + config, + ); + } else { + const formData = new FormData(); + formData.append("file", state.selectedFiles[0]); formData.append("name", datasourceName); - // formData.append("view", "TRUE"); - // formData.append("backend", "TRUE"); formData.append("replace", ""); - endpoint = "add_datasource"; - } else if (fileExtension === "tiff") { - formData.append("project_name", projectName); - // endpoint = "upload" - } - const config = { - headers: { - "Content-Type": "multipart/form-data", - }, - onUploadProgress: (progressEvent) => { - const percentComplete = Math.round( - (progressEvent.loaded * 100) / progressEvent.total, - ); - setProgress(percentComplete); // Update the progress as the file uploads - }, - }; - - try { - const response = await axios.post( + response = await axios.post( `${root}/add_datasource`, formData, config, ); - console.log("File uploaded successfully", response.data); - - if (response.status === 200) { - dispatch({ type: "SET_IS_UPLOADING", payload: false }); - dispatch({ type: "SET_SUCCESS", payload: true }); - - // Perform second request if the file is TIFF - if (fileExtension === "tiff") { - try { - const metadataResponse = await axios.post( - `${root}/add_or_update_image_datasource`, - { - tiffMetadata: state.tiffMetadata, - datasourceName: datasourceName, - }, - ); - console.log( - "Metadata updated successfully", - metadataResponse.data, - ); - chartManager.saveState(); - } catch (metadataError) { - console.error( - "Error updating metadata:", - metadataError, - ); - dispatch({ - type: "SET_ERROR", - payload: { - message: "Failed to update metadata.", - traceback: metadataError.message, - }, - }); - } - } - } else { - console.error( - `Failed to confirm: Server responded with status ${response.status}`, - ); - dispatch({ type: "SET_IS_UPLOADING", payload: false }); - dispatch({ - type: "SET_ERROR", - payload: { - message: `Confirmation failed with status: ${response.status}`, - status: response.status, - traceback: "Server responded with non-200 status", - }, - }); + } + + console.log("File uploaded successfully", response.data); + if (response.status === 200) { + dispatch({ type: "SET_IS_UPLOADING", payload: false }); + dispatch({ type: "SET_SUCCESS", payload: true }); + + if (fileExtension === "tiff") { + chartManager.saveState(); } - } catch (error) { - console.error("Error uploading file:", error); + } else { + console.error( + `Failed to confirm: Server responded with status ${response.status}`, + ); dispatch({ type: "SET_IS_UPLOADING", payload: false }); dispatch({ type: "SET_ERROR", payload: { - message: "Upload failed due to a network error.", - traceback: error.message, + message: `Confirmation failed with status: ${response.status}`, + status: response.status, + traceback: "Server responded with non-200 status", }, }); } - }; - - const handleClose = async () => { - dispatch({ type: "SET_FILE_SUMMARY", payload: null }); - onResize(450, 320); - onClose(); - }; - - return ( - - {state.isUploading ? ( - - - {"Your file is being uploaded, please wait..."} - - - - ) : state.isInserting ? ( - - - {"Your file is being processed, please wait..."} - - - - ) : state.success ? ( - <> - - Success! - - The file was uploaded successfully to the database. - - - - - ) : state.error ? ( - <> - - - An error occurred while uploading the file: - -

{state.error.message}

- {state.error.traceback && ( -
{state.error.traceback}
- )} -
- - ) : state.isValidating ? ( - - {"Validating data, please wait..."} - - - ) : state.validationResult ? ( - <> - {state.fileType === "csv" && ( - <> - - - {"Uploaded File Summary"} - - - - - - {"File name"}{" "} - {csvSummary.fileName} - - - {"Number of rows"}{" "} - {csvSummary.rowCount} - - - {"Number of columns"}{" "} - {csvSummary.columnCount} - - - {"File size"}{" "} - {csvSummary.fileSize} MB - - - - -
- - -
- - )} - - {state.fileType === "tiff" && state.tiffMetadata && ( - <> -
-
-
-
- - - {"Uploaded File Summary"} - - - - {"File name:"} - {" "} - {tiffSummary.fileName} - - - - {"File size:"} - {" "} - {tiffSummary.fileSize} MB - - - -
-
-
-
- - Image Preview: - - -
-
-
-
-
- {showMetadata ? ( - - ) : ( - - )} -
-
- -
- - -
-
-
-
- - )} - + } catch (error) { + console.error("Error uploading file:", error); + dispatch({ type: "SET_IS_UPLOADING", payload: false }); + dispatch({ + type: "SET_ERROR", + payload: { + message: "Upload failed due to a network error.", + traceback: error.message, + }, + }); + } + }; + + const handleClose = async () => { + dispatch({ type: "SET_FILE_SUMMARY", payload: null }); + onResize(450, 320); + onClose(); + }; + + return ( + + {state.isUploading ? ( + + {"Your file is being uploaded, please wait..."} + + + ) : state.isInserting ? ( + + {"Your file is being processed, please wait..."} + + + ) : state.success ? ( + <> + + Success! + + The file was uploaded successfully to the database. + + + + + + ) : state.error ? ( + <> + + An error occurred while uploading the file: +

{state.error.message}

+ {state.error.traceback && ( +
{state.error.traceback}
+ )} +
+ + ) : state.isValidating ? ( + + {"Validating data, please wait..."} + + + ) : state.validationResult ? ( + <> + {state.fileType === "csv" && ( + <> + + {"Uploaded File Summary"} + + {"Datasource name"} {csvSummary.datasourceName} + + + {"File name"} {csvSummary.fileName} + + + {"Number of rows"} {csvSummary.rowCount} + + + {"Number of columns"} {csvSummary.columnCount} + + + {"File size"} {csvSummary.fileSize} MB + + + +
+ + +
+ + )} + + + {state.fileType === "tiff" && state.tiffMetadata && ( + <> +
+
+
+
+ + {"Uploaded File Summary"} + + {"File name:"} {tiffSummary.fileName} + + + {"File size:"} {tiffSummary.fileSize} MB + + + + +
+
+
+
+ + Image Preview: + + +
+
+
+
+
+ {showMetadata ? : } +
+
+ +
+ + +
+
+
+
+ + )} + + ) : ( + <> + + + {isDragActive ? ( + ) : ( - <> - - -
- - {isDragActive ? ( - - ) : ( - 0 - ? `Selected file: ${state.selectedFiles[0].name}` - : rejectionMessage - } - className={`${rejectionMessageStyle} text-sm`} - /> - )} - - {"Choose File"} - -
-
- + 0 ? `Selected file: ${state.selectedFiles[0].name}` : rejectionMessage} className={rejectionMessageStyle} /> )} -
- ); + {"Choose File"} + +
+ + +
+ + )} +
+ ); }; -export default observer(FileUploadDialogComponent); +export default observer(FileUploadDialogComponent);; \ No newline at end of file