diff --git a/frontend/src/components/RugnuxHero.tsx b/frontend/src/components/RugnuxHero.tsx index b6b2f2895..f750a3429 100644 --- a/frontend/src/components/RugnuxHero.tsx +++ b/frontend/src/components/RugnuxHero.tsx @@ -47,7 +47,7 @@ function Tile({value, caption, sub, tip, tone}: {caption} @@ -115,7 +115,8 @@ function ModelTile({s, modelLoaded}: { s: rugnux_live_status, modelLoaded: boole tip="The run ended without placing the model - the steps and warnings say why."/>; } -function RugnuxHero({s, modelLoaded}: { s: rugnux_live_status, modelLoaded: boolean }) { +// column: the tiles as a rail, top to bottom, sharing the rail's height +function RugnuxHero({s, modelLoaded, column}: { s: rugnux_live_status, modelLoaded: boolean, column?: boolean }) { // the numbers are of a merge that exists: rotation - the run is over; serial - a provisional merge is in const merged = s.d_min !== undefined || (s.shells !== undefined && s.shells.length > 0); const pending: Tone = merged ? "plain" : "pending"; @@ -134,8 +135,9 @@ function RugnuxHero({s, modelLoaded}: { s: rugnux_live_status, modelLoaded: bool ? 100 * s.stills.images_indexed / s.stills.images_analysed : undefined; return ( - + *": {display: "flex", flexDirection: "column", justifyContent: "center"}} + : {gridTemplateColumns: {xs: "repeat(2, minmax(0, 1fr))", md: "repeat(4, minmax(0, 1fr))", lg: "repeat(8, minmax(0, 1fr))"}})}}> `${x.toFixed(2)} Å`)} caption="Resolution" sub={later} tone={pending} tip="High-resolution limit of the merge (d_min); the Resolution row of the result says where the CC½ fall-off is fitted."/> x.toFixed(3))} caption="CC½" sub={later ?? "all reflections"} tone={pending} diff --git a/frontend/src/components/RugnuxPage.tsx b/frontend/src/components/RugnuxPage.tsx index 0cbc46b37..f643ac638 100644 --- a/frontend/src/components/RugnuxPage.tsx +++ b/frontend/src/components/RugnuxPage.tsx @@ -18,12 +18,13 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import CheckIcon from "@mui/icons-material/Check"; import {lazy, Suspense, useEffect, useRef, useState} from "react"; -// The live page as one control-room screen: a dark stage that holds, top to bottom, the state and the two -// progress bars (the detector's and rugnux's), the hero numbers of the merge, and the stage proper - the placed -// model in Mol* on the left, the triage list and the merging statistics by shell on the right. While there is -// no model the left half shows the run's steps. Everything else - the timeline, step notes, summary rows, -// warnings, the serial pre-pass geometry - sits under a "Details" fold below the stage, and the settings -// under that, on the page's ordinary light background. +// The live page as one control-room screen: a dark stage the height of the window, edged in the colour of the +// run's phase. Across the top, the phase, the two progress bars (the detector's and rugnux's) and the clock the +// run is judged by. Below, three columns: the hero numbers of the merge as a rail at the left, the placed model +// in Mol* filling the centre (the run's steps until there is one), and at the right the triage list over the +// merging statistics by shell. Everything else - the timeline, step notes, summary rows, warnings, the serial +// pre-pass geometry - sits under a "Details" fold below the stage, and the settings under that, on the page's +// ordinary light background. // Mol* is large: loaded only when there is a model to show. const RugnuxStructureViewer = lazy(() => import("./RugnuxStructureViewer")); @@ -34,9 +35,9 @@ const liveTheme = createTheme({ mode: "dark", primary: {main: indigo[300]}, secondary: {main: lime[500]}, - background: {default: "#0c1122", paper: "#151c33"}, + background: {default: "#0b0f1d", paper: "#151c33"}, text: {primary: "#e9edf7", secondary: "#9aa5c4", disabled: "#596483"}, - divider: "rgba(154,165,196,.22)", + divider: "rgba(154,165,196,.2)", }, shape: {borderRadius: 8}, }); @@ -51,13 +52,16 @@ const pulse = keyframes` `; const runningSx = { - backgroundImage: "linear-gradient(135deg, rgba(255,255,255,.22) 25%, transparent 25%, transparent 50%, " - + "rgba(255,255,255,.22) 50%, rgba(255,255,255,.22) 75%, transparent 75%, transparent)", + backgroundImage: "linear-gradient(135deg, rgba(255,255,255,.25) 25%, transparent 25%, transparent 50%, " + + "rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 75%, transparent 75%, transparent)", backgroundSize: "40px 40px", animation: `${stripes} 1s linear infinite`, "@media (prefers-reduced-motion: reduce)": {animation: "none"}, }; +const LABEL_SX = {fontSize: "0.7rem", fontWeight: 700, letterSpacing: ".1em", textTransform: "uppercase" as const, + color: "text.secondary", whiteSpace: "nowrap" as const, overflow: "hidden", textOverflow: "ellipsis"}; + // What the run is doing, in one word, with the colour the rest of the screen takes from it. function phase(s: rugnux_live_status): { label: string, colour: string } { if (s.state === "Running") @@ -82,31 +86,30 @@ function prepassLine(st: rugnux_live_stills) { return "Pre-pass failed: the stated beam centre and mask are used"; const shift = st.beam_x_pxl !== undefined && st.beam_y_pxl !== undefined ? Math.hypot(st.beam_x_pxl - st.file_beam_x_pxl, st.beam_y_pxl - st.file_beam_y_pxl) : undefined; + const distance = st.distance_mm !== undefined ? st.distance_mm - st.file_distance_mm : undefined; return `Pre-pass on ${st.images_held} images in ${st.prepass_s?.toFixed(1)} s` + (shift !== undefined ? ` · beam centre moved ${shift.toFixed(1)} px` : "") - + (st.distance_mm !== undefined ? ` · distance ${(st.distance_mm - st.file_distance_mm) >= 0 ? "+" : ""}${(st.distance_mm - st.file_distance_mm).toFixed(2)} mm` : "") + + (distance !== undefined ? ` · distance ${distance >= 0 ? "+" : ""}${distance.toFixed(2)} mm` : "") + (st.analysis_rate_hz !== undefined ? ` · GPU analysis ${st.analysis_rate_hz.toFixed(0)} Hz` : ""); } -// A progress bar with its caption inside: fraction undefined = busy without a measure (striped, full). +// A thin progress bar under its caption line: fraction undefined = busy without a measure (striped, full). function Bar({caption, detail, fraction, colour, busy}: { caption: string, detail: string, fraction?: number, colour: string, busy: boolean }) { const width = fraction === undefined ? (busy ? 100 : 0) : 100 * Math.min(1, Math.max(0, fraction)); return ( - - - - - {caption} - - + + + {caption} + {detail} + + + ); } @@ -148,11 +151,11 @@ function Header({s, deadline_s}: { s: rugnux_live_status, deadline_s: number }) const deadline = running && lastImage !== undefined && deadline_s > 0 ? lastImage + deadline_s - s.elapsed_s : undefined; return ( - - - - + + + {p.label} @@ -160,21 +163,19 @@ function Header({s, deadline_s}: { s: rugnux_live_status, deadline_s: number }) {s.message || s.verdict_text || (s.stills ? prepassLine(s.stills) : s.probe) || "Rugnux live"} - 0 ? s.images_received / s.images_expected : undefined}/> - - - - - {clock.caption} + + 0 ? s.images_received / s.images_expected : undefined}/> + + + + + {clock.caption} + + {clock.value.toFixed(1)}s - - {clock.value.toFixed(1)}s - - + {deadline !== undefined ? `deadline in ${Math.max(0, deadline).toFixed(0)} s` : s.cut ? `analysis cut (${s.cut})${s.cut_s !== undefined ? ` at ${s.cut_s.toFixed(1)} s` : ""}` : s.start_to_first_image_s !== undefined ? `START → first image ${s.start_to_first_image_s.toFixed(1)} s` : ""} @@ -184,58 +185,61 @@ function Header({s, deadline_s}: { s: rugnux_live_status, deadline_s: number }) ); } -function Panel({title, children, sx}: { title: React.ReactNode, children: React.ReactNode, sx?: object }) { - return ( - - - {title} - - {children} - - ); -} - function Later({text}: { text: string }) { - return {text}; + return {text}; } -// The run's steps as a sequence, the running one with its progress: what fills the stage while there is no -// model to show. -function StepSequence({s}: { s: rugnux_live_status }) { +// The run's steps as a console - the running one large with its progress, the ones behind it as a list with +// their durations - filling the centre until there is a model to show. +function RunConsole({s}: { s: rugnux_live_status }) { const running = s.state === "Running"; - const row = (step: rugnux_live_step, i: number) => { - const open = step.duration_s === undefined; - return ( - - {open && running - ? - : } - - {step.title} - {step.notes.length > 0 && - {step.notes[step.notes.length - 1]}} - - - {open ? (running && s.step_progress !== undefined ? `${(100 * s.step_progress).toFixed(0)} %` : "") : `${step.duration_s!.toFixed(1)} s`} - + const current = running && s.steps.length > 0 && s.steps[s.steps.length - 1].duration_s === undefined + ? s.steps[s.steps.length - 1] : undefined; + const done = current ? s.steps.slice(0, -1) : s.steps; + const row = (step: rugnux_live_step, i: number) => ( + + + + {step.title} + {step.notes.length > 0 && + {step.notes[step.notes.length - 1]}} - ); - }; + + {step.duration_s !== undefined ? `${step.duration_s.toFixed(1)} s` : ""} + + + ); return ( - + + {current && + now + + {current.title}{s.step_progress !== undefined && {(100 * s.step_progress).toFixed(0)} %} + + {current.notes.length > 0 && {current.notes[current.notes.length - 1]}} + } {s.steps.length === 0 && } - {s.steps.map(row)} - {s.probe && + {done.map(row)} + {s.probe && Lattice probe{s.probe_s !== undefined && ` at ${s.probe_s.toFixed(1)} s`}: {s.probe} } ); } -function Stage({s}: { s: rugnux_live_status }) { +// One division of the stage: a small-caps label, then its content filling the rest +function Division({label, children, sx}: { label: React.ReactNode, children: React.ReactNode, sx?: object }) { + return ( + + {label} + {children} + + ); +} + +function Centre({s}: { s: rugnux_live_status }) { const m = s.model; // The model shows once a rotation run is over; serial data place it on each provisional merge, and the viewer // reloads when the model step has a new result, not every time the status is polled. @@ -244,23 +248,19 @@ function Stage({s}: { s: rugnux_live_status }) { ? `${m.placement} ${m.r_work} ${m.r_free} ${m.refinement?.r_free} ${s.refined_model}` : `${s.elapsed_s}`; return ( - {m.placement === "MR" ? "Molecular replacement" : "Model"}{m.predicted && " · predicted model"} - {s.refined_model && " · refined"} · {m.model_name}} sx={{flexGrow: 1}}> + {m.placement === "MR" ? "Molecular replacement" : "Model"}{m.predicted && " · predicted model"} + {s.refined_model && " · refined"} · {m.model_name}}> }> - + ); } - return ( - - - - ); + return ; } // Height of an element in px, kept up to date as the page resizes: the shell plots need theirs in pixels. @@ -281,22 +281,25 @@ function Dashboard({s, deadline_s, modelLoaded}: { s: rugnux_live_status, deadli const [plotsRef, plotsHeight] = useHeight(); const attention = s.triage?.filter(i => i.severity === "serious" || i.severity === "attention").length ?? 0; const later = s.stills ? "at the first provisional merge" : "when the run is over"; + const hairline = {borderLeft: {lg: "1px solid"}, borderColor: {lg: "divider"}, pl: {lg: 2.5}}; return ( - +
- - - - + + + + + - - Triage{attention > 0 && · {attention} need attention}} - sx={{flex: "0 1 auto", maxHeight: "45%", overflowY: "auto", "& > *": {flexShrink: 0}}}> + + *": {flexShrink: 0}}} + label={<>Triage{attention > 0 && · {attention} need attention}}> {s.triage && s.triage.length > 0 ? : } - - By resolution{s.shells_merge && ` · merge of ${s.shells_merge}`}} sx={{flex: "1 1 0"}}> + + By resolution{s.shells_merge && ` · merge of ${s.shells_merge}`}}> {s.shells && s.shells.length > 0 ? @@ -304,7 +307,7 @@ function Dashboard({s, deadline_s, modelLoaded}: { s: rugnux_live_status, deadli : } - + @@ -368,7 +371,8 @@ function RugnuxPage({measuring}: { measuring: boolean }) { return ( - {empty || !s ? diff --git a/frontend/src/components/RugnuxShellPlots.tsx b/frontend/src/components/RugnuxShellPlots.tsx index 8b93f6e0e..75193134e 100644 --- a/frontend/src/components/RugnuxShellPlots.tsx +++ b/frontend/src/components/RugnuxShellPlots.tsx @@ -30,9 +30,11 @@ function RugnuxShellPlots({shells, merge, height = 300}: { shells: rugnux_live_s const theme = useTheme(); const dark = theme.palette.mode === "dark"; const grid = dark ? "rgba(255,255,255,.12)" : "#eee"; + // type that grows with the panel, so a 4k screen does not get 11 px labels + const font = Math.round(Math.min(17, Math.max(11, height / 24))); const x = shells.map(sh => 1 / (sh.d_min * sh.d_min)); - // At most six d labels, the finest shell always among them - const step = Math.ceil(shells.length / 6); + // At most six d labels (four on a small panel), the finest shell always among them + const step = Math.ceil(shells.length / (height < 320 ? 4 : 6)); const ticks = shells.filter((_, i) => (shells.length - 1 - i) % step === 0); return ( @@ -59,16 +61,16 @@ function RugnuxShellPlots({shells, merge, height = 300}: { shells: rugnux_live_s style={{width: "100%", height}} data={data} layout={{ - title: {text: panel.title, font: {size: 14}}, + title: {text: panel.title, font: {size: font + 3}}, height, margin: {t: 32, b: 40, l: 50, r: 12}, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", - font: {color: theme.palette.text.secondary, size: 11}, + font: {color: theme.palette.text.secondary, size: font}, showlegend: data.length > 1, legend: panel.legendLeft ? {x: 0, xanchor: "left", y: 1} : {x: 1, xanchor: "right", y: 1}, xaxis: { - title: {text: "d [Å]", standoff: 4}, + title: {text: "d [Å]", standoff: 4}, tickangle: 0, gridcolor: grid, zerolinecolor: grid, tickvals: ticks.map(sh => 1 / (sh.d_min * sh.d_min)), ticktext: ticks.map(sh => sh.d_min.toFixed(2)), diff --git a/frontend/src/components/RugnuxStructureViewer.tsx b/frontend/src/components/RugnuxStructureViewer.tsx index 74c4f41d2..d85ecaa12 100644 --- a/frontend/src/components/RugnuxStructureViewer.tsx +++ b/frontend/src/components/RugnuxStructureViewer.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useRef, useState} from "react"; -import {Alert, Box, Slider, Switch, Typography} from "@mui/material"; +import {Alert, Box, IconButton, Popover, Slider, Switch, Tooltip, Typography} from "@mui/material"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import type {PluginContext} from "molstar/lib/mol-plugin/context"; import {authHeaders, noteResponse} from "../auth"; import MolstarCanvas from "./structure/MolstarCanvas"; @@ -14,22 +15,20 @@ import {Blob, BLOB_COLORS, loadBlobs, setBlobsVisible} from "./structure/loadBlo // markers (off at first) - with its 2mFo-DFc, mFo-DFc and (where the merge kept the Bijvoet pairs) anomalous // difference maps, in Mol*. Set up as aaredb's structure viewer (gitea.psi.ch/mx/aaredb, pages/StructureView.tsx // and components/Structure/, the PSI MX group's own code): its plugin, Coot mouse controls, model presets, map -// meshes, crop and carve, panel and defaults - on a black stage. Departures for the live page: every map is carved +// meshes, crop and carve, and defaults - on a black stage. Departures for the live page: every map is carved // within the same radius of the atoms (aaredb carves 2Fo-Fc only), Fo-Fc starts at +-3.5 sigma, the anomalous map -// is shown at first only where the data carry anomalous signal, and the first model makes an entrance (reveal.ts). Mol* is only loaded with this component -// (React.lazy in RugnuxPage), and every Mol* import lives in ./structure. +// is shown at first only where the data carry anomalous signal, the first model makes an entrance (reveal.ts), and +// the controls are a toolbar laid over the canvas rather than aaredb's side panel, so the molecule has the whole +// panel. Mol* is only loaded with this component (React.lazy in RugnuxPage), and every Mol* import lives in +// ./structure. type Kind = 'twofofc' | 'fofc' | 'anom'; const hex = (c: number) => '#' + c.toString(16).padStart(6, '0'); -const SectionTitle: React.FC<{ children: React.ReactNode }> = ({children}) => ( - {children} -); - -// one map row: colored dot · name · switch · σ slider + readout (aaredb StructureView.tsx:47-77) -const MapRow: React.FC<{ +// A map as a chip on the toolbar: its colour, name and level; dimmed while off. Clicking it opens the switch and +// the level slider in a popover, so the canvas keeps the space. +const MapChip: React.FC<{ label: string; dotColor: string; on: boolean; @@ -40,21 +39,48 @@ const MapRow: React.FC<{ step: number; onSigma: (v: number) => void; disabled?: boolean; -}> = ({label, dotColor, on, onToggle, sigma, min, max, step, onSigma, disabled}) => ( - - - - {label} - - {sigma.toFixed(1)} σ - onToggle(e.target.checked)}/> +}> = ({label, dotColor, on, onToggle, sigma, min, max, step, onSigma, disabled}) => { + const [anchor, setAnchor] = useState(null); + return <> + setAnchor(e.currentTarget)} disabled={disabled} active={on}> + + {label} + {sigma.toFixed(1)} σ + + setAnchor(null)} + anchorOrigin={{vertical: 'bottom', horizontal: 'right'}} transformOrigin={{vertical: 'top', horizontal: 'right'}}> + + + {label} + onToggle(e.target.checked)}/> + + onSigma(v as number)} aria-label={`${label} contour level`} + valueLabelDisplay="auto" valueLabelFormat={(v) => `${v.toFixed(1)} σ`} + sx={{color: dotColor, mt: 1}}/> + + + ; +}; + +// A toolbar item: a small pill, lit while active +const ToolButton: React.FC<{ children: React.ReactNode; active?: boolean; disabled?: boolean; title?: string; + onClick: (e: React.MouseEvent) => void }> = ({children, active, disabled, title, onClick}) => ( + + + {children} - onSigma(v as number)} aria-label={`${label} contour level`} - sx={{mx: 0.5, width: 'calc(100% - 8px)', color: dotColor, '& .MuiSlider-thumb': {width: 12, height: 12}}}/> - + ); +const MOUSE = [['left drag', 'rotate'], ['right drag', 'zoom'], ['middle drag', 'pan'], ['middle click', 'center on residue'], + ['scroll', 'clip']] as const; + async function fetchBytes(path: string, signal: AbortSignal): Promise { const res = await fetch(path, {headers: authHeaders(), signal}); noteResponse(res); @@ -74,11 +100,11 @@ const BLOB_LABELS: [string, string][] = [['LIGAND', 'ligand'], ['WATER', 'water' // resultKey: changes when a new result is there to load; anomalous: it has an anomalous difference map; // anomalousSignal: the data carry usable anomalous signal, so that map is worth showing from the start; -// refined: the broker serves the refined model and its maps; blobs: what the refined model leaves unexplained; -// fill: take the parent's height (the live page's stage) instead of the viewer's own. -function RugnuxStructureViewer({resultKey, anomalous, anomalousSignal, refined, blobs, fill}: +// blobs: what the refined model leaves unexplained; fill: take the parent's height (the live page's stage) +// instead of the viewer's own. +function RugnuxStructureViewer({resultKey, anomalous, anomalousSignal, blobs, fill}: { resultKey: string, anomalous: boolean, anomalousSignal: boolean, - refined: boolean, blobs: Blob[], fill?: boolean }) { + blobs: Blob[], fill?: boolean }) { const pluginRef = useRef(null); const [pluginReady, setPluginReady] = useState(0); const [error, setError] = useState(); @@ -219,92 +245,72 @@ function RugnuxStructureViewer({resultKey, anomalous, anomalousSignal, refined, }, []); const mapsDisabled = mapsLoading; + const [help, setHelp] = useState(null); + const blobKinds = BLOB_LABELS.filter(([k]) => blobs.some(b => b.kind === k)) + .map(([k, label]) => `${blobs.filter(b => b.kind === k).length} ${label}`).join(' · '); return ( - - - - - - {(modelLoading || (mapsLoading && !revealed)) && !error && - - {modelLoading ? 'loading model…' : 'computing density maps…'} - } + + + - - {error && {error}} - {refined ? 'Model · refined' : 'Model'} - - {(['cartoon', 'sticks', 'both'] as const).map((m) => ( - setDisplay(m, showWaters)} - onKeyDown={(e) => { if (e.key === 'Enter') setDisplay(m, showWaters); }} - sx={{fontSize: '0.8125rem', px: 1, py: '3px', borderRadius: '8px', - border: '1px solid', cursor: 'pointer', - color: reprMode === m ? 'primary.main' : 'text.secondary', - borderColor: reprMode === m ? 'primary.main' : 'divider'}} - >{m === 'both' ? 'cartoon + sticks' : m} - ))} - - - Waters - setDisplay(reprMode, e.target.checked)}/> - + {(modelLoading || (mapsLoading && !revealed)) && !error && + + {modelLoading ? 'loading model…' : 'computing density maps…'} + } + {error && {error}} - Maps - {hasMap.twofofc && toggleLayer('twofofc', v)} sigma={sigma.twofofc} - min={0.5} max={4} step={0.1} onSigma={(v) => applySigma('twofofc', v)} - disabled={mapsDisabled}/>} - {hasMap.fofc && toggleLayer('fofc', v)} sigma={sigma.fofc} - min={1} max={8} step={0.5} onSigma={(v) => applySigma('fofc', v)} - disabled={mapsDisabled}/>} - {hasMap.anom && toggleLayer('anom', v)} sigma={sigma.anom} - min={1} max={8} step={0.5} onSigma={(v) => applySigma('anom', v)} - disabled={mapsDisabled}/>} - {mapsLoading && - computing density maps…} - {!modelLoading && !mapsLoading && !hasMap.twofofc && - No maps} - - {blobs.length > 0 && <> - Unexplained blobs - - {blobs.length} in mFo−DFc - toggleBlobs(e.target.checked)}/> - - - {BLOB_LABELS.filter(([k]) => blobs.some(b => b.kind === k)).map(([k, label]) => ( - - - - {blobs.filter(b => b.kind === k).length} {label} + {/* the toolbar, over the top edge of the canvas */} + + {(['cartoon', 'sticks', 'both'] as const).map((m) => ( + setDisplay(m, showWaters)}> + {m === 'both' ? 'cartoon + sticks' : m} + + ))} + setDisplay(reprMode, !showWaters)}>waters + + {hasMap.twofofc && toggleLayer('twofofc', v)} sigma={sigma.twofofc} + min={0.5} max={4} step={0.1} onSigma={(v) => applySigma('twofofc', v)} + disabled={mapsDisabled}/>} + {hasMap.fofc && toggleLayer('fofc', v)} sigma={sigma.fofc} + min={1} max={8} step={0.5} onSigma={(v) => applySigma('fofc', v)} + disabled={mapsDisabled}/>} + {hasMap.anom && toggleLayer('anom', v)} sigma={sigma.anom} + min={1} max={8} step={0.5} onSigma={(v) => applySigma('anom', v)} + disabled={mapsDisabled}/>} + {blobs.length > 0 && + toggleBlobs(!showBlobs)} + title={`Unexplained mFo−DFc blobs: ${blobKinds}`}> + {BLOB_LABELS.filter(([k]) => blobs.some(b => b.kind === k)).map(([k]) => + )} + {blobs.length} blobs + } + setHelp(e.currentTarget)} aria-label="mouse controls" + sx={{color: 'text.secondary', bgcolor: 'rgba(8,12,26,.8)'}}> + + + setHelp(null)} + anchorOrigin={{vertical: 'bottom', horizontal: 'right'}} transformOrigin={{vertical: 'top', horizontal: 'right'}}> + + {MOUSE.map(([k, v]) => ( + + {k} + {v} ))} - } - - Controls - {([ - ['left drag', 'rotate'], - ['right drag', 'zoom'], - ['middle drag', 'pan'], - ['middle click', 'center on residue'], - ['scroll', 'clip'], - ] as const).map(([k, v]) => ( - - {k} - {v} - - ))} + + {mapsLoading && + computing density maps…} + {!modelLoading && !mapsLoading && !hasMap.twofofc && !error && + No maps} ); } diff --git a/frontend/src/components/RugnuxTriage.tsx b/frontend/src/components/RugnuxTriage.tsx index 0fea3f0e8..ca70fb9a3 100644 --- a/frontend/src/components/RugnuxTriage.tsx +++ b/frontend/src/components/RugnuxTriage.tsx @@ -4,9 +4,9 @@ import WarningIcon from "@mui/icons-material/Warning"; import ErrorIcon from "@mui/icons-material/Error"; import {rugnux_triage_item} from "../client"; -// The pathology checklist of the rugnux report (the SUMMARY of _report.txt), one line per condition: -// green - looked for and not found, orange - its warning fired, red - nothing usable came out. -// What this run could not look for is named in one grey line at the end. +// The pathology checklist of the rugnux report (the SUMMARY of _report.txt): a condition whose warning fired +// (orange) or that left nothing usable (red) gets a line with the report's words; one looked for and not found +// (green) or not looked for on this run (grey) is a badge with its name, the words on hover. const ORDER: Record = {serious: 0, attention: 1}; @@ -17,41 +17,55 @@ const ICON: Record = { }; function TriageLine({item}: { item: rugnux_triage_item }) { - const tip = ( - - {item.label}{item.text && `: ${item.text}`} - {item.detail && {item.detail}} - {item.codes.join(" ")} - - ); return ( - - + + {ICON[item.severity]} - {item.label} + {item.label} {item.text} ); } -// Problems one to a line, so their words have the width; the fine ones packed in columns. -export default function RugnuxTriage({items}: { items: rugnux_triage_item[] }) { - const problems = items.filter(i => i.severity === "serious" || i.severity === "attention") - .sort((a, b) => ORDER[a.severity] - ORDER[b.severity]); - const fine = items.filter(i => i.severity === "ok"); - const notMeasured = items.filter(i => i.severity === "not_measured"); +function tip(item: rugnux_triage_item) { return ( + + {item.label}{item.text && `: ${item.text}`} + {item.detail && {item.detail}} + {item.codes.join(" ")} + + ); +} + +// A condition as a badge: its name only, the report's words on hover +function Badge({item}: { item: rugnux_triage_item }) { + const measured = item.severity !== "not_measured"; + return ( + + + {measured && } + {item.label} + + + ); +} + +// Problems one to a line, so their words have the width; the fine ones as a row of badges, the unmeasured ones +// greyed among them. +export default function RugnuxTriage({items}: { items: rugnux_triage_item[] }) { + const problems = items.filter(i => i.severity === "serious" || i.severity === "attention") + .sort((a, b) => ORDER[a.severity] - ORDER[b.severity]); + const rest = items.filter(i => i.severity === "ok" || i.severity === "not_measured"); + return ( {problems.map(item => )} - 0 ? 0.5 : 0}}> - {fine.map(item => )} + {problems.length === 0 && Nothing needs attention} + + {rest.map(item => )} - {notMeasured.length > 0 && - - Not measured: {notMeasured.map(i => i.label).join(", ")} - } ); }