Merge frontend-rugnux-fix into rc176: rugnux live gets a full-width dashboard row and a timeline that fits the run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
This commit is contained in:
commit
9a382c5ef4
7 files changed
+241
-108
No files matched your search
@@ -171,11 +171,9 @@ function App() {
|
||||
<DataProcessingPlots height={550} type={plot_type.BKG_ESTIMATE}/>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={4}>
|
||||
<Stack spacing={3}>
|
||||
<MeasurementStatistics s={s.measurement ?? {}}/>
|
||||
<RugnuxStatus/>
|
||||
</Stack>
|
||||
<MeasurementStatistics s={s.measurement ?? {}}/>
|
||||
</Grid>
|
||||
<RugnuxStatus/>
|
||||
<Grid item xs={12} lg={8}>
|
||||
<DataProcessingPlots height={800} type={plot_type.INDEXING_RATE}/>
|
||||
</Grid>
|
||||
|
||||
@@ -2,6 +2,8 @@ import {memo, ReactNode} from 'react';
|
||||
|
||||
import {broker_status} from "../client";
|
||||
import {Alert, AlertColor} from "@mui/material";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getConfigRugnuxOptions, getRugnuxStatusOptions} from "../client/@tanstack/react-query.gen";
|
||||
|
||||
type MyProps = {
|
||||
s?: broker_status
|
||||
@@ -27,13 +29,32 @@ function alert(input: AlertColor, message: string) : ReactNode {
|
||||
return <Alert severity={input}>{message}</Alert>
|
||||
}
|
||||
|
||||
// The broker messages that report on a collection (rather than on a calibration or a configuration).
|
||||
const COLLECTION_MESSAGE = /measuring|data collection|stream receiver/i;
|
||||
|
||||
function ErrorMessage({s}: MyProps) {
|
||||
// The broker's message is about the collection; with rugnux live on, its analysis of the same
|
||||
// collection runs on after it and can fail where the collection did not, so it is named alongside.
|
||||
const {data: rugnuxSettings} = useQuery(getConfigRugnuxOptions());
|
||||
const {data: rugnux} = useQuery({
|
||||
...getRugnuxStatusOptions(),
|
||||
refetchInterval: 1000,
|
||||
enabled: rugnuxSettings?.enable === true,
|
||||
});
|
||||
|
||||
if (s === undefined)
|
||||
return alert("error", "Not connected to Jungfraujoch instance; check if jfjoch_broker is running");
|
||||
|
||||
if (s.message === undefined)
|
||||
return alert("info", "");
|
||||
|
||||
if (rugnuxSettings?.enable === true && rugnux !== undefined && rugnux.state !== 'Idle'
|
||||
&& COLLECTION_MESSAGE.test(s.message)) {
|
||||
const failed = rugnux.state === 'Failed';
|
||||
const sev = failed && s.message_severity === 'success' ? "warning" : severity(s.message_severity);
|
||||
return alert(sev, `${s.message} · rugnux analysis: ${rugnux.state.toLowerCase()}`);
|
||||
}
|
||||
|
||||
return alert(severity(s.message_severity), s.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function MeasurementStatistics({s}: MyProps) {
|
||||
? s.bkg_estimate.toPrecision(7) : "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row"> Unit cell: </TableCell>
|
||||
<TableCell component="th" scope="row"> Unit cell (given): </TableCell>
|
||||
<TableCell align="right">{s.unit_cell}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Alert, Box, Chip, LinearProgress, Paper, Stack, Typography} from "@mui/material";
|
||||
import {indigo, lime, deepOrange, grey} from "@mui/material/colors";
|
||||
import {Alert, Box, LinearProgress, Paper, Stack, Typography} from "@mui/material";
|
||||
import {indigo, lime, grey} from "@mui/material/colors";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getConfigRugnuxOptions, getRugnuxStatusOptions} from "../client/@tanstack/react-query.gen";
|
||||
import {rugnux_live_status} from "../client";
|
||||
@@ -7,19 +7,13 @@ import {useTokenRequired} from "../auth";
|
||||
import EmptyState from "./EmptyState";
|
||||
import RugnuxTimeline from "./RugnuxTimeline";
|
||||
import RugnuxSettings from "./RugnuxSettings";
|
||||
import {CutAlert, lastImageTime, ProbeAlert, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import {CutAlert, lastImageTime, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import BoltIcon from "@mui/icons-material/Bolt";
|
||||
|
||||
const STATE_COLOUR: Record<string, { bg: string, fg: string }> = {
|
||||
Running: {bg: indigo[500], fg: "#fff"},
|
||||
Done: {bg: lime[600], fg: "rgba(0,0,0,.87)"},
|
||||
Failed: {bg: deepOrange[700], fg: "#fff"},
|
||||
Cancelled: {bg: grey[500], fg: "#fff"},
|
||||
Skipped: {bg: grey[400], fg: "rgba(0,0,0,.87)"},
|
||||
};
|
||||
|
||||
// The summary rows rugnux writes; the ones a crystallographer looks at first get a stronger tile.
|
||||
const KEY_ROW = /cell|space group|resolution|cc1\/2|r-?free|model/i;
|
||||
// The summary rows rugnux writes; the ones a crystallographer looks at first get a stronger tile, and
|
||||
// space group and cell share a row.
|
||||
const KEY_ROW = /unit cell|space group|resolution|signal|r-?free|model/i;
|
||||
const PAIRED_ROW = /space group|unit cell/i;
|
||||
|
||||
function Section({title, children}: { title: string, children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -34,35 +28,34 @@ function Section({title, children}: { title: string, children: React.ReactNode }
|
||||
}
|
||||
|
||||
function Header({s}: { s: rugnux_live_status }) {
|
||||
const colour = STATE_COLOUR[s.state] ?? STATE_COLOUR.Skipped;
|
||||
const latency = s.last_image_to_written_s;
|
||||
const lastImage = lastImageTime(s);
|
||||
return (
|
||||
<Paper sx={{p: 3, display: "flex", alignItems: "center", gap: 3, flexWrap: "wrap",
|
||||
background: `linear-gradient(90deg, ${indigo[50]} 0%, #fff 70%)`}}>
|
||||
<Box sx={{flexGrow: 1, minWidth: 280}}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Typography variant="h4" sx={{fontWeight: 700, color: "primary.dark"}}>Rugnux live</Typography>
|
||||
<Chip label={s.state} sx={{bgcolor: colour.bg, color: colour.fg, fontWeight: 700,
|
||||
fontSize: "1rem", height: 34, px: 1}}/>
|
||||
</Stack>
|
||||
<Typography sx={{mt: 1, color: "text.secondary", fontSize: "1.1rem"}}>
|
||||
{s.images_received} / {s.images_expected} images
|
||||
{lastImage !== undefined && ` · collected in ${lastImage.toFixed(1)} s`}
|
||||
{` · ${s.elapsed_s.toFixed(1)} s since the first image`}
|
||||
</Typography>
|
||||
{s.message && <Typography sx={{mt: 0.5, color: "error.main"}}>{s.message}</Typography>}
|
||||
</Box>
|
||||
<Box sx={{textAlign: "right"}}>
|
||||
<Typography sx={{fontSize: "0.85rem", fontWeight: 700, letterSpacing: ".08em",
|
||||
textTransform: "uppercase", color: "text.secondary"}}>
|
||||
{latency !== undefined ? "Last image → all results written" : "Processing for"}
|
||||
</Typography>
|
||||
<Typography sx={{fontSize: "4.5rem", lineHeight: 1.05, fontWeight: 800,
|
||||
color: latency === undefined ? indigo[300] : s.state === 'Done' ? lime[900] : indigo[700]}}>
|
||||
{(latency ?? s.elapsed_s).toFixed(1)} s
|
||||
</Typography>
|
||||
<Paper sx={{p: 3, background: `linear-gradient(90deg, ${indigo[50]} 0%, #fff 70%)`}}>
|
||||
<Box sx={{display: "flex", alignItems: "center", gap: 3, flexWrap: "wrap"}}>
|
||||
<Box sx={{flexGrow: 1, minWidth: 280}}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Typography variant="h4" sx={{fontWeight: 700, color: "primary.dark"}}>Rugnux live</Typography>
|
||||
<StateChip state={s.state} large/>
|
||||
</Stack>
|
||||
<Typography sx={{mt: 1, color: "text.secondary", fontSize: "1.1rem"}}>
|
||||
{s.images_received} / {s.images_expected} images
|
||||
{lastImage !== undefined && ` · collected in ${lastImage.toFixed(1)} s`}
|
||||
{` · ${s.elapsed_s.toFixed(1)} s since the first image`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{textAlign: "right", ml: "auto"}}>
|
||||
<Typography sx={{fontSize: "0.85rem", fontWeight: 700, letterSpacing: ".08em",
|
||||
textTransform: "uppercase", color: "text.secondary"}}>
|
||||
{latency !== undefined ? "Last image → all results written" : "Processing for"}
|
||||
</Typography>
|
||||
<Typography sx={{fontSize: "4.5rem", lineHeight: 1.05, fontWeight: 800,
|
||||
color: latency === undefined ? indigo[300] : s.state === 'Done' ? lime[900] : indigo[700]}}>
|
||||
{(latency ?? s.elapsed_s).toFixed(1)} s
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<RunMessage s={s} sx={{mt: 2}}/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -90,10 +83,10 @@ function SummaryTiles({s}: { s: rugnux_live_status }) {
|
||||
return (
|
||||
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", sm: "1fr 1fr"}, gap: 1.5, mt: 2}}>
|
||||
{s.summary.map((row, i) => {
|
||||
const key = KEY_ROW.test(row.label) || KEY_ROW.test(row.value);
|
||||
const key = KEY_ROW.test(row.label);
|
||||
const short = row.value.length <= 24;
|
||||
return (
|
||||
<Box key={i} sx={{p: 1.5, borderRadius: 2, gridColumn: key ? "auto" : "1 / -1",
|
||||
<Box key={i} sx={{p: 1.5, borderRadius: 2, gridColumn: PAIRED_ROW.test(row.label) ? "auto" : "1 / -1",
|
||||
bgcolor: key ? indigo[50] : grey[50],
|
||||
border: "1px solid", borderColor: key ? indigo[100] : grey[200]}}>
|
||||
<Typography sx={{fontSize: "0.75rem", fontWeight: 700, letterSpacing: ".06em",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {Alert, AlertTitle, Box, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Typography} from "@mui/material";
|
||||
import {Alert, AlertTitle, Box, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Typography} from "@mui/material";
|
||||
import {indigo, lime, deepOrange, grey} from "@mui/material/colors";
|
||||
import {SxProps, Theme} from "@mui/material/styles";
|
||||
import {rugnux_live_status, rugnux_live_step} from "../client";
|
||||
|
||||
// Pieces of the rugnux live status shared by the dashboard card and the Rugnux page.
|
||||
@@ -19,6 +21,31 @@ export const verdictSeverity = (verdict: string) => {
|
||||
export const lastImageTime = (s: rugnux_live_status) =>
|
||||
s.last_image_s ?? (s.last_image_to_written_s !== undefined ? s.elapsed_s - s.last_image_to_written_s : undefined);
|
||||
|
||||
const STATE_COLOUR: Record<string, { bg: string, fg: string }> = {
|
||||
Running: {bg: indigo[500], fg: "#fff"},
|
||||
Done: {bg: lime[600], fg: "rgba(0,0,0,.87)"},
|
||||
Failed: {bg: deepOrange[700], fg: "#fff"},
|
||||
Cancelled: {bg: grey[500], fg: "#fff"},
|
||||
Skipped: {bg: grey[400], fg: "rgba(0,0,0,.87)"},
|
||||
};
|
||||
|
||||
export function StateChip({state, large}: { state: string, large?: boolean }) {
|
||||
const colour = STATE_COLOUR[state] ?? STATE_COLOUR.Skipped;
|
||||
return <Chip label={state} sx={{bgcolor: colour.bg, color: colour.fg, fontWeight: 700,
|
||||
...(large && {fontSize: "1rem", height: 34, px: 1})}}/>;
|
||||
}
|
||||
|
||||
// Why the run failed (or what it reports), as rugnux words it.
|
||||
export function RunMessage({s, sx}: { s: rugnux_live_status, sx?: SxProps<Theme> }) {
|
||||
if (!s.message)
|
||||
return null;
|
||||
return (
|
||||
<Alert severity={s.state === 'Failed' ? "error" : "info"} sx={[{width: "100%", textAlign: "left"}, ...(Array.isArray(sx) ? sx : [sx])]}>
|
||||
{s.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export function CutAlert({s}: { s: rugnux_live_status }) {
|
||||
if (!s.cut)
|
||||
return null;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import {memo} from 'react';
|
||||
|
||||
import {Alert, Box, LinearProgress, Typography} from "@mui/material";
|
||||
import {Alert, Box, Grid, LinearProgress, Stack, Table, TableBody, TableCell, TableRow, Typography} from "@mui/material";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getConfigRugnuxOptions, getRugnuxStatusOptions} from "../client/@tanstack/react-query.gen";
|
||||
import {useTokenRequired} from "../auth";
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import {CutAlert, ProbeAlert, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import RugnuxTimeline from "./RugnuxTimeline";
|
||||
import {CutAlert, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
|
||||
// Live rugnux processing of the last collection, as a dashboard card: the step it is on, the steps
|
||||
// behind it with what they found, and once it is over the verdict. Nothing while rugnux has not run,
|
||||
// and nothing - not even the status polling - while the mode is off (the settings are read once, on mount).
|
||||
// The summary rows worth a glance on the dashboard; the Rugnux live tab has all of them.
|
||||
const KEY_ROW = /space group|unit cell|resolution|completeness|signal/i;
|
||||
|
||||
// Live rugnux processing of the last collection, as a full-width dashboard row: the timeline, the
|
||||
// step it is on and the steps behind it, and once it is over the verdict with the key results.
|
||||
// Nothing while rugnux has not run, and nothing - not even the status polling - while the mode is off
|
||||
// (the settings are read once, on mount). Renders its own grid item, so an idle card leaves no gap.
|
||||
function RugnuxStatus() {
|
||||
const {data: settings} = useQuery(getConfigRugnuxOptions());
|
||||
const enabled = settings?.enable === true;
|
||||
@@ -24,44 +29,73 @@ function RugnuxStatus() {
|
||||
return null;
|
||||
if (isError && tokenRequired)
|
||||
return (
|
||||
<SettingsPanel title="Rugnux">
|
||||
<Typography>Dataset token required (key button in the top bar)</Typography>
|
||||
</SettingsPanel>
|
||||
<Grid item xs={12}>
|
||||
<SettingsPanel title="Rugnux live">
|
||||
<Typography>Dataset token required (key button in the top bar)</Typography>
|
||||
</SettingsPanel>
|
||||
</Grid>
|
||||
);
|
||||
if (isError || s === undefined || s.state === 'Idle')
|
||||
return null;
|
||||
|
||||
const current = s.state === 'Running' && s.steps.length > 0 ? s.steps[s.steps.length - 1] : undefined;
|
||||
const latency = s.last_image_to_written_s;
|
||||
const keyRows = s.summary.filter(row => KEY_ROW.test(row.label));
|
||||
// a failed run has nothing beside its steps
|
||||
const twoColumns = current !== undefined || !!s.verdict || keyRows.length > 0 || s.warnings.length > 0;
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Rugnux" spacing={2}>
|
||||
<Typography variant="h6">
|
||||
{s.state} · {s.images_received} / {s.images_expected} images · {s.elapsed_s.toFixed(0)} s
|
||||
</Typography>
|
||||
{s.message && <Typography color="text.secondary">{s.message}</Typography>}
|
||||
<CutAlert s={s}/>
|
||||
<ProbeAlert s={s}/>
|
||||
<Grid item xs={12}>
|
||||
<SettingsPanel title="Rugnux live" spacing={2}>
|
||||
<Stack spacing={2} sx={{width: "100%", textAlign: "left"}}>
|
||||
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap" useFlexGap>
|
||||
<StateChip state={s.state}/>
|
||||
<Typography variant="h6" sx={{flexGrow: 1}}>
|
||||
{s.images_received} / {s.images_expected} images · {s.elapsed_s.toFixed(0)} s
|
||||
</Typography>
|
||||
{latency !== undefined && (
|
||||
<Typography variant="h6">
|
||||
Last image → all written: <Box component="span" sx={{fontWeight: 700}}>{latency.toFixed(1)} s</Box>
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<RunMessage s={s}/>
|
||||
<CutAlert s={s}/>
|
||||
<ProbeAlert s={s}/>
|
||||
<RugnuxTimeline s={s} deadline_s={settings?.deadline_s ?? 0}/>
|
||||
|
||||
{current && (
|
||||
<Box sx={{width: "100%"}}>
|
||||
<Typography>{current.title}</Typography>
|
||||
<LinearProgress variant={s.step_progress !== undefined ? "determinate" : "indeterminate"}
|
||||
value={(s.step_progress ?? 0) * 100}/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{s.last_image_to_written_s !== undefined && (
|
||||
<Typography variant="h5">
|
||||
Last image → all written: {s.last_image_to_written_s.toFixed(1)} s
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<VerdictAlert s={s}/>
|
||||
<StepTable steps={s.steps}/>
|
||||
{s.warnings.map((w, i) => (
|
||||
<Alert key={i} severity="warning" sx={{width: "100%", textAlign: "left"}}>{w}</Alert>
|
||||
))}
|
||||
</SettingsPanel>
|
||||
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", md: twoColumns ? "1fr 1fr" : "1fr"},
|
||||
gap: 2, alignItems: "start"}}>
|
||||
{twoColumns && <Stack spacing={2}>
|
||||
{current && (
|
||||
<Box>
|
||||
<Typography>{current.title}</Typography>
|
||||
<LinearProgress variant={s.step_progress !== undefined ? "determinate" : "indeterminate"}
|
||||
value={(s.step_progress ?? 0) * 100}/>
|
||||
</Box>
|
||||
)}
|
||||
<VerdictAlert s={s}/>
|
||||
{keyRows.length > 0 && (
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
{keyRows.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell component="th" scope="row" sx={{whiteSpace: "nowrap", verticalAlign: "top"}}>
|
||||
{row.label}
|
||||
</TableCell>
|
||||
<TableCell>{row.value}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{s.warnings.map((w, i) => <Alert key={i} severity="warning">{w}</Alert>)}
|
||||
</Stack>}
|
||||
<StepTable steps={s.steps}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SettingsPanel>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {Box, Tooltip, Typography} from "@mui/material";
|
||||
import {keyframes} from "@mui/system";
|
||||
import {indigo, lime, lightBlue, orange, grey, deepOrange} from "@mui/material/colors";
|
||||
@@ -25,6 +26,24 @@ const STEP_COLOURS = [indigo[500], indigo[300], indigo[700], lightBlue[700], ind
|
||||
const LABEL_WIDTH = 120;
|
||||
const LANE_HEIGHT = 64;
|
||||
const AXIS_HEIGHT = 28;
|
||||
const MARKER_ROW = 28;
|
||||
|
||||
// Rough rendered width of a label in px; enough to decide whether it fits and where it goes.
|
||||
const textWidth = (text: string, pxPerChar: number) => text.length * pxPerChar;
|
||||
|
||||
// Width of an element in px, kept up to date as the page resizes.
|
||||
function useWidth() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!ref.current)
|
||||
return;
|
||||
const observer = new ResizeObserver(entries => setWidth(entries[0].contentRect.width));
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return [ref, width] as const;
|
||||
}
|
||||
|
||||
function niceStep(span: number) {
|
||||
for (const s of [1, 2, 5, 10, 20, 30, 60, 120])
|
||||
@@ -33,7 +52,25 @@ function niceStep(span: number) {
|
||||
return 300;
|
||||
}
|
||||
|
||||
function Marker({x, colour, label, top, dashed}: { x: number, colour: string, label: string, top: number, dashed?: boolean }) {
|
||||
type MarkerSpec = { x: number, colour: string, label: string, dashed?: boolean };
|
||||
|
||||
// Labels that would overlap go up one row each: the first row whose last label ends left of this one.
|
||||
function markerRows(markers: MarkerSpec[], width: number) {
|
||||
const rowEnds: number[] = [];
|
||||
return markers.map(m => {
|
||||
const half = (textWidth(m.label, 8.5) + 16) / 2;
|
||||
const centre = m.x / 100 * width;
|
||||
let row = rowEnds.findIndex(end => centre - half > end + 6);
|
||||
if (row < 0) {
|
||||
row = rowEnds.length;
|
||||
rowEnds.push(0);
|
||||
}
|
||||
rowEnds[row] = centre + half;
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function Marker({x, colour, label, top, dashed}: MarkerSpec & { top: number }) {
|
||||
return (
|
||||
<Box sx={{position: "absolute", left: `${x}%`, top, bottom: AXIS_HEIGHT, pointerEvents: "none"}}>
|
||||
<Box sx={{position: "absolute", top: 0, bottom: 0, left: -1.5, width: 0, zIndex: 1,
|
||||
@@ -47,6 +84,17 @@ function Marker({x, colour, label, top, dashed}: { x: number, colour: string, la
|
||||
);
|
||||
}
|
||||
|
||||
// outside the bar the label is opaque, so the "now" line passing behind it does not cross the text
|
||||
function DetectorLabel({text, outside}: { text: string, outside?: boolean }) {
|
||||
return (
|
||||
<Typography sx={{position: "relative", zIndex: 2, fontWeight: 700, fontSize: "1.1rem",
|
||||
color: "rgba(0,0,0,.85)", whiteSpace: "nowrap", bgcolor: outside ? "rgba(255,255,255,.9)" : "rgba(255,255,255,.55)",
|
||||
px: 1, borderRadius: 1}}>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// deadline_s: the configured cutoff after the last image, 0 = none
|
||||
function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: number }) {
|
||||
const lastImage = lastImageTime(s);
|
||||
@@ -55,17 +103,38 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
const rate = collectionEnd > 0 ? s.images_received / collectionEnd : 0;
|
||||
const expectedEnd = rate > 0 ? s.images_expected / rate : 0;
|
||||
|
||||
const deadline = lastImage !== undefined && deadline_s > 0 ? lastImage + deadline_s : undefined;
|
||||
const done = s.state !== 'Running';
|
||||
// The deadline matters only while the analysis runs; once it is over the axis is the run's own.
|
||||
const deadline = !done && lastImage !== undefined && deadline_s > 0 ? lastImage + deadline_s : undefined;
|
||||
const span = Math.max(s.elapsed_s, expectedEnd, deadline ?? 0, 10) * 1.06;
|
||||
const pct = (t: number) => 100 * Math.min(Math.max(t, 0), span) / span;
|
||||
const tick = niceStep(span);
|
||||
const ticks = Array.from({length: Math.floor(span / tick) + 1}, (_, i) => i * tick);
|
||||
|
||||
const done = s.state !== 'Running';
|
||||
const latency = s.last_image_to_written_s;
|
||||
|
||||
const [laneRef, laneWidth] = useWidth();
|
||||
const detectorText = `${s.images_received} / ${s.images_expected} images${rate > 0 ? ` · ${rate.toFixed(0)} Hz` : ""}`;
|
||||
const detectorInside = pct(collectionEnd) / 100 * laneWidth >= textWidth(detectorText, 11) + 48;
|
||||
|
||||
const markers: MarkerSpec[] = [];
|
||||
if (s.probe_s !== undefined)
|
||||
markers.push({x: pct(s.probe_s), label: `probe ${s.probe_s.toFixed(1)} s`,
|
||||
colour: s.probe.startsWith("no lattice") ? orange[800] : lightBlue[800]});
|
||||
if (lastImage !== undefined)
|
||||
markers.push({x: pct(lastImage), dashed: true, colour: grey[800], label: "last image"});
|
||||
if (deadline !== undefined && s.cut !== 'deadline')
|
||||
markers.push({x: pct(deadline), dashed: true, colour: deepOrange[700], label: `deadline ${deadline.toFixed(1)} s`});
|
||||
if (s.cut && s.cut_s !== undefined)
|
||||
markers.push({x: pct(s.cut_s), colour: deepOrange[900], label: `${s.cut} cut ${s.cut_s.toFixed(1)} s`});
|
||||
if (!done)
|
||||
markers.push({x: pct(s.elapsed_s), colour: "#d32f2f", label: "now"});
|
||||
markers.sort((a, b) => a.x - b.x);
|
||||
const rows = markerRows(markers, laneWidth);
|
||||
const rowCount = Math.max(1, ...rows.map(r => r + 1));
|
||||
|
||||
return (
|
||||
<Box sx={{display: "flex", pt: 6, pb: latency !== undefined ? 6 : 1}}>
|
||||
<Box sx={{display: "flex", pt: `${48 + (rowCount - 1) * MARKER_ROW}px`, pb: latency !== undefined ? 6 : 1}}>
|
||||
<Box sx={{width: LABEL_WIDTH, flexShrink: 0}}>
|
||||
{["Detector", "Rugnux"].map((l, i) => (
|
||||
<Box key={l} sx={{height: LANE_HEIGHT, mb: i === 0 ? 1.5 : 0, display: "flex", alignItems: "center"}}>
|
||||
@@ -74,7 +143,7 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box sx={{position: "relative", flexGrow: 1, minWidth: 0}}>
|
||||
<Box ref={laneRef} sx={{position: "relative", flexGrow: 1, minWidth: 0}}>
|
||||
{/* grid lines */}
|
||||
{ticks.map(t => (
|
||||
<Box key={t} sx={{position: "absolute", left: `${pct(t)}%`, top: 0,
|
||||
@@ -86,12 +155,14 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
<Box sx={{position: "absolute", left: 0, top: 0, bottom: 0, width: `${pct(collectionEnd)}%`,
|
||||
bgcolor: lime[600], borderRadius: 2, display: "flex", alignItems: "center", px: 2,
|
||||
overflow: "hidden", ...(collecting && runningSx)}}>
|
||||
<Typography sx={{position: "relative", zIndex: 2, fontWeight: 700, fontSize: "1.1rem",
|
||||
color: "rgba(0,0,0,.85)", whiteSpace: "nowrap", bgcolor: "rgba(255,255,255,.55)",
|
||||
px: 1, borderRadius: 1}}>
|
||||
{s.images_received} / {s.images_expected} images{rate > 0 && ` · ${rate.toFixed(0)} Hz`}
|
||||
</Typography>
|
||||
{detectorInside && <DetectorLabel text={detectorText}/>}
|
||||
</Box>
|
||||
{!detectorInside && (
|
||||
<Box sx={{position: "absolute", left: `calc(${pct(collectionEnd)}% + 8px)`, top: 0, bottom: 0,
|
||||
display: "flex", alignItems: "center"}}>
|
||||
<DetectorLabel text={detectorText} outside/>
|
||||
</Box>
|
||||
)}
|
||||
{collecting && expectedEnd > collectionEnd && (
|
||||
<Box sx={{position: "absolute", left: `${pct(collectionEnd)}%`, top: 0, bottom: 0,
|
||||
width: `${pct(expectedEnd) - pct(collectionEnd)}%`,
|
||||
@@ -105,6 +176,8 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
const open = step.duration_s === undefined;
|
||||
const end = open ? s.elapsed_s : step.started_s + step.duration_s!;
|
||||
const width = Math.max(pct(end) - pct(step.started_s), 0.4);
|
||||
// a title squeezed to its first letter says nothing - the tooltip has it then
|
||||
const showTitle = width / 100 * laneWidth >= Math.min(textWidth(step.title, 8.5) + 12, 72);
|
||||
return (
|
||||
<Tooltip key={i} arrow title={
|
||||
<Box>
|
||||
@@ -118,10 +191,10 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
borderRadius: i === 0 ? "8px 0 0 8px" : 0,
|
||||
...(open && !done && runningSx),
|
||||
...(i === s.steps.length - 1 && {borderTopRightRadius: 8, borderBottomRightRadius: 8})}}>
|
||||
<Typography sx={{position: "relative", zIndex: 2, color: "#fff", fontWeight: 600, fontSize: "0.9rem",
|
||||
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>
|
||||
{showTitle && <Typography sx={{position: "relative", zIndex: 2, color: "#fff", fontWeight: 600,
|
||||
fontSize: "0.9rem", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>
|
||||
{step.title}
|
||||
</Typography>
|
||||
</Typography>}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -139,20 +212,7 @@ function RugnuxTimeline({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
</Box>
|
||||
|
||||
{/* markers */}
|
||||
{s.probe_s !== undefined && (
|
||||
<Marker x={pct(s.probe_s)} top={-8} label={`probe ${s.probe_s.toFixed(1)} s`}
|
||||
colour={s.probe.startsWith("no lattice") ? orange[800] : lightBlue[800]}/>
|
||||
)}
|
||||
{lastImage !== undefined && (
|
||||
<Marker x={pct(lastImage)} top={-8} dashed colour={grey[800]} label="last image"/>
|
||||
)}
|
||||
{deadline !== undefined && s.cut !== 'deadline' && (
|
||||
<Marker x={pct(deadline)} top={-8} dashed colour={deepOrange[700]} label={`deadline ${deadline.toFixed(1)} s`}/>
|
||||
)}
|
||||
{s.cut && s.cut_s !== undefined && (
|
||||
<Marker x={pct(s.cut_s)} top={-8} colour={deepOrange[900]} label={`${s.cut} cut ${s.cut_s.toFixed(1)} s`}/>
|
||||
)}
|
||||
{!done && <Marker x={pct(s.elapsed_s)} top={-8} colour="#d32f2f" label="now"/>}
|
||||
{markers.map((m, i) => <Marker key={m.label} {...m} top={-8 - rows[i] * MARKER_ROW}/>)}
|
||||
|
||||
{/* latency bracket */}
|
||||
{latency !== undefined && lastImage !== undefined && (
|
||||
|
||||
Reference in new issue
Block a user