Skip to content

Commit

Permalink
Merge pull request #2522 from Gkrumbach07/story/RHOAIENG-2983-experim…
Browse files Browse the repository at this point in the history
…ents-data-filters

Story/rhoaieng 2983 experiments data filters
  • Loading branch information
openshift-merge-bot[bot] authored Feb 29, 2024
2 parents 05f6114 + 39e7d55 commit c7a7662
Show file tree
Hide file tree
Showing 22 changed files with 765 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable camelcase */
import { buildMockExperimentKF, buildMockRunKF } from '~/__mocks__';
import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig';
import { mockDataSciencePipelineApplicationK8sResource } from '~/__mocks__/mockDataSciencePipelinesApplicationK8sResource';
import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList';
Expand All @@ -11,24 +12,75 @@ import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource';
import { mockRouteK8sResource } from '~/__mocks__/mockRouteK8sResource';
import { mockStatus } from '~/__mocks__/mockStatus';
import { experimentsTabs } from '~/__tests__/cypress/cypress/pages/pipelines/experiments';
import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes';

const projectName = 'test-project-name';
const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' });
const initialMockPipelineVersion = buildMockPipelineVersionV2({
pipeline_id: initialMockPipeline.pipeline_id,
});
const mockExperimentArray = [
buildMockExperimentKF({
display_name: 'Test experiment 1',
experiment_id: '1',
}),
buildMockExperimentKF({
display_name: 'Test experiment 2',
experiment_id: '2',
}),
buildMockExperimentKF({
display_name: 'Test experiment 3',
experiment_id: '3',
}),
];

const runs = Array.from({ length: 5 }, (_, i) =>
buildMockRunKF({
display_name: 'Test triggered run 1',
run_id: `run-${i}`,
pipeline_version_reference: {
pipeline_id: initialMockPipeline.pipeline_id,
pipeline_version_id: initialMockPipelineVersion.pipeline_version_id,
},
experiment_id: '1',
created_at: '2024-02-01T00:00:00Z',
state: RuntimeStateKF.SUCCEEDED,
}),
);

describe('Pipeline Experiments', () => {
beforeEach(() => {
initIntercepts();
experimentsTabs.mockGetExperiments(mockExperimentArray);
experimentsTabs.visit(projectName);
});

it('shows empty states', () => {
experimentsTabs.mockGetExperiments([]);
experimentsTabs.visit(projectName);
experimentsTabs.findActiveTab().click();
experimentsTabs.findActiveEmptyState().should('exist');
experimentsTabs.getActiveExperimentsTable().findEmptyState().should('exist');
experimentsTabs.findArchivedTab().click();
experimentsTabs.findArchivedEmptyState().should('exist');
experimentsTabs.getArchivedExperimentsTable().findEmptyState().should('exist');
});

it('filters by experiment name', () => {
experimentsTabs.findActiveTab().click();
// Verify initial run rows exist
experimentsTabs.getActiveExperimentsTable().findRows().should('have.length', 3);

// Select the "Name" filter, enter a value to filter by
experimentsTabs.getActiveExperimentsTable().selectFilterByName('Experiment');
experimentsTabs.getActiveExperimentsTable().findFilterTextField().type('Test experiment 2');

// Mock runs (filtered by typed run name)
experimentsTabs.mockGetExperiments(
mockExperimentArray.filter((exp) => exp.display_name.includes('Test experiment 2')),
);

// Verify only rows with the typed run name exist
experimentsTabs.getActiveExperimentsTable().findRows().should('have.length', 1);
experimentsTabs.getActiveExperimentsTable().findRowByName('Test experiment 2');
});
});

Expand All @@ -53,18 +105,15 @@ const initIntercepts = () => {
pathname: `/api/k8s/apis/route.openshift.io/v1/namespaces/${projectName}/routes/ds-pipeline-dspa`,
},
mockRouteK8sResource({
notebookName: 'ds-pipeline-pipelines-definition',
notebookName: 'ds-pipeline-dspa',
namespace: projectName,
}),
);
cy.intercept(
{
pathname: '/api/k8s/apis/project.openshift.io/v1/projects',
},
mockK8sResourceList([
mockProjectK8sResource({ k8sName: projectName }),
mockProjectK8sResource({ k8sName: `${projectName}-2`, displayName: 'Test Project 2' }),
]),
mockK8sResourceList([mockProjectK8sResource({ k8sName: projectName })]),
);

