Merge pull request #26864 from deejay1/feat/26838
feat(user-settings): Search for user avatar also in the entities
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-user-settings': patch
|
||||
---
|
||||
|
||||
`useUserProfile` will now use the user's picture stored in the catalog as a fallback if the identity provider doesn't return a picture.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
Add missing doc string to API
|
||||
@@ -650,6 +650,7 @@ export interface CatalogApi {
|
||||
*
|
||||
* @param entity - Entity to validate
|
||||
* @param locationRef - Location ref in format `url:http://example.com/file`
|
||||
* @param options - Additional options
|
||||
*/
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
|
||||
@@ -17,10 +17,37 @@
|
||||
import React from 'react';
|
||||
import { CatalogEntityPage } from '@backstage/plugin-catalog';
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { userSettingsPlugin, UserSettingsPage } from '../src/plugin';
|
||||
import { UserSettingsPage, userSettingsPlugin } from '../src';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
const userEntity: UserEntity = {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'Guest',
|
||||
},
|
||||
spec: {},
|
||||
};
|
||||
createDevApp()
|
||||
.registerPlugin(userSettingsPlugin)
|
||||
.registerApi({
|
||||
api: catalogApiRef,
|
||||
deps: {},
|
||||
factory() {
|
||||
return {
|
||||
async getEntityByRef(
|
||||
_: string | CompoundEntityRef,
|
||||
): Promise<Entity | undefined> {
|
||||
return userEntity;
|
||||
},
|
||||
} as Partial<CatalogApi> as unknown as CatalogApi;
|
||||
},
|
||||
})
|
||||
.addPage({
|
||||
title: 'Settings',
|
||||
path: '/settings',
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-compat-api": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
|
||||
+17
-4
@@ -15,24 +15,33 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { DefaultSettingsPage } from './DefaultSettingsPage';
|
||||
import { UserSettingsTab } from '../UserSettingsTab';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { SettingsLayout } from '../SettingsLayout';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useOutlet: jest.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
} as any;
|
||||
|
||||
describe('<DefaultSettingsPage />', () => {
|
||||
beforeEach(() => {
|
||||
(useOutlet as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it('should render the settings page with 3 tabs', async () => {
|
||||
const { container } = await renderInTestApp(<DefaultSettingsPage />);
|
||||
const { container } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<DefaultSettingsPage />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
expect(tabs).toHaveLength(3);
|
||||
@@ -45,7 +54,9 @@ describe('<DefaultSettingsPage />', () => {
|
||||
</UserSettingsTab>
|
||||
);
|
||||
const { container } = await renderInTestApp(
|
||||
<DefaultSettingsPage tabs={[advancedTabRoute]} />,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<DefaultSettingsPage tabs={[advancedTabRoute]} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
@@ -60,7 +71,9 @@ describe('<DefaultSettingsPage />', () => {
|
||||
</SettingsLayout.Route>
|
||||
);
|
||||
const { container } = await renderInTestApp(
|
||||
<DefaultSettingsPage tabs={[advancedTabRoute]} />,
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<DefaultSettingsPage tabs={[advancedTabRoute]} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
|
||||
@@ -20,19 +20,27 @@ import React from 'react';
|
||||
import { UserSettingsIdentityCard } from './UserSettingsIdentityCard';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
const apiRegistry = TestApiRegistry.from([
|
||||
identityApiRef,
|
||||
{
|
||||
getProfileInfo: jest.fn(async () => ({})),
|
||||
getBackstageIdentity: jest.fn(async () => ({
|
||||
type: 'user' as const,
|
||||
userEntityRef: 'foo:bar/foobar',
|
||||
ownershipEntityRefs: ['user:default/test-ownership'],
|
||||
})),
|
||||
},
|
||||
]);
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[
|
||||
identityApiRef,
|
||||
{
|
||||
getProfileInfo: jest.fn(async () => ({})),
|
||||
getBackstageIdentity: jest.fn(async () => ({
|
||||
type: 'user' as const,
|
||||
userEntityRef: 'foo:bar/foobar',
|
||||
ownershipEntityRefs: ['user:default/test-ownership'],
|
||||
})),
|
||||
},
|
||||
],
|
||||
[
|
||||
catalogApiRef,
|
||||
{
|
||||
getEntityByRef: jest.fn(),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
describe('<UserSettingsIdentityCard />', () => {
|
||||
it('displays an identity card', async () => {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { UserSettingsProfileCard } from './UserSettingsProfileCard';
|
||||
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[
|
||||
identityApiRef,
|
||||
{
|
||||
getProfileInfo: jest.fn(async () => ({})),
|
||||
getBackstageIdentity: jest.fn(async () => ({
|
||||
type: 'user' as const,
|
||||
userEntityRef: 'foo:bar/foobar',
|
||||
ownershipEntityRefs: ['user:default/test-ownership'],
|
||||
})),
|
||||
},
|
||||
],
|
||||
[
|
||||
catalogApiRef,
|
||||
{
|
||||
getEntityByRef: jest.fn(async () => {
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'User',
|
||||
metadata: {
|
||||
name: 'Guest',
|
||||
annotations: {},
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
picture: 'https://example.com/avatar.png',
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
describe('<UserSettingsProfileCard />', () => {
|
||||
it('displays avatar if it exists in user entity', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<UserSettingsProfileCard />
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
},
|
||||
);
|
||||
expect(screen.getByAltText('Profile picture')).toHaveAttribute(
|
||||
'src',
|
||||
'https://example.com/avatar.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,29 +15,42 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { UserSettingsTab } from '../UserSettingsTab';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { SettingsLayout } from '../SettingsLayout';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { entityRouteRef } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useOutlet: jest.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
} as any;
|
||||
|
||||
describe('<SettingsPage />', () => {
|
||||
beforeEach(() => {
|
||||
(useOutlet as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it('should render the default settings page with 3 tabs', async () => {
|
||||
const { container } = await renderInTestApp(<SettingsPage />, {
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
});
|
||||
const { container } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<SettingsPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
},
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
expect(tabs).toHaveLength(3);
|
||||
@@ -50,9 +63,14 @@ describe('<SettingsPage />', () => {
|
||||
</UserSettingsTab>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
|
||||
const { container } = await renderInTestApp(<SettingsPage />, {
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
});
|
||||
const { container } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<SettingsPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
},
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
expect(tabs).toHaveLength(4);
|
||||
@@ -66,9 +84,14 @@ describe('<SettingsPage />', () => {
|
||||
</SettingsLayout.Route>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(advancedTabRoute);
|
||||
const { container } = await renderInTestApp(<SettingsPage />, {
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
});
|
||||
const { container } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<SettingsPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
},
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
expect(tabs).toHaveLength(4);
|
||||
@@ -91,9 +114,14 @@ describe('<SettingsPage />', () => {
|
||||
</SettingsLayout>
|
||||
);
|
||||
(useOutlet as jest.Mock).mockReturnValue(customLayout);
|
||||
const { container } = await renderInTestApp(<SettingsPage />, {
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
});
|
||||
const { container } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<SettingsPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef },
|
||||
},
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll('[class*=MuiTabs-root] a');
|
||||
expect(tabs).toHaveLength(2);
|
||||
|
||||
@@ -22,16 +22,33 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useEffect } from 'react';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
export const useUserProfile = () => {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
let identityProfile = await identityApi.getProfileInfo();
|
||||
const backStageIdentity = await identityApi.getBackstageIdentity();
|
||||
const catalogProfile = (await catalogApi.getEntityByRef(
|
||||
backStageIdentity.userEntityRef,
|
||||
)) as unknown as UserEntity;
|
||||
if (
|
||||
identityProfile.picture === undefined &&
|
||||
catalogProfile?.spec?.profile?.picture
|
||||
) {
|
||||
identityProfile = {
|
||||
...identityProfile,
|
||||
picture: catalogProfile.spec.profile.picture,
|
||||
};
|
||||
}
|
||||
return {
|
||||
profile: await identityApi.getProfileInfo(),
|
||||
identity: await identityApi.getBackstageIdentity(),
|
||||
profile: identityProfile,
|
||||
identity: backStageIdentity,
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -8438,6 +8438,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-user-settings@workspace:plugins/user-settings"
|
||||
dependencies:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-compat-api": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user