import React, { useEffect, useState } from 'react'; import { DataGridPremium, GridColDef } from '@mui/x-data-grid-premium'; import RunDetails from './RunDetails'; import './SampleImage.css'; import './ResultGrid.css'; import { OpenAPI, SamplesService } from '../../openapi'; // Extend your image info interface if needed. interface ImageInfo { id: number; filepath: string; comment?: string; } // This represents an experiment run as returned by your API. interface ExperimentParameters { id: number; run_number: number; sample_id: number; beamline_parameters: { beamSizeHeight: number; beamSizeWidth: number; beamline: string; beamlineFluxAtSample_ph_s: number; cryojetTemperature_K?: number | null; detector: { beamCenterX_px: number; beamCenterY_px: number; detectorDistance_mm: number; manufacturer: string; model: string; pixelSizeX_um: number; pixelSizeY_um: number; serialNumber: string; type: string; }; rotation?: { chi: number; exposureTime_s: number; numberOfImages: number | null; omegaStart_deg: number; omegaStep: number; phi: number; } | null; gridScan?: { xStart: number; xStep: number; yStart: number; yStep: number; zStart: number; zStep: number; numberOfImages: number; exposureTime_s: number; } | null; focusingOptic?: string; humidifierHumidity?: number | null; humidifierTemperature_K?: number | null; jet?: any; monochromator?: string; ringCurrent_A?: number; ringMode?: string; synchrotron?: string; transmission?: number; undulator?: any; undulatorgap_mm?: any; wavelength?: number; }; } // Represents a sample returned by the API. interface SampleResult { sample_id: number; sample_name: string; puck_name?: string; dewar_name?: string; images: ImageInfo[]; experiment_runs?: ExperimentParameters[]; } // A flat tree row type for the grid. interface TreeRow { id: string; hierarchy: (string | number)[]; type: 'sample' | 'run'; sample_id: number; sample_name?: string; puck_name?: string; dewar_name?: string; images?: ImageInfo[]; run_number?: number; beamline_parameters?: ExperimentParameters['beamline_parameters']; experimentType?: string; numberOfImages?: number; } interface ResultGridProps { activePgroup: string; } const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { const [rows, setRows] = useState<TreeRow[]>([]); const [basePath, setBasePath] = useState(''); const [detailPanelHeights, setDetailPanelHeights] = useState<{ [key: string]: number }>({}); // Store dynamic heights const hasProcessingResults = (row: TreeRow): boolean => { // You can later replace this placeholder with actual logic. // Mocking the logic by returning `true` for demonstration. return row.type === 'run' && !!row.beamline_parameters?.detector; }; // 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; }; const getRowClassName = (params: { row: TreeRow }) => { const { row } = params; // Light green if the run contains processing results if (hasProcessingResults(row)) { return 'row-light-green'; } // Light yellow if the row type is 'run' if (row.type === 'run') { return 'row-light-yellow'; } // No special styling return ''; }; // 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' ? import.meta.env.VITE_OPENAPI_BASE_TEST : mode === 'prod' ? import.meta.env.VITE_OPENAPI_BASE_PROD : import.meta.env.VITE_OPENAPI_BASE_DEV; if (!OpenAPI.BASE) { console.error('OpenAPI.BASE is not set. Falling back to a default value.'); OpenAPI.BASE = 'https://default-url.com'; } setBasePath(`${OpenAPI.BASE}/`); }, []); useEffect(() => { // Fetch sample details and construct rows SamplesService.getSampleResultsSamplesResultsGet(activePgroup) .then((response: SampleResult[]) => { const treeRows: TreeRow[] = []; response.forEach((sample) => { const sampleRow: TreeRow = { id: `sample-${sample.sample_id}`, hierarchy: [sample.sample_id], type: 'sample', sample_id: sample.sample_id, sample_name: sample.sample_name, puck_name: sample.puck_name, dewar_name: sample.dewar_name, images: sample.images, }; treeRows.push(sampleRow); if (sample.experiment_runs) { sample.experiment_runs.forEach((run) => { const experimentType = getExperimentType(run); const numImages = getNumberOfImages(run); const runRow: TreeRow = { id: `run-${sample.sample_id}-${run.run_number}`, hierarchy: [sample.sample_id, run.run_number], type: 'run', sample_id: sample.sample_id, run_number: run.run_number, beamline_parameters: run.beamline_parameters, experimentType, numberOfImages: numImages, images: sample.images, }; treeRows.push(runRow); }); } }); setRows(treeRows); }) .catch((error) => { console.error('Error fetching sample results:', error); }); }, [activePgroup]); // Define the grid columns const columns: GridColDef[] = [ { field: 'sample_name', headerName: 'Sample Name', width: 200, }, { field: 'puck_name', headerName: 'Puck Name', width: 150, }, { field: 'dewar_name', headerName: 'Dewar Name', width: 150, }, { field: 'experimentType', headerName: 'Experiment Type', width: 150, renderCell: (params) => (params.row.type === 'run' ? params.value : null), }, { field: 'numberOfImages', headerName: 'Number of Images', width: 150, renderCell: (params) => (params.row.type === 'run' ? params.value : 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 handleDetailPanelHeightChange = (rowId: string, height: number) => { // Update the height of the specific detail panel dynamically setDetailPanelHeights((prev) => { if (prev[rowId] !== height) { return { ...prev, [rowId]: height }; } return prev; }); }; const getDetailPanelContent = (params: any) => { if (params.row.type === 'run') { return ( <RunDetails run={params.row} onHeightChange={(height: number) => handleDetailPanelHeightChange(params.row.id, height)} // Pass callback for dynamic height /> ); } return null; }; const getDetailPanelHeight = (params: any) => { if (params.row.type === 'run') { // Use the dynamically calculated height from state return detailPanelHeights[params.row.id] || 600; // Fallback to default height if not yet calculated } return 0; }; return ( <DataGridPremium rows={rows} columns={columns} getRowId={(row) => row.id} autoHeight treeData getRowClassName={getRowClassName} 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', }, }} /> ); }; export default ResultGrid;