Compare commits

...

2 Commits

Author SHA1 Message Date
68f87f0d8d Displaying Processing Results in the frontend 2025-03-17 16:45:50 +01:00
5a0047b6d5 Refactor AareDB backend and update schemas and paths.
Revised backend schema definitions, removing unnecessary attributes and adding new configurations. Updated file path references to align with the aaredb structure. Cleaned up redundant notebook content and commented out unused database regeneration logic in the backend.

Added posting a result to the database
2025-03-17 11:51:07 +01:00
10 changed files with 626 additions and 1095 deletions

View File

@ -278,10 +278,14 @@ class Results(Base):
__tablename__ = "results" __tablename__ = "results"
id = Column(Integer, primary_key=True, index=True, autoincrement=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
# pgroup = Column(String(255), nullable=False) result = Column(JSON, nullable=False) # store the full result object as JSON
result = Column(JSON, nullable=True)
result_id = Column(Integer, ForeignKey("experiment_parameters.id"), nullable=False)
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False) sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
run_id = Column(Integer, ForeignKey("experiment_parameters.id"), nullable=False)
# optional relationships if you wish to query easily
# sample = relationship("SampleModel", backref="results")
# experiment_parameters = relationship("ExperimentParametersModel",
# backref="results")
# method = Column(String(255), nullable=False) # method = Column(String(255), nullable=False)

View File

