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 MaskVisualization from "./components/MaskVisualization"; 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]; // PSI branding sits alone in the footer credit bar with clear space (never merged // with the app wordmark or menu icon). The four official logos differ only in the // diffused spots; pick one at module load so it is fixed per page load and never // re-randomises on render. BASE_URL keeps the path correct under the /frontend/ base. // These are the positive (dark) logos, used as-is on the light footer. const PSI_LOGOS = [1, 2, 3, 4].map(i => `${import.meta.env.BASE_URL}psi_0${i}.png`); const PSI_LOGO = PSI_LOGOS[Math.floor(Math.random() * PSI_LOGOS.length)]; const jfjoch_theme = createTheme({ palette: { primary: indigo, secondary: lime, }, // Slightly rounder corners than the MUI default (4px) give panels, inputs and // buttons a softer, more modern feel without changing any component's behaviour. shape: { borderRadius: 8 }, }); /** Flow a page's settings panels into responsive columns (masonry) so a page of * small panels reads wide-and-short instead of as one tall single-file column. * Panels never split across a column break. */ function PanelColumns({ columns = 2, children }: { columns?: number; children: ReactNode }) { return ( *': { breakInside: 'avoid', mb: 2, display: 'block' }, }}> {children} ); } // 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 ( Open in new tab ); } /** 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 ( {md} ); } 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: , render: () => ( )}, ]}, { subheader: 'Acquisition', items: [ { id: 'preview', label: 'Live preview', icon: , render: () => ( )}, { id: 'data-collection', label: 'Data collection', icon: , render: () => ( )}, ]}, { subheader: 'Expert', items: [ { id: 'detector', label: 'Detector', icon: , render: () => ( )}, { id: 'detector-selection', label: 'Detector selection', icon: , render: () => ( )}, { id: 'masks', label: 'Masks', icon: , render: () => ( )}, { id: 'processing', label: 'On-the-fly processing', icon: , render: () => ( {/* ROI holds three wide side-by-side tables — keep it full width. */} )}, { id: 'output', label: 'Output', icon: , render: () => ( )}, { id: 'streaming', label: 'Streaming', icon: , render: () => ( )}, { id: 'calibration', label: 'JUNGFRAU calibration', icon: , disabled: _.isEmpty(s.calibration), render: () => ( )}, { id: 'fpga', label: 'FPGA status', icon: , disabled: _.isEmpty(s.fpga), render: () => ( )}, ]}, { subheader: 'Reference', items: [ { id: 'api-reference', label: 'API reference', icon: , render: () => ( )}, { id: 'documentation', label: 'Documentation', icon: , render: () => ( )}, { id: 'changelog', label: 'Changelog', icon: , render: () => ( )}, { id: 'license', label: 'License', icon: , render: () => ( )}, { id: 'source-code', label: 'Source code', icon: , href: 'https://gitea.psi.ch/mx/jungfraujoch' }, ]}, ]; const activeItem = sections.flatMap(sec => sec.items).find(it => it.id === view) ?? sections[0].items[0]; return {drawerOpen && ( {sections.map((sec, i) => ( {sec.subheader} : undefined}> {sec.items.map(it => ( setView(it.id) })} sx={{ borderLeft: '4px solid transparent', '&.Mui-selected': { bgcolor: drawerSelectedBg, borderColor: 'secondary.main' }, '&.Mui-selected:hover': { bgcolor: drawerSelectedBg } }}> {it.icon} ))} ))} )} {activeItem.render?.()} t.zIndex.drawer + 1, bgcolor: 'background.paper', borderTop: '1px solid', borderColor: 'divider', py: 1.5, px: 2, textAlign: 'center', fontSize: '0.8rem', }}> Developed at{' '} Paul Scherrer Institute {' '}(2019–2026). Main author:{' '} Filip Leonarski ·{' '} J. Synchrotron Rad. (2023). 30, 227–234 {' '}· Version: {JFJOCH_VERSION} } export default App;