Skip to content

Commit 5b93641

Browse files
author
djanaki
committed
chore(new-component-model): new component list page
1 parent 53ae401 commit 5b93641

File tree

4 files changed

+200
-15
lines changed

4 files changed

+200
-15
lines changed

src/components/ComponentList/ComponentList.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import {
88
TextVariants,
99
Title,
1010
} from '@patternfly/react-core';
11-
import { capitalize } from 'lodash-es';
1211
import { FilterContext } from '~/components/Filter/generic/FilterContext';
1312
import { BaseTextFilterToolbar } from '~/components/Filter/toolbars/BaseTextFIlterToolbar';
1413
import { FeatureFlagIndicator } from '~/feature-flags/FeatureFlagIndicator';
1514
import { useAllComponents } from '~/hooks/useComponents';
1615
import { getErrorState } from '~/shared/utils/error-utils';
17-
import { pipelineRunStatus } from '~/utils/pipeline-utils';
1816
import emptyStateImgUrl from '../../assets/Components.svg';
1917
import pipelineImg from '../../assets/Pipeline.svg';
2018
import { PipelineRunLabel } from '../../consts/pipelinerun';
@@ -41,7 +39,7 @@ const ComponentList: React.FC = () => {
4139
status: unparsedFilters.status ? (unparsedFilters.status as string[]) : [],
4240
});
4341

44-
const { name: nameFilter, status: statusFilter } = filters;
42+
const { name: nameFilter } = filters;
4543

4644
const [allComponents, allComponentsLoaded, allComponentsError] = useAllComponents(namespace);
4745

