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
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>
293 lines
14 KiB
TypeScript
293 lines
14 KiB
TypeScript
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>
|
||
{' '}(2019–2026). 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>, 227–234</Link>
|
||
{' '}· Version: {JFJOCH_VERSION}
|
||
</Box>
|
||
</ThemeProvider>
|
||
}
|
||
|
||
export default App;
|