Refactor RunDetails and integrate with ResultGrid

Reworked `RunDetails` to enhance details presentation and added new UI components like images and processing results. Incorporated the `RunDetails` expansion panel into `ResultGrid` for better user interaction and streamlined grid functionalities.
This commit is contained in:
GotthardG 2025-03-04 14:32:27 +01:00
parent fff3e9f884
commit 75998a1d22
2 changed files with 216 additions and 176 deletions

View File

@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { DataGridPremium, GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium'; import { DataGridPremium, GridColDef } from '@mui/x-data-grid-premium';
import IconButton from '@mui/material/IconButton'; import RunDetails from './RunDetails';
import InfoIcon from '@mui/icons-material/Info';
import { OpenAPI, SamplesService } from '../../openapi';
import './SampleImage.css'; import './SampleImage.css';
import { OpenAPI, SamplesService } from '../../openapi';
// Extend your image info interface if needed. // Extend your image info interface if needed.
interface ImageInfo { interface ImageInfo {
@ -97,38 +96,44 @@ interface ResultGridProps {
activePgroup: string; 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 ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
const [rows, setRows] = useState<TreeRow[]>([]); const [rows, setRows] = useState<TreeRow[]>([]);
const [basePath, setBasePath] = useState(''); 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(() => { useEffect(() => {
// Set OpenAPI.BASE depending on environment mode
const mode = import.meta.env.MODE; const mode = import.meta.env.MODE;
OpenAPI.BASE = OpenAPI.BASE =
mode === 'test' 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.'); console.error('OpenAPI.BASE is not set. Falling back to a default value.');
OpenAPI.BASE = 'https://default-url.com'; OpenAPI.BASE = 'https://default-url.com';
} }
console.log('Environment Mode:', mode);
console.log('Resolved OpenAPI.BASE:', OpenAPI.BASE);
setBasePath(`${OpenAPI.BASE}/`); setBasePath(`${OpenAPI.BASE}/`);
}, []); }, []);
useEffect(() => { useEffect(() => {
console.log('Fetching sample results for active_pgroup:', activePgroup); // Fetch sample details and construct rows
SamplesService.getSampleResultsSamplesResultsGet(activePgroup) SamplesService.getSampleResultsSamplesResultsGet(activePgroup)
.then((response: SampleResult[]) => { .then((response: SampleResult[]) => {
console.log('Response received:', response);
const treeRows: TreeRow[] = []; const treeRows: TreeRow[] = [];
response.forEach((sample) => { response.forEach((sample) => {
// Add the sample row.
const sampleRow: TreeRow = { const sampleRow: TreeRow = {
id: `sample-${sample.sample_id}`, id: `sample-${sample.sample_id}`,
hierarchy: [sample.sample_id], hierarchy: [sample.sample_id],
@ -169,7 +168,6 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
}; };
treeRows.push(sampleRow); treeRows.push(sampleRow);
// Add experiment run rows.
if (sample.experiment_runs) { if (sample.experiment_runs) {
sample.experiment_runs.forEach((run) => { sample.experiment_runs.forEach((run) => {
const experimentType = getExperimentType(run); const experimentType = getExperimentType(run);
@ -197,25 +195,22 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
}); });
}, [activePgroup]); }, [activePgroup]);
// Define the grid columns, including the new processing results column. // Define the grid columns
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
field: 'sample_name', field: 'sample_name',
headerName: 'Sample Name', headerName: 'Sample Name',
width: 200, width: 200,
renderCell: (params) => (params.row.type === 'sample' ? params.value : null),
}, },
{ {
field: 'puck_name', field: 'puck_name',
headerName: 'Puck Name', headerName: 'Puck Name',
width: 150, width: 150,
renderCell: (params) => (params.row.type === 'sample' ? params.value : null),
}, },
{ {
field: 'dewar_name', field: 'dewar_name',
headerName: 'Dewar Name', headerName: 'Dewar Name',
width: 150, width: 150,
renderCell: (params) => (params.row.type === 'sample' ? params.value : null),
}, },
{ {
field: 'experimentType', field: 'experimentType',
@ -230,87 +225,77 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
renderCell: (params) => (params.row.type === 'run' ? params.value : null), renderCell: (params) => (params.row.type === 'run' ? params.value : null),
}, },
{ {
field: 'processingResults', field: 'images',
headerName: 'Processing Results', headerName: 'Images',
width: 180, width: 300,
renderCell: (params) => { renderCell: (params) => {
if (params.row.type === 'run') { const images = params.row.images;
if (images && images.length) {
return ( return (
<IconButton <div style={{ display: 'flex', gap: '8px' }}>
aria-label="processing results placeholder" {images.map((image: ImageInfo) => (
onClick={() => { <img
// Placeholder for processing results details. key={image.id}
console.log('Clicked processing details for run', params.row.run_number); src={`${basePath}${image.filepath}`}
}} alt={image.comment || `Image ${image.id}`}
> style={{
<InfoIcon /> height: 50,
</IconButton> width: 50,
objectFit: 'cover',
borderRadius: '4px',
}}
/>
))}
</div>
); );
} }
return null; return null;
}, },
}, },
{
field: 'images',
headerName: 'Images',
width: 300,
renderCell: (params) => {
const imageList: ImageInfo[] = params.row.images;
if (!imageList || imageList.length === 0) {
return null;
}
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>
);
},
},
]; ];
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 ( return (
<div style={{ height: 600, width: '100%' }}> <DataGridPremium
<DataGridPremium rows={rows}
rows={rows} columns={columns}
columns={columns} getRowId={(row) => row.id}
treeData autoHeight
getTreeDataPath={(row: TreeRow) => row.hierarchy} treeData
defaultGroupingExpansionDepth={-1} getTreeDataPath={(row: TreeRow) => {
getRowId={(row) => row.id} if (row.type === 'run') {
sx={{ // Include sample_id to make the path globally unique
'& .MuiDataGrid-cell': { return [`Sample-${row.sample_id}`, `Run-${row.run_number}`];
overflow: 'visible', }
}, // If it's a sample row, it will be at the root
'& .MuiDataGrid-rendererContainer': { return [`Sample-${row.sample_id}`];
overflow: 'visible', }}
position: 'relative',
}, defaultGroupingExpansionDepth={-1}
}} disableColumnMenu
/> getDetailPanelContent={getDetailPanelContent}
getDetailPanelHeight={getDetailPanelHeight}
sx={{
'& .MuiDataGrid-cell': {
overflow: 'visible',
},
}}
/>
</div>
); );
}; };
export default ResultGrid; export default ResultGrid;

View File

@ -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 { interface ExperimentParameters {
id: number; id: number;
@ -12,73 +20,120 @@ interface ExperimentParameters {
manufacturer: string; manufacturer: string;
model: string; model: string;
type: string; type: string;
serial_number: string; serialNumber: string;
detector_distance_mm: number; detectorDistance_mm: number;
beam_center_x_px: number; beamCenterX_px: number;
beam_center_y_px: number; beamCenterY_px: number;
pixel_size_x_um: number; pixelSizeX_um: number;
pixel_size_y_um: number; pixelSizeY_um: number;
number_of_images: number;
exposure_time_s: 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;
} }
<SimpleTreeView interface RunDetailsProps {
defaultCollapseIcon="▾" run: ExperimentParameters;
defaultExpandIcon="▸" onClose: () => void;
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>
</TreeItem> <SimpleTreeView
</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>
);
};
export default RunDetails;