From 045a2dce74f2f975876f602fa3d9dacaca95e5c0 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Mon, 13 May 2024 06:58:35 -0400 Subject: [PATCH] [RHOAIENG-2989] artifact node drawer details section --- .../mlmd/useArtifactsFromMlmdContext.ts | 32 +++++++ .../pipelineRun/PipelineRunDetails.tsx | 46 +++++----- .../PipelineRunDrawerRightContent.tsx | 37 +++++--- .../artifacts/ArtifactNodeDetails.tsx | 88 +++++++++++++++++++ .../artifacts/ArtifactNodeDrawerContent.tsx | 68 ++++++++++++++ .../pipelineRun/artifacts/index.ts | 3 + .../artifacts/usePipelineRunArtifacts.tsx | 17 ++++ .../pipelines/topology/pipelineTaskTypes.ts | 2 + .../topology/usePipelineTaskTopology.tsx | 14 ++- .../artifacts/useGetArtifactsList.ts | 4 +- 10 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 frontend/src/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext.ts create mode 100644 frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx create mode 100644 frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDrawerContent.tsx create mode 100644 frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/index.ts create mode 100644 frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/usePipelineRunArtifacts.tsx diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext.ts new file mode 100644 index 0000000000..a8cdedf4ec --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext.ts @@ -0,0 +1,32 @@ +import React from 'react'; +import { MlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { Artifact } from '~/third_party/mlmd'; +import { GetArtifactsByContextRequest } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, + NotReadyError, +} from '~/utilities/useFetchState'; + +export const useArtifactsFromMlmdContext = ( + context: MlmdContext | null, + refreshRate?: number, +): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const getArtifactsList = React.useCallback>(async () => { + if (!context) { + return Promise.reject(new NotReadyError('No context')); + } + + const request = new GetArtifactsByContextRequest(); + request.setContextId(context.getId()); + const res = await metadataStoreServiceClient.getArtifactsByContext(request); + return res.getArtifactsList(); + }, [metadataStoreServiceClient, context]); + + return useFetchState(getArtifactsList, [], { + refreshRate, + }); +}; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index 231d291403..4598cf4f5a 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -37,25 +37,27 @@ import { PipelineRunType } from '~/pages/pipelines/global/runs/types'; import { routePipelineRunsNamespace } from '~/routes'; import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobReferenceName'; import useExecutionsForPipelineRun from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/useExecutionsForPipelineRun'; +import { usePipelineRunArtifacts } from './artifacts'; const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, contextPath }) => { const { runId } = useParams(); const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); - const [runResource, runLoaded, runError] = usePipelineRunById(runId, true); + const [run, runLoaded, runError] = usePipelineRunById(runId, true); const [version, versionLoaded, versionError] = usePipelineVersionById( - runResource?.pipeline_version_reference?.pipeline_id, - runResource?.pipeline_version_reference?.pipeline_version_id, + run?.pipeline_version_reference?.pipeline_id, + run?.pipeline_version_reference?.pipeline_version_id, ); - const pipelineSpec = version?.pipeline_spec ?? runResource?.pipeline_spec; + const pipelineSpec = version?.pipeline_spec ?? run?.pipeline_spec; const [deleting, setDeleting] = React.useState(false); const [detailsTab, setDetailsTab] = React.useState( RunDetailsTabs.DETAILS, ); const [selectedId, setSelectedId] = React.useState(null); - const [executions, executionsLoaded, executionsError] = useExecutionsForPipelineRun(runResource); - const nodes = usePipelineTaskTopology(pipelineSpec, runResource?.run_details, executions); + const [executions, executionsLoaded, executionsError] = useExecutionsForPipelineRun(run); + const [artifacts, isArtifactsLoaded, artifactsError] = usePipelineRunArtifacts(run); + const nodes = usePipelineTaskTopology(pipelineSpec, run?.run_details, executions, artifacts); const selectedNode = React.useMemo( () => nodes.find((n) => n.id === selectedId), @@ -64,8 +66,8 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, const getFirstNode = (firstId: string) => nodes.find((n) => n.id === firstId); - const loaded = runLoaded && (versionLoaded || !!runResource?.pipeline_spec); - const error = versionError || runError; + const loaded = runLoaded && (versionLoaded || !!run?.pipeline_spec); + const error = versionError || runError || executionsError || artifactsError; if (error) { return ( @@ -80,7 +82,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, ); } - if (!loaded || (!executionsLoaded && !executionsError)) { + if (!loaded || !executionsLoaded || !isArtifactsLoaded) { return ( @@ -95,6 +97,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, panelContent={ setSelectedId(null)} executions={executions} /> @@ -110,29 +113,29 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, setDetailsTab(selection); setSelectedId(null); }} - pipelineRunDetails={runResource && pipelineSpec ? runResource : undefined} + pipelineRunDetails={run && pipelineSpec ? run : undefined} /> } > + run ? ( + ) : ( 'Error loading run' ) } subtext={ - runResource && ( + run && ( ) } description={ - runResource?.description ? ( - + run?.description ? ( + ) : ( '' ) @@ -143,15 +146,12 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, {breadcrumbPath} - + } headerAction={ - setDeleting(true)} - /> + setDeleting(true)} /> } empty={false} > @@ -180,7 +180,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, { if (deleteComplete) { navigate(contextPath ?? routePipelineRunsNamespace(namespace)); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx index 398af65de5..cc9b916457 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent.tsx @@ -12,16 +12,19 @@ import PipelineRunDrawerRightTabs from '~/concepts/pipelines/content/pipelinesDe import './PipelineRunDrawer.scss'; import { PipelineTask } from '~/concepts/pipelines/topology'; import { Execution } from '~/third_party/mlmd'; +import { ArtifactNodeDrawerContent } from './artifacts'; type PipelineRunDrawerRightContentProps = { task?: PipelineTask; executions: Execution[]; + upstreamTaskName?: string; onClose: () => void; }; const PipelineRunDrawerRightContent: React.FC = ({ task, executions, + upstreamTaskName, onClose, }) => { if (!task) { @@ -35,18 +38,28 @@ const PipelineRunDrawerRightContent: React.FC - - - {task.name} {task.type === 'artifact' ? 'Artifact details' : ''} - - {task.status?.podName && {task.status.podName}} - - - - - - - + {task.type === 'artifact' ? ( + + ) : ( + <> + + + {task.name} + + {task.status?.podName && {task.status.podName}} + + + + + + + + + )} ); }; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx new file mode 100644 index 0000000000..293fe531bd --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDetails.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { + Title, + Flex, + FlexItem, + Stack, + DescriptionList, + DescriptionListGroup, + DescriptionListTerm, + DescriptionListDescription, +} from '@patternfly/react-core'; + +import { Artifact } from '~/third_party/mlmd'; +import { artifactsDetailsRoute } from '~/routes'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { getArtifactName } from '~/pages/pipelines/global/experiments/artifacts/utils'; +import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/PipelinesTableRowTime'; +import PipelineRunDrawerRightContent from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent'; + +type ArtifactNodeDetailsProps = Pick< + React.ComponentProps, + 'upstreamTaskName' +> & { + artifact: Artifact.AsObject; +}; + +export const ArtifactNodeDetails: React.FC = ({ + artifact, + upstreamTaskName, +}) => { + const { namespace } = usePipelinesAPI(); + const artifactName = getArtifactName(artifact); + + return ( + + + + Artifact details + + + Upstream task + {upstreamTaskName} + + Artifact name + + {artifactName} + + + Artifact type + {artifact.type} + + Created at + + + + + + + + + + + Artifact URI + + + {artifactName} + {artifact.uri} + + + + + + ); +}; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDrawerContent.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDrawerContent.tsx new file mode 100644 index 0000000000..6d12a033ef --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/ArtifactNodeDrawerContent.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import { + DrawerHead, + Title, + Text, + DrawerActions, + DrawerCloseButton, + DrawerPanelBody, + Tabs, + Tab, + TabTitleText, + EmptyState, + EmptyStateHeader, + EmptyStateVariant, +} from '@patternfly/react-core'; + +import PipelineRunDrawerRightContent from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDrawerRightContent'; +import { ArtifactNodeDetails } from './ArtifactNodeDetails'; + +type ArtifactNodeDrawerContentProps = Omit< + React.ComponentProps, + 'executions' +>; + +export const ArtifactNodeDrawerContent: React.FC = ({ + task, + upstreamTaskName, + onClose, +}) => { + const artifact = task?.metadata?.toObject(); + + return task ? ( + <> + + + {task.name} + + {task.status?.podName && {task.status.podName}} + + + + + + {artifact ? ( + + Artifact details} + aria-label="Overview" + > + + + Visualization} + aria-label="Visualization" + /> + + ) : ( + + + + )} + + + ) : null; +}; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/index.ts b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/index.ts new file mode 100644 index 0000000000..9dfebc53ba --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/index.ts @@ -0,0 +1,3 @@ +export * from './ArtifactNodeDetails'; +export * from './ArtifactNodeDrawerContent'; +export * from './usePipelineRunArtifacts'; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/usePipelineRunArtifacts.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/usePipelineRunArtifacts.tsx new file mode 100644 index 0000000000..575bb4f170 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/usePipelineRunArtifacts.tsx @@ -0,0 +1,17 @@ +import { useArtifactsFromMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useArtifactsFromMlmdContext'; +import { usePipelineRunMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/usePipelineRunMlmdContext'; +import { isPipelineRunFinished } from '~/concepts/pipelines/apiHooks/usePipelineRunById'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import { Artifact } from '~/third_party/mlmd'; +import { FAST_POLL_INTERVAL } from '~/utilities/const'; + +export const usePipelineRunArtifacts = ( + run: PipelineRunKFv2 | null, +): [artifacts: Artifact[], loaded: boolean, error?: Error] => { + const isFinished = isPipelineRunFinished(run); + const refreshRate = isFinished ? 0 : FAST_POLL_INTERVAL; + const [context, , contextError] = usePipelineRunMlmdContext(run?.run_id, refreshRate); + const [artifacts, artifactsLoaded] = useArtifactsFromMlmdContext(context, refreshRate); + + return [artifacts, artifactsLoaded, contextError]; +}; diff --git a/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts b/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts index a43eb61885..bb94eb7717 100644 --- a/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts +++ b/frontend/src/concepts/pipelines/topology/pipelineTaskTypes.ts @@ -5,6 +5,7 @@ import { InputDefinitionParameterType, RuntimeStateKF, } from '~/concepts/pipelines/kfTypes'; +import { Artifact } from '~/third_party/mlmd'; import { VolumeMount } from '~/types'; export type PipelineTaskParam = { @@ -46,6 +47,7 @@ export type PipelineTask = { steps?: PipelineTaskStep[]; inputs?: PipelineTaskInputOutput; outputs?: PipelineTaskInputOutput; + metadata?: Artifact | undefined; /** Run Status */ status?: PipelineTaskRunStatus; /** Volume Mounts */ diff --git a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.tsx b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.tsx index 708981d0d3..5dbc28639a 100644 --- a/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.tsx +++ b/frontend/src/concepts/pipelines/topology/usePipelineTaskTopology.tsx @@ -9,7 +9,7 @@ import { import { createNode } from '~/concepts/topology'; import { PipelineNodeModelExpanded } from '~/concepts/topology/types'; import { createArtifactNode, createGroupNode } from '~/concepts/topology/utils'; -import { Execution } from '~/third_party/mlmd'; +import { Artifact, Execution } from '~/third_party/mlmd'; import { ComponentArtifactMap, composeArtifactType, @@ -34,6 +34,7 @@ const getTaskArtifacts = ( componentRef: string, componentArtifactMap: ComponentArtifactMap, taskArtifactMap: TaskArtifactMap, + artifacts: Artifact[] | undefined, ): PipelineNodeModelExpanded[] => { const artifactsInComponent = componentArtifactMap[componentRef]; const artifactNodes: PipelineNodeModelExpanded[] = []; @@ -47,10 +48,15 @@ const getTaskArtifacts = ( // if no node needs it as an input, we don't really need a well known id, prepend taskId to ensure uniqueness const id = idForTaskArtifact(groupId, artifactId ?? artifactKey); + const artifactData = artifacts?.find( + (artifact) => + artifact.getCustomPropertiesMap().get('display_name')?.getStringValue() === label, + ); const artifactPipelineTask: PipelineTask = { type: 'artifact', name: label, + metadata: artifactData, inputs: { artifacts: [{ label, type: composeArtifactType(data) }], }, @@ -81,6 +87,7 @@ const getNodesForTasks = ( taskArtifactMap: TaskArtifactMap, runDetails?: RunDetailsKF, executions?: Execution[] | null, + artifacts?: Artifact[] | undefined, ): [nestedNodes: PipelineNodeModelExpanded[], children: string[]] => { const nodes: PipelineNodeModelExpanded[] = []; const children: string[] = []; @@ -120,6 +127,7 @@ const getNodesForTasks = ( componentRef, componentArtifactMap, taskArtifactMap, + artifacts, ); if (artifactNodes.length) { nodes.push(...artifactNodes); @@ -176,6 +184,7 @@ export const usePipelineTaskTopology = ( spec?: PipelineSpecVariable, runDetails?: RunDetailsKF, executions?: Execution[] | null, + artifacts?: Artifact[] | undefined, ): PipelineNodeModelExpanded[] => React.useMemo(() => { if (!spec) { @@ -204,6 +213,7 @@ export const usePipelineTaskTopology = ( taskArtifactMap, runDetails, executions, + artifacts, )[0]; // Since we have artifacts that are input only that do not get created, remove any dependencies on them @@ -211,4 +221,4 @@ export const usePipelineTaskTopology = ( ...n, runAfterTasks: n.runAfterTasks?.filter((t) => nodes.find((nextNode) => nextNode.id === t)), })); - }, [executions, runDetails, spec]); + }, [artifacts, executions, runDetails, spec]); diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts b/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts index db9f88d5c2..7ef83a5e84 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts @@ -12,7 +12,7 @@ export interface ArtifactsListResponse { export const useGetArtifactsList = ( refreshRate?: number, -): FetchState => { +): FetchState => { const { pageToken, maxResultSize, filterQuery } = useMlmdListContext(); const { metadataStoreServiceClient } = usePipelinesAPI(); @@ -39,7 +39,7 @@ export const useGetArtifactsList = ( }; }, [filterQuery, pageToken, maxResultSize, metadataStoreServiceClient]); - return useFetchState(fetchArtifactsList, null, { + return useFetchState(fetchArtifactsList, undefined, { refreshRate, }); };