Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(KFLUXUI-125): allow sorting pipeline runs by status and type #68

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
} 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 { 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 [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', '');

Check warning on line 56 in src/components/PipelineRun/PipelineRunListView/PipelineRunsListView.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/PipelineRun/PipelineRunListView/PipelineRunsListView.tsx#L56

Added line #L56 was not covered by tests
const [onLoadName, setOnLoadName] = React.useState(nameFilter);
React.useEffect(() => {
if (nameFilter) {
Expand Down Expand Up @@ -97,6 +101,10 @@

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 @@
}
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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This same code is being used in Other component, Create reusable utilities or component for the toolbar


Check warning on line 132 in src/components/PipelineRun/PipelineRunListView/PipelineRunsListView.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/PipelineRun/PipelineRunListView/PipelineRunsListView.tsx#L132

Added line #L132 was not covered by tests
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 @@
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 @@
]}
</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
Loading