From 5c20c13b8c1f4829f65e73b1b7402c0c9c3b750e Mon Sep 17 00:00:00 2001 From: Gage Krumbach Date: Thu, 14 Mar 2024 09:11:23 -0500 Subject: [PATCH] Refactor imports and remove unused code in ConfusionMatrixCompare.tsx and types.ts Refactor imports and remove unused code in ArtifactDetails.spec.tsx and ArtifactsTable.spec.tsx Refactor imports and remove unused code in ArtifactDetails.spec.tsx and ArtifactsTable.spec.tsx Add TestArtifacts component to GlobalPipelineExperimentsRoutes Refactor ConfusionMatrix component to improve readability and styling Add ConfusionMatrix component and buildConfusionMatrixConfig function Add ConfusionMatrix and ROCCurve components Remove TestArtifacts component and update related routes Refactor ROC curve table components, update imports, and remove unnecessary code Refactor ConfusionMatrixSelect component and add skeleton loading Refactor ROC curve table components and update imports Refactor ROC curve table components and add related constants Refactor ROC curve table components and add related constants Refactor CompareRunsMetricsSection component, update imports, and remove console.log statements Refactor CompareRunsMetricsSection component, update imports, and add ROC curve tab Refactor Dockerfile to use linux/amd64 platform Refactor CompareRunsMetricsSection component, update imports, and add confusion matrix tab Refactor CompareRunsMetricsSection component, add MetricSectionTabLabels enum, and update related types and imports Refactor CompareRunsMetricsSection component, add MetricSectionTabLabels enum, and update related types and imports Refactor CompareRunsMetricsSection component and add MetricSectionTabLabels enum Refactor and remove unused code in usePipelinesUiRoute.ts and ArtifactUriLink.tsx Refactor MLMD API hooks and remove unused code Refactor components to improve readability and add loading spinners Refactor components to improve readability and add loading spinners clean up Refactor imports and remove unused code in ArtifactDetails.spec.tsx and ArtifactsTable.spec.tsx Refactor ConfusionMatrix component to improve readability and styling --- .../concepts/pipelines/apiHooks/mlmd/types.ts | 22 ++- .../apiHooks/mlmd}/useGetArtifactById.ts | 0 .../apiHooks/mlmd/useGetArtifactTypes.ts | 14 +- .../apiHooks/mlmd}/useGetArtifactsList.ts | 0 .../pipelines/apiHooks/mlmd/useMlmdContext.ts | 23 ++- .../content/artifacts/charts/ROCCurve.tsx | 121 ++++++++++++++ .../confusionMatrix/ConfusionMatrix.scss | 70 +++++++++ .../confusionMatrix/ConfusionMatrix.tsx | 136 ++++++++++++++++ .../artifacts/charts/confusionMatrix/types.ts | 11 ++ .../artifacts/charts/confusionMatrix/utils.ts | 10 ++ .../compareRuns/CompareRunsEmptyState.tsx | 23 +++ .../PipelineRunArtifactSelect.tsx | 118 ++++++++++++++ .../ConfusionMatrixCompare.tsx | 147 ++++++++++++++++++ .../metricsSection/confusionMatrix/types.ts | 6 + .../metricsSection/confusionMatrix/utils.ts | 16 ++ .../compareRuns/metricsSection/const.ts | 14 ++ .../markdown/MarkdownCompare.tsx | 136 ++++++++++++++++ .../metricsSection/roc/RocCurveCompare.tsx | 125 +++++++++++++++ .../metricsSection/roc/RocCurveTable.tsx | 53 +++++++ .../metricsSection/roc/RocCurveTableRow.tsx | 50 ++++++ .../compareRuns/metricsSection/roc/const.ts | 25 +++ .../compareRuns/metricsSection/roc/types.ts | 13 ++ .../compareRuns/metricsSection/roc/utils.ts | 24 +++ .../scalar/ScalarMetricTable.tsx | 112 +++++++++++++ .../metricsSection/scalar/types.ts | 19 +++ .../metricsSection/scalar/utils.ts | 95 +++++++++++ .../compareRuns/metricsSection/types.ts | 19 +++ .../useMlmdPackagesForPipelineRuns.ts | 74 +++++++++ .../compareRuns/metricsSection/utils.ts | 145 +++++++++++++++++ .../pipelines/context/usePipelinesUiRoute.ts | 23 --- .../ArtifactDetails/ArtifactDetails.tsx | 2 +- .../ArtifactOverviewDetails.tsx | 94 +++++------ .../experiments/artifacts/ArtifactUriLink.tsx | 61 -------- .../experiments/artifacts/ArtifactsList.tsx | 2 +- .../experiments/artifacts/ArtifactsTable.tsx | 13 +- .../__tests__/ArtifactDetails.spec.tsx | 6 +- .../__tests__/ArtifactsTable.spec.tsx | 6 +- .../compareRuns/CompareRunParamsSection.tsx | 22 +-- .../compareRuns/CompareRunsMetricsSection.tsx | 106 +++++++++++++ .../compareRuns/CompareRunsPage.tsx | 5 + .../executions/details/ExecutionDetails.tsx | 17 +- 41 files changed, 1775 insertions(+), 203 deletions(-) rename frontend/src/{pages/pipelines/global/experiments/artifacts => concepts/pipelines/apiHooks/mlmd}/useGetArtifactById.ts (100%) rename frontend/src/{pages/pipelines/global/experiments/artifacts => concepts/pipelines/apiHooks/mlmd}/useGetArtifactsList.ts (100%) create mode 100644 frontend/src/concepts/pipelines/content/artifacts/charts/ROCCurve.tsx create mode 100644 frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.scss create mode 100644 frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/ConfusionMatrix.tsx create mode 100644 frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/types.ts create mode 100644 frontend/src/concepts/pipelines/content/artifacts/charts/confusionMatrix/utils.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/CompareRunsEmptyState.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/PipelineRunArtifactSelect.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/ConfusionMatrixCompare.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/types.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/confusionMatrix/utils.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/const.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/markdown/MarkdownCompare.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveCompare.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTable.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/RocCurveTableRow.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/const.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/types.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/roc/utils.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/ScalarMetricTable.tsx create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/types.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/scalar/utils.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/types.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/useMlmdPackagesForPipelineRuns.ts create mode 100644 frontend/src/concepts/pipelines/content/compareRuns/metricsSection/utils.ts delete mode 100644 frontend/src/concepts/pipelines/context/usePipelinesUiRoute.ts delete mode 100644 frontend/src/pages/pipelines/global/experiments/artifacts/ArtifactUriLink.tsx create mode 100644 frontend/src/pages/pipelines/global/experiments/compareRuns/CompareRunsMetricsSection.tsx 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}