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;