Files
Jungfraujoch/frontend/src/App.tsx
T
leonarski_f d6389e12da
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 15m31s
Build Packages / build:viewer-tgz:cpu (push) Successful in 5m46s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m9s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m25s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m41s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 9m18s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m26s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m33s
Build Packages / build:rpm (rocky8) (push) Successful in 10m32s
Build Packages / build:rpm (rocky9) (push) Successful in 12m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m12s
Build Packages / DIALS test (push) Successful in 12m6s
Build Packages / XDS test (durin plugin) (push) Successful in 8m15s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m12s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m35s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 12m37s
v1.0.0-rc.156 (#66)
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* jfjoch_process: Major rotation (rot3d) data processing overhaul - robust profile-fit integration, Cauchy-loss scaling with optional absorption surface, de-novo indexing and space-group/centering determination fixes, and merging statistics + ISa in the mmCIF output.
* jfjoch_process: Add EXPERIMENTAL ice-ring detection (--detect-ice-rings) that excludes ice reflections from scaling.
* Compression: Add BSHUF_ZSTD_RLE_HUFF, make compression size-aware (drop frames that don't fit rather than aborting), and add the jfjoch_recompress tool.
* jfjoch_viewer: Report "Multiple lattices detected" and grey out "Analyze dataset" on a live connection.
* jfjoch_broker: Write smargon chi/phi goniometer positions to NXmx; read sensor thickness/material from HDF5 metadata.
* CI: Build Windows (CUDA and non-CUDA) installers.Reviewed-on: #66

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-03 19:18:56 +02:00

293 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ReactNode, useCallback, useState } from 'react';
import {
Box, createTheme, CssBaseline, Drawer, Grid, Link, List, ListItemButton,
ListItemIcon, ListItemText, ListSubheader, Stack, ThemeProvider, Toolbar,
} from "@mui/material";
import { indigo, lime } from "@mui/material/colors";
import { useQuery } from "@tanstack/react-query";
import DashboardIcon from "@mui/icons-material/Dashboard";
import ImageIcon from "@mui/icons-material/Image";
import VideocamIcon from "@mui/icons-material/Videocam";
import PhotoCameraIcon from "@mui/icons-material/PhotoCamera";
import SwitchCameraIcon from "@mui/icons-material/SwitchCamera";
import GridOnIcon from "@mui/icons-material/GridOn";
import BoltIcon from "@mui/icons-material/Bolt";
import SaveIcon from "@mui/icons-material/Save";
import WindPowerIcon from "@mui/icons-material/WindPower";
import StraightenIcon from "@mui/icons-material/Straighten";
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import ApiIcon from "@mui/icons-material/Api";
import MenuBookIcon from "@mui/icons-material/MenuBook";
import HistoryIcon from "@mui/icons-material/History";
import CodeIcon from "@mui/icons-material/Code";
import GavelIcon from "@mui/icons-material/Gavel";
import _ from "lodash";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import StatusBar from "./components/StatusBar";
import ErrorMessage from "./components/ErrorMessage";
import DataProcessingPlots from "./components/DataProcessingPlots";
import MeasurementStatistics from "./components/MeasurementStatistics";
import DataProcessingSettings from "./components/DataProcessingSettings";
import PreviewImage from "./components/PreviewImage";
import DataCollection from "./components/DataCollection";
import DetectorSettings from "./components/DetectorSettings";
import DetectorSelection from "./components/DetectorSelection";
import DetectorStatus from "./components/DetectorStatus";
import PixelMask from "./components/PixelMask";
import DarkMaskSettings from "./components/DarkMaskSettings";
import IndexingSettings from "./components/IndexingSettings";
import BraggIntegrationSettings from "./components/BraggIntegrationSettings";
import AzIntSettings from "./components/AzIntSettings";
import ROI from "./components/ROI";
import FileWriterSettings from "./components/FileWriterSettings";
import ImageFormatSettings from "./components/ImageFormatSettings";
import InstrumentMetadata from "./components/InstrumentMetadata";
import ZeroMQPreview from "./components/ZeroMQPreview";
import ImagePusherStatus from "./components/ImagePusherStatus";
import Calibration from "./components/Calibration";
import FpgaStatus from "./components/FpgaStatus";
import { plot_type } from "./client";
import { getStatisticsOptions } from "./client/@tanstack/react-query.gen";
import { JFJOCH_VERSION } from "./version";
// Bundled at build time, so they always match the running release.
import changelogMd from "../../docs/CHANGELOG.md?raw";
import licenseMd from "../../docs/LICENSE.md?raw";
const drawerWidth = 240;
// Pale indigo drawer that echoes the indigo top bar without repeating it, with a
// lime accent on the selected entry (lime reads well small, garish large).
const drawerBg = indigo[50];
const drawerSelectedBg = indigo[100];
const jfjoch_theme = createTheme({
palette: {
primary: indigo,
secondary: lime,
},
});
// An item either renders a panel inside the app (render) or links out to an
// external page in a new tab (href), e.g. the source repository.
type NavItem = { id: string; label: string; icon: ReactNode; disabled?: boolean;
href?: string; render?: () => ReactNode };
type NavSection = { subheader?: string; items: NavItem[] };
/** Documentation / API reference shown inside the app area, with a new-tab escape hatch. */
function DocFrame({ title, src }: { title: string; src: string }) {
return (
<Box>
<Box sx={{ mb: 1, textAlign: "right" }}>
<Link href={src} target="_blank" rel="noopener"
sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}>
Open in new tab <OpenInNewIcon fontSize="small"/>
</Link>
</Box>
<Box component="iframe" title={title} src={src}
sx={{ width: "100%", height: "calc(100vh - 220px)", border: "1px solid",
borderColor: "divider", borderRadius: 1 }}/>
</Box>
);
}
/** Render bundled Markdown inline, styled with the app theme so it reads as part of
* the frontend rather than the framed Sphinx documentation. */
function MarkdownDoc({ md }: { md: string }) {
return (
<Box sx={{
maxWidth: 900, mx: "auto",
"& h1": { typography: "h4", mt: 0, mb: 2 },
"& h2": { typography: "h5", mt: 4, mb: 1, color: "primary.dark" },
"& h3": { typography: "h6", mt: 3, mb: 1 },
"& ul": { pl: 3, my: 1 },
"& li": { mb: 0.5 },
"& a": { color: "primary.main" },
"& code": { bgcolor: "grey.100", px: 0.5, borderRadius: 0.5, fontSize: "0.85em" },
}}>
<Markdown remarkPlugins={[remarkGfm]}>{md}</Markdown>
</Box>
);
}
function App() {
const [drawerOpen, setDrawerOpen] = useState(true);
const [view, setView] = useState('dashboard');
const toggleDrawer = useCallback(() => setDrawerOpen(o => !o), []);
// Poll general statistics once per second; structural sharing keeps unchanged
// slices referentially stable so memoized panels only re-render when their own
// slice changes.
const { data, isError, refetch } = useQuery({
...getStatisticsOptions(),
refetchInterval: 1000,
});
const s = isError || data === undefined ? {} : data;
const isMeasuring = s.broker?.state === 'Measuring';
// Every panel that used to live behind a toggle is now a single drawer entry;
// only the active one is mounted, so the screen shows one thing at a time.
const sections: NavSection[] = [
{ items: [
{ id: 'dashboard', label: 'Dashboard', icon: <DashboardIcon/>, render: () => (
<Grid container spacing={3} justifyContent="center" alignItems="flex-start">
<Grid item xs={12} lg={8}>
<DataProcessingPlots height={550} type={plot_type.BKG_ESTIMATE}/>
</Grid>
<Grid item xs={12} lg={4}>
<MeasurementStatistics s={s.measurement ?? {}}/>
</Grid>
<Grid item xs={12} lg={8}>
<DataProcessingPlots height={800} type={plot_type.INDEXING_RATE}/>
</Grid>
<Grid item xs={12} lg={4}>
<DataProcessingSettings s={s.data_processing_settings} update={refetch}/>
</Grid>
</Grid>
)},
]},
{ subheader: 'Acquisition', items: [
{ id: 'preview', label: 'Live preview', icon: <ImageIcon/>, render: () => (
<PreviewImage measuring={isMeasuring}
max_image_number={s.buffer?.max_image_number}
min_image_number={s.buffer?.min_image_number}/>
)},
{ id: 'data-collection', label: 'Data collection', icon: <VideocamIcon/>, render: () => (
<DataCollection frame_time_us={s.detector_settings?.frame_time_us ?? 500}/>
)},
]},
{ subheader: 'Expert', items: [
{ id: 'detector', label: 'Detector', icon: <PhotoCameraIcon/>, render: () => (
<Stack spacing={2}>
<DetectorStatus s={s.detector}/>
<DetectorSettings s={s.detector_settings}/>
</Stack>
)},
{ id: 'detector-selection', label: 'Detector selection', icon: <SwitchCameraIcon/>, render: () => (
<DetectorSelection s={s.detector_list}/>
)},
{ id: 'masks', label: 'Masks', icon: <GridOnIcon/>, render: () => (
<Stack spacing={2}>
<PixelMask s={s.pixel_mask}/>
<DarkMaskSettings s={s.dark_mask}/>
</Stack>
)},
{ id: 'processing', label: 'On-the-fly processing', icon: <BoltIcon/>, render: () => (
<Stack spacing={2}>
<IndexingSettings s={s.indexing} status={s.broker}/>
<BraggIntegrationSettings s={s.bragg_integration}/>
<AzIntSettings s={s.az_int}/>
<ROI s={s.roi}/>
</Stack>
)},
{ id: 'output', label: 'Output', icon: <SaveIcon/>, render: () => (
<Stack spacing={2}>
<FileWriterSettings s={s.file_writer_settings}/>
<ImageFormatSettings s={s.image_format_settings}/>
<InstrumentMetadata s={s.instrument_metadata}/>
</Stack>
)},
{ id: 'streaming', label: 'Streaming', icon: <WindPowerIcon/>, render: () => (
<Stack spacing={2}>
<ZeroMQPreview s={s.zeromq_preview}/>
<ImagePusherStatus s={s.image_pusher}/>
</Stack>
)},
{ id: 'calibration', label: 'JUNGFRAU calibration', icon: <StraightenIcon/>,
disabled: _.isEmpty(s.calibration), render: () => (
<Calibration s={s.calibration}/>
)},
{ id: 'fpga', label: 'FPGA status', icon: <DeveloperBoardIcon/>,
disabled: _.isEmpty(s.fpga), render: () => (
<FpgaStatus s={s.fpga}/>
)},
]},
{ subheader: 'Reference', items: [
{ id: 'api-reference', label: 'API reference', icon: <ApiIcon/>, render: () => (
<DocFrame title="API reference" src="/frontend/openapi.html"/>
)},
{ id: 'documentation', label: 'Documentation', icon: <MenuBookIcon/>, render: () => (
<DocFrame title="Documentation" src="/frontend/docs/index.html"/>
)},
{ id: 'changelog', label: 'Changelog', icon: <HistoryIcon/>, render: () => (
<MarkdownDoc md={changelogMd}/>
)},
{ id: 'license', label: 'License', icon: <GavelIcon/>, render: () => (
<MarkdownDoc md={licenseMd}/>
)},
{ id: 'source-code', label: 'Source code', icon: <CodeIcon/>,
href: 'https://gitea.psi.ch/mx/jungfraujoch' },
]},
];
const activeItem = sections.flatMap(sec => sec.items).find(it => it.id === view) ?? sections[0].items[0];
return <ThemeProvider theme={jfjoch_theme}>
<CssBaseline enableColorScheme/>
<Box sx={{ display: 'flex' }}>
<StatusBar s={s.broker} onMenuClick={toggleDrawer}/>
{drawerOpen && (
<Drawer variant="permanent"
sx={{ width: drawerWidth, flexShrink: 0,
'& .MuiDrawer-paper': { width: drawerWidth, boxSizing: 'border-box',
bgcolor: drawerBg, borderRight: 'none' } }}>
<Toolbar/>
<Box sx={{ overflow: 'auto', pb: 6 }}>
{sections.map((sec, i) => (
<List key={i} subheader={sec.subheader
? <ListSubheader sx={{ bgcolor: 'transparent', color: 'primary.dark',
fontWeight: 600 }}>{sec.subheader}</ListSubheader>
: undefined}>
{sec.items.map(it => (
<ListItemButton key={it.id}
disabled={it.disabled}
{...(it.href
? { component: 'a', href: it.href,
target: '_blank', rel: 'noopener' }
: { selected: view === it.id,
onClick: () => setView(it.id) })}
sx={{ borderLeft: '4px solid transparent',
'&.Mui-selected': { bgcolor: drawerSelectedBg,
borderColor: 'secondary.main' },
'&.Mui-selected:hover': { bgcolor: drawerSelectedBg } }}>
<ListItemIcon sx={{ minWidth: 40 }}>{it.icon}</ListItemIcon>
<ListItemText primary={it.label}/>
</ListItemButton>
))}
</List>
))}
</Box>
</Drawer>
)}
<Box component="main" sx={{ flexGrow: 1, p: 3, pb: '64px', minWidth: 0 }}>
<Toolbar/>
<ErrorMessage s={s.broker}/>
<Box sx={{ maxWidth: 1300, mx: 'auto', mt: 2 }}>
{activeItem.render?.()}
</Box>
</Box>
</Box>
<Box component="footer" sx={{
position: 'fixed', bottom: 0, left: 0, right: 0,
zIndex: (t) => t.zIndex.drawer + 1,
bgcolor: 'background.paper', borderTop: '1px solid', borderColor: 'divider',
py: 0.5, px: 2, textAlign: 'center', fontSize: '0.8rem',
}}>
Developed at{' '}
<Link href="https://www.psi.ch" target="_blank" rel="noopener">Paul Scherrer Institute</Link>
{' '}(20192026). Main author:{' '}
<Link href="mailto:filip.leonarski@psi.ch">Filip Leonarski</Link> ·{' '}
<Link href="https://doi.org/10.1107/S1600577522010268"><i>J. Synchrotron Rad.</i> (2023). <b>30</b>, 227234</Link>
{' '}· Version: {JFJOCH_VERSION}
</Box>
</ThemeProvider>
}
export default App;