Skip to content

Commit

Permalink
feat(KFLUXUI-125): allow sorting pipeline runs by status and type
Browse files Browse the repository at this point in the history
  • Loading branch information
marcin-michal committed Jan 9, 2025
1 parent 7c5807c commit 31eb20c
Show file tree
Hide file tree
Showing 4 changed files with 443 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@patternfly/react-core/deprecated';
import { FilterIcon } from '@patternfly/react-icons/dist/esm/icons/filter-icon';
import { debounce } from 'lodash-es';
import { PipelineRunLabel } from '../../../consts/pipelinerun';
import { PipelineRunLabel, PipelineRunType } from '../../../consts/pipelinerun';
import { useComponents } from '../../../hooks/useComponents';
import { usePipelineRuns } from '../../../hooks/usePipelineRuns';
import { usePLRVulnerabilities } from '../../../hooks/useScanResults';
Expand All @@ -34,6 +34,8 @@ import PipelineRunEmptyState from '../PipelineRunEmptyState';
import { PipelineRunListHeaderWithVulnerabilities } from './PipelineRunListHeader';
import { PipelineRunListRowWithVulnerabilities } from './PipelineRunListRow';

const pipelineRunTypes = [PipelineRunType.BUILD as string, PipelineRunType.TEST as string];

type PipelineRunsListViewProps = {
applicationName: string;
componentName?: string;
Expand All @@ -50,6 +52,8 @@ const PipelineRunsListView: React.FC<React.PropsWithChildren<PipelineRunsListVie
const [nameFilter, setNameFilter] = useSearchParam('name', '');
const [statusFilterExpanded, setStatusFilterExpanded] = React.useState<boolean>(false);
const [statusFiltersParam, setStatusFiltersParam] = useSearchParam('status', '');
const [typeFilterExpanded, setTypeFilterExpanded] = React.useState<boolean>(false);
const [typeFiltersParam, setTypeFiltersParam] = useSearchParam('type', '');
const [onLoadName, setOnLoadName] = React.useState(nameFilter);
React.useEffect(() => {
if (nameFilter) {
Expand Down Expand Up @@ -97,6 +101,10 @@ const PipelineRunsListView: React.FC<React.PropsWithChildren<PipelineRunsListVie

const statusFilterObj = React.useMemo(() => {
return pipelineRuns.reduce((acc, plr) => {
if (customFilter && !customFilter(plr)) {
return acc;
}

const stat = pipelineRunStatus(plr);
if (statuses.includes(stat)) {
if (acc[stat] !== undefined) {
Expand All @@ -107,22 +115,51 @@ const PipelineRunsListView: React.FC<React.PropsWithChildren<PipelineRunsListVie
}
return acc;
}, {});
}, [pipelineRuns]);
}, [pipelineRuns, customFilter]);

const typeFilters = React.useMemo(
() => (typeFiltersParam ? typeFiltersParam.split(',') : []),
[typeFiltersParam],
);

const setTypeFilters = (filters: string[]) => setTypeFiltersParam(filters.join(','));

const typeFilterObj = React.useMemo(() => {
return pipelineRuns.reduce((acc, plr) => {
if (customFilter && !customFilter(plr)) {
return acc;
}

const runType = plr?.metadata.labels[PipelineRunLabel.PIPELINE_TYPE];

if (pipelineRunTypes.includes(runType)) {
if (acc[runType] !== undefined) {
acc[runType] = acc[runType] + 1;
} else {
acc[runType] = 1;
}
}
return acc;
}, {});
}, [pipelineRuns, customFilter]);

const filteredPLRs = React.useMemo(
() =>
pipelineRuns
.filter(
(plr) =>
.filter((plr) => {
const runType = plr?.metadata.labels[PipelineRunLabel.PIPELINE_TYPE];
return (
(!nameFilter ||
plr.metadata.name.indexOf(nameFilter) >= 0 ||
plr.metadata.labels?.[PipelineRunLabel.COMPONENT]?.indexOf(
nameFilter.trim().toLowerCase(),
) >= 0) &&
(!statusFilters.length || statusFilters.includes(pipelineRunStatus(plr))),
)
(!statusFilters.length || statusFilters.includes(pipelineRunStatus(plr))) &&
(!typeFilters.length || typeFilters.includes(runType))
);
})
.filter((plr) => !customFilter || customFilter(plr)),
[customFilter, nameFilter, pipelineRuns, statusFilters],
[customFilter, nameFilter, pipelineRuns, statusFilters, typeFilters],
);

const vulnerabilities = usePLRVulnerabilities(nameFilter ? filteredPLRs : pipelineRuns);
Expand All @@ -131,6 +168,7 @@ const PipelineRunsListView: React.FC<React.PropsWithChildren<PipelineRunsListVie
onLoadName.length && setOnLoadName('');
setNameFilter('');
setStatusFilters([]);
setTypeFilters([]);
};
const onNameInput = debounce((n: string) => {
n.length === 0 && onLoadName.length && setOnLoadName('');
Expand Down Expand Up @@ -190,6 +228,41 @@ const PipelineRunsListView: React.FC<React.PropsWithChildren<PipelineRunsListVie
]}
</Select>
</ToolbarItem>
<ToolbarItem>
<Select
placeholderText="Type"
toggleIcon={<FilterIcon />}
toggleAriaLabel="Type filter menu"
variant={SelectVariant.checkbox}
isOpen={typeFilterExpanded}
onToggle={(_, expanded) => setTypeFilterExpanded(expanded)}
onSelect={(event, selection) => {
const checked = (event.target as HTMLInputElement).checked;
setTypeFilters(
checked
? [...typeFilters, String(selection)]
: typeFilters.filter((value) => value !== selection),
);
}}
selections={typeFilters}
isGrouped
>
{[
<SelectGroup label="Type" key="type">
{Object.keys(typeFilterObj).map((filter) => (
<SelectOption
key={filter}
value={filter}
isChecked={typeFilters.includes(filter)}
itemCount={typeFilterObj[filter] ?? 0}
>
{filter}
</SelectOption>
))}
</SelectGroup>,
]}
</Select>
</ToolbarItem>
</ToolbarGroup>
</ToolbarContent>
</Toolbar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { Table as PfTable, TableHeader } from '@patternfly/react-table/deprecated';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { PipelineRunLabel, PipelineRunType } from '../../../../consts/pipelinerun';
import { useComponents } from '../../../../hooks/useComponents';
import { usePipelineRuns } from '../../../../hooks/usePipelineRuns';
// import { usePLRVulnerabilities } from '../../../../hooks/useScanResults';
import { useSearchParam } from '../../../../hooks/useSearchParam';
import { useSnapshots } from '../../../../hooks/useSnapshots';
import { PipelineRunKind } from '../../../../types';
import { PipelineRunKind, PipelineRunStatus } from '../../../../types';
import { createUseWorkspaceInfoMock } from '../../../../utils/test-utils';
import { mockComponentsData } from '../../../ApplicationDetails/__data__';
import { PipelineRunListRow } from '../PipelineRunListRow';
Expand Down Expand Up @@ -115,11 +116,20 @@ const pipelineRuns: PipelineRunKind[] = [
uid: '9c1f121c-1eb6-490f-b2d9-befbfc658df1',
labels: {
'appstudio.openshift.io/component': 'sample-component',
[PipelineRunLabel.PIPELINE_TYPE]: PipelineRunType.TEST as string,
},
},
spec: {
key: 'key1',
},
status: {
conditions: [
{
status: 'True',
type: 'Succeeded',
},
],
} as PipelineRunStatus,
},
{
kind: 'PipelineRun',
Expand All @@ -141,6 +151,7 @@ const pipelineRuns: PipelineRunKind[] = [
uid: '9c1f121c-1eb6-490f-b2d9-befbfc658dfb',
labels: {
'appstudio.openshift.io/component': 'test-component',
[PipelineRunLabel.PIPELINE_TYPE]: PipelineRunType.BUILD as string,
},
},
spec: {
Expand All @@ -167,6 +178,7 @@ const pipelineRuns: PipelineRunKind[] = [
uid: '9c1f121c-1eb6-490f-b2d9-befbfc658dfc',
labels: {
'appstudio.openshift.io/component': 'sample-component',
[PipelineRunLabel.PIPELINE_TYPE]: PipelineRunType.BUILD as string,
},
},
spec: {
Expand Down Expand Up @@ -241,7 +253,7 @@ describe('Pipeline run List', () => {
screen.queryByText('Started');
screen.queryByText('Duration');
screen.queryAllByText('Status');
screen.queryByText('Type');
screen.queryAllByText('Type');
screen.queryByText('Component');
});

Expand Down Expand Up @@ -295,6 +307,88 @@ describe('Pipeline run List', () => {
});
});

it('should render filtered pipelinerun list by status', async () => {
usePipelineRunsMock.mockReturnValue([
pipelineRuns,
true,
null,
() => {},
{ isFetchingNextPage: false, hasNextPage: false },
]);

const r = render(<PipelineRunsListView applicationName={appName} />);

const statusFilter = screen.getByRole('button', {
name: /status filter menu/i,
});

fireEvent.click(statusFilter);
expect(statusFilter).toHaveAttribute('aria-expanded', 'true');

const succeededOption = screen.getByLabelText(/succeeded/i, {
selector: 'input',
});

fireEvent.click(succeededOption);

r.rerender(<PipelineRunsListView applicationName={appName} />);

expect(succeededOption).toBeChecked();

await waitFor(() => {
expect(screen.queryByText('basic-node-js-first')).toBeInTheDocument();
expect(screen.queryByText('basic-node-js-second')).not.toBeInTheDocument();
expect(screen.queryByText('basic-node-js-third')).not.toBeInTheDocument();
});

// clean up for other tests
expect(statusFilter).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(succeededOption);
r.rerender(<PipelineRunsListView applicationName={appName} />);
expect(succeededOption).not.toBeChecked();
});

it('should render filtered pipelinerun list by type', async () => {
usePipelineRunsMock.mockReturnValue([
pipelineRuns,
true,
null,
() => {},
{ isFetchingNextPage: false, hasNextPage: false },
]);

const r = render(<PipelineRunsListView applicationName={appName} />);

const typeFilter = screen.getByRole('button', {
name: /type filter menu/i,
});

fireEvent.click(typeFilter);
expect(typeFilter).toHaveAttribute('aria-expanded', 'true');

const testOption = screen.getByLabelText(/test/i, {
selector: 'input',
});

fireEvent.click(testOption);

r.rerender(<PipelineRunsListView applicationName={appName} />);

expect(testOption).toBeChecked();

await waitFor(() => {
expect(screen.queryByText('basic-node-js-first')).toBeInTheDocument();
expect(screen.queryByText('basic-node-js-second')).not.toBeInTheDocument();
expect(screen.queryByText('basic-node-js-third')).not.toBeInTheDocument();
});

// clean up for other tests
expect(typeFilter).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(testOption);
r.rerender(<PipelineRunsListView applicationName={appName} />);
expect(testOption).not.toBeChecked();
});

xit('should clear the filters and render the list again in the table', async () => {
usePipelineRunsMock.mockReturnValue([
pipelineRuns,
Expand Down
Loading

0 comments on commit 31eb20c

Please sign in to comment.