diff --git a/frontend/src/components/ResultGrid.tsx b/frontend/src/components/ResultGrid.tsx index 36e4a7be98a03af687e8161a2b669f0b64bd143c..de486d6313fec72961da0db2c766bb06fecfb5cc 100644 --- a/frontend/src/components/ResultGrid.tsx +++ b/frontend/src/components/ResultGrid.tsx @@ -1,9 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { DataGridPremium, GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium'; -import IconButton from '@mui/material/IconButton'; -import InfoIcon from '@mui/icons-material/Info'; -import { OpenAPI, SamplesService } from '../../openapi'; +import { DataGridPremium, GridColDef } from '@mui/x-data-grid-premium'; +import RunDetails from './RunDetails'; import './SampleImage.css'; +import { OpenAPI, SamplesService } from '../../openapi'; // Extend your image info interface if needed. interface ImageInfo { @@ -97,38 +96,44 @@ interface ResultGridProps { activePgroup: string; } -// Helper function to safely get the number of images. -const getNumberOfImages = (run: ExperimentParameters): number => { - const params = run.beamline_parameters; - if (params.rotation && params.rotation.numberOfImages != null) { - return params.rotation.numberOfImages; - } else if (params.gridScan && params.gridScan.numberOfImages != null) { - return params.gridScan.numberOfImages; - } - return 0; -}; - -// Helper function to determine the experiment type. -const getExperimentType = (run: ExperimentParameters): string => { - const params = run.beamline_parameters; - if (params.rotation && params.rotation.numberOfImages != null && params.rotation.omegaStep != null) { - const numImages = params.rotation.numberOfImages; - const omegaStep = params.rotation.omegaStep; - if ([1, 2, 4].includes(numImages) && omegaStep === 90) { - return "Characterization"; - } - return "Rotation"; - } else if (params.gridScan) { - return "Grid Scan"; - } - return "Rotation"; -}; const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { const [rows, setRows] = useState<TreeRow[]>([]); const [basePath, setBasePath] = useState(''); + // Helper function to safely get the number of images. + const getNumberOfImages = (run: ExperimentParameters): number => { + const params = run.beamline_parameters; + if (params.rotation && params.rotation.numberOfImages != null) { + return params.rotation.numberOfImages; + } else if (params.gridScan && params.gridScan.numberOfImages != null) { + return params.gridScan.numberOfImages; + } + return 0; + }; + + // Helper function to determine the experiment type. + const getExperimentType = (run: ExperimentParameters): string => { + const params = run.beamline_parameters; + if ( + params.rotation && + params.rotation.numberOfImages != null && + params.rotation.omegaStep != null + ) { + const numImages = params.rotation.numberOfImages; + const omegaStep = params.rotation.omegaStep; + if ([1, 2, 4].includes(numImages) && omegaStep === 90) { + return 'Characterization'; + } + return 'Rotation'; + } else if (params.gridScan) { + return 'Grid Scan'; + } + return 'Rotation'; + }; + useEffect(() => { + // Set OpenAPI.BASE depending on environment mode const mode = import.meta.env.MODE; OpenAPI.BASE = mode === 'test' @@ -141,22 +146,16 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { console.error('OpenAPI.BASE is not set. Falling back to a default value.'); OpenAPI.BASE = 'https://default-url.com'; } - - console.log('Environment Mode:', mode); - console.log('Resolved OpenAPI.BASE:', OpenAPI.BASE); - setBasePath(`${OpenAPI.BASE}/`); }, []); useEffect(() => { - console.log('Fetching sample results for active_pgroup:', activePgroup); + // Fetch sample details and construct rows SamplesService.getSampleResultsSamplesResultsGet(activePgroup) .then((response: SampleResult[]) => { - console.log('Response received:', response); const treeRows: TreeRow[] = []; response.forEach((sample) => { - // Add the sample row. const sampleRow: TreeRow = { id: `sample-${sample.sample_id}`, hierarchy: [sample.sample_id], @@ -169,7 +168,6 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { }; treeRows.push(sampleRow); - // Add experiment run rows. if (sample.experiment_runs) { sample.experiment_runs.forEach((run) => { const experimentType = getExperimentType(run); @@ -197,25 +195,22 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { }); }, [activePgroup]); - // Define the grid columns, including the new processing results column. + // Define the grid columns const columns: GridColDef[] = [ { field: 'sample_name', headerName: 'Sample Name', width: 200, - renderCell: (params) => (params.row.type === 'sample' ? params.value : null), }, { field: 'puck_name', headerName: 'Puck Name', width: 150, - renderCell: (params) => (params.row.type === 'sample' ? params.value : null), }, { field: 'dewar_name', headerName: 'Dewar Name', width: 150, - renderCell: (params) => (params.row.type === 'sample' ? params.value : null), }, { field: 'experimentType', @@ -229,88 +224,78 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { width: 150, renderCell: (params) => (params.row.type === 'run' ? params.value : null), }, - { - field: 'processingResults', - headerName: 'Processing Results', - width: 180, - renderCell: (params) => { - if (params.row.type === 'run') { - return ( - <IconButton - aria-label="processing results placeholder" - onClick={() => { - // Placeholder for processing results details. - console.log('Clicked processing details for run', params.row.run_number); - }} - > - <InfoIcon /> - </IconButton> - ); - } - return null; - }, - }, { field: 'images', headerName: 'Images', width: 300, renderCell: (params) => { - const imageList: ImageInfo[] = params.row.images; - - if (!imageList || imageList.length === 0) { - return null; + const images = params.row.images; + if (images && images.length) { + return ( + <div style={{ display: 'flex', gap: '8px' }}> + {images.map((image: ImageInfo) => ( + <img + key={image.id} + src={`${basePath}${image.filepath}`} + alt={image.comment || `Image ${image.id}`} + style={{ + height: 50, + width: 50, + objectFit: 'cover', + borderRadius: '4px', + }} + /> + ))} + </div> + ); } - - return ( - <div style={{ display: 'flex', gap: '10px' }}> - {imageList.map((img) => { - const url = `${basePath}${img.filepath}`; - return ( - <div key={img.id} style={{ position: 'relative' }}> - <img - src={url} - alt={img.comment || 'sample'} - className="zoom-image" - style={{ - width: 40, - height: 40, - borderRadius: 4, - cursor: 'pointer', - }} - /> - </div> - ); - })} - </div> - ); + return null; }, }, ]; + const getDetailPanelContent = (params: any) => { + if (params.row.type === 'run') { + return <RunDetails run={params.row} />; + } + return null; + }; + + const getDetailPanelHeight = (params: any) => { + if (params.row.type === 'run') return 300; + return 0; + }; return ( - <div style={{ height: 600, width: '100%' }}> - <DataGridPremium - rows={rows} - columns={columns} - treeData - getTreeDataPath={(row: TreeRow) => row.hierarchy} - defaultGroupingExpansionDepth={-1} - getRowId={(row) => row.id} - sx={{ - '& .MuiDataGrid-cell': { - overflow: 'visible', - }, - '& .MuiDataGrid-rendererContainer': { - overflow: 'visible', - position: 'relative', - }, - }} - /> + <DataGridPremium + rows={rows} + columns={columns} + getRowId={(row) => row.id} + autoHeight + treeData + getTreeDataPath={(row: TreeRow) => { + if (row.type === 'run') { + // Include sample_id to make the path globally unique + return [`Sample-${row.sample_id}`, `Run-${row.run_number}`]; + } + // If it's a sample row, it will be at the root + return [`Sample-${row.sample_id}`]; + }} + + defaultGroupingExpansionDepth={-1} + disableColumnMenu + getDetailPanelContent={getDetailPanelContent} + getDetailPanelHeight={getDetailPanelHeight} + sx={{ + '& .MuiDataGrid-cell': { + overflow: 'visible', + }, + }} + /> - </div> ); }; -export default ResultGrid; \ No newline at end of file +export default ResultGrid; + diff --git a/frontend/src/components/RunDetails.tsx b/frontend/src/components/RunDetails.tsx index 60003f3b5d4140e5cdc4524962b03d6def1a6bb2..9f4215e46e42841940a4fa4348d72b0b5996f7a0 100644 --- a/frontend/src/components/RunDetails.tsx +++ b/frontend/src/components/RunDetails.tsx @@ -1,6 +1,14 @@ -import {SimpleTreeView, TreeItem} from "@mui/x-tree-view"; -import React from "react"; - +import React from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + Typography, + Grid, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { SimpleTreeView, TreeItem } from '@mui/x-tree-view'; interface ExperimentParameters { id: number; @@ -12,73 +20,120 @@ interface ExperimentParameters { manufacturer: string; model: string; type: string; - serial_number: string; - detector_distance_mm: number; - beam_center_x_px: number; - beam_center_y_px: number; - pixel_size_x_um: number; - pixel_size_y_um: number; - number_of_images: number; - exposure_time_s: number; + serialNumber: string; + detectorDistance_mm: number; + beamCenterX_px: number; + beamCenterY_px: number; + pixelSizeX_um: number; + pixelSizeY_um: number; }; - // Add additional fields if needed. + // Include additional parameters as needed. }; + // Optionally, add fields for images and processing results. + images?: Array<{ + id: number; + filepath: string; + comment?: string; + }>; + processingResults?: any; +} + +interface RunDetailsProps { + run: ExperimentParameters; + onClose: () => void; } -<SimpleTreeView - defaultCollapseIcon="▾" - defaultExpandIcon="▸" - sx={{ fontSize: '0.875rem' }} -> - <TreeItem nodeId="detector-group" label={<strong>Detector Details</strong>}> - <TreeItem nodeId="detector-group" label={<strong>Detector Details</strong>}> - <TreeItem - nodeId="detector-manufacturer" - label={`Manufacturer: ${detector?.manufacturer || 'N/A'}`} - /> - <TreeItem - nodeId="detector-model" - label={`Model: ${detector?.model || 'N/A'}`} - /> - <TreeItem - nodeId="detector-type" - label={`Type: ${detector?.type || 'N/A'}`} - /> - <TreeItem - nodeId="detector-serial" - label={`Serial Number: ${detector?.serial_number || 'N/A'}`} - /> - <TreeItem - nodeId="detector-distance" - label={`Distance (mm): ${detector?.detector_distance_mm ?? 'N/A'}`} - /> - <TreeItem - nodeId="beam-center" - label={ - detector - ? `Beam Center: x:${detector.beam_center_x_px}, y:${detector.beam_center_y_px}` - : 'Beam Center: N/A' - } - /> - <TreeItem - nodeId="pixel-size" - label={ - detector - ? `Pixel Size (µm): x:${detector.pixel_size_x_um}, y:${detector.pixel_size_y_um}` - : 'Pixel Size: N/A' - } - /> - <TreeItem - nodeId="img-count" - label={`Number of Images: ${detector?.number_of_images ?? 'N/A'}`} - /> - <TreeItem - nodeId="exposure-time" - label={`Exposure Time (s): ${detector?.exposure_time_s ?? 'N/A'}`} - /> - </TreeItem> +const RunDetails: React.FC<RunDetailsProps> = ({ run }) => { + const { beamline_parameters } = run; + const { synchrotron, beamline, detector } = beamline_parameters; + + return ( + <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '4px' }}> + <Typography variant="h6" gutterBottom> + Run {run.run_number} Details + </Typography> + <Typography variant="subtitle1" gutterBottom> + Beamline: {beamline} | Synchrotron: {synchrotron} + </Typography> + + <SimpleTreeView + defaultCollapseIcon="▾" + defaultExpandIcon="▸" + sx={{ fontSize: '0.875rem' }} + > + <TreeItem nodeId="detector-group" label={<strong>Detector Details</strong>}> + <TreeItem + nodeId="detector-manufacturer" + label={`Manufacturer: ${detector?.manufacturer || 'N/A'}`} + /> + <TreeItem + nodeId="detector-model" + label={`Model: ${detector?.model || 'N/A'}`} + /> + <TreeItem + nodeId="detector-type" + label={`Type: ${detector?.type || 'N/A'}`} + /> + <TreeItem + nodeId="detector-serial" + label={`Serial Number: ${detector?.serialNumber || 'N/A'}`} + /> + <TreeItem + nodeId="detector-distance" + label={`Distance (mm): ${detector?.detectorDistance_mm ?? 'N/A'}`} + /> + <TreeItem + nodeId="beam-center" + label={ + detector + ? `Beam Center: x: ${detector.beamCenterX_px}, y: ${detector.beamCenterY_px}` + : 'Beam Center: N/A' + } + /> + <TreeItem + nodeId="pixel-size" + label={ + detector + ? `Pixel Size (µm): x: ${detector.pixelSizeX_um}, y: ${detector.pixelSizeY_um}` + : 'Pixel Size: N/A' + } + /> + </TreeItem> + </SimpleTreeView> + <Typography variant="h6" sx={{ mt: 2 }}> + Associated Images + </Typography> + {run.images && run.images.length > 0 ? ( + <Grid container spacing={2} sx={{ mt: 1 }}> + {run.images.map((img) => ( + <Grid item xs={4} key={img.id}> + <img + src={img.filepath} + alt={img.comment || 'Sample Image'} + style={{ width: '100%', border: '1px solid #ccc' }} + /> + </Grid> + ))} + </Grid> + ) : ( + <Typography>No images available.</Typography> + )} + <Typography variant="h6" sx={{ mt: 2 }}> + Processing Results + </Typography> + {run.processingResults ? ( + <Typography variant="body2" sx={{ mt: 1 }}> + {JSON.stringify(run.processingResults, null, 2)} + </Typography> + ) : ( + <Typography variant="body2" sx={{ mt: 1 }}> + Processing details and results go here. + </Typography> + )} + </div> + ); +}; - </TreeItem> -</SimpleTreeView> \ No newline at end of file +export default RunDetails; \ No newline at end of file