Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1314d8a
Add skeleton plot component
yousefmoazzam Sep 11, 2026
1514209
Display single JPEG image artifact
yousefmoazzam Sep 11, 2026
91f6fd4
Include tiff files in plot artifact selector
yousefmoazzam Sep 11, 2026
7ed11f1
Determine if plotting single or multi image artifact
yousefmoazzam Sep 11, 2026
3043611
Attach whole artifact object to menu item component
yousefmoazzam Sep 11, 2026
595ef91
Define tuple type for task name + artifacts array
yousefmoazzam Sep 11, 2026
ffbf808
Fix incorrect function return type
yousefmoazzam Sep 11, 2026
3f20079
Enable sliding through multi-page tiff file artifact
yousefmoazzam Sep 14, 2026
670720a
Display progress of fetching multi-page tiff artifact
yousefmoazzam Sep 14, 2026
b41d9af
Rename state to indicate generic loading operation
yousefmoazzam Sep 14, 2026
0224ab8
Display indeterminate progress when fetching multi-page tiff artifact
yousefmoazzam Sep 14, 2026
b3536fb
Move data plotter to separate component
yousefmoazzam Sep 14, 2026
f31dd52
Move displayed image index state to data plotter
yousefmoazzam Sep 14, 2026
c449388
Render data plotter when no. of total images is known
yousefmoazzam Sep 14, 2026
02da473
Render data plotter when artifact metadata is known
yousefmoazzam Sep 14, 2026
d095908
Change unnecessary else-if to else
yousefmoazzam Sep 14, 2026
e94940b
Move data loading progress indicator to separate component
yousefmoazzam Sep 14, 2026
c24dd88
Allow total images to be null to display data fetching progress indic…
yousefmoazzam Sep 14, 2026
f1faffe
Display indeterminate progress indicator when loading single image ar…
yousefmoazzam Sep 14, 2026
d0d5283
Add box with horizontally centerd content helper component
yousefmoazzam Sep 14, 2026
8eb6997
Add second hardcoded workflow for testing plot component
yousefmoazzam Sep 14, 2026
ca5fdd7
Reset plot component if session changes
yousefmoazzam Sep 14, 2026
a88241e
Remove unnecessary console logging
yousefmoazzam Sep 14, 2026
9fc1bf8
Change console log to error
yousefmoazzam Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions frontend/unified/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { WorkflowForm } from "./components/WorkflowForm";
import { DisplayLogMeta } from "./components/InspectLogMeta";
import { Beamline, Technique } from "./types";
import { ParameterConfiguration } from "./components/ParameterConfiguration/ParameterConfiguration";
import { Plot } from "./components/Plot/Plot";
import { ApolloProvider, useQuery } from "@apollo/client/react";
import { apolloClientWorkflows } from "../../src/ApolloClient";
import { gql, type TypedDocumentNode } from "@apollo/client";
Expand Down Expand Up @@ -302,10 +303,14 @@ export const App: React.FC = () => {
<Stack spacing={VERTICAL_SPACING} width="500px">
<Typography variant="h5">Plot</Typography>

<PlaceholderComponent
placeholderText="Plot component placeholder"
height={200}
width={500}
<Plot
workflowName={
beamline === Beamline.DIAD
? "example-template-599zg"
: "generate-multi-page-tiff-7mwgh"
}
visit={selectedVisit}
key={sessionName}
/>
<Divider sx={{ width: "100%" }} />
<Typography variant="h5">Log</Typography>
Expand Down
169 changes: 169 additions & 0 deletions frontend/unified/src/components/Plot/ArtifactSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { gql, TypedDocumentNode } from "@apollo/client";
import {
GetWorkflowArtifactsQuery,
GetWorkflowArtifactsQueryVariables,
} from "./__generated__/ArtifactSelector.generated";
import { useSuspenseQuery } from "@apollo/client/react";
import { Visit } from "../JobsViewer/JobsViewer";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useState } from "react";

const GET_WORKFLOW_ARTIFACTS: TypedDocumentNode<
GetWorkflowArtifactsQuery,
GetWorkflowArtifactsQueryVariables
> = gql`
query GetWorkflowArtifacts($visit: VisitInput!, $name: String!) {
workflow(visit: $visit, name: $name) {
name
status {
__typename
... on WorkflowPendingStatus {
message
}
... on WorkflowRunningStatus {
tasks {
id
name
status
stepType
artifacts {
name
url
mimeType
}
}
}
... on WorkflowSucceededStatus {
__typename
startTime
tasks {
id
name
status
stepType
artifacts {
name
url
mimeType
}
}
}
... on WorkflowFailedStatus {
tasks {
id
name
status
stepType
artifacts {
name
url
mimeType
}
}
}
... on WorkflowErroredStatus {
tasks {
id
name
status
stepType
artifacts {
name
url
mimeType
}
}
}
}
}
}
`;

type NonNullWorkflow = NonNullable<GetWorkflowArtifactsQuery["workflow"]>;
type NonNullWorkflowStatus = NonNullable<NonNullWorkflow["status"]>;
type WorkflowSucceededStatus = Extract<
NonNullWorkflowStatus,
{ __typename: "WorkflowSucceededStatus" }
>;
export type Artifact = WorkflowSucceededStatus["tasks"][0]["artifacts"][0];

