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:
leonarski_fandClaude Opus 5.5 committed 2026-10-18 12:51:58 +02:00
commit 9a382c5ef4
7 files changed
+241 -108

No files matched your search

+2 -4
View File
@@ -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>
+21
View File
@@ -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>
+33 -40
View File
@@ -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",
+28 -1
View File
@@ -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;
+69 -35
View File
@@ -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>
);
}
+87 -27
View File
@@ -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 && (