@ -14,8 +14,10 @@ from app.schemas import (
SampleResult, SampleResult,
ExperimentParametersCreate, ExperimentParametersCreate,
ExperimentParametersRead, ExperimentParametersRead,
# ResultResponse, ImageInfo,
# ResultCreate, ResultResponse,
ResultCreate,
Results as ProcessingResults,
) )
from app.models import ( from app.models import (
Puck as PuckModel, Puck as PuckModel,
@ -25,7 +27,7 @@ from app.models import (
Dewar as DewarModel, Dewar as DewarModel,
ExperimentParameters as ExperimentParametersModel, ExperimentParameters as ExperimentParametersModel,
# ExperimentParameters, # ExperimentParameters,
# Results, Results as ResultsModel,
) )
from app.dependencies import get_db from app.dependencies import get_db
import logging import logging
@ -246,8 +248,13 @@ async def get_sample_results(active_pgroup: str, db: Session = Depends(get_db)):
results = [] results = []
for sample in samples: for sample in samples:
# Query images associated with the sample. # Query images associated with the sample, including the related event_type
images = db.query(ImageModel).filter(ImageModel.sample_id == sample.id).all() images = (
db.query(ImageModel)
.options(joinedload(ImageModel.sample_event))
.filter(ImageModel.sample_id == sample.id)
.all()
)
# Query experiment parameters (which include beamline parameters) for the # Query experiment parameters (which include beamline parameters) for the
# sample. # sample.
@ -259,27 +266,34 @@ async def get_sample_results(active_pgroup: str, db: Session = Depends(get_db)):
print("Experiment Parameters for sample", sample.id, experiment_parameters) print("Experiment Parameters for sample", sample.id, experiment_parameters)
results.append( results.append(
{ SampleResult(
"sample_id": sample.id, sample_id=sample.id,
"sample_name": sample.sample_name, sample_name=sample.sample_name,
"puck_name": sample.puck.puck_name if sample.puck else None, puck_name=sample.puck.puck_name if sample.puck else None,
"dewar_name": sample.puck.dewar.dewar_name dewar_name=sample.puck.dewar.dewar_name
if (sample.puck and sample.puck.dewar) if (sample.puck and sample.puck.dewar)
else None, else None,
"images": [ images=[
{"id": img.id, "filepath": img.filepath, "comment": img.comment} ImageInfo(
id=img.id,
filepath=img.filepath,
event_type=img.sample_event.event_type
if img.sample_event
else "Unknown",
comment=img.comment,
)
for img in images for img in images
], ],
"experiment_runs": [ experiment_runs=[
{ ExperimentParametersRead(
"id": ex.id, id=ex.id,
"run_number": ex.run_number, run_number=ex.run_number,
"beamline_parameters": ex.beamline_parameters, beamline_parameters=ex.beamline_parameters,
"sample_id": ex.sample_id, sample_id=ex.sample_id,
} )
for ex in experiment_parameters for ex in experiment_parameters
], ],
} )
) )
return results return results
@ -318,43 +332,72 @@ def create_experiment_parameters_for_sample(
db.commit() db.commit()
db.refresh(new_exp) db.refresh(new_exp)
# Create a "Collecting" sample event associated with the new experiment parameters
new_event = SampleEventModel(
sample_id=sample_id,
event_type="Collecting", # The event type
timestamp=datetime.now(), # Use current timestamp
)
db.add(new_event)
db.commit()
return new_exp return new_exp
# @router.post("/results", response_model=ResultResponse) @router.post("/processing-results", response_model=ResultResponse)
# def create_result(result: ResultCreate, db: Session = Depends(get_db)): def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
# # Validate sample_id and result_id (optional but recommended) # Check experiment existence
# sample = db.query(SampleModel).filter_by(id=result.sample_id).first() experiment = (
# if not sample: db.query(ExperimentParametersModel)
# raise HTTPException(status_code=404, detail="Sample not found") .filter(ExperimentParametersModel.id == payload.run_id)
# .first()
# experiment = db.query(ExperimentParameters).filter_by(id=result.result_id).first() )
# if not experiment: if not experiment:
# raise HTTPException(status_code=404, detail="Experiment parameters not found") raise HTTPException(
# status_code=404, detail="Experiment parameters (run) not found"
# # Create a new Results entry )
# result_obj = Results(
# sample_id=result.sample_id, result_entry = ResultsModel(
# result_id=result.result_id, sample_id=payload.sample_id,
# result=result.result run_id=payload.run_id,
# ) result=payload.result.model_dump(), # Serialize entire result to JSON
# db.add(result_obj) )
# db.commit()
# db.refresh(result_obj) db.add(result_entry)
# db.commit()
# return result_obj db.refresh(result_entry)
#
# @router.get("/results", response_model=list[ResultResponse]) return ResultResponse(
# def get_results(sample_id: int, result_id: int, db: Session = Depends(get_db)): id=result_entry.id,
# query = db.query(Results) sample_id=result_entry.sample_id,
# run_id=result_entry.run_id,
# if sample_id: result=payload.result, # return original payload directly
# query = query.filter(Results.sample_id == sample_id) )
# if result_id:
# query = query.filter(Results.result_id == result_id)
# @router.get(
# results = query.all() "/processing-results/{sample_id}/{run_id}", response_model=List[ResultResponse]
# if not results: )
# raise HTTPException(status_code=404, detail="No results found") async def get_results_for_run_and_sample(
# sample_id: int, run_id: int, db: Session = Depends(get_db)
# return results ):
results = (
db.query(ResultsModel)
.filter(ResultsModel.sample_id == sample_id, ResultsModel.run_id == run_id)
.all()
)
if not results:
raise HTTPException(status_code=404, detail="Results not found.")
formatted_results = [
ResultResponse(
id=result.id,
sample_id=result.sample_id,
run_id=result.run_id,
result=ProcessingResults(**result.result),
)
for result in results
]
return formatted_results

View File

@ -352,16 +352,6 @@ class SampleEventCreate(BaseModel):
event_type: Literal[ event_type: Literal[
"Mounting", "Centering", "Failed", "Lost", "Collecting", "Unmounting" "Mounting", "Centering", "Failed", "Lost", "Collecting", "Unmounting"
] ]
# event_type: str
# Validate event_type against accepted event types
# @field_validator("event_type", mode="before")
# def validate_event_type(cls, value):
# allowed = {"Mounting", "Centering", "Failed",
# "Lost", "Collecting", "Unmounting"}
# if value not in allowed:
# raise ValueError(f"Invalid event_type: {value}.
# Accepted values are: {allowed}")
# return value
class SampleEventResponse(SampleEventCreate): class SampleEventResponse(SampleEventCreate):
@ -374,10 +364,7 @@ class SampleEventResponse(SampleEventCreate):
class Results(BaseModel): class Results(BaseModel):
id: int pipeline: str
pgroup: str
sample_id: int
method: str
resolution: float resolution: float
unit_cell: str unit_cell: str
spacegroup: str spacegroup: str
@ -393,10 +380,6 @@ class Results(BaseModel):
unique_refl: int unique_refl: int
comments: Optional[constr(max_length=200)] = None comments: Optional[constr(max_length=200)] = None
# Define attributes for Results here
class Config:
from_attributes = True
class ContactCreate(BaseModel): class ContactCreate(BaseModel):
pgroups: str pgroups: str
@ -822,6 +805,21 @@ class ImageInfo(BaseModel):
id: int id: int
filepath: str filepath: str
comment: Optional[str] = None comment: Optional[str] = None
event_type: str
# run_number: Optional[int]
class Config:
from_attributes = True
class characterizationParameters(BaseModel):
omegaStart_deg: float
oscillation_deg: float
omegaStep: float
chi: float
phi: float
numberOfImages: int
exposureTime_s: float
class RotationParameters(BaseModel): class RotationParameters(BaseModel):
@ -882,6 +880,7 @@ class BeamlineParameters(BaseModel):
beamSizeWidth: Optional[float] = None beamSizeWidth: Optional[float] = None
beamSizeHeight: Optional[float] = None beamSizeHeight: Optional[float] = None
# dose_MGy: float # dose_MGy: float
characterization: Optional[characterizationParameters] = None
rotation: Optional[RotationParameters] = None rotation: Optional[RotationParameters] = None
gridScan: Optional[gridScanParamers] = None gridScan: Optional[gridScanParamers] = None
jet: Optional[jetParameters] = None jet: Optional[jetParameters] = None
@ -922,15 +921,18 @@ class SampleResult(BaseModel):
class ResultCreate(BaseModel): class ResultCreate(BaseModel):
sample_id: int sample_id: int
result_id: int run_id: int
result: Optional[dict] result: Results
class Config:
from_attributes = True
class ResultResponse(BaseModel): class ResultResponse(BaseModel):
id: int id: int
sample_id: int sample_id: int
result_id: int run_id: int
result: Optional[dict] result: Results
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -156,8 +156,8 @@ def on_startup():
load_slots_data(db) load_slots_data(db)
else: # dev or test environments else: # dev or test environments
print(f"{environment.capitalize()} environment: Regenerating database.") print(f"{environment.capitalize()} environment: Regenerating database.")
Base.metadata.drop_all(bind=engine) # Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine) # Base.metadata.create_all(bind=engine)
# from sqlalchemy.engine import reflection # from sqlalchemy.engine import reflection
# from app.models import ExperimentParameters # adjust the import as needed # from app.models import ExperimentParameters # adjust the import as needed
# inspector = reflection.Inspector.from_engine(engine) # inspector = reflection.Inspector.from_engine(engine)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aareDB" name = "aareDB"
version = "0.1.0a25" version = "0.1.0a26"
description = "Backend for next gen sample management system" description = "Backend for next gen sample management system"
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
license = {text = "MIT"} license = {text = "MIT"}

View File

View File

@ -171,12 +171,12 @@ async function fetchAndGenerate() {
const backendDirectory = (() => { const backendDirectory = (() => {
switch (nodeEnv) { switch (nodeEnv) {
case 'prod': case 'prod':
return path.resolve('/home/jungfrau/heidi-v2/backend/app'); // Production path return path.resolve('/home/jungfrau/aaredb/backend/app'); // Production path
case 'test': case 'test':
return path.resolve('/home/jungfrau/heidi-v2/backend/app'); // Test path return path.resolve('/home/jungfrau/aaredb/backend/app'); // Test path
case 'dev': case 'dev':
default: default:
return path.resolve('/Users/gotthardg/PycharmProjects/heidi-v2/backend/app'); // Development path return path.resolve('/Users/gotthardg/PycharmProjects/aaredb/backend/app'); // Development path
} }
})(); })();

View File

@ -11,6 +11,8 @@ interface ImageInfo {
id: number; id: number;
filepath: string; filepath: string;
comment?: string; comment?: string;
event_type: string;
run_number?:number;
} }
// This represents an experiment run as returned by your API. // This represents an experiment run as returned by your API.
@ -83,11 +85,12 @@ interface TreeRow {
id: string; id: string;
hierarchy: (string | number)[]; hierarchy: (string | number)[];
type: 'sample' | 'run'; type: 'sample' | 'run';
experimentId?: number;
sample_id: number; sample_id: number;
sample_name?: string; sample_name?: string;
puck_name?: string; puck_name?: string;
dewar_name?: string; dewar_name?: string;
images?: ImageInfo[]; images?: ImageInfo[]; // Images associated explicitly with this row (especially run items)
run_number?: number; run_number?: number;
beamline_parameters?: ExperimentParameters['beamline_parameters']; beamline_parameters?: ExperimentParameters['beamline_parameters'];
experimentType?: string; experimentType?: string;
@ -176,7 +179,13 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
// Fetch sample details and construct rows if (!OpenAPI.BASE) {
console.error('OpenAPI.BASE is not set. Falling back to a default value.');
return;
}
setBasePath(`${OpenAPI.BASE}/`);
SamplesService.getSampleResultsSamplesResultsGet(activePgroup) SamplesService.getSampleResultsSamplesResultsGet(activePgroup)
.then((response: SampleResult[]) => { .then((response: SampleResult[]) => {
const treeRows: TreeRow[] = []; const treeRows: TreeRow[] = [];
@ -190,28 +199,28 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
sample_name: sample.sample_name, sample_name: sample.sample_name,
puck_name: sample.puck_name, puck_name: sample.puck_name,
dewar_name: sample.dewar_name, dewar_name: sample.dewar_name,
images: sample.images, images: sample.images.filter(img => img.event_type === "Centering"),
}; };
treeRows.push(sampleRow); treeRows.push(sampleRow);
if (sample.experiment_runs) { sample.experiment_runs?.forEach(run => {
sample.experiment_runs.forEach((run) => { const experimentType = getExperimentType(run);
const experimentType = getExperimentType(run); const numImages = getNumberOfImages(run);
const numImages = getNumberOfImages(run); const runRow: TreeRow = {
const runRow: TreeRow = { id: `run-${sample.sample_id}-${run.run_number}`,
id: `run-${sample.sample_id}-${run.run_number}`, hierarchy: [sample.sample_id, run.run_number],
hierarchy: [sample.sample_id, run.run_number], type: 'run',
type: 'run', experimentId: run.id,
sample_id: sample.sample_id, sample_id: sample.sample_id,
run_number: run.run_number, run_number: run.run_number,
beamline_parameters: run.beamline_parameters, beamline_parameters: run.beamline_parameters,
experimentType, experimentType,
numberOfImages: numImages, numberOfImages: numImages,
images: sample.images, images: sample.images.filter(img =>
}; img.event_type === "Collecting" ),
treeRows.push(runRow); };
}); treeRows.push(runRow);
} });
}); });
setRows(treeRows); setRows(treeRows);
@ -221,6 +230,7 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
}); });
}, [activePgroup]); }, [activePgroup]);
// Define the grid columns // Define the grid columns
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
@ -299,8 +309,10 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
return ( return (
<RunDetails <RunDetails
run={params.row} run={params.row}
runId={params.row.experimentId}
sample_id={params.row.sample_id}
basePath={basePath} basePath={basePath}
onHeightChange={(height: number) => handleDetailPanelHeightChange(params.row.id, height)} // Pass callback for dynamic height onHeightChange={height => handleDetailPanelHeightChange(params.row.id, height)}
/> />
); );
} }