type TaskNameAndArtifactTuple = [string, Artifact[]];

type ArtifactSelectorProps = {
workflowName: string;
visit: Visit;
setArtifact: (_: Artifact | null) => void;
isPlottingEnabled: boolean;
};

const IMAGE_ARTIFACT_MIME_TYPES = ["image/jpeg", "image/tiff"];

export const ArtifactSelector: React.FC<ArtifactSelectorProps> = ({
workflowName,
visit,
setArtifact,
isPlottingEnabled,
}: ArtifactSelectorProps) => {
const [selectedArtifact, setSelectedArtifact] = useState<string>("");
const { error, data } = useSuspenseQuery(GET_WORKFLOW_ARTIFACTS, {
variables: {
name: workflowName,
visit: visit,
},
});

if (error) return <p>Error: {error.message}</p>;

const generateArtifactList = (): React.ReactNode[] => {
switch (data.workflow?.status?.__typename) {
case "WorkflowSucceededStatus": {
const taskNamesAndImageArtifacts: TaskNameAndArtifactTuple[] =
data.workflow.status.tasks
.map(
(task) =>
[
task.name,
task.artifacts.filter((artifact) =>
IMAGE_ARTIFACT_MIME_TYPES.includes(artifact.mimeType)
),
] as TaskNameAndArtifactTuple
)
.filter(([_, artifacts]) => artifacts.length > 0);

return taskNamesAndImageArtifacts.map(([taskName, artifacts]) => {
return artifacts.map((artifact) => {
const label = `${taskName}: ${artifact.name}`;
return (
<MenuItem key={label} value={label} data-artifact={artifact}>
{label}
</MenuItem>
);
});
});
}
default:
console.error("Handle other workflow status cases");
return [<MenuItem>default</MenuItem>];
}
};

return (
<FormControl>
<InputLabel>Artifact</InputLabel>
<Select
disabled={!isPlottingEnabled}
onChange={(_, value) => {
if (value === null || value === undefined) {
throw Error(
"Value of selected artifact should be a component but is null or undefined"
);
}
setSelectedArtifact(value.props.value);
setArtifact(value.props["data-artifact"]);
}}
value={selectedArtifact}
children={generateArtifactList()}
/>
</FormControl>
);
};
77 changes: 77 additions & 0 deletions frontend/unified/src/components/Plot/DataLoadingProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Box, CircularProgress, Typography } from "@mui/material";

type DataLoadingProgressProps = {
mimeType: string;
loadingImageIndex: number | null;
totalImages: number | null;
};

const BoxHorizontallyCenteredContent = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{children}
</Box>
);
};

export const DataLoadingProgress: React.FC<DataLoadingProgressProps> = ({
mimeType,
loadingImageIndex,
totalImages,
}) => {
const displayDataLoadingProgress = () => {
if (mimeType === "image/jpeg") {
return (
<BoxHorizontallyCenteredContent
children={<CircularProgress enableTrackSlot size={80} />}
/>
);
}

return (
<BoxHorizontallyCenteredContent>
{loadingImageIndex !== null && totalImages !== null ? (
<>
<CircularProgress
variant="determinate"
enableTrackSlot
size={80}
value={Math.round(
(loadingImageIndex / totalImages) * 100 +
(1 / totalImages) * 100
)}
/>
<Box
sx={{
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography
variant="caption"
component="div"
>{`${loadingImageIndex + 1} / ${totalImages}`}</Typography>
</Box>
</>
) : (
<CircularProgress enableTrackSlot size={80} />
)}
</BoxHorizontallyCenteredContent>
);
};

return displayDataLoadingProgress();
};
59 changes: 59 additions & 0 deletions frontend/unified/src/components/Plot/DataPlotter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from "react";
import { Slider } from "@mui/material";
import { HeatmapPlot, NDT } from "@diamondlightsource/davidia";
import { Artifact } from "./ArtifactSelector";

type DataPlotterProps = {
artifact: Artifact;
data: NDT[];
totalImages: number;
};

export const DataPlotter: React.FC<DataPlotterProps> = ({
artifact,
data,
totalImages,
}: DataPlotterProps) => {
const [displayedImageIndex, setDisplayedImageIndex] = useState<number>(0);

const displayDataPlotter = () => {
if (artifact.mimeType === "image/jpeg") {
return (
<HeatmapPlot
domain={[0, 255]}
values={data[displayedImageIndex]}
plotConfig={{
title: "Test plot",
xLabel: "x",
yLabel: "y",
}}
/>
);
}

return (
<>
<HeatmapPlot
domain={[0, 255]}
values={data[displayedImageIndex]}
plotConfig={{
title: "Test plot",
xLabel: "x",
yLabel: "y",
}}
/>
<Slider
marks
valueLabelDisplay="auto"
step={1}
min={0}
max={totalImages - 1}
defaultValue={0}
onChange={(_, value: number) => setDisplayedImageIndex(value)}
/>
</>
);
};

return displayDataPlotter();
};
Loading
Loading