Skip to content

Commit

Permalink
Add compare runs list
Browse files Browse the repository at this point in the history
  • Loading branch information
DaoDaoNoCode committed Apr 5, 2024
1 parent d2117a5 commit 4d97a44
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ const mockRun = buildMockRunKF({
experiment_id: mockExperiment.experiment_id,
});

const mockRun2 = buildMockRunKF({
display_name: 'Run 2',
run_id: 'run-2',
pipeline_version_reference: {
pipeline_id: initialMockPipeline.pipeline_id,
pipeline_version_id: initialMockPipelineVersion.pipeline_version_id,
},
experiment_id: mockExperiment.experiment_id,
});

describe('Compare runs', () => {
beforeEach(() => {
initIntercepts();
Expand All @@ -47,9 +57,15 @@ describe('Compare runs', () => {
});

it('valid number of runs', () => {
compareRunsGlobal.visit(projectName, mockExperiment.experiment_id, [mockRun.run_id]);
compareRunsGlobal.visit(projectName, mockExperiment.experiment_id, [
mockRun.run_id,
mockRun2.run_id,
]);
cy.wait('@validRun');
compareRunsGlobal.findInvalidRunsError().should('not.exist');

compareRunsGlobal.findRunListRowByName('Run 1').should('exist');
compareRunsGlobal.findRunListRowByName('Run 2').should('exist');
});
it('valid number of runs but it is invalid', () => {
cy.intercept(
Expand Down Expand Up @@ -169,4 +185,10 @@ const initIntercepts = () => {
},
mockRun,
).as('validRun');
cy.intercept(
{
pathname: `/api/proxy/apis/v2beta1/runs/${mockRun2.run_id}`,
},
mockRun2,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ class CompareRunsGlobal {
findInvalidRunsError() {
return cy.findByTestId('compare-runs-invalid-number-runs');
}

findRunList() {
return cy.findByTestId('compare-runs-table');
}

findRunListRowByName(name: string) {
return this.findRunList().findByText(name);
}
}

export const compareRunsGlobal = new CompareRunsGlobal();
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from 'react';
import { TableText, Td, Tr } from '@patternfly/react-table';
import { Link } from 'react-router-dom';
import { Skeleton } from '@patternfly/react-core';
import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes';
import { CheckboxTd, TableRowTitleDescription } from '~/components/table';
import {
RunCreated,
RunDuration,
RunStatus,
} from '~/concepts/pipelines/content/tables/renderUtils';
import useExperimentById from '~/concepts/pipelines/apiHooks/useExperimentById';
import usePipelineRunVersionInfo from '~/concepts/pipelines/content/tables/usePipelineRunVersionInfo';
import { PipelineVersionLink } from '~/concepts/pipelines/content/PipelineVersionLink';
import { runDetailsRoute } from '~/routes';
import { usePipelinesAPI } from '~/concepts/pipelines/context';

type CompareRunTableRowProps = {
isChecked: boolean;
onToggleCheck: () => void;
run: PipelineRunKFv2;
};

const CompareRunTableRow: React.FC<CompareRunTableRowProps> = ({
isChecked,
onToggleCheck,
run,
}) => {
const { namespace } = usePipelinesAPI();
const [experiment, isExperimentLoaded] = useExperimentById(run.experiment_id);
const { version, loaded: isVersionLoaded, error: versionError } = usePipelineRunVersionInfo(run);

return (
<Tr>
<CheckboxTd id={run.run_id} isChecked={isChecked} onToggle={onToggleCheck} />
<Td dataLabel="Run">
<TableRowTitleDescription
title={
<TableText wrapModifier="truncate">
<Link to={runDetailsRoute(namespace, run.run_id, run.experiment_id)}>
{run.display_name}
</Link>
</TableText>
}
description={run.description}
/>
</Td>
<Td dataLabel="Experiment">
{isExperimentLoaded ? experiment?.display_name || 'Default' : <Skeleton />}
</Td>
<Td modifier="truncate" dataLabel="Pipeline version">
<PipelineVersionLink
displayName={version?.display_name}
version={version}
error={versionError}
loaded={isVersionLoaded}
/>
</Td>
<Td dataLabel="Started">
<RunCreated run={run} />
</Td>
<Td dataLabel="Duration">
<RunDuration run={run} />
</Td>
<Td dataLabel="Status">
<RunStatus justIcon run={run} />
</Td>
</Tr>
);
};

export default CompareRunTableRow;
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as React from 'react';
import { Button, TextInput, ToolbarItem } from '@patternfly/react-core';
import { useNavigate, useParams } from 'react-router';
import PipelineFilterBar from '~/concepts/pipelines/content/tables/PipelineFilterBar';
import SimpleDropdownSelect from '~/components/SimpleDropdownSelect';
import { FilterOptions } from '~/concepts/pipelines/content/tables/usePipelineFilter';
import ExperimentSearchInput from '~/concepts/pipelines/content/tables/ExperimentSearchInput';
import { RuntimeStateKF, runtimeStateLabels } from '~/concepts/pipelines/kfTypes';
import DashboardDatePicker from '~/components/DashboardDatePicker';
import PipelineVersionSelect from '~/concepts/pipelines/content/pipelineSelector/CustomPipelineVersionSelect';
import { PipelineRunVersionsContext } from '~/pages/pipelines/global/runs/PipelineRunVersionsContext';
import { experimentsBaseRoute, experimentsManageCompareRunsRoute } from '~/routes';
import { useCompareRuns } from '~/concepts/pipelines/content/compareRuns/CompareRunsContext';

export type FilterProps = Pick<
React.ComponentProps<typeof PipelineFilterBar>,
'filterData' | 'onFilterUpdate' | 'onClearFilters'
>;

const CompareRunTableToolbar: React.FC<FilterProps> = ({ ...toolbarProps }) => {
const { versions } = React.useContext(PipelineRunVersionsContext);
const { runs } = useCompareRuns();
const navigate = useNavigate();
const { namespace, experimentId } = useParams();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED]: unspecifiedState, ...statusRuntimeStates } =
runtimeStateLabels;

const options = React.useMemo(
() => ({
[FilterOptions.NAME]: 'Run',
[FilterOptions.EXPERIMENT]: 'Experiment',
[FilterOptions.PIPELINE_VERSION]: 'Pipeline version',
[FilterOptions.CREATED_AT]: 'Started',
[FilterOptions.STATUS]: 'Status',
}),
[],
);

if (!namespace || !experimentId) {
navigate(experimentsBaseRoute(namespace));
return null;
}

return (
<PipelineFilterBar
{...toolbarProps}
filterOptions={options}
filterOptionRenders={{
[FilterOptions.NAME]: ({ onChange, ...props }) => (
<TextInput
{...props}
data-testid="search-for-run-name"
aria-label="Search for a run name"
placeholder="Search..."
onChange={(_event, value) => onChange(value)}
/>
),
[FilterOptions.EXPERIMENT]: ({ onChange, value, label }) => (
<ExperimentSearchInput
onChange={(data) => onChange(data?.value, data?.label)}
selected={value && label ? { value, label } : undefined}
/>
),
[FilterOptions.PIPELINE_VERSION]: ({ onChange, label }) => (
<PipelineVersionSelect
versions={versions}
selection={label}
onSelect={(version) => onChange(version.pipeline_version_id, version.display_name)}
/>
),
[FilterOptions.CREATED_AT]: ({ onChange, ...props }) => (
<DashboardDatePicker
{...props}
hideError
aria-label="Select a start date"
onChange={(_, value, date) => {
if (date || !value) {
onChange(value);
}
}}
/>
),
[FilterOptions.STATUS]: ({ value, onChange, ...props }) => (
<SimpleDropdownSelect
{...props}
value={value ?? ''}
aria-label="Select a status"
options={Object.values(statusRuntimeStates).map((v) => ({
key: v,
label: v,
}))}
onChange={(v) => onChange(v)}
data-testid="runtime-status-dropdown"
/>
),
}}
>
<ToolbarItem>
<Button
variant="primary"
onClick={() =>
navigate(
experimentsManageCompareRunsRoute(
namespace,
experimentId,
runs.map((r) => r.run_id),
),
)
}
>
Manage runs
</Button>
</ToolbarItem>
</PipelineFilterBar>
);
};

export default CompareRunTableToolbar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import { ExpandableSection } from '@patternfly/react-core';
import { TableVariant } from '@patternfly/react-table';
import { PipelinesFilter } from '~/concepts/pipelines/types';
import { Table } from '~/components/table';
import CompareRunTableRow from '~/concepts/pipelines/content/compareRuns/CompareRunTableRow';
import { compareRunColumns } from '~/concepts/pipelines/content/compareRuns/columns';
import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView';
import usePipelineFilter, {
FilterOptions,
getDataValue,
} from '~/concepts/pipelines/content/tables/usePipelineFilter';
import CompareRunTableToolbar from '~/concepts/pipelines/content/compareRuns/CompareRunTableToolbar';
import { useCompareRuns } from '~/concepts/pipelines/content/compareRuns/CompareRunsContext';
import useCompareRunsCheckboxTable from '~/concepts/pipelines/content/compareRuns/useCompareRunsCheckboxTable';

const CompareRunsRunList: React.FC = () => {
const { runs, loaded } = useCompareRuns();
const [isExpanded, setExpanded] = React.useState(true);
const [, setFilter] = React.useState<PipelinesFilter | undefined>();
const filterToolbarProps = usePipelineFilter(setFilter);
const {
tableProps: checkboxTableProps,
toggleSelection,
isSelected,
} = useCompareRunsCheckboxTable();

const filteredRuns = React.useMemo(() => {
const { filterData: data } = filterToolbarProps;
const runName = getDataValue(data[FilterOptions.NAME])?.toLowerCase();
const startedTime = getDataValue(data[FilterOptions.CREATED_AT]);
const startedDate = startedTime && new Date(startedTime);
const state = getDataValue(data[FilterOptions.STATUS])?.toLowerCase();
const experimentId = getDataValue(data[FilterOptions.EXPERIMENT]);
const pipelineVersionId = getDataValue(data[FilterOptions.PIPELINE_VERSION]);
return runs.filter((run) => {
const nameMatch = !runName || run.display_name.toLowerCase().includes(runName);
const dateTimeMatch = !startedDate || new Date(run.created_at) >= startedDate;
const stateMatch = !state || run.state.toLowerCase() === state;
const experimentIdMatch = !experimentId || run.experiment_id === experimentId;
const pipelineVersionIdMatch =
!pipelineVersionId ||
run.pipeline_version_reference.pipeline_version_id === pipelineVersionId;

return (
nameMatch && dateTimeMatch && stateMatch && experimentIdMatch && pipelineVersionIdMatch
);
});
}, [runs, filterToolbarProps]);

return (
<ExpandableSection
toggleText="Run list"
isExpanded={isExpanded}
onToggle={(_, expanded) => setExpanded(expanded)}
>
<Table
{...checkboxTableProps}
defaultSortColumn={1}
loading={!loaded}
data={filteredRuns}
columns={compareRunColumns}
enablePagination="compact"
emptyTableView={
<DashboardEmptyTableView onClearFilters={filterToolbarProps.onClearFilters} />
}
toolbarContent={
<CompareRunTableToolbar
data-testid="compare-runs-table-toolbar"
{...filterToolbarProps}
/>
}
rowRenderer={(run) => (
<CompareRunTableRow
key={run.run_id}
isChecked={isSelected(run)}
onToggleCheck={() => toggleSelection(run)}
run={run}
/>
)}
variant={TableVariant.compact}
data-testid="compare-runs-table"
id="compare-runs-table"
/>
</ExpandableSection>
);
};

export default CompareRunsRunList;
41 changes: 41 additions & 0 deletions frontend/src/concepts/pipelines/content/compareRuns/columns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { checkboxTableColumn, SortableData } from '~/components/table';
import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes';

export const compareRunColumns: SortableData<PipelineRunKFv2>[] = [
checkboxTableColumn(),
{
label: 'Run',
field: 'name',
sortable: (a, b) => a.display_name.localeCompare(b.display_name),
},
{
label: 'Experiment',
field: 'experiment',
sortable: false,
width: 15,
},
{
label: 'Pipeline version',
field: 'pipeline_version',
sortable: false,
width: 15,
},
{
label: 'Started',
field: 'created_at',
sortable: true,
width: 15,
},
{
label: 'Duration',
field: 'duration',
sortable: false,
width: 15,
},
{
label: 'Status',
field: 'status',
sortable: (a, b) => a.state.localeCompare(b.state),
width: 10,
},
];
Loading

0 comments on commit 4d97a44

Please sign in to comment.