Skip to content

Commit b0d352a

Browse files
authored
fix(dynamic-home-page): display entity title instead of name in catalog section (#2241)
1 parent 0e939a0 commit b0d352a

9 files changed

Lines changed: 234 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-dynamic-home-page': patch
3+
---
4+
5+
Fixed entity cards in "Explore Your Software Catalog" section to display `metadata.title` when available, falling back to `metadata.name` only if title is not set

workspaces/homepage/examples/entities.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,31 @@ spec:
1919
system: examples
2020
providesApis: [example-grpc-api]
2121
---
22+
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component
23+
apiVersion: backstage.io/v1alpha1
24+
kind: Component
25+
metadata:
26+
name: example-website-with-title
27+
title: Example website with title
28+
spec:
29+
type: website
30+
lifecycle: experimental
31+
owner: guests
32+
system: examples
33+
providesApis: [example-grpc-api]
34+
---
35+
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component
36+
apiVersion: backstage.io/v1alpha1
37+
kind: Component
38+
metadata:
39+
name: example-website
40+
spec:
41+
type: website
42+
lifecycle: experimental
43+
owner: guests
44+
system: examples
45+
providesApis: [example-grpc-api]
46+
---
2247
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api
2348
apiVersion: backstage.io/v1alpha1
2449
kind: API

workspaces/homepage/examples/org.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ kind: User
55
metadata:
66
namespace: development
77
name: guest
8+
title: Guest (Title)
89
spec:
910
profile:
10-
displayName: Guest
11+
displayName: Guest (Display Name)
1112
memberOf: [guests]
1213
---
1314
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group
1415
apiVersion: backstage.io/v1alpha1
1516
kind: Group
1617
metadata:
18+
namespace: development
1719
name: guests
20+
title: Guests
1821
spec:
1922
type: team
2023
children: []

workspaces/homepage/packages/app/e2e-tests/homepageCards.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ test.describe('Homepage Card Individual Tests', () => {
142142

143143
await testUtils.verifyLinkInCard(
144144
translations.entities.title,
145-
evaluateMessage(translations.entities.viewAll, '4'),
145+
evaluateMessage(translations.entities.viewAll, '5'),
146146
);
147147
});
148148

workspaces/homepage/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { defineConfig } from '@playwright/test';
1818

1919
export default defineConfig({
20-
timeout: 2 * 60 * 1000,
20+
timeout: 10 * 1000,
2121

2222
expect: {
2323
timeout: 5000,

workspaces/homepage/plugins/dynamic-home-page/dev/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ const entities /* : Entity[]*/ = [
288288
kind: 'Component',
289289
metadata: {
290290
name: 'service-a',
291-
description: 'Hello, I am service A',
291+
description: 'Hello, I am service A with a title',
292+
title: 'Service A',
292293
},
293294
},
294295
{
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import type { ReactNode } from 'react';
17+
18+
import { render, screen } from '@testing-library/react';
19+
import { MemoryRouter } from 'react-router-dom';
20+
import { ThemeProvider, createTheme } from '@mui/material/styles';
21+
22+
import { EntitySection } from './EntitySection';
23+
import { useEntities } from '../../hooks/useEntities';
24+
25+
jest.mock('../../hooks/useEntities');
26+
const mockUseEntities = useEntities as jest.MockedFunction<typeof useEntities>;
27+
28+
jest.mock('@backstage/plugin-user-settings', () => ({
29+
useUserProfile: () => ({
30+
displayName: 'Test User',
31+
loading: false,
32+
}),
33+
}));
34+
35+
jest.mock('@backstage/plugin-catalog-react', () => ({
36+
EntityRefLink: ({ children }: { children: ReactNode }) => (
37+
<span>{children}</span>
38+
),
39+
}));
40+
41+
jest.mock('@backstage/core-components', () => ({
42+
CodeSnippet: ({ text }: { text: string }) => <pre>{text}</pre>,
43+
WarningPanel: ({ children }: { children: ReactNode }) => (
44+
<div>{children}</div>
45+
),
46+
Link: ({ children, to }: { children: ReactNode; to: string }) => (
47+
<a href={to}>{children}</a>
48+
),
49+
MarkdownContent: ({ content }: { content: string }) => <span>{content}</span>,
50+
}));
51+
52+
jest.mock('../../hooks/useTranslation', () => ({
53+
useTranslation: () => ({
54+
t: (key: string) => key,
55+
}),
56+
}));
57+
58+
jest.mock('../Trans', () => ({
59+
Trans: ({ message }: { message: string }) => <span>{message}</span>,
60+
}));
61+
62+
jest.mock('./ViewMoreLink', () => ({
63+
ViewMoreLink: ({ children }: { children: ReactNode }) => (
64+
<span>{children}</span>
65+
),
66+
}));
67+
68+
jest.mock('../../utils/utils', () => ({
69+
hasEntityIllustrationUserDismissed: () => true,
70+
addDismissedEntityIllustrationUsers: jest.fn(),
71+
}));
72+
73+
jest.mock('../../images/homepage-entities-1.svg', () => 'mock-image.svg');
74+
75+
const theme = createTheme();
76+
77+
const renderComponent = () =>
78+
render(
79+
<ThemeProvider theme={theme}>
80+
<MemoryRouter>
81+
<EntitySection />
82+
</MemoryRouter>
83+
</ThemeProvider>,
84+
);
85+
86+
describe('<EntitySection />', () => {
87+
beforeEach(() => {
88+
jest.clearAllMocks();
89+
});
90+
91+
it('displays entity title when metadata.title is set', () => {
92+
mockUseEntities.mockReturnValue({
93+
data: {
94+
items: [
95+
{
96+
apiVersion: 'backstage.io/v1alpha1',
97+
kind: 'Component',
98+
metadata: {
99+
name: 'my-component-name',
100+
title: 'My Component Title',
101+
namespace: 'default',
102+
},
103+
spec: {},
104+
},
105+
],
106+
totalItems: 1,
107+
pageInfo: {},
108+
},
109+
isLoading: false,
110+
error: undefined,
111+
});
112+
113+
renderComponent();
114+
115+
expect(screen.getByText('My Component Title')).toBeInTheDocument();
116+
expect(screen.queryByText('my-component-name')).not.toBeInTheDocument();
117+
});
118+
119+
it('displays entity name when metadata.title is not set', () => {
120+
mockUseEntities.mockReturnValue({
121+
data: {
122+
items: [
123+
{
124+
apiVersion: 'backstage.io/v1alpha1',
125+
kind: 'Component',
126+
metadata: {
127+
name: 'my-component-name',
128+
namespace: 'default',
129+
},
130+
spec: {},
131+
},
132+
],
133+
totalItems: 1,
134+
pageInfo: {},
135+
},
136+
isLoading: false,
137+
error: undefined,
138+
});
139+
140+
renderComponent();
141+
142+
expect(screen.getByText('my-component-name')).toBeInTheDocument();
143+
});
144+
145+
it('displays title for entities with title and name for entities without', () => {
146+
mockUseEntities.mockReturnValue({
147+
data: {
148+
items: [
149+
{
150+
apiVersion: 'backstage.io/v1alpha1',
151+
kind: 'Component',
152+
metadata: {
153+
name: 'component-with-title',
154+
title: 'Component With Title',
155+
namespace: 'default',
156+
},
157+
spec: {},
158+
},
159+
{
160+
apiVersion: 'backstage.io/v1alpha1',
161+
kind: 'API',
162+
metadata: {
163+
name: 'api-without-title',
164+
namespace: 'default',
165+
},
166+
spec: {},
167+
},
168+
],
169+
totalItems: 2,
170+
pageInfo: {},
171+
},
172+
isLoading: false,
173+
error: undefined,
174+
});
175+
176+
renderComponent();
177+
178+
expect(screen.getByText('Component With Title')).toBeInTheDocument();
179+
expect(screen.queryByText('component-with-title')).not.toBeInTheDocument();
180+
expect(screen.getByText('api-without-title')).toBeInTheDocument();
181+
});
182+
183+
it('shows loading state', () => {
184+
mockUseEntities.mockReturnValue({
185+
data: undefined,
186+
isLoading: true,
187+
error: undefined,
188+
});
189+
190+
renderComponent();
191+
192+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
193+
});
194+
});

workspaces/homepage/plugins/dynamic-home-page/src/components/EntitySection/EntitySection.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,7 @@ export const EntitySection = () => {
200200
>
201201
<EntityCard
202202
entity={item}
203-
title={
204-
item.spec?.profile?.displayName ?? item.metadata.name
205-
}
203+
title={item.metadata.title ?? item.metadata.name}
206204
version="latest"
207205
description={item.metadata.description ?? ''}
208206
tags={item.metadata?.tags ?? []}

workspaces/homepage/plugins/dynamic-home-page/src/components/TemplateSection/TemplateSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const TemplateSection = () => {
9696
title={item.metadata.title}
9797
description={item.metadata.description}
9898
kind={item.kind}
99-
type={item.spec.type}
99+
type={item.spec?.type}
100100
/>
101101
</Grid>
102102
))}

0 commit comments

Comments
 (0)