Skip to content

Commit

Permalink
[RHOAIENG-7481] Add metrics columns to pipeline run table and modal p…
Browse files Browse the repository at this point in the history
…aram selector
  • Loading branch information
jpuzz0 committed Jun 7, 2024
1 parent 34187a9 commit 049be11
Show file tree
Hide file tree
Showing 14 changed files with 658 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class CompareRunParamsTable {
}

findEmptyState() {
return this.find().parent().findByTestId('compare-runs-params-empty-state');
return this.find().parent().parent().findByTestId('compare-runs-params-empty-state');
}

findColumnByName(name: string) {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/table/CheckboxTd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type CheckboxTrProps = {
onToggle: () => void;
isDisabled?: boolean;
tooltip?: string;
tdProps?: React.ComponentProps<typeof Td>;
};

const CheckboxTd: React.FC<CheckboxTrProps> = ({
Expand All @@ -16,6 +17,7 @@ const CheckboxTd: React.FC<CheckboxTrProps> = ({
onToggle,
isDisabled,
tooltip,
tdProps,
}) => {
let content = (
<Checkbox
Expand All @@ -31,7 +33,11 @@ const CheckboxTd: React.FC<CheckboxTrProps> = ({
content = <Tooltip content={tooltip}>{content}</Tooltip>;
}

return <Td dataLabel="Checkbox">{content}</Td>;
return (
<Td dataLabel="Checkbox" {...tdProps}>
{content}
</Td>
);
};

export default CheckboxTd;
33 changes: 22 additions & 11 deletions frontend/src/components/table/TableBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Tbody,
Td,
TbodyProps,
InnerScrollContainer,
} from '@patternfly/react-table';
import { EitherNotBoth } from '~/typeHelpers';
import { GetColumnSort, SortableData } from './types';
Expand Down Expand Up @@ -159,6 +160,8 @@ const TableBase = <T,>({
ref={selectAllRef}
colSpan={col.colSpan}
rowSpan={col.rowSpan}
isStickyColumn={col.isStickyColumn}
stickyMinWidth={col.stickyMinWidth}
select={{
isSelected: selectAll.selected,
onSelect: (e, value) => selectAll.onSelect(value),
Expand All @@ -182,6 +185,8 @@ const TableBase = <T,>({
info={col.info}
isSubheader={isSubheader}
isStickyColumn={col.isStickyColumn}
stickyMinWidth={col.stickyMinWidth}
stickyLeftOffset={col.stickyLeftOffset}
hasRightBorder={col.hasRightBorder}
modifier={col.modifier}
className={col.className}
Expand Down Expand Up @@ -268,17 +273,23 @@ const TableBase = <T,>({
</ToolbarContent>
</Toolbar>
)}
<Table {...props} ref={tableRef}>
{caption && <Caption>{caption}</Caption>}
<Thead noWrap hasNestedHeader={hasNestedHeader}>
<Tr>{columns.map((col, i) => renderColumnHeader(col, i))}</Tr>
{subColumns?.length ? (
<Tr>{subColumns.map((col, i) => renderColumnHeader(col, columns.length + i, true))}</Tr>
) : null}
</Thead>
{disableRowRenderSupport ? renderRows() : <Tbody {...tbodyProps}>{renderRows()}</Tbody>}
{footerRow && footerRow(page)}
</Table>

<InnerScrollContainer>
<Table {...props} ref={tableRef}>
{caption && <Caption>{caption}</Caption>}
<Thead noWrap hasNestedHeader={hasNestedHeader}>
<Tr>{columns.map((col, i) => renderColumnHeader(col, i))}</Tr>
{subColumns?.length ? (
<Tr>
{subColumns.map((col, i) => renderColumnHeader(col, columns.length + i, true))}
</Tr>
) : null}
</Thead>
{disableRowRenderSupport ? renderRows() : <Tbody {...tbodyProps}>{renderRows()}</Tbody>}
{footerRow && footerRow(page)}
</Table>
</InnerScrollContainer>

{!loading && emptyTableView && data.length === 0 && (
<div style={{ padding: 'var(--pf-global--spacer--2xl) 0', textAlign: 'center' }}>
{emptyTableView}
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ export type GetColumnSort = (columnIndex: number) => ThProps['sort'];

export type SortableData<T> = Pick<
ThProps,
'hasRightBorder' | 'isStickyColumn' | 'modifier' | 'width' | 'info' | 'className'
| 'hasRightBorder'
| 'isStickyColumn'
| 'stickyMinWidth'
| 'stickyLeftOffset'
| 'modifier'
| 'width'
| 'info'
| 'className'
> & {
label: string;
field: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { testHook, standardUseFetchState } from '~/__tests__/unit/testUtils/hooks';
import {
MetadataStoreServicePromiseClient,
Artifact,
Execution,
Event,
Context,
} from '~/third_party/mlmd';
import { usePipelinesAPI } from '~/concepts/pipelines/context';
import { getMlmdContext } from '~/concepts/pipelines/apiHooks/mlmd/useMlmdContext';
import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes';
import {
GetArtifactsByContextResponse,
GetExecutionsByContextResponse,
GetEventsByExecutionIDsResponse,
} from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb';
import { useGetArtifactsByRuns } from '~/concepts/pipelines/apiHooks/mlmd/useGetArtifactsByRuns';

// Mock the usePipelinesAPI and getMlmdContext hooks
jest.mock('~/concepts/pipelines/context', () => ({
usePipelinesAPI: jest.fn(),
}));

jest.mock('~/concepts/pipelines/apiHooks/mlmd/useMlmdContext', () => ({
getMlmdContext: jest.fn(),
}));

// Mock the MetadataStoreServicePromiseClient
jest.mock('~/third_party/mlmd', () => {
const originalModule = jest.requireActual('~/third_party/mlmd');
return {
...originalModule,
MetadataStoreServicePromiseClient: jest.fn().mockImplementation(() => ({
getArtifactsByContext: jest.fn(),
getExecutionsByContext: jest.fn(),
getEventsByExecutionIDs: jest.fn(),
})),
GetArtifactsByContextRequest: originalModule.GetArtifactsByContextRequest,
GetExecutionsByContextRequest: originalModule.GetExecutionsByContextRequest,
GetEventsByExecutionIDsRequest: originalModule.GetEventsByExecutionIDsRequest,
};
});

describe('useGetArtifactsByRuns', () => {
const mockClient = new MetadataStoreServicePromiseClient('');
const mockUsePipelinesAPI = jest.mocked(
usePipelinesAPI as () => Partial<ReturnType<typeof usePipelinesAPI>>,
);
const mockGetMlmdContext = jest.mocked(getMlmdContext);
const mockGetArtifactsByContext = jest.mocked(mockClient.getArtifactsByContext);
const mockGetExecutionsByContext = jest.mocked(mockClient.getExecutionsByContext);
const mockGetEventsByExecutionIDs = jest.mocked(mockClient.getEventsByExecutionIDs);

const mockContext = new Context();
mockContext.setId(1);

const mockArtifact = new Artifact();
mockArtifact.setId(1);
mockArtifact.setName('artifact1');

const mockExecution = new Execution();
mockExecution.setId(1);

const mockEvent = new Event();
mockEvent.getArtifactId = jest.fn().mockReturnValue(1);
mockEvent.getExecutionId = jest.fn().mockReturnValue(1);

// eslint-disable-next-line camelcase
const mockRun = { run_id: 'test-run-id' } as PipelineRunKFv2;

beforeEach(() => {
jest.clearAllMocks();
mockUsePipelinesAPI.mockReturnValue({
metadataStoreServiceClient: mockClient,
});
});

it('throws error when no MLMD context is found', async () => {
mockGetMlmdContext.mockResolvedValue(undefined);
const renderResult = testHook(useGetArtifactsByRuns)([mockRun]);

// wait for update
await renderResult.waitForNextUpdate();

expect(renderResult.result.current).toEqual(
standardUseFetchState([], false, new Error('No context for run: test-run-id')),
);
});

it('should fetch and return MLMD packages for pipeline runs', async () => {
mockGetMlmdContext.mockResolvedValue(mockContext);
mockGetArtifactsByContext.mockResolvedValue({
getArtifactsList: () => [mockArtifact],
} as GetArtifactsByContextResponse);
mockGetExecutionsByContext.mockResolvedValue({
getExecutionsList: () => [mockExecution],
} as GetExecutionsByContextResponse);
mockGetEventsByExecutionIDs.mockResolvedValue({
getEventsList: () => [mockEvent],
} as GetEventsByExecutionIDsResponse);

const renderResult = testHook(useGetArtifactsByRuns)([mockRun]);

expect(renderResult.result.current).toStrictEqual(standardUseFetchState([]));
expect(renderResult).hookToHaveUpdateCount(1);

// wait for update
await renderResult.waitForNextUpdate();

expect(renderResult.result.current).toStrictEqual(
standardUseFetchState(
[
{
[mockRun.run_id]: [mockArtifact],
},
],
true,
),
);
expect(renderResult).hookToHaveUpdateCount(2);
});

it('should handle errors from getMlmdContext', async () => {
const error = new Error('Cannot fetch context');
mockGetMlmdContext.mockRejectedValue(error);

const renderResult = testHook(useGetArtifactsByRuns)([mockRun]);

expect(renderResult.result.current).toStrictEqual(standardUseFetchState([]));
expect(renderResult).hookToHaveUpdateCount(1);

// wait for update
await renderResult.waitForNextUpdate();

expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error));
expect(renderResult).hookToHaveUpdateCount(2);
});

it('should handle errors from getArtifactsByContext', async () => {
const error = new Error('Cannot fetch artifacts');
mockGetMlmdContext.mockResolvedValue(mockContext);
mockGetArtifactsByContext.mockRejectedValue(error);

const renderResult = testHook(useGetArtifactsByRuns)([mockRun]);

expect(renderResult.result.current).toStrictEqual(standardUseFetchState([]));
expect(renderResult).hookToHaveUpdateCount(1);

// wait for update
await renderResult.waitForNextUpdate();

expect(renderResult.result.current).toStrictEqual(standardUseFetchState([], false, error));
expect(renderResult).hookToHaveUpdateCount(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';

import { Artifact } from '~/third_party/mlmd';
import { GetArtifactsByContextRequest } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_service_pb';
import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState';
import { usePipelinesAPI } from '~/concepts/pipelines/context';
import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes';
import { MlmdContextTypes } from './types';
import { getMlmdContext } from './useMlmdContext';

export const useGetArtifactsByRuns = (
runs: PipelineRunKFv2[],
): FetchState<Record<string, Artifact[]>[]> => {
const { metadataStoreServiceClient } = usePipelinesAPI();

const call = React.useCallback<FetchStateCallbackPromise<Record<string, Artifact[]>[]>>(
() =>
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}`);
}

const request = new GetArtifactsByContextRequest();
request.setContextId(context.getId());

const response = await metadataStoreServiceClient.getArtifactsByContext(request);
const artifacts = response.getArtifactsList();

return {
[run.run_id]: artifacts,
};
},
),
),
),
[metadataStoreServiceClient, runs],
);

return useFetchState(call, []);
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { extractS3UriComponents } from '~/concepts/pipelines/content/artifacts/u
import MarkdownView from '~/components/MarkdownView';
import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas';
import { bytesAsRoundedGiB } from '~/utilities/number';
import { getArtifactProperties } from './utils';

interface ArtifactVisualizationProps {
artifact: Artifact;
Expand Down Expand Up @@ -116,32 +117,15 @@ export const ArtifactVisualization: React.FC<ArtifactVisualizationProps> = ({ ar
}

if (artifactType === ArtifactType.METRICS) {
const scalarMetrics = artifact
.toObject()
.customPropertiesMap.reduce(
(
acc: { name: string; value: string }[],
[customPropKey, { stringValue, intValue, doubleValue, boolValue }],
) => {
if (customPropKey !== 'display_name') {
acc.push({
name: customPropKey,
value: stringValue || (intValue || doubleValue || boolValue).toString(),
});
}

return acc;
},
[],
);
const artifactProperties = getArtifactProperties(artifact);

return (
<Stack className="pf-v5-u-pt-lg pf-v5-u-pb-lg">
<Title headingLevel="h3">Scalar metrics</Title>

<StackItem>
<Table
data={scalarMetrics}
data={artifactProperties}
columns={[
{
label: 'Name',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Artifact, Value } from '~/third_party/mlmd';
import { getArtifactProperties } from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/artifacts/utils';

describe('getArtifactProperties', () => {
const mockArtifact = new Artifact();
mockArtifact.setId(1);
mockArtifact.setName('artifact-1');
mockArtifact.getCustomPropertiesMap().set('display_name', new Value());

it('returns empty array when no custom props exist other than display_name', () => {
const result = getArtifactProperties(mockArtifact);
expect(result).toEqual([]);
});

it('returns artifact properties when custom props exist other than display_name', () => {
mockArtifact
.getCustomPropertiesMap()
.set('metric-string', new Value().setStringValue('some string'));
mockArtifact.getCustomPropertiesMap().set('metric-int', new Value().setIntValue(10));
mockArtifact.getCustomPropertiesMap().set('metric-double', new Value().setDoubleValue(1.1));
mockArtifact.getCustomPropertiesMap().set('metric-bool', new Value().setBoolValue(true));

const result = getArtifactProperties(mockArtifact);
expect(result).toEqual([
{ name: 'metric-bool', value: 'true' },
{ name: 'metric-double', value: '1.1' },
{ name: 'metric-int', value: '10' },
{ name: 'metric-string', value: 'some string' },
]);
});
});
Loading

0 comments on commit 049be11

Please sign in to comment.