diff --git a/frontend/unified/src/App.tsx b/frontend/unified/src/App.tsx index dde8a391..64c941f4 100644 --- a/frontend/unified/src/App.tsx +++ b/frontend/unified/src/App.tsx @@ -24,10 +24,8 @@ import JobsViewer from "./components/JobsViewer/JobsViewer"; import { templateOptions } from "./data/templates"; 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 { apolloClientWorkflows } from "../../src/ApolloClient"; import { useAuth } from "@diamondlightsource/sci-react-ui"; import { @@ -123,7 +121,6 @@ export const App: React.FC = () => { null ); const [selectedScanIds, setSelectedScanIds] = useState([]); - const [selectedWorkflow, setSelectedWorkflow] = useState(null); const { loading, error, data } = useQuery(SESSION_QUERY, { variables: { username: useFedid() }, }); @@ -358,51 +355,7 @@ export const App: React.FC = () => { - - - - }> - Jobs - - - - - - - - }> - Log - - - {selectedWorkflow !== null ? ( - - ) : ( -

No workflow selected

- )} -
-
- - - }> - Plot - - - - - -
+ diff --git a/frontend/unified/src/components/InspectLogMeta.tsx b/frontend/unified/src/components/InspectLogMeta.tsx deleted file mode 100644 index 8960903b..00000000 --- a/frontend/unified/src/components/InspectLogMeta.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { FC } from "react"; -import { - Button, - Stack, - Select, - SelectChangeEvent, - MenuItem, - Box, - FormControl, - InputLabel, -} from "@mui/material"; - -import { useQuery } from "@apollo/client/react"; -import { gql, type TypedDocumentNode } from "@apollo/client"; - -import { - LogQueryQuery, - LogQueryQueryVariables, -} from "./__generated__/InspectLogMeta.generated"; -import { Visit } from "@diamondlightsource/sci-react-ui"; - -export const InspectLog_Query: TypedDocumentNode< - LogQueryQuery, - LogQueryQueryVariables -> = gql` - query logQuery($visitobj: VisitInput!, $name: String!) { - workflow(visit: $visitobj, name: $name) { - name - id - status { - __typename - ... on WorkflowSucceededStatus { - message - startTime - tasks { - name - artifacts { - name - url - mimeType - } - } - } - ... on WorkflowErroredStatus { - message - tasks { - name - artifacts { - name - url - mimeType - } - } - } - ... on WorkflowFailedStatus { - message - tasks { - name - artifacts { - name - url - mimeType - } - } - } - } - } - } -`; - -type DisplayLogMetaProps = { - visit: Visit; - workflowName: string; -}; - -export const DisplayLogMeta: FC = (props: { - visit: Visit; - workflowName: string; -}) => { - const { loading, error, data } = useQuery(InspectLog_Query, { - variables: { visitobj: props.visit, name: props.workflowName }, - }); - - const artifactUrl: string[] = []; - const LogFileTuples: string[] = ["No log Selected"]; - const [selectLogState, setSelectLogState] = React.useState("No log Selected"); - const [currentUrl, setCurrenturl] = React.useState(""); - const [indexValue, setindexValue] = React.useState(-1); - const handleSelectState = (event: SelectChangeEvent) => { - setindexValue(LogFileTuples.indexOf(event.target.value)); - setSelectLogState(event.target.value as string); - if (selectLogState === undefined) { - setSelectLogState("No log Selected"); - setindexValue(0); - } - setCurrenturl(artifactUrl[indexValue]); - }; - - function reset() { - if (indexValue === -1) { - setSelectLogState(LogFileTuples[0]); - } else { - setSelectLogState(LogFileTuples[indexValue]); - } - } - - React.useEffect(reset, [props.workflowName]); - - if (data === undefined) { - return

Data undefined

; - } - - const openInNewTab = (url: string) => { - if (url !== "undefined") { - const w = window.open(url, "_blank"); - w?.focus(); - } - }; - - function processLogData(data: LogQueryQuery) { - const procData: string[] = []; - let tasknum: number = 0; - data.workflow.status.tasks.forEach((task: any) => { - let artnum: number = 0; - task.artifacts.forEach((artifact: any) => { - if (artifact.mimeType == "text/plain") { - artifactUrl.push(artifact.url); - let tmp: string = tasknum + "-" + artnum + " " + task.name + ".log"; - procData.push(tmp); - tasknum = tasknum + 1; - artnum = artnum + 1; - } - }); - }); - return procData; - } - - if ( - data.workflow?.status?.__typename == "WorkflowSucceededStatus" || - data.workflow?.status?.__typename == "WorkflowErroredStatus" || - data.workflow?.status?.__typename == "WorkflowFailedStatus" - ) { - let procData = processLogData(data); - procData.forEach((tmp) => { - LogFileTuples.push(tmp); - }); - } - - return ( - - - - Select a Log - {LogFileTuples[0] !== undefined ? ( - - ) : ( -

No logs

- )} -
-
- -
- ); -}; -export default DisplayLogMeta; diff --git a/frontend/unified/src/components/JobsViewer/InspectLogMeta.tsx b/frontend/unified/src/components/JobsViewer/InspectLogMeta.tsx new file mode 100644 index 00000000..95ffd925 --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/InspectLogMeta.tsx @@ -0,0 +1,160 @@ +import { useState } from "react"; +import { + Button, + Stack, + Select, + MenuItem, + Box, + FormControl, + InputLabel, +} from "@mui/material"; + +import { useSuspenseFragment } from "@apollo/client/react"; +import { gql, type TypedDocumentNode } from "@apollo/client"; + +import { InspectLogMetaFragmentFragment } from "./__generated__/InspectLogMeta.generated"; + +export const INSPECTLOGMETA_FRAGMENT: TypedDocumentNode = gql` + fragment InspectLogMetaFragment on Workflow { + status { + __typename + ... on WorkflowSucceededStatus { + startTime + message + tasks { + name + artifacts { + name + url + mimeType + } + } + } + ... on WorkflowErroredStatus { + startTime + message + tasks { + name + artifacts { + name + url + mimeType + } + } + } + ... on WorkflowFailedStatus { + startTime + message + tasks { + name + artifacts { + name + url + mimeType + } + } + } + } + } +`; + +type Artifact = { + name: string; + url: unknown; + mimeType: string; +}; + +type Task = { + name: string; + artifacts: Artifact[]; +}; + +const getTasks = (data: InspectLogMetaFragmentFragment): Task[] => { + if ( + data.status && + (data.status.__typename === "WorkflowSucceededStatus" || + data.status.__typename === "WorkflowErroredStatus" || + data.status.__typename === "WorkflowFailedStatus") + ) { + return data.status.tasks; + } + return []; +}; + +const InspectLogMeta = ({ + queryData, +}: { + queryData: InspectLogMetaFragmentFragment; +}) => { + const [logUrl, setLogUrl] = useState(null); + const { data } = useSuspenseFragment({ + fragment: INSPECTLOGMETA_FRAGMENT, + fragmentName: "InspectLogMetaFragment", + from: queryData, + }); + + if (!data) return <>No Data; + + const artifacts = getTasks(data).flatMap((task, taskIndex) => { + const filteredArtifacts = task.artifacts.filter( + (artifact) => artifact.mimeType === "text/plain" + ); + return filteredArtifacts.map((a, idx) => { + return { + url: a.url, + label: `${taskIndex}-${idx} ${task.name}.log`, + }; + }); + }); + + if (data === undefined) { + return

Data undefined

; + } + + const openInNewTab = (url: string) => { + if (url !== "undefined") { + const w = window.open(url, "_blank"); + w?.focus(); + } + }; + + return ( + + + + Select a Log + + + + + + ); +}; +export default InspectLogMeta; diff --git a/frontend/unified/src/components/JobsViewer/JobDataViewer.tsx b/frontend/unified/src/components/JobsViewer/JobDataViewer.tsx new file mode 100644 index 00000000..2da8c3d7 --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/JobDataViewer.tsx @@ -0,0 +1,96 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Typography, +} from "@mui/material"; +import { Visit } from "@diamondlightsource/sci-react-ui"; +import LogStreamer, { LOGSTREAMER_FRAGMENT } from "./LogStreamer/LogStreamer"; +import { Plot } from "./Plot/Plot"; +import { gql, TypedDocumentNode } from "@apollo/client"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { + JobDataViewerQueryQuery, + JobDataViewerQueryQueryVariables, +} from "./__generated__/JobDataViewer.generated"; +import InspectLogMeta, { INSPECTLOGMETA_FRAGMENT } from "./InspectLogMeta"; +import { ChevronDown } from "lucide-react"; +import { ARTIFACTSELECTOR_FRAGMENT } from "./Plot/ArtifactSelector"; + +const JOBDATAVIEWER_QUERY: TypedDocumentNode< + JobDataViewerQueryQuery, + JobDataViewerQueryQueryVariables +> = gql` + query JobDataViewerQuery($visit: VisitInput!, $name: String!) { + workflow(visit: $visit, name: $name) { + name + id + ...LogStreamerFragment + ...InspectLogMetaFragment + ...ArtifactSelectorFragment + } + } + ${LOGSTREAMER_FRAGMENT} + ${INSPECTLOGMETA_FRAGMENT} + ${ARTIFACTSELECTOR_FRAGMENT} +`; + +const JobDataViewer = ({ + visit, + selectedWorkflow, +}: { + visit: Visit; + selectedWorkflow: string; +}) => { + const { data, error } = useSuspenseQuery(JOBDATAVIEWER_QUERY, { + variables: { + visit: visit, + name: selectedWorkflow, + }, + }); + + if (error) return <>Error: {error.message}; + if (!data) return <>No data; + if (!data.workflow) return <>No workflow data; + + return ( + <> + + }> + Log + + + + + + + + }> + Log Stream + + + + + + + + }> + Plot + + + + + + + ); +}; + +export default JobDataViewer; diff --git a/frontend/unified/src/components/JobsViewer/BaseTableRow.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/BaseTableRow.tsx similarity index 92% rename from frontend/unified/src/components/JobsViewer/BaseTableRow.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/BaseTableRow.tsx index 0151187e..ad6d1b4e 100644 --- a/frontend/unified/src/components/JobsViewer/BaseTableRow.tsx +++ b/frontend/unified/src/components/JobsViewer/JobsTable/BaseTableRow.tsx @@ -5,7 +5,7 @@ import { useFragment } from "@apollo/client/react"; import { BaseTableRowFragmentFragment } from "./__generated__/BaseTableRow.generated"; import { getWorkflowStatusIcon } from "./StatusIcons"; import { DeepPartial } from "@apollo/client/utilities"; -import { TableRowRelayFragmentFragment } from "./__generated__/TableRowRelay.generated"; +import { TableRowWrapperFragmentFragment } from "./__generated__/TableRowWrapper.generated"; import { JobTableRow } from "./JobTableRow"; export const BASETABLEROW_FRAGMENT = gql` @@ -26,7 +26,8 @@ const BaseTableRow = ({ setSelectedWorkflow, }: { queryData: - TableRowRelayFragmentFragment | DeepPartial; + | TableRowWrapperFragmentFragment + | DeepPartial; selectedWorkflow: string | null; setSelectedWorkflow: (_: string | null) => void; }) => { diff --git a/frontend/unified/src/components/JobsViewer/JobTableRow.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/JobTableRow.tsx similarity index 100% rename from frontend/unified/src/components/JobsViewer/JobTableRow.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/JobTableRow.tsx diff --git a/frontend/unified/src/components/JobsViewer/JobsTable.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/JobsTable.tsx similarity index 94% rename from frontend/unified/src/components/JobsViewer/JobsTable.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/JobsTable.tsx index 0a3d5f74..dd95412a 100644 --- a/frontend/unified/src/components/JobsViewer/JobsTable.tsx +++ b/frontend/unified/src/components/JobsViewer/JobsTable/JobsTable.tsx @@ -2,9 +2,8 @@ import { WorkflowsQueryQuery, WorkflowsQueryQueryVariables, } from "./__generated__/JobsTable.generated"; -import { Suspense, useCallback, useState, useEffect } from "react"; - -import { Visit } from "./JobsViewer"; +import { Suspense, useCallback, useState } from "react"; +import { Visit } from "@diamondlightsource/sci-react-ui"; import TableContent, { TABLECONTENT_FRAGMENT } from "./TableContent"; import { gql, TypedDocumentNode } from "@apollo/client"; import { useQuery } from "@apollo/client/react"; diff --git a/frontend/unified/src/components/JobsViewer/LiveTableRow.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/LiveTableRow.tsx similarity index 96% rename from frontend/unified/src/components/JobsViewer/LiveTableRow.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/LiveTableRow.tsx index 5d74e1c0..e94b6138 100644 --- a/frontend/unified/src/components/JobsViewer/LiveTableRow.tsx +++ b/frontend/unified/src/components/JobsViewer/JobsTable/LiveTableRow.tsx @@ -2,7 +2,7 @@ import { gql, TypedDocumentNode } from "@apollo/client"; import BaseTableRow, { BASETABLEROW_FRAGMENT } from "./BaseTableRow"; import { useState } from "react"; import { BaseTableRowFragmentFragment } from "./__generated__/BaseTableRow.generated"; -import { apolloClientWorkflows } from "../../../../src/ApolloClient"; +import { apolloClientWorkflows } from "../../../../../src/ApolloClient"; import { LiveTableRowSubscriptionSubscription, LiveTableRowSubscriptionSubscriptionVariables, diff --git a/frontend/unified/src/components/JobsViewer/StatusIcons.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/StatusIcons.tsx similarity index 100% rename from frontend/unified/src/components/JobsViewer/StatusIcons.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/StatusIcons.tsx diff --git a/frontend/unified/src/components/JobsViewer/TableContent.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/TableContent.tsx similarity index 98% rename from frontend/unified/src/components/JobsViewer/TableContent.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/TableContent.tsx index 9a72b21f..7eb58d29 100644 --- a/frontend/unified/src/components/JobsViewer/TableContent.tsx +++ b/frontend/unified/src/components/JobsViewer/JobsTable/TableContent.tsx @@ -12,7 +12,7 @@ import { } from "@mui/material"; import { useFragment } from "@apollo/client/react"; import { TableContentFragmentFragment } from "./__generated__/TableContent.generated"; -import { ChangeEvent, MouseEvent, useState } from "react"; +import { ChangeEvent, MouseEvent } from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { gql, TypedDocumentNode } from "@apollo/client"; import TableRowWrapper, { TABLEROWWRAPPER_FRAGMENT } from "./TableRowWrapper"; diff --git a/frontend/unified/src/components/JobsViewer/TableRowWrapper.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/TableRowWrapper.tsx similarity index 100% rename from frontend/unified/src/components/JobsViewer/TableRowWrapper.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/TableRowWrapper.tsx diff --git a/frontend/unified/src/components/JobsViewer/utils/BaseStack.tsx b/frontend/unified/src/components/JobsViewer/JobsTable/utils/BaseStack.tsx similarity index 100% rename from frontend/unified/src/components/JobsViewer/utils/BaseStack.tsx rename to frontend/unified/src/components/JobsViewer/JobsTable/utils/BaseStack.tsx diff --git a/frontend/unified/src/components/JobsViewer/utils/types.ts b/frontend/unified/src/components/JobsViewer/JobsTable/utils/types.ts similarity index 100% rename from frontend/unified/src/components/JobsViewer/utils/types.ts rename to frontend/unified/src/components/JobsViewer/JobsTable/utils/types.ts diff --git a/frontend/unified/src/components/JobsViewer/utils/utils.ts b/frontend/unified/src/components/JobsViewer/JobsTable/utils/utils.ts similarity index 100% rename from frontend/unified/src/components/JobsViewer/utils/utils.ts rename to frontend/unified/src/components/JobsViewer/JobsTable/utils/utils.ts diff --git a/frontend/unified/src/components/JobsViewer/JobsViewer.tsx b/frontend/unified/src/components/JobsViewer/JobsViewer.tsx index 2f34ec21..6ed18b97 100644 --- a/frontend/unified/src/components/JobsViewer/JobsViewer.tsx +++ b/frontend/unified/src/components/JobsViewer/JobsViewer.tsx @@ -1,29 +1,40 @@ -import { Suspense } from "react"; -import JobsTable from "./JobsTable"; +import { Suspense, useState } from "react"; +import JobsTable from "./JobsTable/JobsTable"; +import { Visit } from "@diamondlightsource/sci-react-ui"; +import JobDataViewer from "./JobDataViewer"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Stack, + Typography, +} from "@mui/material"; +import { ChevronDown } from "lucide-react"; -export type Visit = { - proposalCode: string; - proposalNumber: number; - number: number; -}; - -const JobsViewer = ({ - visit, - setSelectedWorkflow, - selectedWorkflow, -}: { - visit: Visit; - selectedWorkflow: string | null; - setSelectedWorkflow: (_: string | null) => void; -}) => { +const JobsViewer = ({ visit }: { visit: Visit }) => { + const [selectedWorkflow, setSelectedWorkflow] = useState(null); return ( - - - + + + + }> + Jobs + + + + + + + {selectedWorkflow && ( + + + + )} + ); }; diff --git a/frontend/unified/src/components/JobsViewer/LogStreamer/LogLine.tsx b/frontend/unified/src/components/JobsViewer/LogStreamer/LogLine.tsx new file mode 100644 index 00000000..59f676f6 --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/LogStreamer/LogLine.tsx @@ -0,0 +1,6 @@ +import { styled, Typography } from "@mui/material"; + +export const LogLine = styled(Typography)(() => ({ + fontFamily: "monospace", + fontSize: "12px", +})); diff --git a/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamContent.tsx b/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamContent.tsx new file mode 100644 index 00000000..dca75f9d --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamContent.tsx @@ -0,0 +1,162 @@ +import { gql, TypedDocumentNode } from "@apollo/client"; +import { Visit } from "@diamondlightsource/sci-react-ui"; +import { Box } from "@mui/material"; +import { + Dispatch, + Fragment, + SetStateAction, + useEffect, + useRef, + useState, +} from "react"; +import { LogLine } from "./LogLine"; +import { useSubscription } from "@apollo/client/react"; +import { + LogStreamContentSubscriptionSubscription, + LogStreamContentSubscriptionSubscriptionVariables, +} from "./__generated__/LogStreamContent.generated"; + +const LOGSTREAMCONTENT_SUBSCRIPTION: TypedDocumentNode< + LogStreamContentSubscriptionSubscription, + LogStreamContentSubscriptionSubscriptionVariables +> = gql` + subscription LogStreamContentSubscription( + $visit: VisitInput! + $workflowName: String! + $taskId: String! + ) { + logs(visit: $visit, workflowName: $workflowName, taskId: $taskId) { + content + podName + } + } +`; + +const LogStreamContent = ({ + visit, + workflowName, + taskId, +}: { + visit: Visit; + workflowName: string; + taskId: string | null; +}) => { + const [logLines, setLogLines] = useState([]); + const [subscriptionError, setSubscriptionError] = useState( + null + ); + const [logUnavailable, setLogUnavailable] = useState(false); + const [taskFinished, setTaskFinished] = useState(false); + + const containerRef = useRef(null); + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [logLines]); + + return ( + <> + {taskId && !logUnavailable && !taskFinished && ( + + )} + + {logUnavailable ? ( + Log not available + ) : subscriptionError ? ( + {subscriptionError} + ) : logLines.length > 0 ? ( + logLines.map((line, index) => ( + + {(index + 1).toString().padStart(3, " ")} {line} + {index < logLines.length - 1 && "\n"} + + )) + ) : ( + + {taskId ? "Waiting for logs..." : "No task selected"} + + )} + + + ); +}; + +const LogStreamSubscription = ({ + visit, + workflowName, + taskId, + setLogLines, + setSubscriptionError, + setLogUnavailable, + setTaskFinished, +}: { + visit: Visit; + workflowName: string; + taskId: string; + setLogLines: Dispatch>; + setSubscriptionError: Dispatch>; + setLogUnavailable: Dispatch>; + setTaskFinished: Dispatch>; +}) => { + useSubscription(LOGSTREAMCONTENT_SUBSCRIPTION, { + variables: { + visit: visit, + workflowName: workflowName, + taskId: taskId, + }, + onData: (payload) => { + if (payload.data.error) { + const message = payload.data.error.message; + console.log("Log subscription error:", message); + const logUnavailable = + message.includes("Log not available") || + message.includes("NoSuchKey") || + message.includes("No logs") || + message.includes("Failed to retrieve archived log artifact"); + if (logUnavailable) { + setSubscriptionError("Log not available"); + setLogUnavailable(true); + return; + } + // For non-terminal errors + setSubscriptionError("Unable to retrieve task logs"); + } + const line = payload.data.data?.logs.content; + if (line) { + setLogLines((previousLines) => [...previousLines, line]); + } + }, + onError: (error) => { + console.log("Log subscription error:", error.message); + setSubscriptionError("Unable to retrieve task logs"); + }, + onComplete: () => { + setTaskFinished(true); + console.log("Log subscription completed"); + }, + }); + return null; +}; + +export default LogStreamContent; diff --git a/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamer.tsx b/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamer.tsx new file mode 100644 index 00000000..fa7f1acc --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/LogStreamer/LogStreamer.tsx @@ -0,0 +1,117 @@ +import { gql, TypedDocumentNode } from "@apollo/client"; +import { Visit } from "@diamondlightsource/sci-react-ui"; +import { useSuspenseFragment } from "@apollo/client/react"; +import { + FormControl, + InputLabel, + ListItemIcon, + MenuItem, + Select, + Stack, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import { setFetchedTasks } from "./utils"; +import LogStreamContent from "./LogStreamContent"; +import { LogStreamerFragmentFragment } from "./__generated__/LogStreamer.generated"; +import { getTaskStatusIcon } from "./getTaskStatusIcon"; + +export const LOGSTREAMER_FRAGMENT: TypedDocumentNode = gql` + fragment LogStreamerFragment on Workflow { + status { + __typename + ... on WorkflowRunningStatus { + tasks { + id + name + status + } + } + ... on WorkflowSucceededStatus { + tasks { + id + name + status + } + } + ... on WorkflowFailedStatus { + tasks { + id + name + status + } + } + ... on WorkflowErroredStatus { + tasks { + id + name + status + } + } + } + } +`; + +const LogStreamer = ({ + visit, + selectedWorkflow, + queryData, +}: { + visit: Visit; + selectedWorkflow: string; + queryData: LogStreamerFragmentFragment; +}) => { + const [selectedTaskId, setSelectedTaskId] = useState(null); + const { data } = useSuspenseFragment({ + fragment: LOGSTREAMER_FRAGMENT, + fragmentName: "LogStreamerFragment", + from: queryData, + }); + + if (!data) return <>No Data; + + const fetchedTasks = setFetchedTasks(data); + + return ( + + + Select Task + + + + + + ); +}; + +export default LogStreamer; diff --git a/frontend/unified/src/components/JobsViewer/LogStreamer/getTaskStatusIcon.tsx b/frontend/unified/src/components/JobsViewer/LogStreamer/getTaskStatusIcon.tsx new file mode 100644 index 00000000..8fca68ad --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/LogStreamer/getTaskStatusIcon.tsx @@ -0,0 +1,21 @@ +import { + CircleAlert, + CircleCheckBig, + CircleEllipsis, + CircleMinus, + LoaderCircle, +} from "lucide-react"; +import { TaskStatus } from "../../../types"; + +export const getTaskStatusIcon = (status: TaskStatus) => { + const taskStatusIconMap: { [key in TaskStatus]: React.JSX.Element } = { + PENDING: , + RUNNING: , + SUCCEEDED: , + SKIPPED: , + FAILED: , + ERROR: , + OMITTED: , + }; + return taskStatusIconMap[status]; +}; diff --git a/frontend/unified/src/components/JobsViewer/LogStreamer/utils.ts b/frontend/unified/src/components/JobsViewer/LogStreamer/utils.ts new file mode 100644 index 00000000..d3a20f6b --- /dev/null +++ b/frontend/unified/src/components/JobsViewer/LogStreamer/utils.ts @@ -0,0 +1,15 @@ +import { Task } from "../../../types"; +import { LogStreamerFragmentFragment } from "./__generated__/LogStreamer.generated"; + +export const setFetchedTasks = (data: LogStreamerFragmentFragment): Task[] => { + if ( + data.status && + (data.status?.__typename === "WorkflowRunningStatus" || + data.status?.__typename === "WorkflowSucceededStatus" || + data.status?.__typename === "WorkflowFailedStatus" || + data.status?.__typename === "WorkflowErroredStatus") + ) { + return data.status.tasks; + } + return []; +}; diff --git a/frontend/unified/src/components/Plot/ArtifactSelector.tsx b/frontend/unified/src/components/JobsViewer/Plot/ArtifactSelector.tsx similarity index 53% rename from frontend/unified/src/components/Plot/ArtifactSelector.tsx rename to frontend/unified/src/components/JobsViewer/Plot/ArtifactSelector.tsx index 6b2ca80b..9062128f 100644 --- a/frontend/unified/src/components/Plot/ArtifactSelector.tsx +++ b/frontend/unified/src/components/JobsViewer/Plot/ArtifactSelector.tsx @@ -1,77 +1,59 @@ 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 { ArtifactSelectorFragmentFragment } from "./__generated__/ArtifactSelector.generated"; +import { useSuspenseFragment } from "@apollo/client/react"; 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 +export const ARTIFACTSELECTOR_FRAGMENT: TypedDocumentNode = gql` + fragment ArtifactSelectorFragment on Workflow { + name + status { + __typename + ... on WorkflowRunningStatus { + startTime + tasks { + id + name + artifacts { name - status - stepType - artifacts { - name - url - mimeType - } + url + mimeType } } - ... on WorkflowSucceededStatus { - __typename - startTime - tasks { - id + } + ... on WorkflowSucceededStatus { + startTime + tasks { + id + name + artifacts { name - status - stepType - artifacts { - name - url - mimeType - } + url + mimeType } } - ... on WorkflowFailedStatus { - tasks { - id + } + ... on WorkflowFailedStatus { + startTime + tasks { + id + name + artifacts { name - status - stepType - artifacts { - name - url - mimeType - } + url + mimeType } } - ... on WorkflowErroredStatus { - tasks { - id + } + ... on WorkflowErroredStatus { + startTime + tasks { + id + name + artifacts { name - status - stepType - artifacts { - name - url - mimeType - } + url + mimeType } } } @@ -79,49 +61,42 @@ const GET_WORKFLOW_ARTIFACTS: TypedDocumentNode< } `; -type NonNullWorkflow = NonNullable; -type NonNullWorkflowStatus = NonNullable; -type WorkflowSucceededStatus = Extract< - NonNullWorkflowStatus, - { __typename: "WorkflowSucceededStatus" } ->; -export type Artifact = WorkflowSucceededStatus["tasks"][0]["artifacts"][0]; +export type Artifact = { + name: string; + url: unknown; + mimeType: string; +}; -type TaskNameAndArtifactTuple = [string, Artifact[]]; +type TaskNameAndArtifactTuple = [taskName: string, artifacts: Artifact[]]; type ArtifactSelectorProps = { - workflowName: string; - visit: Visit; setArtifact: (_: Artifact | null) => void; isPlottingEnabled: boolean; + queryData: ArtifactSelectorFragmentFragment; }; const IMAGE_ARTIFACT_MIME_TYPES = ["image/jpeg", "image/tiff"]; export const ArtifactSelector: React.FC = ({ - workflowName, - visit, setArtifact, isPlottingEnabled, + queryData, }: ArtifactSelectorProps) => { const [selectedArtifact, setSelectedArtifact] = useState(""); - const { error, data } = useSuspenseQuery(GET_WORKFLOW_ARTIFACTS, { - variables: { - name: workflowName, - visit: visit, - }, + const { data } = useSuspenseFragment({ + fragment: ARTIFACTSELECTOR_FRAGMENT, + fragmentName: "ArtifactSelectorFragment", + from: queryData, }); - if (error) return

Error: {error.message}

; - const generateArtifactList = (): React.ReactNode[] => { - switch (data.workflow?.status?.__typename) { + switch (data.status?.__typename) { case "WorkflowSucceededStatus": case "WorkflowRunningStatus": case "WorkflowFailedStatus": case "WorkflowErroredStatus": { const taskNamesAndImageArtifacts: TaskNameAndArtifactTuple[] = - data.workflow.status.tasks + data.status.tasks .map( (task) => [ @@ -153,7 +128,7 @@ export const ArtifactSelector: React.FC = ({ return ( Artifact - {data.workflow?.status?.__typename !== "WorkflowPendingStatus" ? ( + {data.status?.__typename !== "WorkflowPendingStatus" ? (