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:
parent
fff3e9f884
commit
75998a1d22
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user