cy.intercept(
Expand All @@ -81,4 +130,10 @@ const initIntercepts = () => {
},
buildMockPipelineVersionsV2([initialMockPipelineVersion]),
);
cy.intercept(
{
pathname: '/api/proxy/apis/v2beta1/runs',
},
{ runs },
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable camelcase */

import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes';

class ExperimentsTabs {
visit(namespace?: string, tab?: string) {
cy.visitWithLogin(
Expand All @@ -21,12 +23,80 @@ class ExperimentsTabs {
return cy.findByTestId('experiments-archived-tab');
}

findActiveEmptyState() {
return cy.findByTestId('global-no-active-experiments');
getActiveExperimentsTable() {
return new ExperimentsTable(() => cy.findByTestId('experiments-active-tab-content'));
}

getArchivedExperimentsTable() {
return new ExperimentsTable(() => cy.findByTestId('experiments-archived-tab-content'));
}

mockGetExperiments(experiments: ExperimentKFv2[]) {
return cy.intercept(
{
method: 'POST',
pathname: '/api/proxy/apis/v2beta1/experiments',
},
{ experiments, total_size: experiments.length },
);
}
}

class ExperimentsTable {
private findContainer: () => Cypress.Chainable<JQuery<HTMLElement>>;

constructor(findContainer: () => Cypress.Chainable<JQuery<HTMLElement>>) {
this.findContainer = findContainer;
}

find() {
return this.findContainer().findByTestId('experiment-table');
}

findRowByName(name: string) {
return this.find().findAllByRole('cell', { name }).parents('tr');
}

findRows() {
return this.find().find('[data-label=Experiment]').parents('tr');
}

findRowKebabByName(name: string) {
return this.findRowByName(name).findByRole('button', { name: 'Kebab toggle' });
}

findActionsKebab() {
return this.findContainer()
.findByTestId('experiment-table-toolbar')
.findByTestId('experiment-table-toolbar-actions');
}

findEmptyState() {
return this.findContainer().findByTestId('global-no-experiments');
}

selectFilterByName(name: string) {
this.findContainer()
.findByTestId('experiment-table-toolbar')
.findByTestId('pipeline-filter-dropdown')
.findDropdownItem(name)
.click();
}

findFilterTextField() {
return this.findContainer()
.findByTestId('experiment-table-toolbar')
.findByTestId('run-table-toolbar-filter-text-field');
}

findArchivedEmptyState() {
return cy.findByTestId('global-no-archived-experiments');
selectRowActionByName(rowName: string, actionName: string) {
this.findRowKebabByName(rowName).click();
this.findContainer()
.findByRole('menu')
.get('span')
.contains(actionName)
.parents('button')
.click();
}
}

Expand Down
11 changes: 10 additions & 1 deletion frontend/src/api/pipelines/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,16 @@ export const listPipelines: ListPipelinesAPI = (hostPath) => (opts, params) =>

export const listPipelineRuns: ListPipelinesRunAPI = (hostPath) => (opts, params) =>
handlePipelineFailures(
proxyGET(hostPath, '/apis/v2beta1/runs', pipelineParamsToQuery(params), opts),
proxyGET(
hostPath,
'/apis/v2beta1/runs',
{
...pipelineParamsToQuery(params),
// eslint-disable-next-line camelcase
experiment_id: params?.experiment_id,
},
opts,
),
);

export const listPipelineActiveRuns: ListPipelinesRunAPI = (hostPath) => (opts, params) => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/SimpleMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Item = {
label: React.ReactNode;
onClick: (key: string) => void;
isDisabled?: boolean;
tooltip?: React.ReactNode;
};
type Spacer = { isSpacer: true };
const isSpacer = (v: Item | Spacer): v is Spacer => 'isSpacer' in v;
Expand Down Expand Up @@ -39,6 +40,7 @@ const SimpleMenuActions: React.FC<SimpleDropdownProps> = ({ dropdownItems, ...pr
<DropdownItem
key={itemOrSpacer.key}
isDisabled={itemOrSpacer.isDisabled}
tooltip={itemOrSpacer.tooltip}
onClick={() => {
itemOrSpacer.onClick(itemOrSpacer.key);
setOpen(false);
Expand Down
41 changes: 34 additions & 7 deletions frontend/src/concepts/pipelines/apiHooks/useExperiments.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import * as React from 'react';
import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes';
import { ExperimentKFv2, PipelinesFilterOp, StorageStateKF } from '~/concepts/pipelines/kfTypes';
import { usePipelinesAPI } from '~/concepts/pipelines/context';
import usePipelineQuery from '~/concepts/pipelines/apiHooks/usePipelineQuery';
import { PipelineListPaged, PipelineOptions } from '~/concepts/pipelines/types';
import { FetchState } from '~/utilities/useFetchState';

const useExperiments = (
const useExperimentsByStorageState = (
options?: PipelineOptions,
storageState?: StorageStateKF,
): FetchState<PipelineListPaged<ExperimentKFv2>> => {
const { api } = usePipelinesAPI();
return usePipelineQuery<ExperimentKFv2>(
React.useCallback(
(opts, params) =>
api
.listExperiments(opts, params)
.then((result) => ({ ...result, items: result.experiments })),
[api],
(opts, params) => {
const predicates = params?.filter?.predicates || [];
if (storageState) {
predicates.push({
key: 'storage_state',
operation: PipelinesFilterOp.EQUALS,
// eslint-disable-next-line camelcase
string_value: storageState,
});
}
return api
.listExperiments(opts, {
...params,
filter: { predicates },
})
.then((result) => ({ ...result, items: result.experiments }));
},
[api, storageState],
),
options,
);
};

const useExperiments = (options?: PipelineOptions): FetchState<PipelineListPaged<ExperimentKFv2>> =>
useExperimentsByStorageState(options);

export const useActiveExperiments = (
options?: PipelineOptions,
): FetchState<PipelineListPaged<ExperimentKFv2>> =>
useExperimentsByStorageState(options, StorageStateKF.AVAILABLE);

export const useArchivedExperiments = (
options?: PipelineOptions,
): FetchState<PipelineListPaged<ExperimentKFv2>> =>
useExperimentsByStorageState(options, StorageStateKF.ARCHIVED);

export default useExperiments;
19 changes: 19 additions & 0 deletions frontend/src/concepts/pipelines/apiHooks/usePipelineRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,22 @@ export const usePipelineArchivedRuns = (
options,
);
};

export const usePipelineRunsByExperiment = (
experimentId: string,
options?: PipelineOptions,
): FetchState<PipelineListPaged<PipelineRunKFv2>> => {
const { api } = usePipelinesAPI();

return usePipelineQuery<PipelineRunKFv2>(
React.useCallback(
(opts, params) =>
api
// eslint-disable-next-line camelcase
.listPipelineRuns(opts, { ...params, experiment_id: experimentId })
.then((result) => ({ ...result, items: result.runs })),
[api, experimentId],
),
options,
);
};
26 changes: 26 additions & 0 deletions frontend/src/concepts/pipelines/content/tables/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PipelineKFv2,
PipelineRunJobKFv2,
PipelineRunKFv2,
ExperimentKFv2,
} from '~/concepts/pipelines/kfTypes';

export const pipelineColumns: SortableData<PipelineKFv2>[] = [
Expand Down Expand Up @@ -58,6 +59,31 @@ export const pipelineVersionColumns: SortableData<PipelineVersionKFv2>[] = [
kebabTableColumn(),
];

export const experimentColumns: SortableData<ExperimentKFv2>[] = [
checkboxTableColumn(),
{
label: 'Experiment',
field: 'display_name',
sortable: true,
},
{
label: 'Description',
field: 'description',
sortable: true,
},
{
label: 'Created',
field: 'created_at',
sortable: true,
},
{
label: 'Last 5 runs',
field: 'last_5_runs',
sortable: false,
},
kebabTableColumn(),
];

export const pipelineRunColumns: SortableData<PipelineRunKFv2>[] = [
checkboxTableColumn(),
{
Expand Down
Loading

0 comments on commit c7a7662

Please sign in to comment.