diff --git a/.changeset/stale-pugs-kiss.md b/.changeset/stale-pugs-kiss.md new file mode 100644 index 0000000000..db25d1ab2a --- /dev/null +++ b/.changeset/stale-pugs-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Display entity titles in `StarredEntities` home page card (if defined) and don't show entities which no longer exist diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx index 9087ed3605..2b9fae8221 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -15,6 +15,7 @@ */ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { + catalogApiRef, starredEntitiesApiRef, MockStarredEntitiesApi, entityRouteRef, @@ -22,14 +23,44 @@ import { import React from 'react'; import { Content } from './Content'; +const entities = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity-2', + title: 'Mock Starred Entity 2!', + }, + }, +]; + describe('StarredEntitiesContent', () => { - it('should render list of tools', async () => { + it('should render list of starred entities', async () => { const mockedApi = new MockStarredEntitiesApi(); mockedApi.toggleStarred('component:default/mock-starred-entity'); mockedApi.toggleStarred('component:default/mock-starred-entity-2'); + mockedApi.toggleStarred('component:default/mock-starred-entity-3'); - const { getByText } = await renderInTestApp( - + const mockCatalogApi = { + getEntities: jest + .fn() + .mockImplementation(async () => ({ items: entities })), + }; + + const { getByText, queryByText } = await renderInTestApp( + , { @@ -40,12 +71,13 @@ describe('StarredEntitiesContent', () => { ); expect(getByText('mock-starred-entity')).toBeInTheDocument(); - expect(getByText('mock-starred-entity-2')).toBeInTheDocument(); + expect(getByText('Mock Starred Entity 2!')).toBeInTheDocument(); + expect(queryByText('mock-starred-entity-3')).not.toBeInTheDocument(); expect(getByText('mock-starred-entity').closest('a')).toHaveAttribute( 'href', '/catalog/default/component/mock-starred-entity', ); - expect(getByText('mock-starred-entity-2').closest('a')).toHaveAttribute( + expect(getByText('Mock Starred Entity 2!').closest('a')).toHaveAttribute( 'href', '/catalog/default/component/mock-starred-entity-2', ); diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx index e82d915e3b..31fa00378e 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx @@ -15,12 +15,14 @@ */ import { + catalogApiRef, useStarredEntities, + entityRouteParams, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { parseEntityRef } from '@backstage/catalog-model'; -import { useRouteRef } from '@backstage/core-plugin-api'; -import { Link } from '@backstage/core-components'; +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { Link, Progress, ResponseErrorPanel } from '@backstage/core-components'; import { List, ListItem, @@ -32,6 +34,7 @@ import { } from '@material-ui/core'; import StarIcon from '@material-ui/icons/Star'; import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; /** * A component to display a list of starred entities for the user. @@ -39,9 +42,37 @@ import React from 'react'; * @public */ export const Content = () => { + const catalogApi = useApi(catalogApiRef); const catalogEntityRoute = useRouteRef(entityRouteRef); const { starredEntities, toggleStarredEntity } = useStarredEntities(); + // Grab starred entities from catalog to ensure they still exist and also retrieve display titles + const entities = useAsync(async () => { + if (!starredEntities.size) { + return []; + } + + const filter = [...starredEntities] + .map(ent => parseEntityRef(ent)) + .map(ref => ({ + kind: ref.kind, + 'metadata.namespace': ref.namespace, + 'metadata.name': ref.name, + })); + + return ( + await catalogApi.getEntities({ + filter, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.title', + ], + }) + ).items; + }, [catalogApi, starredEntities]); + if (starredEntities.size === 0) return ( @@ -49,26 +80,40 @@ export const Content = () => { ); - return ( + if (entities.loading) { + return ; + } + + return entities.error ? ( + + ) : ( - {Array.from(starredEntities).map(entity => ( - - - - - - - toggleStarredEntity(entity)} - > - - - - - - ))} + {entities.value + ?.sort((a, b) => + (a.metadata.title ?? a.metadata.name).localeCompare( + b.metadata.title ?? b.metadata.name, + ), + ) + .map(entity => ( + + + + + + + toggleStarredEntity(entity)} + > + + + + + + ))} ); }; diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index 1b01820482..a5e05e56c1 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -17,6 +17,7 @@ import { HomePageStarredEntities } from '../../plugin'; import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { + catalogApiRef, starredEntitiesApiRef, MockStarredEntitiesApi, entityRouteRef, @@ -30,12 +31,56 @@ starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2'); starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3'); starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4'); +const entities = [ + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity', + title: 'Mock Starred Entity!', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity-2', + title: 'Mock Starred Entity 2!', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity-3', + title: 'Mock Starred Entity 3!', + }, + }, + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'mock-starred-entity-4', + title: 'Mock Starred Entity 4!', + }, + }, +]; + +const mockCatalogApi = { + getEntities: async () => ({ items: entities }), +}; + export default { title: 'Plugins/Home/Components/StarredEntities', decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( - + , {