From a6c3ef459056a09e6414b2d463c2aad3809b7b94 Mon Sep 17 00:00:00 2001 From: Eugene Chen <332385214+echen-dls@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:23:45 +0000 Subject: [PATCH 1/3] Add scan metadata display --- frontend/src/Layout.tsx | 2 +- frontend/unified/src/App.tsx | 21 +++- frontend/unified/src/beamline.config.ts | 22 +++- .../src/components/MetadataDisplay.tsx | 101 ++++++++++++++++ .../components/MetadataDisplayCondensed.tsx | 108 ++++++++++++++++++ .../unified/src/components/ScanSelector.tsx | 11 +- .../src/components/ScanSelectorCombined.tsx | 39 +++++-- frontend/unified/src/components/Sidebar.tsx | 13 ++- .../WorkflowsDataFetcher.tsx | 60 ++++++---- 9 files changed, 327 insertions(+), 50 deletions(-) create mode 100644 frontend/unified/src/components/MetadataDisplay.tsx create mode 100644 frontend/unified/src/components/MetadataDisplayCondensed.tsx diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 26392f49..4910684a 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -66,7 +66,7 @@ const Layout = () => { = ({ session }: AppProps) => { - const [selectedScanIds, setSelectedScanIds] = useState([]); + const [selectedScanIds, setSelectedScanIds] = useState( + [] + ); + const [showParamConfigDrawer, setShowParamConfigDrawer] = useState(false); const [workflowsViewMode, setWorkflowsViewMode] = useState( @@ -48,7 +56,7 @@ export const App: React.FC = ({ session }: AppProps) => { }, [sessionRef]); return ( - + = ({ session }: AppProps) => { @@ -84,7 +92,7 @@ export const App: React.FC = ({ session }: AppProps) => { visit={selectedVisit} beamline={beamline} startTime={session.startTime} - scanIds={selectedScanIds} + scanIds={selectedScanIds.map((x) => x.scanNumber)} /> @@ -95,6 +103,7 @@ export const App: React.FC = ({ session }: AppProps) => { diff --git a/frontend/unified/src/beamline.config.ts b/frontend/unified/src/beamline.config.ts index a6c8f774..9398ecc0 100644 --- a/frontend/unified/src/beamline.config.ts +++ b/frontend/unified/src/beamline.config.ts @@ -138,9 +138,25 @@ const BEAMLINE_CONFIG: BeamlineConfig[] = [ scanRequest: (v) => v.data?.scan_parameters?.scan_request, components: [ { - title: "Sample name", - getter: (v) => v.data?.sample_information?.name, - formatter: FORMATTER.NONE, + groupTitle: "Experiment", + children: [ + { + title: "Sample name", + getter: (v) => v.data?.sample_information?.name, + formatter: FORMATTER.NONE, + }, + { + title: "Experiment type", + getter: (v) => { + const req = v?.scanRequest?.processingRequest?.request; + if (req) { + return Object.keys(req); + } + return null; + }, + formatter: (v) => v.join(", "), + }, + ], }, { groupTitle: "Times", diff --git a/frontend/unified/src/components/MetadataDisplay.tsx b/frontend/unified/src/components/MetadataDisplay.tsx new file mode 100644 index 00000000..a376f811 --- /dev/null +++ b/frontend/unified/src/components/MetadataDisplay.tsx @@ -0,0 +1,101 @@ +import { Typography, Stack, Skeleton } from "@mui/material"; +import type { ScanNodeProcessed } from "../types"; +import { getBeamlineConfig, isMetadataComponent } from "../beamline.config"; + +type MetadataDisplayProp = { + selectedScanData: ScanNodeProcessed[]; +}; +export function MetadataDisplay({ selectedScanData }: MetadataDisplayProp) { + // const [schemaJSON, setSchemaJSON] = useState<{[key: string]: any} | null>(null); + + console.log("scanNumber HubScanData ", selectedScanData); + const firstScan = selectedScanData.at(0); + + if (!firstScan) { + return Select a scan; + } + + const beamline = firstScan.beamline?.toLowerCase(); + const schema = firstScan.dataSchema; + + const config = getBeamlineConfig(schema); + const title = makeTitle(selectedScanData); + const multipleSelected = selectedScanData.length > 1; + + console.log("config", config); + + return ( + + {!title ? : title} + + + {config && + firstScan && + beamline && + !multipleSelected && + config.components.map((x) => { + if (isMetadataComponent(x)) { + const val = x.getter(firstScan); + return ( + + ); + } else { + return ( + // Draw containers + + {x.children.map((y) => { + const val = y.getter(firstScan); + return ( + + ); + })} + + ); + } + })} + + + ); +} + +type SingleMetadataProp = { + label: string; + value: string; +}; +export function SingleMetadata({ label, value }: SingleMetadataProp) { + return ( + + + {label} + + + {value} + + + ); +} + +function makeTitle(scanData: ScanNodeProcessed[]): string | null { + if (scanData.length === 0) { + return null; + } else if (scanData.length === 1) { + const scanNumber = scanData.at(0)?.scanNumber; + if (scanNumber) { + return `${scanNumber}`; + } else { + return null; + } + } else { + return `Selected ${scanData.length} files`; + } +} diff --git a/frontend/unified/src/components/MetadataDisplayCondensed.tsx b/frontend/unified/src/components/MetadataDisplayCondensed.tsx new file mode 100644 index 00000000..cc632170 --- /dev/null +++ b/frontend/unified/src/components/MetadataDisplayCondensed.tsx @@ -0,0 +1,108 @@ +import { Typography, Stack, Skeleton, Grid } from "@mui/material"; +import type { ScanNodeProcessed } from "../types"; +import { getBeamlineConfig, isMetadataComponent } from "../beamline.config"; + +type MetadataDisplayProp = { + selectedScanData: ScanNodeProcessed[]; +}; +export function MetadataDisplayCondensed({ + selectedScanData, +}: MetadataDisplayProp) { + // const [schemaJSON, setSchemaJSON] = useState<{[key: string]: any} | null>(null); + + console.log("scanNumber HubScanData ", selectedScanData); + const firstScan = selectedScanData.at(0); + + if (!firstScan) { + return Select a scan; + } + + const beamline = firstScan.beamline?.toLowerCase(); + const schema = firstScan.dataSchema; + + const config = getBeamlineConfig(schema); + const title = makeTitle(selectedScanData); + const multipleSelected = selectedScanData.length > 1; + + console.log("config", config); + + return ( + + {!title ? : title} + + + {config && firstScan && beamline && !multipleSelected ? ( + // TODO: Handle case where `beamline` doesn't exist in enum + config.components.slice(0, 2).map((x) => { + if (isMetadataComponent(x)) { + const val = x.getter(firstScan); + return ( + + + + ); + } else { + return ( + // Draw containers + + {x.children.map((y) => { + const val = y.getter(firstScan); + return ( + + + + ); + })} + + ); + } + }) + ) : ( + <> + )} + + + ); +} + +type SingleMetadataProp = { + label: string; + value: string; +}; +export function SingleMetadata({ label, value }: SingleMetadataProp) { + return ( + + + {label} + + + {value} + + + ); +} + +function makeTitle(scanData: ScanNodeProcessed[]): string | null { + if (scanData.length === 0) { + return null; + } else if (scanData.length === 1) { + const scanNumber = scanData.at(0)?.scanNumber; + if (scanNumber) { + return `${scanNumber}`; + } else { + return null; + } + } else { + return `Selected ${scanData.length} files`; + } +} diff --git a/frontend/unified/src/components/ScanSelector.tsx b/frontend/unified/src/components/ScanSelector.tsx index 6a44729e..4b8095f3 100644 --- a/frontend/unified/src/components/ScanSelector.tsx +++ b/frontend/unified/src/components/ScanSelector.tsx @@ -44,9 +44,9 @@ const columns = [ type ScanSelectorProp = { sessionRef: string; - onScanSelect: (scans: ScanNodeProcessed[]) => void; + updateScanData: (scans: ScanNodeProcessed[]) => void; }; -export function ScanSelector({ sessionRef, onScanSelect }: ScanSelectorProp) { +export function ScanSelector({ sessionRef, updateScanData }: ScanSelectorProp) { const tableContainerRef = useRef(null); //we can get access to the underlying TableContainer element and react to its scroll events const FETCH_SIZE = 30; @@ -218,7 +218,7 @@ export function ScanSelector({ sessionRef, onScanSelect }: ScanSelectorProp) { const selectedData = selectedIdx .filter((x) => rowSelection[x]) .map((x) => table.getRow(x).original); - onScanSelect(selectedData); + updateScanData(selectedData); // `onScanSelect` should not be placed into dependency list as it causes an infinite loop }, [table.getState().rowSelection]); @@ -244,5 +244,10 @@ export function ScanSelector({ sessionRef, onScanSelect }: ScanSelectorProp) { prevDesc.current = isDescSort(sorting); }, [sorting, client, fetchMore, sessionRef]); + // Clear row selection when session ref changes + useEffect(() => { + setRowSelection({}); + }, [sessionRef]); + return ; } diff --git a/frontend/unified/src/components/ScanSelectorCombined.tsx b/frontend/unified/src/components/ScanSelectorCombined.tsx index 2ea49883..f9167163 100644 --- a/frontend/unified/src/components/ScanSelectorCombined.tsx +++ b/frontend/unified/src/components/ScanSelectorCombined.tsx @@ -2,16 +2,17 @@ import { Stack, ToggleButtonGroup, ToggleButton } from "@mui/material"; import { useState } from "react"; import { ScanSelector } from "./ScanSelector"; import { ScanSelector as ScanSelectorLegacy } from "./ScanSelectorLegacy"; +import { ScanNodeProcessed } from "../types"; type ScanSelectorCombinedProps = { sessionRef: string; - scanIds: number[]; - setScanIds: (v: number[]) => void; + scanData: ScanNodeProcessed[]; + updateScanData: (v: ScanNodeProcessed[]) => void; }; export const ScanSelectorCombined = ({ sessionRef, - scanIds, - setScanIds, + scanData, + updateScanData, }: ScanSelectorCombinedProps) => { const [selectorType, setSelectorType] = useState("legacy"); @@ -35,13 +36,33 @@ export const ScanSelectorCombined = ({ {selectorType === "new" ? ( - setScanIds(v.map((x) => x.scanNumber))} - sessionRef={sessionRef} - /> + ) : ( - + x.scanNumber)} + setScanIds={(v) => updateScanData(generateScanData(v))} + /> )} ); }; + +// Generates dummy detailed scan data when using the legacy +// scan selector +const generateScanData = (scanIds: number[]): ScanNodeProcessed[] => { + return scanIds.map((x) => { + return { + __typename: "Dataset", + scanNumber: x, + dataSchema: "", + description: null, + id: -1, + title: "Title", + beamline: null, + startTime: null, + endTime: null, + scanRequest: null, + data: {}, + }; + }); +}; diff --git a/frontend/unified/src/components/Sidebar.tsx b/frontend/unified/src/components/Sidebar.tsx index 817b3e63..b6339e8a 100644 --- a/frontend/unified/src/components/Sidebar.tsx +++ b/frontend/unified/src/components/Sidebar.tsx @@ -1,16 +1,17 @@ import { Navbar } from "@diamondlightsource/sci-react-ui"; import { Box, Typography, Stack } from "@mui/material"; import { ScanSelectorCombined } from "./ScanSelectorCombined"; +import { ScanNodeProcessed } from "../types"; type SidebarProps = { - scanIds: number[]; - updateSelectedScanIds: (_: number[]) => void; + scanData: ScanNodeProcessed[]; + updateScanData: (_: ScanNodeProcessed[]) => void; sessionRef: string; }; export const Sidebar: React.FC = ({ - scanIds, - updateSelectedScanIds, + scanData, + updateScanData, sessionRef, }: SidebarProps) => { return ( @@ -37,8 +38,8 @@ export const Sidebar: React.FC = ({ This is to ensure infinite scroll works correctly. */} diff --git a/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx b/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx index 322c4de9..362292aa 100644 --- a/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx +++ b/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx @@ -5,20 +5,25 @@ import { Accordion, AccordionDetails, AccordionSummary, + Box, Stack, Typography, } from "@mui/material"; import { ChevronDown } from "lucide-react"; import WorkflowDataViewer from "./WorkflowDataViewer"; import { WorkflowsList } from "./WorkflowsList/WorkflowsList"; -import { WorkflowsViewMode } from "../../types"; +import { ScanNodeProcessed, WorkflowsViewMode } from "../../types"; +import { MetadataDisplay } from "../MetadataDisplay"; +import { MetadataDisplayCondensed } from "../MetadataDisplayCondensed"; const WorkflowsDataFetcher = ({ visit, workflowsViewMode, + scanData, }: { visit: Visit; workflowsViewMode: WorkflowsViewMode; + scanData: ScanNodeProcessed[]; }) => { const [selectedWorkflow, setSelectedWorkflow] = useState(null); @@ -26,33 +31,44 @@ const WorkflowsDataFetcher = ({ <> {workflowsViewMode === WorkflowsViewMode.Table ? ( - - - }> - Jobs - - - + + + + + + + }> + Jobs + + + + + + {selectedWorkflow && ( + - - - {selectedWorkflow && ( - - )} + )} + ) : ( - + + + + + + {selectedWorkflow && ( Date: Fri, 30 Oct 2026 15:01:21 +0000 Subject: [PATCH 2/3] Fix app scrolling --- frontend/unified/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/unified/src/App.tsx b/frontend/unified/src/App.tsx index c8feaf61..d70b58f2 100644 --- a/frontend/unified/src/App.tsx +++ b/frontend/unified/src/App.tsx @@ -56,7 +56,7 @@ export const App: React.FC = ({ session }: AppProps) => { }, [sessionRef]); return ( - + = ({ session }: AppProps) => { - + Date: Fri, 30 Oct 2026 15:44:37 +0000 Subject: [PATCH 3/3] Combine the normal and condensed metadata display components --- .../src/components/MetadataDisplay.tsx | 66 +++++++---- .../components/MetadataDisplayCondensed.tsx | 108 ------------------ .../WorkflowsDataFetcher.tsx | 3 +- 3 files changed, 42 insertions(+), 135 deletions(-) delete mode 100644 frontend/unified/src/components/MetadataDisplayCondensed.tsx diff --git a/frontend/unified/src/components/MetadataDisplay.tsx b/frontend/unified/src/components/MetadataDisplay.tsx index a376f811..db838ccf 100644 --- a/frontend/unified/src/components/MetadataDisplay.tsx +++ b/frontend/unified/src/components/MetadataDisplay.tsx @@ -1,65 +1,81 @@ -import { Typography, Stack, Skeleton } from "@mui/material"; +import { Typography, Stack, Skeleton, Grid } from "@mui/material"; import type { ScanNodeProcessed } from "../types"; import { getBeamlineConfig, isMetadataComponent } from "../beamline.config"; type MetadataDisplayProp = { selectedScanData: ScanNodeProcessed[]; + condensed?: boolean; }; -export function MetadataDisplay({ selectedScanData }: MetadataDisplayProp) { - // const [schemaJSON, setSchemaJSON] = useState<{[key: string]: any} | null>(null); - - console.log("scanNumber HubScanData ", selectedScanData); +export function MetadataDisplay({ + selectedScanData, + condensed, +}: MetadataDisplayProp) { const firstScan = selectedScanData.at(0); if (!firstScan) { - return Select a scan; + return No scans selected; } - const beamline = firstScan.beamline?.toLowerCase(); const schema = firstScan.dataSchema; const config = getBeamlineConfig(schema); const title = makeTitle(selectedScanData); const multipleSelected = selectedScanData.length > 1; - console.log("config", config); + let maxColumns = 1; + let components = config?.components; + if (condensed) { + // If condensed, limit the data display to the first two groups, + // and place each group into its own row + const columns = config?.components.map((x) => { + if (!isMetadataComponent(x)) { + return x.children.length; + } else { + return 1; + } + }); + maxColumns = columns ? Math.max(...columns) : 1; + components = components?.slice(0, 2); + } return ( {!title ? : title} - - - {config && + + {components && firstScan && - beamline && !multipleSelected && - config.components.map((x) => { + components.map((x) => { if (isMetadataComponent(x)) { const val = x.getter(firstScan); return ( - + + + ); } else { return ( // Draw containers - + {x.children.map((y) => { const val = y.getter(firstScan); return ( - + + + ); })} - + ); } })} - + ); } @@ -85,7 +101,7 @@ export function SingleMetadata({ label, value }: SingleMetadataProp) { ); } -function makeTitle(scanData: ScanNodeProcessed[]): string | null { +export function makeTitle(scanData: ScanNodeProcessed[]): string | null { if (scanData.length === 0) { return null; } else if (scanData.length === 1) { diff --git a/frontend/unified/src/components/MetadataDisplayCondensed.tsx b/frontend/unified/src/components/MetadataDisplayCondensed.tsx deleted file mode 100644 index cc632170..00000000 --- a/frontend/unified/src/components/MetadataDisplayCondensed.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Typography, Stack, Skeleton, Grid } from "@mui/material"; -import type { ScanNodeProcessed } from "../types"; -import { getBeamlineConfig, isMetadataComponent } from "../beamline.config"; - -type MetadataDisplayProp = { - selectedScanData: ScanNodeProcessed[]; -}; -export function MetadataDisplayCondensed({ - selectedScanData, -}: MetadataDisplayProp) { - // const [schemaJSON, setSchemaJSON] = useState<{[key: string]: any} | null>(null); - - console.log("scanNumber HubScanData ", selectedScanData); - const firstScan = selectedScanData.at(0); - - if (!firstScan) { - return Select a scan; - } - - const beamline = firstScan.beamline?.toLowerCase(); - const schema = firstScan.dataSchema; - - const config = getBeamlineConfig(schema); - const title = makeTitle(selectedScanData); - const multipleSelected = selectedScanData.length > 1; - - console.log("config", config); - - return ( - - {!title ? : title} - - - {config && firstScan && beamline && !multipleSelected ? ( - // TODO: Handle case where `beamline` doesn't exist in enum - config.components.slice(0, 2).map((x) => { - if (isMetadataComponent(x)) { - const val = x.getter(firstScan); - return ( - - - - ); - } else { - return ( - // Draw containers - - {x.children.map((y) => { - const val = y.getter(firstScan); - return ( - - - - ); - })} - - ); - } - }) - ) : ( - <> - )} - - - ); -} - -type SingleMetadataProp = { - label: string; - value: string; -}; -export function SingleMetadata({ label, value }: SingleMetadataProp) { - return ( - - - {label} - - - {value} - - - ); -} - -function makeTitle(scanData: ScanNodeProcessed[]): string | null { - if (scanData.length === 0) { - return null; - } else if (scanData.length === 1) { - const scanNumber = scanData.at(0)?.scanNumber; - if (scanNumber) { - return `${scanNumber}`; - } else { - return null; - } - } else { - return `Selected ${scanData.length} files`; - } -} diff --git a/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx b/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx index 362292aa..e1f8205a 100644 --- a/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx +++ b/frontend/unified/src/components/WorkflowsDataFetcher/WorkflowsDataFetcher.tsx @@ -14,7 +14,6 @@ import WorkflowDataViewer from "./WorkflowDataViewer"; import { WorkflowsList } from "./WorkflowsList/WorkflowsList"; import { ScanNodeProcessed, WorkflowsViewMode } from "../../types"; import { MetadataDisplay } from "../MetadataDisplay"; -import { MetadataDisplayCondensed } from "../MetadataDisplayCondensed"; const WorkflowsDataFetcher = ({ visit, @@ -61,7 +60,7 @@ const WorkflowsDataFetcher = ({ - +