View File

@ -1,196 +1,279 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { import {
Accordion, Accordion, AccordionSummary, AccordionDetails, Typography, Grid, Modal, Box
AccordionSummary,
AccordionDetails,
Typography,
Grid,
Modal,
Box
} from '@mui/material'; } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import './SampleImage.css'; import './SampleImage.css';
import { DataGridPremium, GridColDef } from "@mui/x-data-grid-premium";
import { SamplesService } from "../../openapi";
interface RunDetailsProps { interface RunDetailsProps {
run: ExperimentParameters; run: TreeRow;
runId: number;
sample_id: number;
basePath: string; basePath: string;
onHeightChange?: (height: number) => void; // Callback to notify the parent about height changes onHeightChange?: (height: number) => void;
} }
const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath }) => {
const containerRef = useRef<HTMLDivElement | null>(null); // Ref to track component height interface ExperimentParameters {
run_number: number;
id: number;
sample_id: number;
beamline_parameters: BeamlineParameters;
images: Image[];
}
interface ProcessingResults {
pipeline: string;
resolution: number;
unit_cell: string;
spacegroup: string;
rmerge: number;
rmeas: number;
isig: number;
cc: number;
cchalf: number;
completeness: number;
multiplicity: number;
nobs: number;
total_refl: number;
unique_refl: number;
comments?: string | null;
}
const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath, runId, sample_id }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [currentHeight, setCurrentHeight] = useState<number>(0); const [currentHeight, setCurrentHeight] = useState<number>(0);
const [modalOpen, setModalOpen] = useState<boolean>(false); // For modal state const [modalOpen, setModalOpen] = useState<boolean>(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null); // Tracks the selected image for the modal const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [expandedResults, setExpandedResults] = useState(false);
const [processingResult, setProcessingResult] = useState<ProcessingResults[] | null>(null);
const { beamline_parameters, images } = run; const {beamline_parameters, images} = run;
const { synchrotron, beamline, detector } = beamline_parameters; const {synchrotron, beamline, detector} = beamline_parameters;
useEffect(() => {
fetchResults(sample_id, runId); // fetching based on experimentId
}, [runId]);
const fetchResults = async (sample_id: number, runId: number) => {
try {
const results = await SamplesService.getResultsForRunAndSampleSamplesProcessingResultsSampleIdRunIdGet(sample_id, runId);
// Explicitly handle nested results
const mappedResults: ProcessingResults[] = results.map((res): ProcessingResults => ({
pipeline: res.result?.pipeline || 'N/A',
resolution: res.result.resolution ?? 0,
unit_cell: res.result?.unit_cell || 'N/A',
spacegroup: res.result?.spacegroup || 'N/A',
rmerge: res.result?.rmerge ?? 0,
rmeas: res.result?.rmeas ?? 0,
isig: res.result?.isig ?? 0,
cc: res.result?.cc ?? 0,
cchalf: res.result?.cchalf ?? 0,
completeness: res.result?.completeness ?? 0,
multiplicity: res.result?.multiplicity ?? 0,
nobs: res.result?.nobs ?? 0,
total_refl: res.result?.total_refl ?? 0,
unique_refl: res.result?.unique_refl ?? 0,
comments: res.result?.comments || null,
}));
setProcessingResult(mappedResults);
} catch (error) {
console.error('Error fetching results:', error);
}
};
const resultColumns: GridColDef[] = [
{field: 'pipeline', headerName: 'Pipeline', flex: 1},
{field: 'resolution', headerName: 'Resolution (Å)', flex: 1},
{field: 'unit_cell', headerName: 'Unit Cell (Å)', flex: 1.5},
{field: 'spacegroup', headerName: 'Spacegroup', flex: 1},
{field: 'rmerge', headerName: 'Rmerge', flex: 1},
{field: 'rmeas', headerName: 'Rmeas', flex: 1},
{field: 'isig', headerName: 'I/sig(I)', flex: 1},
{field: 'cc', headerName: 'CC', flex: 1},
{field: 'cchalf', headerName: 'CC(1/2)', flex: 1},
{field: 'completeness', headerName: 'Completeness (%)', flex: 1},
{field: 'multiplicity', headerName: 'Multiplicity', flex: 1},
{field: 'nobs', headerName: 'N obs.', flex: 1},
{field: 'total_refl', headerName: 'Total Reflections', flex: 1},
{field: 'unique_refl', headerName: 'Unique Reflections', flex: 1},
{field: 'comments', headerName: 'Comments', flex: 2},
];
// Calculate and notify the parent about height changes
const updateHeight = () => { const updateHeight = () => {
if (containerRef.current) { if (containerRef.current) {
const newHeight = containerRef.current.offsetHeight; const newHeight = containerRef.current.offsetHeight;
if (newHeight !== currentHeight) { if (newHeight !== currentHeight && onHeightChange) {
setCurrentHeight(newHeight); setCurrentHeight(newHeight);
if (onHeightChange) { onHeightChange(newHeight);
onHeightChange(newHeight);
}
} }
} }
}; };
useEffect(() => { useEffect(() => {
updateHeight(); // Update height on initial render
}, []);
useEffect(() => {
// Update height whenever the component content changes
const observer = new ResizeObserver(updateHeight); const observer = new ResizeObserver(updateHeight);
if (containerRef.current) { if (containerRef.current) observer.observe(containerRef.current);
observer.observe(containerRef.current); return () => observer.disconnect();
} }, [containerRef.current, processingResult]);
return () => {
observer.disconnect();
};
}, [containerRef]);
const handleImageClick = (imagePath: string) => { const handleImageClick = (imagePath: string) => {
setSelectedImage(imagePath); setSelectedImage(imagePath);
setModalOpen(true); // Open the modal when the image is clicked setModalOpen(true);
}; };
const closeModal = () => { const closeModal = () => {
setSelectedImage(null); // Clear the current image setSelectedImage(null);
setModalOpen(false); setModalOpen(false);
}; };
return ( return (
<div <div
className="details-panel" // Add the class here className="details-panel"
ref={containerRef} // Attach the ref to the main container ref={containerRef}
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', // Stack children vertically
gap: '16px', gap: '16px',
padding: '16px', padding: '16px',
border: '1px solid #ccc', border: '1px solid #ccc',
borderRadius: '4px', borderRadius: '4px',
alignItems: 'flex-start',
}} }}
> >
{/* Main Details Section */} {/* Wrap details and images together */}
<div style={{ flexGrow: 1 }}> <div style={{display: 'flex', gap: '16px', alignItems: 'flex-start'}}>
<Typography variant="h6" gutterBottom> {/* Main Details Section */}
Run {run.run_number} Details <div style={{flexGrow: 1}}>
</Typography> <Typography variant="h6" gutterBottom>
<Typography variant="subtitle1" gutterBottom> Run {run.run_number} Details
Beamline: {beamline} | Synchrotron: {synchrotron} </Typography>
</Typography> <Typography variant="subtitle1" gutterBottom>
Beamline: {beamline} | Synchrotron: {synchrotron}
</Typography>
{/* Detector Details Accordion */} {/* Detector Details Accordion */}
<Accordion> <Accordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon/>}
aria-controls="detector-content" aria-controls="detector-content"
id="detector-header" id="detector-header"
> >
<Typography> <Typography><strong>Detector Details</strong></Typography>
<strong>Detector Details</strong> </AccordionSummary>
</Typography> <AccordionDetails>
</AccordionSummary> <Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography>
<AccordionDetails> <Typography>Model: {detector?.model || 'N/A'}</Typography>
<Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography> <Typography>Type: {detector?.type || 'N/A'}</Typography>
<Typography>Model: {detector?.model || 'N/A'}</Typography> <Typography>
<Typography>Type: {detector?.type || 'N/A'}</Typography> Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'},
<Typography> y: {detector?.beamCenterY_px || 'N/A'}
Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'}, y: {detector?.beamCenterY_px || 'N/A'} </Typography>
</Typography> </AccordionDetails>
</AccordionDetails> </Accordion>
</Accordion>
{/* Beamline Details Accordion */} {/* Beamline Details Accordion */}
<Accordion> <Accordion>
<AccordionSummary <AccordionSummary expandIcon={<ExpandMoreIcon/>}>
expandIcon={<ExpandMoreIcon />} <Typography><strong>Beamline Details</strong></Typography>
aria-controls="beamline-content" </AccordionSummary>
id="beamline-header" <AccordionDetails>
> <Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography>
<Typography> <Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography>
<strong>Beamline Details</strong> <Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography>
</Typography> <Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography>
</AccordionSummary> <Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography>
<AccordionDetails> <Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography>
<Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography> <Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography>
<Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography> <Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography>
<Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography> </AccordionDetails>
<Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography> </Accordion>
<Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography>
<Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography>
<Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography>
<Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
{/* Beam Characteristics Accordion */} {/* Beam Characteristics Accordion */}
<Accordion> <Accordion>
<AccordionSummary <AccordionSummary expandIcon={<ExpandMoreIcon/>}>
expandIcon={<ExpandMoreIcon />} <Typography><strong>Beam Characteristics</strong></Typography>
aria-controls="beam-content" </AccordionSummary>
id="beam-header" <AccordionDetails>
> <Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography>
<Typography> <Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography>
<strong>Beam Characteristics</strong> <Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography>
<Typography>
Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'},
horizontal:{' '}
{beamline_parameters?.beamSizeWidth || 'N/A'}
</Typography>
<Typography>Flux at sample
(ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
</div>
{/* Image Section */}
<div style={{width: '900px'}}>
<Typography variant="h6" gutterBottom>
Associated Images
</Typography>
{images && images.length > 0 ? (
<Grid container spacing={1}>
{images.map((img) => (
<Grid item xs={4} key={img.id}>
<div
className="image-container"
onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)}
style={{cursor: 'pointer'}}
>
<img
src={`${basePath || ''}${img.filepath}`}
alt={img.comment || 'Image'}
className="zoom-image"
style={{
width: '100%',
maxWidth: '100%',
borderRadius: '4px',
}}
/>
</div>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="textSecondary">
No images available.
</Typography> </Typography>
</AccordionSummary> )}
<AccordionDetails> </div>
<Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography>
<Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography>
<Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography>
<Typography>
Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'}, horizontal:{' '}
{beamline_parameters?.beamSizeWidth || 'N/A'}
</Typography>
<Typography>Flux at sample (ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography>
</AccordionDetails>
</Accordion>
</div> </div>
{/* Image Section */} {/* Processing Results Accordion - Full Width Below */}
<div style={{ width: '900px' }}> <div style={{width: '100%'}}>
<Typography variant="h6" gutterBottom> <Accordion expanded={expandedResults} onChange={(e, expanded) => setExpandedResults(expanded)}>
Associated Images <AccordionSummary expandIcon={<ExpandMoreIcon/>}>
</Typography> <Typography><strong>Processing Results</strong></Typography>
{images && images.length > 0 ? ( </AccordionSummary>
<Grid container spacing={1}> <AccordionDetails style={{width: '100%', overflowX: 'auto'}}>
{images.map((img) => ( {processingResult ? (
<Grid item xs={4} key={img.id}> <div style={{width: '100%'}}>
<div <DataGridPremium
className="image-container" rows={processingResult.map((res, idx) => ({id: idx, ...res}))}
onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)} // Open modal with image columns={resultColumns}
style={{ autoHeight
cursor: 'pointer', hideFooter
}} columnVisibilityModel={{id: false}}
> disableColumnResize={false}
<img />
src={`${basePath || ''}${img.filepath}`} // Ensure basePath </div>
alt={img.comment || 'Image'} ) : (
className="zoom-image" <Typography variant="body2" color="textSecondary">Loading results...</Typography>
style={{ )}
width: '100%', // Ensure the image takes the full width of its container </AccordionDetails>
maxWidth: '100%', // Prevent any overflow </Accordion>
borderRadius: '4px',
}}
/>
</div>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="textSecondary">
No images available.
</Typography>
)}
</div> </div>
{/* Modal for Zoomed Image */} {/* Modal for Zoomed Image */}
@ -224,5 +307,4 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath }
</div> </div>
); );
}; };
export default RunDetails; export default RunDetails;

File diff suppressed because it is too large Load Diff