@@ -75,16 +73,9 @@ const ComponentList: React.FC = () => {
7573
const filteredComponents = React.useMemo(
7674
() =>
7775
componentsWithLatestBuild.filter((component) => {
78-
const compStatus = statusFilter?.length
79-
? pipelineRunStatus(component.latestBuildPipelineRun)
80-
: 'unknown';
81-
82-
return (
83-
(!nameFilter || component.metadata.name.indexOf(nameFilter) !== -1) &&
84-
(!statusFilter?.length || statusFilter.includes(capitalize(compStatus)))
85-
);
76+
return !nameFilter || component.metadata.name.indexOf(nameFilter) !== -1;
8677
}),
87-
[componentsWithLatestBuild, statusFilter, nameFilter],
78+
[componentsWithLatestBuild, nameFilter],
8879
);
8980

9081
const NoDataEmptyMessage = () => (

src/components/ComponentList/__tests__/ComponentList.spec.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ jest.mock('~/shared/providers/Namespace/useNamespaceInfo', () => ({
2424
jest.mock('../ComponentListRow', () => ({
2525
__esModule: true,
2626
default: ({ obj }: { obj: ComponentKind }) => (
27-
<tr data-test="component-list-item">
28-
<td>{obj.metadata.name}</td>
29-
</tr>
27+
<>
28+
<td data-test="component-list-item">{obj.metadata.name}</td>
29+
</>
3030
),
3131
}));
3232

@@ -141,4 +141,36 @@ describe('ComponentList', () => {
141141
renderComponentList();
142142
expect(useLatestPushBuildPipelinesMock).toHaveBeenCalledWith('test-ns', '', ['c1']);
143143
});
144+
145+
it('filters components by name when name filter is applied via context', async () => {
146+
const components = [createMockComponent('nodejs-app'), createMockComponent('go-service')];
147+
useAllComponentsMock.mockReturnValue([components, true, undefined]);
148+
useLatestPushBuildPipelinesMock.mockReturnValue([[], true]);
149+
const setFiltersMock = jest.fn();
150+
const onClearFiltersMock = jest.fn();
151+
const { FilterContext: FilterContextObj } = await import(
152+
'~/components/Filter/generic/FilterContext'
153+
);
154+
renderWithQueryClient(
155+
<FilterContextObj.Provider
156+
value={{
157+
filters: { name: 'nodejs', status: [] },
158+
setFilters: setFiltersMock,
159+
onClearFilters: onClearFiltersMock,
160+
}}
161+
>
162+
<ComponentList />
163+
</FilterContextObj.Provider>,
164+
);
165+
expect(screen.getByText('nodejs-app')).toBeInTheDocument();
166+
expect(screen.queryByText('go-service')).not.toBeInTheDocument();
167+
});
168+
169+
it('passes customData with pipelineRunsLoaded to Table', () => {
170+
const components = [createMockComponent('one')];
171+
useAllComponentsMock.mockReturnValue([components, true, undefined]);
172+
useLatestPushBuildPipelinesMock.mockReturnValue([[], false]);
173+
renderComponentList();
174+
expect(screen.getByText('one')).toBeInTheDocument();
175+
});
144176
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import ComponentsListHeader, { componentsTableColumnClasses } from '../ComponentListHeader';
2+
3+
describe('ComponentListHeader', () => {
4+
it('returns column definitions with expected titles', () => {
5+
const headers = ComponentsListHeader();
6+
expect(headers).toHaveLength(5);
7+
expect(headers.map((h) => h.title)).toEqual([
8+
'Name',
9+
'Git Repository',
10+
'Image Registry',
11+
'Component Versions',
12+
' ',
13+
]);
14+
});
15+
16+
it('applies componentsTableColumnClasses to each column', () => {
17+
const headers = ComponentsListHeader();
18+
expect(headers[0].props.className).toBe(componentsTableColumnClasses.component);
19+
expect(headers[1].props.className).toBe(componentsTableColumnClasses.gitRepository);
20+
expect(headers[2].props.className).toBe(componentsTableColumnClasses.imageRegistry);
21+
expect(headers[3].props.className).toBe(componentsTableColumnClasses.componentVersions);
22+
expect(headers[4].props.className).toBe(componentsTableColumnClasses.kebab);
23+
});
24+
25+
it('exports componentsTableColumnClasses with expected keys', () => {
26+
expect(componentsTableColumnClasses).toEqual({
27+
component: 'pf-m-width-30 wrap-column',
28+
gitRepository: 'pf-m-width-35 wrap-column',
29+
imageRegistry: 'pf-m-width-40 wrap-column',
30+
componentVersions: 'pf-m-width-20',
31+
kebab: 'pf-m-width-20 component-list-view__actions',
32+
});
33+
});
34+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { screen } from '@testing-library/react';
2+
import { COMPONENT_DETAILS_V2_PATH } from '~/routes/paths';
3+
import type { ComponentKind } from '~/types';
4+
import { renderWithQueryClientAndRouter } from '~/unit-test-utils/rendering-utils';
5+
import ComponentsListRow from '../ComponentListRow';
6+
7+
jest.mock('~/shared/providers/Namespace/useNamespaceInfo', () => ({
8+
useNamespace: jest.fn(),
9+
}));
10+
11+
jest.mock('~/components/Components/component-actions', () => ({
12+
useComponentActions: () => [],
13+
}));
14+
15+
const useNamespaceMock = jest.requireMock('~/shared/providers/Namespace/useNamespaceInfo')
16+
.useNamespace as jest.Mock;
17+
18+
const createMockComponent = (overrides: Partial<ComponentKind> = {}): ComponentKind =>
19+
({
20+
apiVersion: 'appstudio.redhat.com/v1alpha1',
21+
kind: 'Component',
22+
metadata: { name: 'test-component', namespace: 'test-ns', uid: 'uid-1' },
23+
spec: {
24+
application: 'test-app',
25+
componentName: 'test-component',
26+
source: { git: { url: 'https://github.com/org/repo', revision: 'main' } },
27+
},
28+
...overrides,
29+
}) as ComponentKind;
30+
31+
const renderRow = (component: ComponentKind) => {
32+
const obj = { ...component };
33+
return renderWithQueryClientAndRouter(
34+
<table>
35+
<tbody>
36+
<tr>
37+
<ComponentsListRow obj={obj} />
38+
</tr>
39+
</tbody>
40+
</table>,
41+
);
42+
};
43+
44+
describe('ComponentListRow', () => {
45+
beforeEach(() => {
46+
useNamespaceMock.mockReturnValue('test-ns');
47+
});
48+
49+
it('renders component name as link', () => {
50+
renderRow(createMockComponent());
51+
expect(screen.getByRole('link', { name: 'test-component' })).toBeInTheDocument();
52+
});
53+
54+
it('links to component details page with correct path', () => {
55+
renderRow(
56+
createMockComponent({ metadata: { name: 'my-comp', namespace: 'test-ns', uid: '1' } }),
57+
);
58+
useNamespaceMock.mockReturnValue('test-ns');
59+
const expectedPath = COMPONENT_DETAILS_V2_PATH.createPath({
60+
workspaceName: 'test-ns',
61+
componentName: 'my-comp',
62+
});
63+
expect(screen.getByRole('link', { name: 'my-comp' })).toHaveAttribute('href', expectedPath);
64+
});
65+
66+
it('renders component versions count', () => {
67+
renderRow(
68+
createMockComponent({
69+
spec: {
70+
application: 'test-app',
71+
componentName: 'test-component',
72+
source: {
73+
git: { url: 'https://github.com/org/repo', revision: 'main' },
74+
versions: [
75+
{ name: 'v1', revision: 'main' },
76+
{ name: 'v2', revision: 'branch' },
77+
],
78+
},
79+
},
80+
} as ComponentKind),
81+
);
82+
expect(screen.getByTestId('component-versions-count')).toHaveTextContent('2');
83+
});
84+
85+
it('renders versions count 0 when source.versions is undefined', () => {
86+
renderRow(createMockComponent());
87+
expect(screen.getByTestId('component-versions-count')).toHaveTextContent('0');
88+
});
89+
90+
it('renders GitRepoLink when spec.source.git is present', () => {
91+
renderRow(
92+
createMockComponent({
93+
spec: {
94+
application: 'test-app',
95+
componentName: 'test-component',
96+
source: {
97+
git: {
98+
url: 'https://github.com/org/repo',
99+
revision: 'main',
100+
context: './src',
101+
},
102+
},
103+
},
104+
} as ComponentKind),
105+
);
106+
const link = screen.getByRole('link', { name: /org\/repo/i });
107+
expect(link).toBeInTheDocument();
108+
});
109+
110+
it('does not render GitRepoLink when spec.source.git is absent', () => {
111+
renderRow(
112+
createMockComponent({
113+
spec: {
114+
application: 'test-app',
115+
componentName: 'test-component',
116+
source: {},
117+
},
118+
} as ComponentKind),
119+
);
120+
expect(screen.getAllByRole('link')).toHaveLength(1);
121+
expect(screen.getByRole('link', { name: 'test-component' })).toBeInTheDocument();
122+
});
123+
124+
it('renders row with data-test component-list-item', () => {
125+
renderRow(createMockComponent());
126+
expect(screen.getByTestId('component-list-item')).toBeInTheDocument();
127+
});
128+
});

0 commit comments

Comments
 (0)