Skip to content

Commit 09e5b6f

Browse files
debsmita1BethGriggs
authored andcommitted
fix(global-header): fix global-header to prioritize 'spec.profile.displayname'
1 parent c9816e9 commit 09e5b6f

5 files changed

Lines changed: 200 additions & 25 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-global-header': patch
3+
---
4+
5+
fix global-header to prioritize 'spec.profile.displayname' or 'metadata.title' over profilename

workspaces/global-header/plugins/global-header/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@backstage/plugin-search-common": "^1.2.17",
4848
"@backstage/plugin-search-react": "^1.8.5",
4949
"@backstage/plugin-signals-react": "^0.0.9",
50+
"@backstage/plugin-user-settings": "^0.8.18",
5051
"@backstage/theme": "^0.6.3",
5152
"@mui/icons-material": "5.16.13",
5253
"@mui/material": "5.16.13",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 React from 'react';
17+
import { screen } from '@testing-library/react';
18+
import type { CatalogApi } from '@backstage/catalog-client';
19+
import { useUserProfile } from '@backstage/plugin-user-settings';
20+
import { catalogApiRef } from '@backstage/plugin-catalog-react';
21+
22+
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
23+
import { ProfileDropdown } from './ProfileDropdown';
24+
25+
jest.mock('@backstage/plugin-user-settings', () => ({
26+
useUserProfile: jest.fn(),
27+
}));
28+
29+
jest.mock('../../hooks/useProfileDropdownMountPoints', () => ({
30+
useProfileDropdownMountPoints: () => [
31+
{
32+
Component: jest.fn(),
33+
config: {
34+
props: {
35+
icon: 'someicon',
36+
title: 'sometitle',
37+
link: 'somelink',
38+
},
39+
priority: '100',
40+
},
41+
},
42+
],
43+
}));
44+
45+
describe('ProfileDropdown', () => {
46+
it('should render the configured profile displayname', async () => {
47+
(useUserProfile as jest.Mock).mockReturnValue({
48+
displayName: 'Test User',
49+
backstageIdentity: { userEntityRef: 'user:default/test-user' },
50+
profile: { picture: 'picture' },
51+
loading: false,
52+
});
53+
const mockCatalogApi = {
54+
getEntityByRef: () => ({
55+
apiVersion: 'backstage.io/v1alpha1',
56+
kind: 'User',
57+
metadata: {
58+
name: 'test-user',
59+
title: 'Test User',
60+
},
61+
spec: {
62+
profile: { displayName: 'Test User DN' },
63+
memberOf: ['janus-authors'],
64+
},
65+
}),
66+
} as any as CatalogApi;
67+
68+
await renderInTestApp(
69+
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
70+
<ProfileDropdown />
71+
</TestApiProvider>,
72+
);
73+
74+
expect(screen.getByText(/Test User DN/i)).toBeInTheDocument();
75+
});
76+
77+
it('should render the title if profile displayname is not configured', async () => {
78+
(useUserProfile as jest.Mock).mockReturnValue({
79+
displayName: 'testuser1',
80+
backstageIdentity: { userEntityRef: 'user:default/test-user' },
81+
profile: { picture: 'picture' },
82+
loading: false,
83+
});
84+
const mockCatalogApi = {
85+
getEntityByRef: () => ({
86+
apiVersion: 'backstage.io/v1alpha1',
87+
kind: 'User',
88+
metadata: {
89+
name: 'test-user',
90+
title: 'Test User',
91+
},
92+
spec: {
93+
memberOf: ['janus-authors'],
94+
},
95+
}),
96+
} as any as CatalogApi;
97+
98+
await renderInTestApp(
99+
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
100+
<ProfileDropdown />
101+
</TestApiProvider>,
102+
);
103+
104+
expect(screen.getByText(/Test User/i)).toBeInTheDocument();
105+
});
106+
107+
it('should render the user entity ref when user is not configured in the catalog', async () => {
108+
(useUserProfile as jest.Mock).mockReturnValue({
109+
displayName: 'user:default/test',
110+
backstageIdentity: { userEntityRef: 'user:default/test' },
111+
profile: { picture: 'picture' },
112+
loading: false,
113+
});
114+
const mockCatalogApi = {
115+
getEntityByRef: () => ({
116+
apiVersion: 'backstage.io/v1alpha1',
117+
kind: 'User',
118+
metadata: {
119+
name: 'test-user',
120+
},
121+
spec: {
122+
memberOf: ['janus-authors'],
123+
},
124+
}),
125+
} as any as CatalogApi;
126+
127+
await renderInTestApp(
128+
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
129+
<ProfileDropdown />
130+
</TestApiProvider>,
131+
);
132+
133+
expect(screen.getByText(/Test/i)).toBeInTheDocument();
134+
});
135+
});

workspaces/global-header/plugins/global-header/src/components/HeaderDropdownComponent/ProfileDropdown.tsx

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
*/
1616

