From dbcaa6387a3779197778c38d7ed29f68e51274c5 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 14 Sep 2021 13:18:45 +0200 Subject: [PATCH] Catalog: Add refresh button to AboutCard Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .changeset/nice-lions-hug.md | 5 ++ .changeset/slimy-impalas-admire.md | 6 ++ packages/catalog-client/src/CatalogClient.ts | 18 +++++ packages/catalog-client/src/types/api.ts | 4 ++ .../lib/catalog/CatalogIdentityClient.test.ts | 1 + .../src/providers/aws-alb/provider.test.ts | 2 +- .../badges-backend/src/service/router.test.ts | 1 + .../src/api/CatalogImportClient.test.ts | 1 + .../StepPrepareCreatePullRequest.test.tsx | 1 + plugins/catalog/src/CatalogClientWrapper.ts | 9 +++ .../components/AboutCard/AboutCard.test.tsx | 68 ++++++++++++++++--- .../src/components/AboutCard/AboutCard.tsx | 41 ++++++++--- .../DefaultExplorePage.test.tsx | 1 + .../DomainExplorerContent.test.tsx | 1 + .../GroupsExplorerContent.test.tsx | 1 + .../components/FossaPage/FossaPage.test.tsx | 1 + .../src/service/TodoReaderService.test.ts | 1 + 17 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 .changeset/nice-lions-hug.md create mode 100644 .changeset/slimy-impalas-admire.md diff --git a/.changeset/nice-lions-hug.md b/.changeset/nice-lions-hug.md new file mode 100644 index 0000000000..69dd33fceb --- /dev/null +++ b/.changeset/nice-lions-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Updates the `AboutCard` with a refresh button that allows the entity to be scheduled for refresh. diff --git a/.changeset/slimy-impalas-admire.md b/.changeset/slimy-impalas-admire.md new file mode 100644 index 0000000000..853d3eee65 --- /dev/null +++ b/.changeset/slimy-impalas-admire.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-client': minor +'@backstage/plugin-catalog-react': minor +--- + +Extends the `CatalogClient` interface with a `refreshEntity` method. diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 410d921d3b..3b697fc597 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -136,6 +136,24 @@ export class CatalogClient implements CatalogApi { ); } + async refreshEntity(entityRef: string, options?: CatalogRequestOptions) { + const response = await fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/refresh`, + { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify({ entityRef }), + }, + ); + + if (response.status !== 200) { + throw new Error(await response.text()); + } + } + async addLocation( { type = 'url', target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index e78bd9d713..b95ea335c2 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -53,6 +53,10 @@ export interface CatalogApi { uid: string, options?: CatalogRequestOptions, ): Promise; + refreshEntity( + entityRef: string, + options?: CatalogRequestOptions, + ): Promise; // Locations getLocationById( diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 6ffcfd6f46..5dd3c768bb 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -33,6 +33,7 @@ describe('CatalogIdentityClient', () => { getOriginLocationByEntity: jest.fn(), getLocationByEntity: jest.fn(), removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), }; const tokenIssuer: jest.Mocked = { issueToken: jest.fn(), diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index a1606e18f2..c9647176cd 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -68,7 +68,6 @@ beforeEach(() => { describe('AwsALBAuthProvider', () => { const catalogApi = { - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ addLocation: jest.fn(), removeLocationById: jest.fn(), getEntities: jest.fn(), @@ -77,6 +76,7 @@ describe('AwsALBAuthProvider', () => { getLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), + refreshEntity: jest.fn(), }; const mockRequest = { diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 5d32e64f19..5dc877ec32 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -66,6 +66,7 @@ describe('createRouter', () => { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), }; config = new ConfigReader({ diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 2936f956e4..6a2881e760 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -104,6 +104,7 @@ describe('CatalogImportClient', () => { getLocationByEntity: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), }; let catalogImportClient: CatalogImportClient; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 08b8177291..5a4ccef9a2 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -42,6 +42,7 @@ describe('', () => { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index a46edb56b7..46ca8ec0f9 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -109,4 +109,13 @@ export class CatalogClientWrapper implements CatalogApi { token: options?.token ?? (await this.identityApi.getIdToken()), }); } + + async refreshEntity( + entityRef: string, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.refreshEntity(entityRef, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } } diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 6c85ebcab7..1dbf849a2d 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -24,13 +24,28 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + EntityProvider, + CatalogApi, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), + } as any; + it('renders info', async () => { const entity = { apiVersion: 'v1', @@ -62,7 +77,7 @@ describe('', () => { integrations: {}, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( @@ -109,7 +124,7 @@ describe('', () => { }, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( @@ -155,7 +170,7 @@ describe('', () => { }, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByTitle } = await renderInTestApp( @@ -188,7 +203,7 @@ describe('', () => { const apis = ApiRegistry.with( scmIntegrationsApiRef, ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( @@ -200,6 +215,43 @@ describe('', () => { expect(getByText('View Source').closest('a')).not.toHaveAttribute('href'); }); + it('triggers a refresh', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const apis = ApiRegistry.with( + scmIntegrationsApiRef, + ScmIntegrationsApi.fromConfig(new ConfigReader({})), + ).with(catalogApiRef, catalogApi); + + const { getByTitle } = await renderInTestApp( + + + + + , + ); + + expect(catalogApi.refreshEntity).not.toHaveBeenCalledWith( + 'component:default/software', + ); + + userEvent.click(getByTitle('Schedule entity refresh')); + + expect(catalogApi.refreshEntity).toHaveBeenCalledWith( + 'component:default/software', + ); + }); + it('renders techdocs link', async () => { const entity = { apiVersion: 'v1', @@ -230,7 +282,7 @@ describe('', () => { }, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( @@ -278,7 +330,7 @@ describe('', () => { }, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( @@ -321,7 +373,7 @@ describe('', () => { }, }), ), - ); + ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 582b464afa..ec3d47a204 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -19,6 +19,7 @@ import { ENTITY_DEFAULT_NAMESPACE, RELATION_CONSUMES_API, RELATION_PROVIDES_API, + stringifyEntityRef, } from '@backstage/catalog-model'; import { HeaderIconLinkRow, @@ -26,12 +27,13 @@ import { InfoCardVariants, Link, } from '@backstage/core-components'; -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationIcon, scmIntegrationsApiRef, } from '@backstage/integration-react'; import { + catalogApiRef, getEntityMetadataEditUrl, getEntityRelations, getEntitySourceLocation, @@ -45,10 +47,11 @@ import { IconButton, makeStyles, } from '@material-ui/core'; +import CachedIcon from '@material-ui/icons/Cached'; import DocsIcon from '@material-ui/icons/Description'; import EditIcon from '@material-ui/icons/Edit'; import ExtensionIcon from '@material-ui/icons/Extension'; -import React from 'react'; +import React, { useCallback } from 'react'; import { viewTechDocRouteRef } from '../../routes'; import { AboutContent } from './AboutContent'; @@ -82,6 +85,8 @@ export function AboutCard({ variant }: AboutCardProps) { const classes = useStyles(); const { entity } = useEntity(); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); + const catalogApi = useApi(catalogApiRef); + const alertApi = useApi(alertApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); const entitySourceLocation = getEntitySourceLocation( @@ -142,20 +147,34 @@ export function AboutCard({ variant }: AboutCardProps) { cardContentClass = classes.fullHeightCardContent; } + const refreshEntity = useCallback(async () => { + await catalogApi.refreshEntity(stringifyEntityRef(entity)); + alertApi.post({ message: 'Refresh scheduled', severity: 'info' }); + }, [catalogApi, alertApi, entity]); + return ( - - + <> + + + + + + + } subheader={ diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index fc12a7f660..84b922bef5 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -31,6 +31,7 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), + refreshEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index db44631c4c..5fd2b02ee9 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -33,6 +33,7 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), + refreshEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 52def47f7c..56dfd59aa8 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -32,6 +32,7 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), + refreshEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 1baeb36465..b9edb0746a 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -32,6 +32,7 @@ describe('', () => { getOriginLocationByEntity: jest.fn(), removeEntityByUid: jest.fn(), removeLocationById: jest.fn(), + refreshEntity: jest.fn(), }; const fossaApi: jest.Mocked = { getFindingSummary: jest.fn(), diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index a95387817a..777fb0acf3 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -50,6 +50,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), }; if (entity) { mock.getEntityByName.mockReturnValue(entity);