diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts index 6365e5b2f8..d3e2251ad5 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/types.ts @@ -1,4 +1,5 @@ -import { Artifact, Context, ContextType, Event } from '~/third_party/mlmd'; +import { Artifact, Context, ContextType, Event, Execution } from '~/third_party/mlmd'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; export type MlmdContext = Context; @@ -8,9 +9,20 @@ 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 { +// each artifact is linked to an event +export type LinkedArtifact = { event: Event; artifact: Artifact; -} +}; + +// each execution can have multiple output artifacts +export type ExecutionArtifact = { + execution: Execution; + linkedArtifacts: LinkedArtifact[]; +}; + +// each run has multiple executions, each execution can have multiple artifacts +export type RunArtifact = { + run: PipelineRunKFv2; + executionArtifacts: ExecutionArtifact[]; +}; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactById.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactById.ts similarity index 100% rename from frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactById.ts rename to frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactById.ts diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts index 4f0ec1830d..708f094301 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes.ts @@ -1,22 +1,18 @@ import React from 'react'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import { GetArtifactTypesRequest } from '~/third_party/mlmd'; +import { ArtifactType, GetArtifactTypesRequest } from '~/third_party/mlmd'; import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; -export const useGetArtifactTypeMap = (): FetchState> => { +export const useGetArtifactTypes = (): FetchState => { const { metadataStoreServiceClient } = usePipelinesAPI(); - const call = React.useCallback>>(async () => { + 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; + return res.getArtifactTypesList(); }, [metadataStoreServiceClient]); - return useFetchState(call, {}); + return useFetchState(call, []); }; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactsList.ts similarity index 100% rename from frontend/src/pages/pipelines/global/experiments/artifacts/useGetArtifactsList.ts rename to frontend/src/concepts/pipelines/apiHooks/mlmd/useGetArtifactsList.ts diff --git a/frontend/src/concepts/pipelines/apiHooks/mlmd/useMlmdContext.ts b/frontend/src/concepts/pipelines/apiHooks/mlmd/useMlmdContext.ts index 68daf18600..894a274afb 100644 --- a/frontend/src/concepts/pipelines/apiHooks/mlmd/useMlmdContext.ts +++ b/frontend/src/concepts/pipelines/apiHooks/mlmd/useMlmdContext.ts @@ -1,13 +1,28 @@ import React from 'react'; import { MlmdContext, MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import { GetContextByTypeAndNameRequest } from '~/third_party/mlmd'; +import { + GetContextByTypeAndNameRequest, + MetadataStoreServicePromiseClient, +} from '~/third_party/mlmd'; import useFetchState, { FetchState, FetchStateCallbackPromise, NotReadyError, } from '~/utilities/useFetchState'; +export const getMlmdContext = async ( + client: MetadataStoreServicePromiseClient, + name: string, + type: MlmdContextTypes, +): Promise => { + const request = new GetContextByTypeAndNameRequest(); + request.setTypeName(type); + request.setContextName(name); + const res = await client.getContextByTypeAndName(request); + return res.getContext(); +}; + /** * A hook used to use the MLMD service and fetch the MLMD context * If being used without name/type, this hook will throw an error @@ -28,11 +43,7 @@ export const useMlmdContext = ( return Promise.reject(new NotReadyError('No context name')); } - const request = new GetContextByTypeAndNameRequest(); - request.setTypeName(type); - request.setContextName(name); - const res = await metadataStoreServiceClient.getContextByTypeAndName(request); - const context = res.getContext(); + const context = await getMlmdContext(metadataStoreServiceClient, name, type); if (!context) { return Promise.reject(new Error('Cannot find specified context')); } diff --git a/frontend/src/concepts/pipelines/content/artifacts/charts/ROCCurve.tsx b/frontend/src/concepts/pipelines/content/artifacts/charts/ROCCurve.tsx new file mode 100644 index 0000000000..e873c878c7 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/charts/ROCCurve.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { + Chart, + ChartAxis, + ChartGroup, + ChartLine, + ChartVoronoiContainer, +} from '@patternfly/react-charts'; +import { + chart_color_blue_100 as chartColorBlue100, + chart_color_blue_200 as chartColorBlue200, + chart_color_blue_300 as chartColorBlue300, + chart_color_blue_400 as chartColorBlue400, + chart_color_blue_500 as chartColorBlue500, + chart_color_cyan_100 as chartColorCyan100, + chart_color_cyan_200 as chartColorCyan200, + chart_color_cyan_300 as chartColorCyan300, + chart_color_cyan_400 as chartColorCyan400, + chart_color_cyan_500 as chartColorCyan500, + chart_color_black_100 as chartColorBlack100, +} from '@patternfly/react-tokens'; + +export type ROCCurveConfig = { + index: number; + data: { + name: string; + x: number; + y: number; + index: number; + }[]; +}; + +export const RocCurveChartColorScale = [ + chartColorBlue100.value, + chartColorBlue200.value, + chartColorBlue300.value, + chartColorBlue400.value, + chartColorBlue500.value, + chartColorCyan100.value, + chartColorCyan200.value, + chartColorCyan300.value, + chartColorCyan400.value, + chartColorCyan500.value, +]; + +type ROCCurveProps = { + configs: ROCCurveConfig[]; + maxDimension?: number; +}; + +const ROCCurve: React.FC = ({ configs, maxDimension }) => { + const width = maxDimension || 800; + const height = width; + const baseLineData = Array.from(Array(100).keys()).map((x) => ({ x: x / 100, y: x / 100 })); + + return ( +
+ `threshold (Series #${datum.index + 1}): ${datum.name}`} + /> + } + height={height} + width={width} + padding={{ bottom: 150, left: 100, right: 50, top: 50 }} + legendAllowWrap + legendPosition="bottom-left" + legendData={configs.map((config) => ({ + name: `Series #${config.index + 1}`, + symbol: { + fill: RocCurveChartColorScale[config.index % RocCurveChartColorScale.length], + type: 'square', + }, + }))} + > + x / 10)} + /> + x / 20)} + /> + + + {configs.map((config, idx) => ( + + ))} + + +
+ ); +}; + +export default ROCCurve; diff --git a/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.scss b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.scss new file mode 100644 index 0000000000..4303d49978 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.scss @@ -0,0 +1,70 @@ +.confusionMatrix { + margin: 20px; + margin-right: 75px; + display: flex; + } + + .confusionMatrix-table { + border-collapse: collapse; + } + + .confusionMatrix-cell { + border: solid 1px white; + position: relative; + text-align: center; + vertical-align: middle; + } + + .confusionMatrix-labelCell { + text-align: right; + white-space: nowrap; + padding-right: 10px; + } + + .confusionMatrix-gradientLegendOuter { + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .confusionMatrix-gradientLegend { + border-right: solid 1px #777; + margin-left: 30px; + min-width: 10px; + position: relative; + width: 10px; + } + + .confusionMatrix-gradientLegendMaxOuter { + top: 0; + border-top: solid 1px #777; + left: 100%; + padding-left: 5px; + position: absolute; + width: 5px; + } + + .confusionMatrix-gradientLegendMaxLabel { + left: 15px; + position: absolute; + top: -7px; + } + + .confusionMatrix-verticalMarker { + writing-mode: vertical-lr; + white-space: nowrap; + } + + .confusionMatrix-markerLabel { + border-top: solid 1px #777; + left: 100%; + padding-left: 5px; + position: absolute; + width: 5px; + } + + .confusionMatrix-trueLabel { + font-weight: bold; + padding-left: 20px; + } + \ No newline at end of file diff --git a/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.tsx b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.tsx new file mode 100644 index 0000000000..375360e288 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { Text } from '@patternfly/react-core'; +import './ConfusionMatrix.scss'; + +export type ConfusionMatrixInput = { + annotationSpecs: { + displayName: string; + }[]; + rows: { row: number[] }[]; +}; + +export interface ConfusionMatrixConfig { + data: number[][]; + labels: string[]; +} + +export function buildConfusionMatrixConfig( + confusionMatrix: ConfusionMatrixInput, +): ConfusionMatrixConfig { + return { + labels: confusionMatrix.annotationSpecs.map((annotation) => annotation.displayName), + data: confusionMatrix.rows.map((x) => x.row), + }; +} + +type ConfusionMatrixProps = { + config: ConfusionMatrixConfig; + size?: number; +}; + +const ConfusionMatrix: React.FC = ({ + config: { data, labels }, + size = 100, +}) => { + const max = Math.max(...data.flat()); + + // Function to get color based on the cell value + const getColor = (value: number) => { + const opacity = value / max; // Normalize the value to get opacity + return `rgba(41, 121, 255, ${opacity})`; // Use blue color with calculated opacity + }; + + // Determine the size for all cells, including labels + const cellSize = `${size}px`; + + // Generate the gradient for the legend + const gradientLegend = `linear-gradient(to bottom, rgba(41, 121, 255, 1) 0%, rgba(41, 121, 255, 0) 100%)`; + + return ( +
+ + + {data.map((row, rowIndex) => ( + + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + + + ))} + + +
+ {labels[rowIndex]} + + {cell} +
+ {labels.map((label, i) => ( + +
+ + {label} + +
+
+
+
+
+ {max} +
+ {new Array(5).fill(0).map((_, i) => ( +
+ + {Math.floor((i / 5) * max)} + +
+ ))} +
+
True label
+
+
+ ); +}; + +export default ConfusionMatrix; diff --git a/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/types.ts b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/types.ts new file mode 100644 index 0000000000..7eb050da3f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/types.ts @@ -0,0 +1,11 @@ +export type ConfusionMatrixInput = { + annotationSpecs: { + displayName: string; + }[]; + rows: { row: number[] }[]; +}; + +export interface ConfusionMatrixConfig { + data: number[][]; + labels: string[]; +} diff --git a/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils.ts b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils.ts new file mode 100644 index 0000000000..4076bd2943 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils.ts @@ -0,0 +1,10 @@ +import { ConfusionMatrixConfig, ConfusionMatrixInput } from './types'; + +export function buildConfusionMatrixConfig( + confusionMatrix: ConfusionMatrixInput, +): ConfusionMatrixConfig { + return { + labels: confusionMatrix.annotationSpecs.map((annotation) => annotation.displayName), + data: confusionMatrix.rows.map((x) => x.row), + }; +} diff --git a/frontend/src/concepts/pipelines/content/compareRuns/CompareRunsEmptyState.tsx b/frontend/src/concepts/pipelines/content/compareRuns/CompareRunsEmptyState.tsx new file mode 100644 index 0000000000..f20c8e110e --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/CompareRunsEmptyState.tsx @@ -0,0 +1,23 @@ +import { + EmptyState, + EmptyStateVariant, + EmptyStateHeader, + EmptyStateBody, +} from '@patternfly/react-core'; +import React from 'react'; + +type CompareRunsEmptyStateProps = Omit, 'children'> & { + title?: string; +}; + +export const CompareRunsEmptyState: React.FC = ({ + title = 'No runs selected', + ...props +}) => ( + + + + Select runs from the Run list to compare parameters. + + +); diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx new file mode 100644 index 0000000000..b016f14639 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { + Select, + SelectOption, + SelectList, + Text, + Stack, + StackItem, + MenuToggle, + Skeleton, + SplitItem, + Button, + Split, +} from '@patternfly/react-core'; +import { CompressIcon, ExpandIcon } from '@patternfly/react-icons'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; + +type ArtifactDisplayConfig = { config: T; title: string }; + +type PipelineRunArtifactSelectProps = { + run?: PipelineRunKFv2; + renderArtifact: (config: ArtifactDisplayConfig) => React.ReactNode; + data: ArtifactDisplayConfig[]; + setExpandedGraph: (config?: ArtifactDisplayConfig) => void; + expandedGraph?: ArtifactDisplayConfig; +}; + +export const PipelineRunArtifactSelect = ({ + run, + renderArtifact, + data, + setExpandedGraph, + expandedGraph, +}: PipelineRunArtifactSelectProps): React.ReactNode => { + const [isOpen, setIsOpen] = React.useState(false); + const [selectedItemTitles, setSelectedItemTitles] = React.useState( + data.map(({ title }) => title), + ); + + const selectedConfigs = data.filter(({ title }) => selectedItemTitles.includes(title)); + + return ( + + {!expandedGraph && run && ( + + + + )} + + {selectedConfigs.length === 0 && ( + + + + )} + {selectedConfigs.map((displayConfig) => ( + + + + + + {displayConfig.title} + + + + + + + + + {renderArtifact(displayConfig)} + + ))} + + ); +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx new file mode 100644 index 0000000000..60ec951f71 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { + Bullseye, + Divider, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateVariant, + Flex, + FlexItem, + Spinner, +} from '@patternfly/react-core'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; + +import { FullArtifactPath } from '~/concepts/pipelines/content/compareRuns/metricsSection/types'; +import { + getFullArtifactPaths, + getFullArtifactPathLabel, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; +import { PipelineRunArtifactSelect } from '~/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect'; +import { ConfusionMatrixConfig } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/types'; +import { buildConfusionMatrixConfig } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils'; +import ConfusionMatrix from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix'; +import { isConfusionMatrix } from './utils'; +import { ConfusionMatrixConfigAndTitle } from './types'; + +type ConfusionMatrixCompareProps = { + runArtifacts?: RunArtifact[]; + isLoaded: boolean; +}; + +const ConfusionMatrixCompare: React.FC = ({ + runArtifacts, + isLoaded, +}) => { + const [expandedGraph, setExpandedGraph] = React.useState< + ConfusionMatrixConfigAndTitle | undefined + >(undefined); + + const fullArtifactPaths: FullArtifactPath[] = React.useMemo(() => { + if (!runArtifacts) { + return []; + } + + return getFullArtifactPaths(runArtifacts); + }, [runArtifacts]); + + const { configMap, runMap } = React.useMemo( + () => + fullArtifactPaths.reduce( + (acc, fullPath) => { + const customProperties = fullPath.linkedArtifact.artifact.getCustomPropertiesMap(); + const data = customProperties.get('confusionMatrix')?.getStructValue()?.toJavaScript(); + + if (data) { + const confusionMatrixData = data.struct as unknown; + if (isConfusionMatrix(confusionMatrixData)) { + const runId = fullPath.run.run_id; + const title = getFullArtifactPathLabel(fullPath); + const metric = { + title, + config: buildConfusionMatrixConfig(confusionMatrixData), + }; + + // Add run to runMapBuilder + acc.runMap[runId] = fullPath.run; + + // Add or append the metric to the configMapBuilder + if (runId in acc.configMap) { + acc.configMap[runId].push(metric); + } else { + acc.configMap[runId] = [metric]; + } + } + } + + return acc; + }, + { + runMap: {} as Record, + configMap: {} as Record, + }, + ), + [fullArtifactPaths], + ); + + if (!isLoaded) { + return ( + + + + ); + } + + if (!runArtifacts || runArtifacts.length === 0) { + return ; + } + if (Object.keys(configMap).length === 0) { + return ( + + + + There are no confusion matrix artifacts available on the selected runs. + + + ); + } + + return ( +
+ {expandedGraph ? ( + + setExpandedGraph(config)} + expandedGraph={expandedGraph} + renderArtifact={(config) => } + /> + + ) : ( + + {Object.entries(configMap).map(([runId, matrixData]) => ( + + + setExpandedGraph(config)} + expandedGraph={expandedGraph} + renderArtifact={(config) => } + /> + + + + ))} + + )} +
+ ); +}; +export default ConfusionMatrixCompare; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/types.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/types.ts new file mode 100644 index 0000000000..23fa3b6b48 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/types.ts @@ -0,0 +1,6 @@ +import { ConfusionMatrixConfig } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/types'; + +export type ConfusionMatrixConfigAndTitle = { + title: string; + config: ConfusionMatrixConfig; +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts new file mode 100644 index 0000000000..47fea29c76 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts @@ -0,0 +1,16 @@ +import { ConfusionMatrixInput } from '~/concepts/pipelines/content/artifacts/charts/confusionMatrix/types'; + +export const isConfusionMatrix = (obj: unknown): obj is ConfusionMatrixInput => { + const matrix = obj as ConfusionMatrixInput; + return ( + Array.isArray(matrix.annotationSpecs) && + matrix.annotationSpecs.every( + (annotationSpec) => + !!annotationSpec.displayName && typeof annotationSpec.displayName === 'string', + ) && + Array.isArray(matrix.rows) && + matrix.rows.every( + (row) => Array.isArray(row.row) && row.row.every((value) => typeof value === 'number'), + ) + ); +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/const.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/const.ts new file mode 100644 index 0000000000..bd374c85d4 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/const.ts @@ -0,0 +1,14 @@ +export enum MetricSectionTabLabels { + SCALAR = 'Scalar metrics', + CONFUSION_MATRIX = 'Confusion matrix', + ROC_CURVE = 'ROC curve', + MARKDOWN = 'Markdown', +} + +export enum MetricsType { + SCALAR_METRICS, + CONFUSION_MATRIX, + ROC_CURVE, + HTML, + MARKDOWN, +} diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx new file mode 100644 index 0000000000..322a379fc5 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx @@ -0,0 +1,136 @@ +import * as React from 'react'; +import { + Bullseye, + Divider, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateVariant, + Flex, + FlexItem, + Spinner, +} from '@patternfly/react-core'; + +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; + +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { FullArtifactPath } from '~/concepts/pipelines/content/compareRuns/metricsSection/types'; +import { + getFullArtifactPaths, + getFullArtifactPathLabel, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; +import { PipelineRunArtifactSelect } from '~/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect'; +import MarkdownView from '~/components/MarkdownView'; + +type MarkdownCompareProps = { + runArtifacts?: RunArtifact[]; + isLoaded: boolean; +}; + +export type MarkdownAndTitle = { + title: string; + config: string; +}; + +const MarkdownCompare: React.FC = ({ runArtifacts, isLoaded }) => { + const [expandedGraph, setExpandedGraph] = React.useState(undefined); + + const fullArtifactPaths: FullArtifactPath[] = React.useMemo(() => { + if (!runArtifacts) { + return []; + } + + return getFullArtifactPaths(runArtifacts); + }, [runArtifacts]); + + const { configMap, runMap } = React.useMemo(() => { + const configMapBuilder: Record = {}; + const runMapBuilder: Record = {}; + + fullArtifactPaths + .map((fullPath) => ({ + run: fullPath.run, + title: getFullArtifactPathLabel(fullPath), + uri: fullPath.linkedArtifact.artifact.getUri(), + })) + .filter((markdown) => !!markdown.uri) + .forEach(async ({ uri, title, run }) => { + const data = uri; // TODO: fetch data from uri: https://issues.redhat.com/browse/RHOAIENG-7206 + + runMapBuilder[run.run_id] = run; + + const config = { + title, + config: data, + }; + + if (run.run_id in configMapBuilder) { + configMapBuilder[run.run_id].push(config); + } else { + configMapBuilder[run.run_id] = [config]; + } + }); + + return { configMap: configMapBuilder, runMap: runMapBuilder }; + }, [fullArtifactPaths]); + + if (!isLoaded) { + return ( + + + + ); + } + + if (!runArtifacts || runArtifacts.length === 0) { + return ; + } + if (Object.keys(configMap).length === 0) { + return ( + + + + There are no markdown artifacts available on the selected runs. + + + ); + } + + return ( +
+ {expandedGraph ? ( + + setExpandedGraph(config)} + expandedGraph={expandedGraph} + renderArtifact={(config) => } + /> + + ) : ( + + {Object.entries(configMap).map(([runId, configs]) => ( + + + setExpandedGraph(config)} + expandedGraph={expandedGraph} + renderArtifact={(config) => } + /> + + + + ))} + + )} +
+ ); +}; +export default MarkdownCompare; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare.tsx new file mode 100644 index 0000000000..77593e4b8a --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateVariant, + Flex, + Spinner, + Split, + SplitItem, + Text, +} from '@patternfly/react-core'; +import DashboardHelpTooltip from '~/concepts/dashboard/DashboardHelpTooltip'; +import { useCheckboxTableBase } from '~/components/table'; +import ROCCurve from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { + getFullArtifactPathLabel, + getFullArtifactPaths, + getLinkedArtifactId, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; +import RocCurveTable from './RocCurveTable'; +import { FullArtifactPathsAndConfig } from './types'; +import { isConfidenceMetric, buildRocCurveConfig } from './utils'; + +type RocCurveCompareProps = { + runArtifacts?: RunArtifact[]; + isLoaded: boolean; +}; + +const RocCurveCompare: React.FC = ({ runArtifacts, isLoaded }) => { + const [search, setSearch] = React.useState(''); + const [selected, setSelected] = React.useState([]); + + const configs = React.useMemo(() => { + if (!runArtifacts) { + return []; + } + + const fullArtifactPaths = getFullArtifactPaths(runArtifacts); + return fullArtifactPaths + .map((fullArtifactPath) => { + const customProperties = fullArtifactPath.linkedArtifact.artifact.getCustomPropertiesMap(); + return { + data: customProperties.get('confidenceMetrics')?.getStructValue()?.toJavaScript(), + fullArtifactPath, + }; + }) + .filter((confidenceMetrics) => !!confidenceMetrics.data) + .map(({ data, fullArtifactPath }, i) => { + // validate the custom properties + const confidenceMetricsArray = data?.list as unknown[]; + if (!confidenceMetricsArray.every(isConfidenceMetric)) { + throw new Error('Invalid confidence metrics data'); + } + return { + config: buildRocCurveConfig(confidenceMetricsArray, i), + fullArtifactPath, + }; + }); + }, [runArtifacts]); + + // Set the selected artifacts to all by default + React.useEffect(() => { + setSelected(configs); + }, [configs]); + + const checkboxTableProps = useCheckboxTableBase( + configs, + selected, + setSelected, + React.useCallback( + ({ fullArtifactPath }) => getLinkedArtifactId(fullArtifactPath.linkedArtifact), + [], + ), + ); + + const filteredConfigs = configs.filter(({ fullArtifactPath }) => + getFullArtifactPathLabel(fullArtifactPath).includes(search), + ); + + if (!isLoaded) { + return ( + + + + ); + } + + if (!runArtifacts || runArtifacts.length === 0) { + return ; + } + if (Object.keys(configs).length === 0) { + return ( + + + + There are no ROC curve artifacts available on the selected runs. + + + ); + } + + return ( + + + + ROC Curve: multiple artifacts + + + + + + r.config)} /> + + + ); +}; +export default RocCurveCompare; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTable.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTable.tsx new file mode 100644 index 0000000000..677ceb451f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTable.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { TextInput, ToolbarItem } from '@patternfly/react-core'; +import { Table, useCheckboxTableBase } from '~/components/table'; +import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; +import { getLinkedArtifactId } from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import RocCurveTableRow from './RocCurveTableRow'; +import { rocCurveColumns } from './const'; +import { FullArtifactPathsAndConfig } from './types'; + +type RocCurveTableBaseProps = ReturnType< + typeof useCheckboxTableBase +> & { + fullArtifactPaths: FullArtifactPathsAndConfig[]; + setSearch: (search: string) => void; + isSelected: (id: FullArtifactPathsAndConfig) => boolean; + toggleSelection: (id: FullArtifactPathsAndConfig) => void; +}; + +const RocCurveTable: React.FC = ({ + fullArtifactPaths, + setSearch, + isSelected, + toggleSelection, + ...checkboxTableProps +}) => ( + setSearch('')} />} + toolbarContent={ + + setSearch(value)} + /> + + } + rowRenderer={(fullArtifactPathAndConfig) => ( + toggleSelection(fullArtifactPathAndConfig)} + fullArtifactPathAndConfig={fullArtifactPathAndConfig} + /> + )} + data-testid="roc-curve-filter-table" + variant="compact" + /> +); +export default RocCurveTable; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTableRow.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTableRow.tsx new file mode 100644 index 0000000000..a4f15c9127 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTableRow.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Td, Tr } from '@patternfly/react-table'; +import { ChartLegend } from '@patternfly/react-charts'; +import { CheckboxTd } from '~/components/table'; +import { RocCurveChartColorScale } from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; +import { + getFullArtifactPathLabel, + getLinkedArtifactId, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { FullArtifactPathsAndConfig } from './types'; + +type RocCurveTableRowProps = { + isChecked: boolean; + onToggleCheck: () => void; + fullArtifactPathAndConfig: FullArtifactPathsAndConfig; +}; + +const RocCurveTableRow: React.FC = ({ + isChecked, + onToggleCheck, + fullArtifactPathAndConfig: { fullArtifactPath, config }, +}) => ( + + + + + + +); + +export default RocCurveTableRow; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/const.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/const.ts new file mode 100644 index 0000000000..8d9278be8c --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/const.ts @@ -0,0 +1,25 @@ +import { SortableData, checkboxTableColumn } from '~/components/table'; +import { getFullArtifactPathLabel } from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { FullArtifactPathsAndConfig } from './types'; + +export const rocCurveColumns: SortableData[] = [ + checkboxTableColumn(), + { + label: 'Execution name > Artifact name', + field: 'execution-name-artifact-name', + sortable: (a, b) => + getFullArtifactPathLabel(a.fullArtifactPath).localeCompare( + getFullArtifactPathLabel(b.fullArtifactPath), + ), + }, + { + label: 'Run name', + field: 'run-name', + sortable: false, + }, + { + label: 'Curve legend', + field: 'curve-legend', + sortable: false, + }, +]; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/types.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/types.ts new file mode 100644 index 0000000000..4d2c575d15 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/types.ts @@ -0,0 +1,13 @@ +import { ROCCurveConfig } from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; +import { FullArtifactPath } from '~/concepts/pipelines/content/compareRuns/metricsSection/types'; + +export type ConfidenceMetric = { + confidenceThreshold: number; + falsePositiveRate: number; + recall: number; +}; + +export type FullArtifactPathsAndConfig = { + fullArtifactPath: FullArtifactPath; + config: ROCCurveConfig; +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts new file mode 100644 index 0000000000..36d8f8ac5f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts @@ -0,0 +1,24 @@ +import { ROCCurveConfig } from '~/concepts/pipelines/content/artifacts/charts/ROCCurve'; +import { ConfidenceMetric } from './types'; + +export const isConfidenceMetric = (obj: unknown): obj is ConfidenceMetric => { + const metric = obj as ConfidenceMetric; + return ( + typeof metric.confidenceThreshold === 'number' && + typeof metric.falsePositiveRate === 'number' && + typeof metric.recall === 'number' + ); +}; + +export const buildRocCurveConfig = ( + confidenceMetricsArray: ConfidenceMetric[], + index: number, +): ROCCurveConfig => ({ + index, + data: confidenceMetricsArray.map((metric) => ({ + name: metric.confidenceThreshold.toString(), + x: metric.falsePositiveRate, + y: metric.recall, + index, + })), +}); diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/ScalarMetricTable.tsx b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/ScalarMetricTable.tsx new file mode 100644 index 0000000000..8bb866d334 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/ScalarMetricTable.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { InnerScrollContainer, TableVariant, Td, Tr } from '@patternfly/react-table'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateVariant, + Flex, + Spinner, + Switch, +} from '@patternfly/react-core'; +import { Table } from '~/components/table'; +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; +import { ScalarTableData } from './types'; +import { generateTableStructure } from './utils'; + +type ScalarMetricTableProps = { + runArtifacts?: RunArtifact[]; + isLoaded: boolean; +}; + +const ScalarMetricTable: React.FC = ({ runArtifacts, isLoaded }) => { + const { columns, data, subColumns } = generateTableStructure(runArtifacts ?? []); + + const [isHideSameRowsChecked, setIsHideSameRowsChecked] = React.useState(false); + + const hasScalarMetrics = data.length > 0 || !runArtifacts; + + const rowRenderer = ({ key, values }: ScalarTableData) => { + // Hide rows with no differences if the switch is on + if (values.every((value) => value === values[0]) && isHideSameRowsChecked) { + return null; + } + + return ( + + + + {values.map((value, index) => { + const hasRightBorder = index !== values.length - 1 && { + hasRightBorder: true, + }; + + return ( + + ); + })} + + ); + }; + + if (!isLoaded) { + return ( + + + + ); + } + + if (!runArtifacts || runArtifacts.length === 0) { + return ; + } + if (!hasScalarMetrics) { + return ( + + + + There are no scalar metric artifacts available on the selected runs. + + + ); + } + + return ( + + setIsHideSameRowsChecked(checked)} + id="hide-same-scalar-metrics-switch" + data-testid="hide-same-scalar-metrics-switch" + /> + + +
{getFullArtifactPathLabel(fullArtifactPath)}{fullArtifactPath.run.display_name} + +
+ {key} + + {value} +
+ + + ); +}; + +export default ScalarMetricTable; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/types.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/types.ts new file mode 100644 index 0000000000..8dbf478868 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/types.ts @@ -0,0 +1,19 @@ +import { SortableData } from '~/components/table'; + +export type XParentLabel = { + label: string; + colSpan: number; +}; + +export type ScalarRowData = { + row: string[]; + dataCount: number; +}; + +export type ScalarTableData = { key: string; values: string[] }; + +export type ScalarTableProps = { + columns: SortableData[]; + subColumns: SortableData[]; + data: ScalarTableData[]; +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/utils.ts new file mode 100644 index 0000000000..37f503f431 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/utils.ts @@ -0,0 +1,95 @@ +import { SortableData } from '~/components/table'; +import { + getExecutionDisplayName, + getArtifactName, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { getMlmdMetadataValue } from '~/pages/pipelines/global/experiments/executions/utils'; +import { ScalarTableProps, XParentLabel, ScalarRowData, ScalarTableData } from './types'; + +export const generateTableStructure = (scalarMetricsArtifacts: RunArtifact[]): ScalarTableProps => { + const subColumnLabels: string[] = []; + const columnLabels: XParentLabel[] = []; + const dataMap: { [key: string]: ScalarRowData } = {}; + + scalarMetricsArtifacts.forEach((runArtifact) => { + const runName = runArtifact.run.display_name || '-'; + const startArtifactIndex = subColumnLabels.length; + + runArtifact.executionArtifacts.forEach((executionArtifact) => { + executionArtifact.linkedArtifacts.forEach((linkedArtifact) => { + const xLabel = `${getExecutionDisplayName(executionArtifact.execution) || '-'} > ${ + getArtifactName(linkedArtifact) || '-' + }`; + subColumnLabels.push(xLabel); + + const customProperties = linkedArtifact.artifact.getCustomPropertiesMap(); + + customProperties.getEntryList().forEach(([key]) => { + if (key !== 'display_name') { + if (!(key in dataMap)) { + dataMap[key] = { row: Array(subColumnLabels.length).fill(''), dataCount: 0 }; + } + dataMap[key].row[subColumnLabels.length - 1] = JSON.stringify( + getMlmdMetadataValue(customProperties.get(key)), + ); + dataMap[key].dataCount++; + } + }); + }); + }); + + columnLabels.push({ label: runName, colSpan: subColumnLabels.length - startArtifactIndex }); + }); + + // Prepare columns and subColumns + const columns: SortableData[] = [ + { + label: 'Run name', + field: 'run-name', + isStickyColumn: true, + hasRightBorder: true, + className: 'pf-v5-u-background-color-200', + sortable: false, + }, + ...columnLabels.map( + (parent, index): SortableData => ({ + label: parent.label, + field: parent.label, + colSpan: parent.colSpan, + sortable: false, + modifier: 'nowrap', + hasRightBorder: index !== columnLabels.length - 1, + }), + ), + ]; + + const subColumns: SortableData[] = [ + { + label: 'Execution name > Artifact name', + field: 'execution-name-artifact-name', + isStickyColumn: true, + hasRightBorder: true, + className: 'pf-v5-u-background-color-200', + sortable: false, + }, + ...subColumnLabels.map( + (label, index): SortableData => ({ + label, + field: label, + sortable: false, + modifier: 'nowrap', + hasRightBorder: index !== subColumnLabels.length - 1, + }), + ), + ]; + + // Prepare data rows and sort them + const sortedDataList = Object.entries(dataMap).sort((a, b) => b[1].dataCount - a[1].dataCount); + + return { + columns, + subColumns, + data: sortedDataList.map(([key]) => ({ key, values: dataMap[key].row })), + }; +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/types.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/types.ts new file mode 100644 index 0000000000..021402a88c --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/types.ts @@ -0,0 +1,19 @@ +// An artifact which has associated event. + +import { LinkedArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import { Artifact, Event, Execution } from '~/third_party/mlmd'; + +// each run can have multiple executions, artifacts, and events +export type PipelineRunRelatedMlmd = { + run: PipelineRunKFv2; + executions: Execution[]; + artifacts: Artifact[]; + events: Event[]; +}; + +export type FullArtifactPath = { + linkedArtifact: LinkedArtifact; + run: PipelineRunKFv2; + execution: Execution; +}; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns.ts new file mode 100644 index 0000000000..eb4ca684d7 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns.ts @@ -0,0 +1,74 @@ +import React from 'react'; +import { MlmdContextTypes } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { getMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useMlmdContext'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState'; +import { + GetArtifactsByContextRequest, + GetEventsByExecutionIDsRequest, + GetExecutionsByContextRequest, +} from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb'; +import { PipelineRunRelatedMlmd } from './types'; + +const useMlmdPackagesForPipelineRuns = ( + runs: PipelineRunKFv2[], +): FetchState => { + const { metadataStoreServiceClient } = usePipelinesAPI(); + + const call = React.useCallback>( + () => + Promise.all( + runs.map((run) => + getMlmdContext(metadataStoreServiceClient, run.run_id, MlmdContextTypes.RUN).then( + async (context) => { + if (!context) { + throw new Error(`No context for run: ${run.run_id}`); + } + // get artifacts + const artifactRequest = new GetArtifactsByContextRequest(); + artifactRequest.setContextId(context.getId()); + const artifactRes = await metadataStoreServiceClient.getArtifactsByContext( + artifactRequest, + ); + const artifacts = artifactRes.getArtifactsList(); + + // get executions + const executionRequest = new GetExecutionsByContextRequest(); + executionRequest.setContextId(context.getId()); + const executionRes = await metadataStoreServiceClient.getExecutionsByContext( + executionRequest, + ); + const executions = executionRes.getExecutionsList(); + + // get events + const eventRequest = new GetEventsByExecutionIDsRequest(); + executions.forEach((exec) => { + const execId = exec.getId(); + if (!execId) { + throw new Error('Execution must have an ID'); + } + eventRequest.addExecutionIds(execId); + }); + const eventRes = await metadataStoreServiceClient.getEventsByExecutionIDs( + eventRequest, + ); + const events = eventRes.getEventsList(); + + return { + run, + artifacts, + events, + executions, + }; + }, + ), + ), + ), + [metadataStoreServiceClient, runs], + ); + + return useFetchState(call, []); +}; + +export default useMlmdPackagesForPipelineRuns; diff --git a/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts new file mode 100644 index 0000000000..38a034f605 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts @@ -0,0 +1,145 @@ +import { ArtifactType, Event, Execution } from '~/third_party/mlmd'; + +import { + FullArtifactPath, + PipelineRunRelatedMlmd, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/types'; +import { + LinkedArtifact, + RunArtifact, + ExecutionArtifact, +} from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { MetricsType } from './const'; + +export const getLinkedArtifactId = (linkedArtifact: LinkedArtifact): string => + `${linkedArtifact.event.getExecutionId()}-${linkedArtifact.event.getArtifactId()}`; + +const metricsTypeToFilter = (metricsType: MetricsType): string => { + switch (metricsType) { + case MetricsType.SCALAR_METRICS: + return 'system.Metrics'; + case MetricsType.CONFUSION_MATRIX: + return 'system.ClassificationMetrics'; + case MetricsType.ROC_CURVE: + return 'system.ClassificationMetrics'; + case MetricsType.HTML: + return 'system.HTML'; + case MetricsType.MARKDOWN: + return 'system.Markdown'; + default: + return ''; + } +}; + +function filterLinkedArtifactsByType( + artifactTypeName: string, + artifactTypes: ArtifactType[], + artifacts: LinkedArtifact[], +): LinkedArtifact[] { + const artifactTypeIds = artifactTypes + .filter((artifactType) => artifactType.getName() === artifactTypeName) + .map((artifactType) => artifactType.getId()); + return artifacts.filter((x) => artifactTypeIds.includes(x.artifact.getTypeId())); +} +export const getRunArtifacts = (mlmdPackages: PipelineRunRelatedMlmd[]): RunArtifact[] => + mlmdPackages.map((mlmdPackage) => { + const events = mlmdPackage.events.filter((e) => e.getType() === Event.Type.OUTPUT); + + // Match artifacts to executions. + const artifactMap = new Map(); + mlmdPackage.artifacts.forEach((artifact) => artifactMap.set(artifact.getId(), artifact)); + const executionArtifacts = mlmdPackage.executions.map((execution) => { + const executionEvents = events.filter((e) => e.getExecutionId() === execution.getId()); + const linkedArtifacts: LinkedArtifact[] = []; + for (const event of executionEvents) { + const artifactId = event.getArtifactId(); + const artifact = artifactMap.get(artifactId); + if (artifact) { + linkedArtifacts.push({ + event, + artifact, + } as LinkedArtifact); + } else { + throw new Error(`The artifact with the following ID was not found: ${artifactId}`); + } + } + return { + execution, + linkedArtifacts, + } as ExecutionArtifact; + }); + return { + run: mlmdPackage.run, + executionArtifacts, + } as RunArtifact; + }); + +export const filterRunArtifactsByType = ( + runArtifacts: RunArtifact[], + artifactTypes: ArtifactType[], + metricsType: MetricsType, +): RunArtifact[] => { + const metricsFilter = metricsTypeToFilter(metricsType); + const typeRuns: RunArtifact[] = []; + for (const runArtifact of runArtifacts) { + const typeExecutions: ExecutionArtifact[] = []; + for (const e of runArtifact.executionArtifacts) { + let typeArtifacts: LinkedArtifact[] = filterLinkedArtifactsByType( + metricsFilter, + artifactTypes, + e.linkedArtifacts, + ); + if (metricsType === MetricsType.CONFUSION_MATRIX) { + typeArtifacts = typeArtifacts.filter((x) => + x.artifact.getCustomPropertiesMap().has('confusionMatrix'), + ); + } else if (metricsType === MetricsType.ROC_CURVE) { + typeArtifacts = typeArtifacts.filter((x) => + x.artifact.getCustomPropertiesMap().has('confidenceMetrics'), + ); + } + if (typeArtifacts.length > 0) { + typeExecutions.push({ + execution: e.execution, + linkedArtifacts: typeArtifacts, + } as ExecutionArtifact); + } + } + if (typeExecutions.length > 0) { + typeRuns.push({ + run: runArtifact.run, + executionArtifacts: typeExecutions, + } as RunArtifact); + } + } + return typeRuns; +}; + +// general utils +export function getExecutionDisplayName(execution: Execution): string | undefined { + return execution.getCustomPropertiesMap().get('display_name')?.getStringValue(); +} + +export function getArtifactNameFromEvent(event: Event): string | undefined { + return event.getPath()?.getStepsList()[0].getKey(); +} + +export function getArtifactName(linkedArtifact: LinkedArtifact): string | undefined { + return getArtifactNameFromEvent(linkedArtifact.event); +} + +export const getFullArtifactPathLabel = (fullArtifactPath: FullArtifactPath): string => + `${getExecutionDisplayName(fullArtifactPath.execution)} > ${getArtifactName( + fullArtifactPath.linkedArtifact, + )}`; + +export const getFullArtifactPaths = (runArtifacts: RunArtifact[]): FullArtifactPath[] => + runArtifacts.flatMap((runArtifact) => + runArtifact.executionArtifacts.flatMap((executionArtifact) => + executionArtifact.linkedArtifacts.map((linkedArtifact) => ({ + linkedArtifact, + run: runArtifact.run, + execution: executionArtifact.execution, + })), + ), + ); diff --git a/frontend/src/concepts/pipelines/context/usePipelinesUiRoute.ts b/frontend/src/concepts/pipelines/context/usePipelinesUiRoute.ts deleted file mode 100644 index 29349e5c9e..0000000000 --- a/frontend/src/concepts/pipelines/context/usePipelinesUiRoute.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PIPELINE_ROUTE_NAME_PREFIX } from '~/concepts/pipelines/const'; -import { usePipelinesAPI } from './PipelinesContext'; -import usePipelineNamespaceCR, { dspaLoaded } from './usePipelineNamespaceCR'; -import usePipelinesAPIRoute from './usePipelinesAPIRoute'; - -export const usePipelinesUiRoute = (): [string, boolean] => { - const { namespace } = usePipelinesAPI(); - const crState = usePipelineNamespaceCR(namespace); - const isCrReady = dspaLoaded(crState); - const [pipelinesApiRoute, isPipelinesApiRouteLoaded] = usePipelinesAPIRoute( - isCrReady, - crState[0]?.metadata.name ?? '', - namespace, - ); - let uiRoute = ''; - - if (pipelinesApiRoute) { - const [protocol, appHost] = pipelinesApiRoute.split(PIPELINE_ROUTE_NAME_PREFIX); - uiRoute = `${protocol}${PIPELINE_ROUTE_NAME_PREFIX}ui-${appHost}`; - } - - return [uiRoute, isPipelinesApiRouteLoaded]; -}; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx index 11182e0976..ac2dc7a688 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactDetails.tsx @@ -20,9 +20,9 @@ import { ExclamationCircleIcon } from '@patternfly/react-icons'; import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/types'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import { useGetArtifactById } from '~/pages/pipelines/global/experiments/artifacts/useGetArtifactById'; import { getArtifactName } from '~/pages/pipelines/global/experiments/artifacts/utils'; import { ArtifactDetailsTabKey } from '~/pages/pipelines/global/experiments/artifacts/constants'; +import { useGetArtifactById } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactById'; import { ArtifactOverviewDetails } from './ArtifactOverviewDetails'; export const ArtifactDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) => { diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx index a1fd6c7f82..7697eaa293 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactDetails/ArtifactOverviewDetails.tsx @@ -12,68 +12,56 @@ import { } from '@patternfly/react-core'; import { Artifact } from '~/third_party/mlmd'; -import { usePipelinesUiRoute } from '~/concepts/pipelines/context/usePipelinesUiRoute'; -import { ArtifactUriLink } from '~/pages/pipelines/global/experiments/artifacts/ArtifactUriLink'; import { ArtifactPropertyDescriptionList } from './ArtifactPropertyDescriptionList'; interface ArtifactOverviewDetailsProps { artifact: Artifact.AsObject | undefined; } -export const ArtifactOverviewDetails: React.FC = ({ artifact }) => { - const [pipelinesUiRoute, isPipelinesUiRouteLoaded] = usePipelinesUiRoute(); +export const ArtifactOverviewDetails: React.FC = ({ artifact }) => ( + + + + Live system dataset + + + {artifact?.uri && ( + <> + URI + {artifact.uri} + + )} + + + + - return ( - + {!!artifact?.propertiesMap.length && ( - Live system dataset - - - {artifact?.uri && ( - <> - URI - - - - - )} - - + Properties + + )} - {!!artifact?.propertiesMap.length && ( - - - Properties - - - - )} - - {!!artifact?.customPropertiesMap.length && ( - - - Custom properties - - - - )} - - ); -}; + {!!artifact?.customPropertiesMap.length && ( + + + Custom properties + + + + )} + +); diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx deleted file mode 100644 index 6e8bcacc4d..0000000000 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -import { Flex, FlexItem, Skeleton, Truncate } from '@patternfly/react-core'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons'; - -import { generateGcsConsoleUri, generateMinioArtifactUrl, generateS3ArtifactUrl } from './utils'; - -interface ArtifactUriLinkProps { - uri: string; - downloadHost: string; - isLoaded?: boolean; -} - -export const ArtifactUriLink: React.FC = ({ - uri, - downloadHost, - isLoaded = true, -}) => { - let uriLinkTo = ''; - - if (!isLoaded) { - return ; - } - - if (uri.startsWith('gs:')) { - uriLinkTo = generateGcsConsoleUri(uri); - } - - if (uri.startsWith('s3:')) { - uriLinkTo = `${downloadHost}/${generateS3ArtifactUrl(uri)}`; - } - - if (uri.startsWith('http:') || uri.startsWith('https:')) { - uriLinkTo = uri; - } - - if (uri.startsWith('minio:')) { - uriLinkTo = `${downloadHost}/${generateMinioArtifactUrl(uri)}`; - } - - return uriLinkTo ? ( - - - - - - - - - - - - ) : ( - uri - ); -}; diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsList.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsList.tsx index b35fcb139c..68ec98d277 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsList.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsList.tsx @@ -12,7 +12,7 @@ import { import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; import { useMlmdListContext } from '~/concepts/pipelines/context'; -import { useGetArtifactsList } from './useGetArtifactsList'; +import { useGetArtifactsList } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactsList'; import { ArtifactsTable } from './ArtifactsTable'; export const ArtifactsList: React.FC = () => { diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx index 87681a338b..1e3a65eed7 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactsTable.tsx @@ -13,10 +13,8 @@ import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { ArtifactType } from '~/concepts/pipelines/kfTypes'; import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context'; import { artifactsDetailsRoute } from '~/routes'; -import { usePipelinesUiRoute } from '~/concepts/pipelines/context/usePipelinesUiRoute'; import { FilterOptions, columns, initialFilterData, options } from './constants'; import { getArtifactName } from './utils'; -import { ArtifactUriLink } from './ArtifactUriLink'; interface ArtifactsTableProps { artifacts: Artifact[] | null | undefined; @@ -36,7 +34,6 @@ export const ArtifactsTable: React.FC = ({ setMaxResultSize, } = useMlmdListContext(nextPageToken); const { namespace } = usePipelinesAPI(); - const [pipelinesUiRoute, isPipelinesUiRouteLoaded] = usePipelinesUiRoute(); const [page, setPage] = React.useState(1); const [filterData, setFilterData] = React.useState(initialFilterData); const onClearFilters = React.useCallback(() => setFilterData(initialFilterData), []); @@ -152,19 +149,13 @@ export const ArtifactsTable: React.FC = ({ - + ), - [isPipelinesUiRouteLoaded, namespace, pipelinesUiRoute], + [namespace], ); return ( diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx index cf353f448e..9da0401ea1 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactDetails.spec.tsx @@ -6,10 +6,9 @@ import '@testing-library/jest-dom'; import { Artifact } from '~/third_party/mlmd'; import { artifactsBaseRoute } from '~/routes'; -import * as useGetArtifactById from '~/pages/pipelines/global/experiments/artifacts/useGetArtifactById'; -import * as usePipelinesUiRoute from '~/concepts/pipelines/context/usePipelinesUiRoute'; import { ArtifactDetails } from '~/pages/pipelines/global/experiments/artifacts/ArtifactDetails'; import GlobalPipelineCoreDetails from '~/pages/pipelines/global/GlobalPipelineCoreDetails'; +import * as useGetArtifactById from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactById'; jest.mock('~/redux/selectors', () => ({ ...jest.requireActual('~/redux/selectors'), @@ -38,7 +37,6 @@ jest.mock('~/concepts/pipelines/context/PipelinesContext', () => ({ describe('ArtifactDetails', () => { const useGetArtifactByIdSpy = jest.spyOn(useGetArtifactById, 'useGetArtifactById'); - const usePipelinesUiRouteSpy = jest.spyOn(usePipelinesUiRoute, 'usePipelinesUiRoute'); beforeEach(() => { useGetArtifactByIdSpy.mockReturnValue([ @@ -72,8 +70,6 @@ describe('ArtifactDetails', () => { undefined, jest.fn(), ]); - - usePipelinesUiRouteSpy.mockReturnValue(['dspa-pipeline-ui-route', true]); }); it('renders page breadcrumbs', () => { diff --git a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx index e0f69c2c1d..124ee72372 100644 --- a/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx +++ b/frontend/src/pages/pipelines/global/experiments/artifacts/__tests__/ArtifactsTable.spec.tsx @@ -5,9 +5,8 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { Artifact } from '~/third_party/mlmd'; -import * as useGetArtifactsList from '~/pages/pipelines/global/experiments/artifacts/useGetArtifactsList'; +import * as useGetArtifactsList from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactsList'; import * as MlmdListContext from '~/concepts/pipelines/context/MlmdListContext'; -import * as usePipelinesUiRoute from '~/concepts/pipelines/context/usePipelinesUiRoute'; import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; import { ArtifactsList } from '~/pages/pipelines/global/experiments/artifacts/ArtifactsList'; @@ -40,7 +39,6 @@ jest.mock('~/concepts/pipelines/context/PipelinesContext', () => ({ describe('ArtifactsTable', () => { const useGetArtifactsListSpy = jest.spyOn(useGetArtifactsList, 'useGetArtifactsList'); const useMlmdListContextSpy = jest.spyOn(MlmdListContext, 'useMlmdListContext'); - const usePipelinesUiRouteSpy = jest.spyOn(usePipelinesUiRoute, 'usePipelinesUiRoute'); beforeEach(() => { useMlmdListContextSpy.mockReturnValue({ @@ -113,8 +111,6 @@ describe('ArtifactsTable', () => { undefined, jest.fn(), ]); - - usePipelinesUiRouteSpy.mockReturnValue(['dspa-pipeline-ui-route', true]); }); it('renders artifacts table with data', () => { diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunParamsSection.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunParamsSection.tsx index 545919c690..4a6c122a7f 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunParamsSection.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunParamsSection.tsx @@ -1,20 +1,13 @@ import React from 'react'; -import { - EmptyState, - EmptyStateBody, - EmptyStateHeader, - EmptyStateVariant, - ExpandableSection, - Flex, - Switch, -} from '@patternfly/react-core'; +import { ExpandableSection, Flex, Switch } from '@patternfly/react-core'; import { InnerScrollContainer, TableVariant, Td, Tr } from '@patternfly/react-table'; import { SortableData, Table } from '~/components/table'; import { useCompareRuns } from '~/concepts/pipelines/content/compareRuns/CompareRunsContext'; import { RuntimeConfigParamValue } from '~/concepts/pipelines/kfTypes'; import { normalizeInputParamValue } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/utils'; +import { CompareRunsEmptyState } from '~/concepts/pipelines/content/compareRuns/CompareRunsEmptyState'; export const CompareRunParamsSection: React.FunctionComponent = () => { const { loaded, selectedRuns } = useCompareRuns(); @@ -145,15 +138,10 @@ export const CompareRunParamsSection: React.FunctionComponent = () => { columns={hasParameters ? runNameColumns : []} hasNestedHeader emptyTableView={ - - - - Select runs from the Run list to compare parameters. - - + /> } rowRenderer={rowRenderer} variant={TableVariant.compact} diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx new file mode 100644 index 0000000000..c6ecf5535e --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx @@ -0,0 +1,106 @@ +import React from 'react'; + +import { ExpandableSection, Tab, TabContentBody, TabTitleText, Tabs } from '@patternfly/react-core'; +import { useCompareRuns } from '~/concepts/pipelines/content/compareRuns/CompareRunsContext'; +import { useGetArtifactTypes } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactTypes'; +import { RunArtifact } from '~/concepts/pipelines/apiHooks/mlmd/types'; +import { + MetricSectionTabLabels, + MetricsType, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/const'; +import { + filterRunArtifactsByType, + getRunArtifacts, +} from '~/concepts/pipelines/content/compareRuns/metricsSection/utils'; +import useMlmdPackagesForPipelineRuns from '~/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns'; +import ScalarMetricTable from '~/concepts/pipelines/content/compareRuns/metricsSection/scalar/ScalarMetricTable'; +import RocCurveCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare'; +import ConfusionMatrixCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare'; +import MarkdownCompare from '~/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare'; + +export const CompareRunMetricsSection: React.FunctionComponent = () => { + const { runs, selectedRuns } = useCompareRuns(); + const [mlmdPackages, mlmdPackagesLoaded] = useMlmdPackagesForPipelineRuns(runs); + const [artifactTypes, artifactTypesLoaded] = useGetArtifactTypes(); + const [isSectionOpen, setIsSectionOpen] = React.useState(true); + const [activeTabKey, setActiveTabKey] = React.useState( + MetricSectionTabLabels.SCALAR, + ); + + const selectedMlmdPackages = React.useMemo( + () => + mlmdPackages.filter((mlmdPackage) => + selectedRuns.some((run) => run.run_id === mlmdPackage.run.run_id), + ), + [mlmdPackages, selectedRuns], + ); + + const isLoaded = mlmdPackagesLoaded && artifactTypesLoaded; + + const [ + scalarMetricsArtifactData, + confusionMatrixArtifactData, + rocCurveArtifactData, + markdownArtifactData, + ] = React.useMemo(() => { + if (!isLoaded) { + return [[], [], [], []]; + } + + const runArtifacts: RunArtifact[] = getRunArtifacts(selectedMlmdPackages); + return [ + filterRunArtifactsByType(runArtifacts, artifactTypes, MetricsType.SCALAR_METRICS), + filterRunArtifactsByType(runArtifacts, artifactTypes, MetricsType.CONFUSION_MATRIX), + filterRunArtifactsByType(runArtifacts, artifactTypes, MetricsType.ROC_CURVE), + filterRunArtifactsByType(runArtifacts, artifactTypes, MetricsType.MARKDOWN), + ]; + }, [artifactTypes, isLoaded, selectedMlmdPackages]); + + return ( + setIsSectionOpen(isOpen)} + isExpanded={isSectionOpen} + isIndented + > + setActiveTabKey(key)}> + {MetricSectionTabLabels.SCALAR}} + > + + + + + {MetricSectionTabLabels.CONFUSION_MATRIX}} + > + + + + + {MetricSectionTabLabels.ROC_CURVE}} + > + + + + + {MetricSectionTabLabels.MARKDOWN}} + isDisabled // TODO enable when markdown can be fetched from storage (s3): https://issues.redhat.com/browse/RHOAIENG-7206 + > + + + + + + + ); +}; diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx index 3f70d248de..eaa594cfd8 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsPage.tsx @@ -6,6 +6,7 @@ import { useCompareRuns } from '~/concepts/pipelines/content/compareRuns/Compare import { CompareRunsInvalidRunCount } from '~/concepts/pipelines/content/compareRuns/CompareRunInvalidRunCount'; import CompareRunsRunList from '~/concepts/pipelines/content/compareRuns/CompareRunsRunList'; import { CompareRunParamsSection } from './CompareRunParamsSection'; +import { CompareRunMetricsSection } from './CompareRunsMetricsSection'; const CompareRunsPage: React.FC = ({ breadcrumbPath }) => { const { runs, loaded } = useCompareRuns(); @@ -37,6 +38,10 @@ const CompareRunsPage: React.FC = ({ breadcrumbPath }) => { + + + + ); diff --git a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx index 35ce43698a..ee0127b8a9 100644 --- a/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx +++ b/frontend/src/pages/pipelines/global/experiments/executions/details/ExecutionDetails.tsx @@ -15,7 +15,7 @@ import { 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 { useGetArtifactTypes } 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'; @@ -41,9 +41,14 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co const { namespace } = usePipelinesAPI(); const [execution, executionLoaded, executionError] = useGetExecutionById(executionId); const [eventsResponse, eventsLoaded, eventsError] = useGetEventsByExecutionId(executionId); - const [artifactTypeMap, artifactTypeMapLoaded] = useGetArtifactTypeMap(); + const [artifactTypes, artifactTypesLoaded] = useGetArtifactTypes(); const allEvents = parseEventsByType(eventsResponse); + const artifactTypeMap = artifactTypes.reduce((acc, artifactType) => { + acc[artifactType.getId()] = artifactType.getName(); + return acc; + }, {} as Record); + const error = executionError || eventsError; if (error) { @@ -115,7 +120,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co @@ -123,7 +128,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co @@ -131,7 +136,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co @@ -139,7 +144,7 @@ const ExecutionDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, co
{artifact.id} {artifact.type} - - {artifact.uri}