1717
import React, { useEffect, useMemo, useRef, useState } from 'react';
18-
import {
19-
identityApiRef,
20-
useApi,
21-
ProfileInfo,
22-
} from '@backstage/core-plugin-api';
18+
import { useUserProfile } from '@backstage/plugin-user-settings';
19+
import { useApi } from '@backstage/core-plugin-api';
20+
import { catalogApiRef } from '@backstage/plugin-catalog-react';
21+
import { UserEntity } from '@backstage/catalog-model';
2322
import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
2423
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
2524
import Typography from '@mui/material/Typography';
2625
import { lighten } from '@mui/material/styles';
26+
import Box from '@mui/material/Box';
27+
28+
import { MenuSection } from './MenuSection';
2729
import { HeaderDropdownComponent } from './HeaderDropdownComponent';
2830
import { useProfileDropdownMountPoints } from '../../hooks/useProfileDropdownMountPoints';
29-
import { MenuSection } from './MenuSection';
3031
import { useDropdownManager } from '../../hooks';
31-
import Box from '@mui/material/Box';
3232

3333
/**
3434
* @public
@@ -40,9 +40,14 @@ export interface ProfileDropdownProps {
4040

4141
export const ProfileDropdown = ({ layout }: ProfileDropdownProps) => {
4242
const { anchorEl, handleOpen, handleClose } = useDropdownManager();
43+
const [user, setUser] = useState<string | null>();
44+
const {
45+
displayName,
46+
backstageIdentity,
47+
loading: profileLoading,
48+
} = useUserProfile();
49+
const catalogApi = useApi(catalogApiRef);
4350

44-
const identityApi = useApi(identityApiRef);
45-
const [user, setUser] = useState<ProfileInfo>();
4651
const profileDropdownMountPoints = useProfileDropdownMountPoints();
4752

4853
const headerRef = useRef<HTMLElement | null>(null);
@@ -66,13 +71,25 @@ export const ProfileDropdown = ({ layout }: ProfileDropdownProps) => {
6671
}, []);
6772

6873
useEffect(() => {
69-
const fetchUser = async () => {
70-
const userProfile = await identityApi.getProfileInfo();
71-
setUser(userProfile);
74+
const fetchUserEntity = async () => {
75+
let userProfile;
76+
try {
77+
if (backstageIdentity?.userEntityRef) {
78+
userProfile = (await catalogApi.getEntityByRef(
79+
backstageIdentity.userEntityRef,
80+
)) as unknown as UserEntity;
81+
}
82+
setUser(
83+
userProfile?.spec?.profile?.displayName ??
84+
userProfile?.metadata?.title,
85+
);
86+
} catch (_err) {
87+
setUser(null);
88+
}
7289
};
7390

74-
fetchUser();
75-
}, [identityApi]);
91+
fetchUserEntity();
92+
}, [backstageIdentity, catalogApi]);
7693

7794
const menuItems = useMemo(() => {
7895
return (profileDropdownMountPoints ?? [])
@@ -90,21 +107,37 @@ export const ProfileDropdown = ({ layout }: ProfileDropdownProps) => {
90107
return null;
91108
}
92109

110+
const profileDisplayName = () => {
111+
const name = user ?? displayName;
112+
const regex = /^[^:/]+:[^/]+\/[^/]+$/;
113+
if (regex.test(name)) {
114+
return name
115+
.charAt(name.indexOf('/') + 1)
116+
.toLocaleUpperCase('en-US')
117+
.concat(name.substring(name.indexOf('/') + 2));
118+
}
119+
return name;
120+
};
121+
93122
return (
94123
<HeaderDropdownComponent
95124
buttonContent={
96125
<Box sx={{ display: 'flex', alignItems: 'center', ...layout }}>
97-
<AccountCircleOutlinedIcon fontSize="small" sx={{ mr: 1 }} />
98-
<Typography
99-
variant="body2"
100-
sx={{
101-
display: { xs: 'none', md: 'block' },
102-
fontWeight: 500,
103-
mr: '1rem',
104-
}}
105-
>
106-
{user?.displayName ?? 'Guest'}
107-
</Typography>
126+
{!profileLoading && (
127+
<>
128+
<AccountCircleOutlinedIcon fontSize="small" sx={{ mr: 1 }} />
129+
<Typography
130+
variant="body2"
131+
sx={{
132+
display: { xs: 'none', md: 'block' },
133+
fontWeight: 500,
134+
mr: '1rem',
135+
}}
136+
>
137+
{profileDisplayName()}
138+
</Typography>
139+
</>
140+
)}
108141
<KeyboardArrowDownOutlinedIcon
109142
sx={{
110143
bgcolor: bgColor,

workspaces/global-header/yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11029,6 +11029,7 @@ __metadata:
1102911029
"@backstage/plugin-search-common": ^1.2.17
1103011030
"@backstage/plugin-search-react": ^1.8.5
1103111031
"@backstage/plugin-signals-react": ^0.0.9
11032+
"@backstage/plugin-user-settings": ^0.8.18
1103211033
"@backstage/test-utils": ^1.7.4
1103311034
"@backstage/theme": ^0.6.3
1103411035
"@mui/icons-material": 5.16.13

0 commit comments

Comments
 (0)