diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts index 928e839713..6365e5b2f8 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts @@ -1,7 +1,16 @@ -import { Context } from '~/third_party/mlmd'; +import { Artifact, Context, ContextType, Event } from '~/third_party/mlmd'; export type MlmdContext = Context; +export type MlmdContextType = ContextType; + export enum MlmdContextTypes { RUN = 'system.PipelineRun', } + +// An artifact which has associated event. +// You can retrieve artifact name from event.path.steps[0].key +export interface LinkedArtifact { + event: Event; + artifact: Artifact; +} diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts new file mode 100644 index 0000000000..4f0ec1830d --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts @@ -0,0 +1,22 @@ +import React from 'react'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { GetArtifactTypesRequest } from '~/third_party/mlmd'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +export const useGetArtifactTypeMap = (): FetchState> => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback>>(async () => { + const request = new GetArtifactTypesRequest(); + + const res = await metadataStoreServiceClient.getArtifactTypes(request); + + const artifactTypeMap: Record = {}; + res.getArtifactTypesList().forEach((artifactType) => { + artifactTypeMap[artifactType.getId()] = artifactType.getName(); + }); + return artifactTypeMap; + }, [metadataStoreServiceClient]); + + return useFetchState(call, {}); +}; diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts new file mode 100644 index 0000000000..30da97f0b8 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId.ts @@ -0,0 +1,32 @@ +import React from 'react'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { + GetEventsByExecutionIDsRequest, + GetEventsByExecutionIDsResponse, +} from '~/third_party/mlmd'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +export const useGetEventsByExecutionId = ( + executionId?: string, +): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback< + FetchStateCallbackPromise + >(async () => { + const numberId = Number(executionId); + const request = new GetEventsByExecutionIDsRequest(); + + if (!executionId || Number.isNaN(numberId)) { + return null; + } + + request.setExecutionIdsList([numberId]); + + const response = await metadataStoreServiceClient.getEventsByExecutionIDs(request); + + return response; + }, [executionId, metadataStoreServiceClient]); + + return useFetchState(call, null); +}; diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionById.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionById.ts new file mode 100644 index 0000000000..ba0fb83d23 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetExecutionById.ts @@ -0,0 +1,25 @@ +import React from 'react'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { Execution, GetExecutionsByIDRequest } from '~/third_party/mlmd'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +export const useGetExecutionById = (executionId?: string): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback>(async () => { + const numberId = Number(executionId); + const request = new GetExecutionsByIDRequest(); + + if (!executionId || Number.isNaN(numberId)) { + return null; + } + + request.setExecutionIdsList([numberId]); + + const response = await metadataStoreServiceClient.getExecutionsByID(request); + + return response.getExecutionsList().length !== 0 ? response.getExecutionsList()[0] : null; + }, [executionId, metadataStoreServiceClient]); + + return useFetchState(call, null); +}; diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetLinkedArtifactsByEvents.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetLinkedArtifactsByEvents.ts new file mode 100644 index 0000000000..15238a3be5 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetLinkedArtifactsByEvents.ts @@ -0,0 +1,34 @@ +import React from 'react'; +import { LinkedArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { Artifact, Event, GetArtifactsByIDRequest } from '~/third_party/mlmd'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +export const useGetLinkedArtifactsByEvents = (events: Event[]): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback>(async () => { + const artifactIds = events + .filter((event) => event.getArtifactId()) + .map((event) => event.getArtifactId()); + const request = new GetArtifactsByIDRequest(); + + if (artifactIds.length === 0) { + return []; + } + + request.setArtifactIdsList(artifactIds); + + const response = await metadataStoreServiceClient.getArtifactsByID(request); + + const artifactMap: Record = {}; + response.getArtifactsList().forEach((artifact) => (artifactMap[artifact.getId()] = artifact)); + + return events.map((event) => { + const artifact = artifactMap[event.getArtifactId()]; + return { event, artifact }; + }); + }, [events, metadataStoreServiceClient]); + + return useFetchState(call, []); +}; diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextByExecution.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextByExecution.ts new file mode 100644 index 0000000000..1b7c522493 --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextByExecution.ts @@ -0,0 +1,36 @@ +import React from 'react'; +import { MlmdContext, MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { useGetMlmdContextType } from '~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { Execution } from '~/third_party/mlmd'; +import { GetContextsByExecutionRequest } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; + +const useGetMlmdContextByExecution = ( + execution: Execution, + type?: MlmdContextTypes, +): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + const executionId = execution.getId(); + const [contextType] = useGetMlmdContextType(type); + + const contextTypeId = contextType?.getId(); + + const call = React.useCallback>(async () => { + const request = new GetContextsByExecutionRequest(); + + request.setExecutionId(executionId); + + const response = await metadataStoreServiceClient.getContextsByExecution(request); + + const result = response.getContextsList().filter((c) => c.getTypeId() === contextTypeId); + + return result.length === 1 ? result[0] : null; + }, [executionId, metadataStoreServiceClient, contextTypeId]); + + return useFetchState(call, null); +}; + +export const useGetPipelineRunContextByExecution = ( + execution: Execution, +): FetchState => useGetMlmdContextByExecution(execution, MlmdContextTypes.RUN); diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType.ts new file mode 100644 index 0000000000..e96f71a4cc --- /dev/null +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextType.ts @@ -0,0 +1,29 @@ +import React from 'react'; +import { MlmdContextType, MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { GetContextTypeRequest } from '~/third_party/mlmd'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, + NotReadyError, +} from '~/utilities/useFetchState'; + +export const useGetMlmdContextType = ( + type?: MlmdContextTypes, +): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback>(async () => { + if (!type) { + return Promise.reject(new NotReadyError('No context type')); + } + + const request = new GetContextTypeRequest(); + request.setTypeName(type); + const res = await metadataStoreServiceClient.getContextType(request); + const contextType = res.getContextType() || null; + return contextType; + }, [metadataStoreServiceClient, type]); + + return useFetchState(call, null); +}; diff --git a/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx b/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx index 9a9d4dc8b3..d2bd1a56da 100644 --- a/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx +++ b/frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx @@ -8,6 +8,8 @@ import { executionsPageTitle, } from '~/pages/pipelines/global/experiments/executions/const'; import GlobalExecutions from '~/pages/pipelines/global/experiments/executions/GlobalExecutions'; +import ExecutionDetails from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetails'; +import GlobalPipelineCoreDetails from '~/pages/pipelines/global/GlobalPipelineCoreDetails'; const GlobalPipelineExecutionsRoutes: React.FC = () => ( @@ -22,6 +24,16 @@ const GlobalPipelineExecutionsRoutes: React.FC = () => ( } > } /> + + } + /> } /> diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionStatus.tsx similarity index 56% rename from frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx rename to frontend/src/pages/pipelines/global/experiments/executions/ExecutionStatus.tsx index a69e941c5d..f79511f3a6 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionStatus.tsx @@ -1,21 +1,24 @@ import React from 'react'; -import { Icon, Tooltip } from '@patternfly/react-core'; +import { Icon, Label, Tooltip } from '@patternfly/react-core'; import { CheckCircleIcon, ExclamationCircleIcon, + InProgressIcon, OutlinedWindowRestoreIcon, - QuestionCircleIcon, + PendingIcon, TimesCircleIcon, } from '@patternfly/react-icons'; import { Execution } from '~/third_party/mlmd'; -type ExecutionsTableRowStatusIconProps = { +type ExecutionStatusProps = { status: Execution.State; + isIcon?: boolean; }; -const ExecutionsTableRowStatusIcon: React.FC = ({ status }) => { +const ExecutionStatus: React.FC = ({ status, isIcon }) => { let tooltip; let icon; + let label; switch (status) { case Execution.State.COMPLETE: icon = ( @@ -24,6 +27,11 @@ const ExecutionsTableRowStatusIcon: React.FC ); tooltip = 'Complete'; + label = ( + + ); break; case Execution.State.CACHED: icon = ( @@ -32,6 +40,11 @@ const ExecutionsTableRowStatusIcon: React.FC ); tooltip = 'Cached'; + label = ( + + ); break; case Execution.State.CANCELED: icon = ( @@ -40,6 +53,7 @@ const ExecutionsTableRowStatusIcon: React.FC ); tooltip = 'Canceled'; + label = ; break; case Execution.State.FAILED: icon = ( @@ -48,30 +62,32 @@ const ExecutionsTableRowStatusIcon: React.FC ); tooltip = 'Failed'; + label = ( + + ); break; case Execution.State.RUNNING: icon = ; tooltip = 'Running'; + label = ; break; - // TODO: change the icon here case Execution.State.NEW: icon = ( - + ); tooltip = 'New'; + label = ; break; default: - icon = ( - - - - ); - tooltip = 'Unknown'; + icon = <>Unknown; + label = ; } - return {icon}; + return isIcon ? {icon} : <>{label}; }; -export default ExecutionsTableRowStatusIcon; +export default ExecutionStatus; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx index 2e02ee0991..3eaff0a405 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/ExecutionsTableRow.tsx @@ -1,23 +1,32 @@ import * as React from 'react'; import { Td, Tr } from '@patternfly/react-table'; +import { Link } from 'react-router-dom'; import { Execution } from '~/third_party/mlmd'; -import ExecutionsTableRowStatusIcon from '~/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon'; +import { getExecutionDisplayName } from '~/pages/pipelines/global/experiments/executions/utils'; +import { executionDetailsRoute } from '~/routes'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import ExecutionStatus from '~/pages/pipelines/global/experiments/executions/ExecutionStatus'; type ExecutionsTableRowProps = { obj: Execution; }; -const ExecutionsTableRow: React.FC = ({ obj }) => ( - - - {obj.getCustomPropertiesMap().get('task_name')?.getStringValue() || '(No name)'} - - - - - {obj.getId()} - {obj.getType()} - -); +const ExecutionsTableRow: React.FC = ({ obj }) => { + const { namespace } = usePipelinesAPI(); + return ( + + + + {getExecutionDisplayName(obj)} + + + + + + {obj.getId()} + {obj.getType()} + + ); +}; export default ExecutionsTableRow; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/const.ts b/frontend/src/pages/pipelines/global/experiments/executions/const.ts index acff4a6ea4..8018169c90 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/const.ts +++ b/frontend/src/pages/pipelines/global/experiments/executions/const.ts @@ -1,5 +1,6 @@ import { ExecutionStatus } from '~/concepts/pipelines/kfTypes'; -import { Execution as MlmdExecution } from '~/third_party/mlmd'; +import { Event, Execution as MlmdExecution } from '~/third_party/mlmd'; +import { PickEnum } from '~/typeHelpers'; export const executionsPageTitle = 'Executions'; export const executionsPageDescription = 'View execution metadata.'; @@ -25,6 +26,19 @@ export const initialFilterData: Record = { [FilterOptions.Status]: '', }; +export const inputOutputSectionTitle: Record< + PickEnum< + Event.Type, + Event.Type.DECLARED_INPUT | Event.Type.INPUT | Event.Type.OUTPUT | Event.Type.DECLARED_OUTPUT + >, + string +> = { + [Event.Type.INPUT]: 'Inputs', + [Event.Type.DECLARED_INPUT]: 'Declared inputs', + [Event.Type.DECLARED_OUTPUT]: 'Declared outputs', + [Event.Type.OUTPUT]: 'Outputs', +}; + export const getMlmdExecutionState = (status: string): MlmdExecution.State => { switch (status) { case ExecutionStatus.NEW: diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx new file mode 100644 index 0000000000..35ce43698a --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx @@ -0,0 +1,151 @@ +import { + Breadcrumb, + BreadcrumbItem, + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateIcon, + Spinner, + Split, + SplitItem, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import React from 'react'; +import { useNavigate, useParams } from 'react-router'; +import { useGetArtifactTypeMap } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes'; +import { useGetEventsByExecutionId } from '~/concepts/pipelines/apiHooks/mlmd/useGetEventsByExecutionId'; +import { useGetExecutionById } from '~/concepts/pipelines/apiHooks/mlmd/useGetExecutionById'; +import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/types'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import ApplicationsPage from '~/pages/ApplicationsPage'; +import { inputOutputSectionTitle } from '~/pages/pipelines/global/experiments/executions/const'; +import ExecutionDetailsCustomPropertiesSection from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsCustomPropertiesSection'; +import ExecutionDetailsIDSection from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsIDSection'; +import ExecutionDetailsInputOutputSection from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsInputOutputSection'; +import ExecutionDetailsPropertiesSection from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesSection'; +import ExecutionDetailsReferenceSection from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsReferenceSection'; +import ExecutionStatus from '~/pages/pipelines/global/experiments/executions/ExecutionStatus'; +import { + getExecutionDisplayName, + parseEventsByType, +} from '~/pages/pipelines/global/experiments/executions/utils'; +import { executionsBaseRoute } from '~/routes'; +import { Event } from '~/third_party/mlmd'; + +const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, contextPath }) => { + const { executionId } = useParams(); + const navigate = useNavigate(); + const { namespace } = usePipelinesAPI(); + const [execution, executionLoaded, executionError] = useGetExecutionById(executionId); + const [eventsResponse, eventsLoaded, eventsError] = useGetEventsByExecutionId(executionId); + const [artifactTypeMap, artifactTypeMapLoaded] = useGetArtifactTypeMap(); + const allEvents = parseEventsByType(eventsResponse); + + const error = executionError || eventsError; + + if (error) { + return ( + + + } + headingLevel="h2" + /> + {error.message} + + + ); + } + + if (!executionLoaded || !eventsLoaded) { + return ( + + + + ); + } + + if (!execution) { + navigate(contextPath ?? executionsBaseRoute(namespace)); + return; + } + + const displayName = getExecutionDisplayName(execution); + + return ( + + + {displayName} + + + + + + } + loaded + breadcrumb={ + + {breadcrumbPath} + {displayName} + + } + empty={false} + provideChildrenPadding + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ExecutionDetails; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsCustomPropertiesSection.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsCustomPropertiesSection.tsx new file mode 100644 index 0000000000..f29f6da45c --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsCustomPropertiesSection.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Stack, + StackItem, + Title, +} from '@patternfly/react-core'; +import { Execution } from '~/third_party/mlmd'; +import ExecutionDetailsPropertiesValue from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesValue'; +import { getMlmdMetadataValue } from '~/pages/pipelines/global/experiments/executions/utils'; + +type ExecutionDetailsCustomPropertiesSectionProps = { + execution: Execution; +}; + +const ExecutionDetailsCustomPropertiesSection: React.FC< + ExecutionDetailsCustomPropertiesSectionProps +> = ({ execution }) => { + const propertiesMap = execution.getCustomPropertiesMap(); + + return ( + + + Custom properties + + + {propertiesMap.getEntryList().length === 0 ? ( + 'No custom properties' + ) : ( + + {propertiesMap.getEntryList().map((p) => ( + + {p[0]} + + + + + ))} + + )} + + + ); +}; + +export default ExecutionDetailsCustomPropertiesSection; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsIDSection.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsIDSection.tsx new file mode 100644 index 0000000000..dba0e0eca7 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsIDSection.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { Execution } from '~/third_party/mlmd'; + +type ExecutionDetailsIDSectionProps = { + execution: Execution; +}; + +const ExecutionDetailsIDSection: React.FC = ({ execution }) => ( + + + ID + {execution.getId()} + + + Type + {execution.getType()} + + +); + +export default ExecutionDetailsIDSection; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsInputOutputSection.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsInputOutputSection.tsx new file mode 100644 index 0000000000..91b351b939 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsInputOutputSection.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Bullseye, Spinner, Stack, StackItem, Title } from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { Event } from '~/third_party/mlmd'; +import { useGetLinkedArtifactsByEvents } from '~/concepts/pipelines/apiHooks/mlmd/useGetLinkedArtifactsByEvents'; +import { getArtifactNameFromEvent } from '~/pages/pipelines/global/experiments/executions/utils'; + +type ExecutionDetailsInputOutputSectionProps = { + isLoaded: boolean; + title: string; + events: Event[]; + artifactTypeMap: Record; +}; + +const ExecutionDetailsInputOutputSection: React.FC = ({ + isLoaded, + title, + events, + artifactTypeMap, +}) => { + const [linkedArtifacts, isLinkedArtifactsLoaded] = useGetLinkedArtifactsByEvents(events); + + if (!isLoaded || !isLinkedArtifactsLoaded) { + return ( + + + + ); + } + + const artifactDataMap: Record = + {}; + linkedArtifacts.forEach((linkedArtifact) => { + const id = linkedArtifact.event.getArtifactId(); + if (!id) { + return; + } + artifactDataMap[id] = { + name: getArtifactNameFromEvent(linkedArtifact.event) || '', + typeId: linkedArtifact.artifact.getTypeId(), + uri: linkedArtifact.artifact.getUri() || '', + }; + }); + + return ( + + + {title} + + + {events.length === 0 ? ( + `No ${title.toLowerCase()}` + ) : ( + + + + + + + + + + + {events.map((event) => { + const id = event.getArtifactId(); + const data = artifactDataMap[id]; + const type = data.typeId ? artifactTypeMap[data.typeId] : null; + return ( + + + {/* TODO: add artifact details page line */} + + + + + ); + })} + +
Artifact IDNameTypeURI
{id}{data.name}{type}{data.uri}
+ )} +
+
+ ); +}; + +export default ExecutionDetailsInputOutputSection; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesSection.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesSection.tsx new file mode 100644 index 0000000000..4168388105 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesSection.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Stack, + StackItem, + Title, +} from '@patternfly/react-core'; +import { Execution } from '~/third_party/mlmd'; +import ExecutionDetailsPropertiesValue from '~/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesValue'; +import { getMlmdMetadataValue } from '~/pages/pipelines/global/experiments/executions/utils'; + +type ExecutionDetailsPropertiesSectionProps = { + execution: Execution; +}; + +const ExecutionDetailsPropertiesSection: React.FC = ({ + execution, +}) => { + const propertiesMap = execution.getPropertiesMap(); + const properties = propertiesMap + .getEntryList() + // From Kubeflow UI code - "TODO: __ALL_META__ is something of a hack, is redundant, and can be ignore" + .filter((k) => k[0] !== '__ALL_META__'); + + return ( + + + Properties + + + {properties.length === 0 ? ( + 'No properties' + ) : ( + + {propertiesMap.getEntryList().map((p) => ( + + {p[0]} + + + + + ))} + + )} + + + ); +}; + +export default ExecutionDetailsPropertiesSection; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesValue.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesValue.tsx new file mode 100644 index 0000000000..9e2baa54a2 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsPropertiesValue.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { CodeEditor } from '@patternfly/react-code-editor'; +import { MlmdMetadataValueType } from '~/pages/pipelines/global/experiments/executions/utils'; + +type ExecutionDetailsPropertiesValueProps = { + value: MlmdMetadataValueType; +}; + +const ExecutionDetailsPropertiesValueCode = ({ code }: { code: string }) => ( + +); + +const ExecutionDetailsPropertiesValue: React.FC = ({ + value, +}) => { + if (!value) { + return ''; + } + if (typeof value === 'string') { + try { + const jsonValue = JSON.parse(value); + return ; + } catch { + // not JSON, return directly + return value; + } + } + if (typeof value === 'number') { + return value; + } + // value is Struct + const jsObject = value.toJavaScript(); + // When Struct is converted to js object, it may contain a top level "struct" + // or "list" key depending on its type, but the key is meaningless and we can + // omit it in visualization. + return ( + + ); +}; + +export default ExecutionDetailsPropertiesValue; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsReferenceSection.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsReferenceSection.tsx new file mode 100644 index 0000000000..4e71112e4c --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetailsReferenceSection.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Skeleton, Stack, StackItem, Title } from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { Link } from 'react-router-dom'; +import { Execution } from '~/third_party/mlmd'; +import { useGetPipelineRunContextByExecution } from '~/concepts/pipelines/apiHooks/mlmd/useGetMlmdContextByExecution'; +import { executionDetailsRoute, routePipelineRunDetailsNamespace } from '~/routes'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; + +type ExecutionDetailsReferenceSectionProps = { + execution: Execution; +}; + +const ExecutionDetailsReferenceSection: React.FC = ({ + execution, +}) => { + const { namespace } = usePipelinesAPI(); + const [context, contextLoaded] = useGetPipelineRunContextByExecution(execution); + + const originalExecutionId = execution + .getCustomPropertiesMap() + .get('cached_execution_id') + ?.getStringValue(); + + return ( + + + Reference + + + + + + + + + + + + + + + {originalExecutionId && ( + + + + + )} + +
NameLink
Pipeline run + {contextLoaded ? ( + context ? ( + + {`runs/details/${context.getName()}`} + + ) : ( + 'Unknown' + ) + ) : ( + + )} +
Original execution + + {`execution/${originalExecutionId}`} + +
+
+
+ ); +}; + +export default ExecutionDetailsReferenceSection; diff --git a/frontend/src/pages/pipelines/global/experiments/executions/utils.ts b/frontend/src/pages/pipelines/global/experiments/executions/utils.ts new file mode 100644 index 0000000000..b191fc6b0d --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/executions/utils.ts @@ -0,0 +1,63 @@ +import { Struct } from 'google-protobuf/google/protobuf/struct_pb'; +import { + Event, + Execution, + GetEventsByExecutionIDsResponse, + Value as MlmdValue, +} from '~/third_party/mlmd'; + +export type MlmdMetadataValueType = string | number | Struct | undefined; + +export const getExecutionDisplayName = (execution?: Execution | null): string => + execution?.getCustomPropertiesMap().get('display_name')?.getStringValue() || '(No name)'; + +export const getMlmdMetadataValue = (value?: MlmdValue): MlmdMetadataValueType => { + if (!value) { + return ''; + } + + switch (value.getValueCase()) { + case MlmdValue.ValueCase.DOUBLE_VALUE: + return value.getDoubleValue(); + case MlmdValue.ValueCase.INT_VALUE: + return value.getIntValue(); + case MlmdValue.ValueCase.STRING_VALUE: + return value.getStringValue(); + case MlmdValue.ValueCase.STRUCT_VALUE: + return value.getStructValue(); + default: + return ''; + } +}; + +export const parseEventsByType = ( + response: GetEventsByExecutionIDsResponse | null, +): Record => { + const events: Record = { + [Event.Type.UNKNOWN]: [], + [Event.Type.DECLARED_INPUT]: [], + [Event.Type.INPUT]: [], + [Event.Type.DECLARED_OUTPUT]: [], + [Event.Type.OUTPUT]: [], + [Event.Type.INTERNAL_INPUT]: [], + [Event.Type.INTERNAL_OUTPUT]: [], + [Event.Type.PENDING_OUTPUT]: [], + }; + + if (!response) { + return events; + } + + response.getEventsList().forEach((event) => { + const type = event.getType(); + const id = event.getArtifactId(); + if (type >= 0 && id > 0) { + events[type].push(event); + } + }); + + return events; +}; + +export const getArtifactNameFromEvent = (event: Event): string | undefined => + event.getPath()?.getStepsList()[0].getKey(); diff --git a/frontend/src/routes/pipelines/executions.ts b/frontend/src/routes/pipelines/executions.ts index 66a549a2af..7f8cccfe66 100644 --- a/frontend/src/routes/pipelines/executions.ts +++ b/frontend/src/routes/pipelines/executions.ts @@ -3,3 +3,11 @@ export const globExecutionsAll = `${executionsRootPath}/*`; export const executionsBaseRoute = (namespace: string | undefined): string => !namespace ? executionsRootPath : `${executionsRootPath}/${namespace}`; + +export const executionDetailsRoute = ( + namespace: string | undefined, + executionId: string | undefined, +): string => + !executionId + ? executionsBaseRoute(namespace) + : `${executionsBaseRoute(namespace)}/${executionId}`; diff --git a/frontend/src/typeHelpers.ts b/frontend/src/typeHelpers.ts index 491e86ab37..6df5120c04 100644 --- a/frontend/src/typeHelpers.ts +++ b/frontend/src/typeHelpers.ts @@ -163,3 +163,15 @@ export const isInEnum = (e: T) => (token: unknown): token is T[keyof T] => Object.values(e).includes(token as T[keyof T]); + +/** + * Pick keys from enum types + * enum MyEnum { + * A = 1; + * B = 2; + * } + * PickEnum + */ +export type PickEnum = { + [P in keyof K]: P extends K ? P : never; +};