diff --git a/.changeset/silly-geckos-learn.md b/.changeset/silly-geckos-learn.md new file mode 100644 index 0000000000..e0b525fd4e --- /dev/null +++ b/.changeset/silly-geckos-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': minor +--- + +Search for profile image also in User Entities diff --git a/.changeset/ten-rings-look.md b/.changeset/ten-rings-look.md new file mode 100644 index 0000000000..b5275253f9 --- /dev/null +++ b/.changeset/ten-rings-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Add missing doc string to API diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index c8fbc7e7ac..85cea17660 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -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, diff --git a/plugins/user-settings/dev/index.tsx b/plugins/user-settings/dev/index.tsx index 90be1e3bad..d7041e96a8 100644 --- a/plugins/user-settings/dev/index.tsx +++ b/plugins/user-settings/dev/index.tsx @@ -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 { + return userEntity; + }, + } as Partial as unknown as CatalogApi; + }, + }) .addPage({ title: 'Settings', path: '/settings', diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 4c20c2f0f3..8c73defa89 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -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:^", diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index e31c9f26fd..14b7b7b48b 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -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 = { + getEntityByRef: jest.fn(), +} as any; + describe('', () => { beforeEach(() => { (useOutlet as jest.Mock).mockReset(); }); it('should render the settings page with 3 tabs', async () => { - const { container } = await renderInTestApp(); + const { container } = await renderInTestApp( + + + , + ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(3); @@ -45,7 +54,9 @@ describe('', () => { ); const { container } = await renderInTestApp( - , + + + , ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); @@ -60,7 +71,9 @@ describe('', () => { ); const { container } = await renderInTestApp( - , + + + , ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); diff --git a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx index 1be797980b..b1a811d7fa 100644 --- a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx @@ -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('', () => { it('displays an identity card', async () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsProfileCard.test.tsx b/plugins/user-settings/src/components/General/UserSettingsProfileCard.test.tsx new file mode 100644 index 0000000000..582daadad7 --- /dev/null +++ b/plugins/user-settings/src/components/General/UserSettingsProfileCard.test.tsx @@ -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('', () => { + it('displays avatar if it exists in user entity', async () => { + await renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); + expect(screen.getByAltText('Profile picture')).toHaveAttribute( + 'src', + 'https://example.com/avatar.png', + ); + }); +}); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx index d4b941c850..acdb6c3f87 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -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 = { + getEntityByRef: jest.fn(), +} as any; + describe('', () => { beforeEach(() => { (useOutlet as jest.Mock).mockReset(); }); it('should render the default settings page with 3 tabs', async () => { - const { container } = await renderInTestApp(, { - mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, - }); + const { container } = await renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(3); @@ -50,9 +63,14 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); - const { container } = await renderInTestApp(, { - mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, - }); + const { container } = await renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(4); @@ -66,9 +84,14 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); - const { container } = await renderInTestApp(, { - mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, - }); + const { container } = await renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(4); @@ -91,9 +114,14 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(customLayout); - const { container } = await renderInTestApp(, { - mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, - }); + const { container } = await renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] a'); expect(tabs).toHaveLength(2); diff --git a/plugins/user-settings/src/components/useUserProfileInfo.ts b/plugins/user-settings/src/components/useUserProfileInfo.ts index ee4578cd6b..f777c3d75b 100644 --- a/plugins/user-settings/src/components/useUserProfileInfo.ts +++ b/plugins/user-settings/src/components/useUserProfileInfo.ts @@ -22,16 +22,30 @@ 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 () => { + const 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.picture = catalogProfile.spec.profile.picture; + } return { - profile: await identityApi.getProfileInfo(), - identity: await identityApi.getBackstageIdentity(), + profile: identityProfile, + identity: backStageIdentity, }; }, []); diff --git a/yarn.lock b/yarn.lock index 9db800de86..231e84a976 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8140,6 +8140,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:^"