diff --git a/.changeset/red-peas-smoke.md b/.changeset/red-peas-smoke.md deleted file mode 100644 index 4640e12b48..0000000000 --- a/.changeset/red-peas-smoke.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-badges': patch -'@backstage/plugin-badges-backend': patch ---- - -New plugin to serve badges. diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index 59f5fc0445..e64360656d 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -6,12 +6,12 @@ Default implementation uses badges, in SVG. Currently, only entity badges are implemented. i.e. badges that may have entity -specific information in them, and as such, are served from a entity specific +specific information in them, and as such, are served from an entity specific endpoint. ## Installation -Install the `@backstage/plugin-badges-backend` package in your backend packages, +Install the `@backstage/plugin-badges-backend` package in your backend package, and then integrate the plugin using the following default setup for `src/plugins/badges.ts`: @@ -56,7 +56,7 @@ factories, and provide them to the badge builder. ### Custom badges -To provide custom badges, create a badges factories function, and use that when +To provide custom badges, create a badge factories function, and use that when creating the badges backend router. ```ts @@ -80,8 +80,8 @@ export const createMyCustomBadgeFactories = (): BadgeFactories => ({ ## API -The badges backend api exposes two main endpoints for entity badges. (the -`/badges` prefix is arbitrary, and the default for the example backend.) +The badges backend api exposes two main endpoints for entity badges. The +`/badges` prefix is arbitrary, and the default for the example backend. - `/badges/entity/:namespace/:kind/:name/badge-specs` List all defined badges for a particular entity, in json format. See diff --git a/plugins/badges-backend/src/badges.ts b/plugins/badges-backend/src/badges.ts index 03e32814fd..2e2848a8af 100644 --- a/plugins/badges-backend/src/badges.ts +++ b/plugins/badges-backend/src/badges.ts @@ -15,6 +15,7 @@ */ import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { InputError } from '@backstage/errors'; import { Badge, BadgeContext, BadgeFactories } from './types'; function appTitle(context: BadgeContext): string { @@ -27,14 +28,14 @@ function entityUrl(context: BadgeContext): string { e.metadata.namespace || ENTITY_DEFAULT_NAMESPACE }/${e.metadata.name}`; const catalogUrl = `${context.config.getString('app.baseUrl')}/catalog`; - return `${catalogUrl}/${entityUri}`; + return `${catalogUrl}/${entityUri}`.toLowerCase(); } export const createDefaultBadgeFactories = (): BadgeFactories => ({ pingback: { - createBadge: (context: BadgeContext): Badge | null => { + createBadge: (context: BadgeContext): Badge => { if (!context.entity) { - return null; + throw new InputError('"pingback" badge only defined for entities'); } return { description: `Link to ${context.entity.metadata.name} in ${appTitle( @@ -50,9 +51,9 @@ export const createDefaultBadgeFactories = (): BadgeFactories => ({ }, lifecycle: { - createBadge: (context: BadgeContext): Badge | null => { + createBadge: (context: BadgeContext): Badge => { if (!context.entity) { - return null; + throw new InputError('"lifecycle" badge only defined for entities'); } return { description: 'Entity lifecycle badge', @@ -66,9 +67,9 @@ export const createDefaultBadgeFactories = (): BadgeFactories => ({ }, owner: { - createBadge: (context: BadgeContext): Badge | null => { + createBadge: (context: BadgeContext): Badge => { if (!context.entity) { - return null; + throw new InputError('"owner" badge only defined for entities'); } return { description: 'Entity owner badge', @@ -82,11 +83,12 @@ export const createDefaultBadgeFactories = (): BadgeFactories => ({ }, docs: { - createBadge: (context: BadgeContext): Badge | null => { + createBadge: (context: BadgeContext): Badge => { if (!context.entity) { - return null; + throw new InputError('"docs" badge only defined for entities'); } return { + description: 'Entity docs badge', kind: 'entity', label: 'docs', link: `${entityUrl(context)}/docs`, diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts index 9fc9bcf32d..a36587f241 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; import { DefaultBadgeBuilder } from './DefaultBadgeBuilder'; import { BadgeBuilder, BadgeOptions } from './types'; import { BadgeContext, BadgeFactories } from '../../types'; describe('DefaultBadgeBuilder', () => { let builder: BadgeBuilder; - let config: jest.Mocked; + let config: Config; let factories: BadgeFactories; const badge = { @@ -32,24 +32,9 @@ describe('DefaultBadgeBuilder', () => { }; beforeAll(() => { - config = { - get: jest.fn(), - getBoolean: jest.fn(), - getConfig: jest.fn(), - getConfigArray: jest.fn(), - getNumber: jest.fn(), - getOptional: jest.fn(), - getOptionalBoolean: jest.fn(), - getOptionalConfig: jest.fn(), - getOptionalConfigArray: jest.fn(), - getOptionalNumber: jest.fn(), - getOptionalString: jest.fn(), - getOptionalStringArray: jest.fn(), - getString: jest.fn(), - getStringArray: jest.fn(), - has: jest.fn(), - keys: jest.fn(), - }; + config = new ConfigReader({ + backend: { baseUrl: 'http://127.0.0.1' }, + }); factories = { testbadge: { @@ -63,25 +48,24 @@ describe('DefaultBadgeBuilder', () => { builder = new DefaultBadgeBuilder(factories); }); - it('getBadgeIds() returns all badge factory ids', async () => { - expect(await builder.getBadgeIds()).toEqual(['testbadge']); + it('getBadges() returns all badge factory ids', async () => { + expect(await builder.getBadges()).toEqual([{ id: 'testbadge' }]); }); - describe('createBadge', () => { + describe('createBadge[Json|Svg]', () => { const context: BadgeContext = { badgeUrl: 'http://127.0.0.1/badge/url', config, }; - it('returns the spec when format is "json"', async () => { + it('badge spec', async () => { const options: BadgeOptions = { - badgeId: 'testbadge', + badgeInfo: { id: 'testbadge' }, context, - format: 'json', }; - const spec = await builder.createBadge(options); - expect(JSON.parse(spec)).toEqual({ + const spec = await builder.createBadgeJson(options); + expect(spec).toEqual({ badge, id: 'testbadge', url: context.badgeUrl, @@ -89,26 +73,24 @@ describe('DefaultBadgeBuilder', () => { }); }); - it('returns the badge image when format is "svg"', async () => { + it('badge image', async () => { const options: BadgeOptions = { - badgeId: 'testbadge', + badgeInfo: { id: 'testbadge' }, context, - format: 'svg', }; - const spec = await builder.createBadge(options); - expect(spec).toEqual(expect.stringMatching(/^]*>.*<\/svg>$/)); + const img = await builder.createBadgeSvg(options); + expect(img).toEqual(expect.stringMatching(/^]*>.*<\/svg>$/)); }); it('returns "unknown" badge for missing factory', async () => { const options: BadgeOptions = { - badgeId: 'other-id', + badgeInfo: { id: 'other-id' }, context, - format: 'json', }; - const spec = await builder.createBadge(options); - expect(JSON.parse(spec)).toEqual({ + const spec = await builder.createBadgeJson(options); + expect(spec).toEqual({ badge: { label: 'unknown badge', message: 'other-id', diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts index ce905d8e9d..42823ebd09 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts @@ -14,72 +14,69 @@ * limitations under the License. */ +import { InputError } from '@backstage/errors'; import { makeBadge, Format } from 'badge-maker'; -import { BadgeBuilder, BadgeOptions } from './types'; +import { BadgeBuilder, BadgeInfo, BadgeOptions, BadgeSpec } from './types'; import { Badge, BadgeFactories } from '../../types'; export class DefaultBadgeBuilder implements BadgeBuilder { constructor(private readonly factories: BadgeFactories) {} - public async getBadgeIds(): Promise { - return Object.keys(this.factories); + public async getBadges(): Promise { + return Object.keys(this.factories).map(id => ({ id })); } - public async createBadge(options: BadgeOptions): Promise { - const factory = this.factories[options.badgeId]; + public async createBadgeJson(options: BadgeOptions): Promise { + const factory = this.factories[options.badgeInfo.id]; const badge = factory ? factory.createBadge(options.context) : ({ label: 'unknown badge', - message: options.badgeId, + message: options.badgeInfo.id, color: 'red', } as Badge); if (!badge) { - return ''; + throw new InputError( + `The badge factory failed to produce a "${options.badgeInfo.id}" badge with the provided context`, + ); } - switch (options.format) { - case 'json': - return JSON.stringify( - { - badge, - id: options.badgeId, - url: options.context.badgeUrl, - markdown: this.getMarkdownCode(badge, options.context.badgeUrl), - }, - null, - 2, - ); - case 'svg': - try { - const format = { - message: badge.message, - color: badge.color || '#36BAA2', - label: badge.label || '', - labelColor: badge.labelColor || '', - style: badge.style || 'flat-square', - } as Format; - return makeBadge(format); - } catch (err) { - return makeBadge({ - label: 'invalid badge', - message: `${err}`, - color: 'red', - }); - } - default: - throw new TypeError(`unsupported badge format: ${options.format}`); + return { + badge, + id: options.badgeInfo.id, + url: options.context.badgeUrl, + markdown: this.getMarkdownCode(badge, options.context.badgeUrl), + }; + } + + public async createBadgeSvg(options: BadgeOptions): Promise { + const { badge } = await this.createBadgeJson(options); + try { + const format = { + message: badge.message, + color: badge.color || '#36BAA2', + label: badge.label || '', + labelColor: badge.labelColor || '', + style: badge.style || 'flat-square', + } as Format; + return makeBadge(format); + } catch (err) { + return makeBadge({ + label: 'invalid badge', + message: `${err}`, + color: 'red', + }); } } - private getMarkdownCode(params: Badge, badge_url: string): string { - let alt_text = `${params.label}: ${params.message}`; + private getMarkdownCode(params: Badge, badgeUrl: string): string { + let altText = `${params.label}: ${params.message}`; if (params.description && params.description !== params.label) { - alt_text = `${params.description}, ${alt_text}`; + altText = `${params.description}, ${altText}`; } const tooltip = params.description ? ` "${params.description}"` : ''; - const img = `![${alt_text}](${badge_url}${tooltip})`; + const img = `![${altText}](${badgeUrl}${tooltip})`; return params.link ? `[${img}](${params.link})` : img; } } diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/index.ts b/plugins/badges-backend/src/lib/BadgeBuilder/index.ts index 20f18b8852..4947db96e2 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/index.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/index.ts @@ -15,4 +15,4 @@ */ export { DefaultBadgeBuilder } from './DefaultBadgeBuilder'; -export * from './types'; +export type { BadgeBuilder, BadgeInfo, BadgeOptions, BadgeSpec } from './types'; diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts index 9c17d8e3c3..fcd1cd4c5b 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts @@ -14,15 +14,33 @@ * limitations under the License. */ -import { BadgeContext } from '../../types'; +import { Badge, BadgeContext } from '../../types'; + +export type BadgeInfo = { + id: string; +}; export type BadgeOptions = { - badgeId: string; + badgeInfo: BadgeInfo; context: BadgeContext; - format: 'svg' | 'json'; +}; + +export type BadgeSpec = { + /** Badge id */ + id: string; + + /** Badge data */ + badge: Badge; + + /** The URL to the badge image */ + url: string; + + /** The markdown code to use the badge */ + markdown: string; }; export type BadgeBuilder = { - createBadge(options: BadgeOptions): Promise; - getBadgeIds(): Promise; + getBadges(): Promise; + createBadgeJson(options: BadgeOptions): Promise; + createBadgeSvg(options: BadgeOptions): Promise; }; diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index c05a3c9c58..e8de5dfd42 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -16,9 +16,13 @@ import express from 'express'; import request from 'supertest'; +import { + PluginEndpointDiscovery, + SingleHostDiscovery, +} from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import type { Entity } from '@backstage/catalog-model'; -import { Config } from '@backstage/config'; +import { Config, ConfigReader } from '@backstage/config'; import { createRouter } from './router'; import { BadgeBuilder } from '../lib'; @@ -26,7 +30,8 @@ describe('createRouter', () => { let app: express.Express; let badgeBuilder: jest.Mocked; let catalog: jest.Mocked; - let config: jest.Mocked; + let config: Config; + let discovery: PluginEndpointDiscovery; const entity: Entity = { apiVersion: 'v1', @@ -38,15 +43,19 @@ describe('createRouter', () => { const badge = { id: 'test-badge', - badge: { label: 'test badge' }, + badge: { + label: 'test', + message: 'badge', + }, url: '/...', markdown: '[![...](...)]', }; beforeAll(async () => { badgeBuilder = { - createBadge: jest.fn(), - getBadgeIds: jest.fn(), + getBadges: jest.fn(), + createBadgeJson: jest.fn(), + createBadgeSvg: jest.fn(), }; catalog = { addLocation: jest.fn(), @@ -56,25 +65,21 @@ describe('createRouter', () => { getLocationById: jest.fn(), removeEntityByUid: jest.fn(), }; - config = { - get: jest.fn(), - getBoolean: jest.fn(), - getConfig: jest.fn(), - getConfigArray: jest.fn(), - getNumber: jest.fn(), - getOptional: jest.fn(), - getOptionalBoolean: jest.fn(), - getOptionalConfig: jest.fn(), - getOptionalConfigArray: jest.fn(), - getOptionalNumber: jest.fn(), - getOptionalString: jest.fn(), - getOptionalStringArray: jest.fn(), - getString: jest.fn(), - getStringArray: jest.fn(), - has: jest.fn(), - keys: jest.fn(), - }; - const router = await createRouter({ badgeBuilder, catalog, config }); + + config = new ConfigReader({ + backend: { + baseUrl: 'http://127.0.0.1', + listen: { port: 7000 }, + }, + }); + discovery = SingleHostDiscovery.fromConfig(config); + + const router = await createRouter({ + badgeBuilder, + catalog, + config, + discovery, + }); app = express().use(router); }); @@ -83,7 +88,12 @@ describe('createRouter', () => { }); it('works', async () => { - const router = await createRouter({ badgeBuilder, catalog, config }); + const router = await createRouter({ + badgeBuilder, + catalog, + config, + discovery, + }); expect(router).toBeDefined(); }); @@ -91,17 +101,15 @@ describe('createRouter', () => { it('returns all badge specs for entity', async () => { catalog.getEntityByName.mockResolvedValueOnce(entity); - badgeBuilder.getBadgeIds.mockResolvedValueOnce([badge.id]); - badgeBuilder.createBadge.mockResolvedValueOnce( - JSON.stringify(badge, null, 2), - ); + badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); const response = await request(app).get( '/entity/default/service/test/badge-specs', ); expect(response.status).toEqual(200); - expect(response.text).toEqual(`[${JSON.stringify(badge, null, 2)}]`); + expect(response.text).toEqual(JSON.stringify([badge], null, 2)); expect(catalog.getEntityByName).toHaveBeenCalledTimes(1); expect(catalog.getEntityByName).toHaveBeenCalledWith({ @@ -110,31 +118,30 @@ describe('createRouter', () => { name: 'test', }); - expect(badgeBuilder.getBadgeIds).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadge).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadge).toHaveBeenCalledWith({ - badgeId: badge.id, + expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeJson).toHaveBeenCalledWith({ + badgeInfo: { id: badge.id }, context: { badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1:\d+\/entity\/default\/service\/test\/test-badge/, + /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/, ), config, entity, }, - format: 'json', }); }); }); - describe('GET /entity/:namespace/:kind/:name/test-badge', () => { + describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { it('returns badge for entity', async () => { catalog.getEntityByName.mockResolvedValueOnce(entity); const image = '...'; - badgeBuilder.createBadge.mockResolvedValueOnce(image); + badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); const response = await request(app).get( - '/entity/default/service/test/test-badge', + '/entity/default/service/test/badge/test-badge', ); expect(response.status).toEqual(200); @@ -147,18 +154,17 @@ describe('createRouter', () => { name: 'test', }); - expect(badgeBuilder.getBadgeIds).toHaveBeenCalledTimes(0); - expect(badgeBuilder.createBadge).toHaveBeenCalledTimes(1); - expect(badgeBuilder.createBadge).toHaveBeenCalledWith({ - badgeId: badge.id, + expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0); + expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1); + expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({ + badgeInfo: { id: badge.id }, context: { badgeUrl: expect.stringMatching( - /http:\/\/127.0.0.1:\d+\/entity\/default\/service\/test\/test-badge/, + /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/, ), config, entity, }, - format: 'svg', }); }); }); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index 797d23f432..bfbb10fc94 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -31,17 +31,14 @@ export interface RouterOptions { badgeFactories?: BadgeFactories; catalog?: CatalogApi; config: Config; - discovery?: PluginEndpointDiscovery; + discovery: PluginEndpointDiscovery; } export async function createRouter( options: RouterOptions, ): Promise { - if (!options.catalog && !options.discovery) { - throw new Error('must provide either catalog api or discovery api'); - } const catalog = - options.catalog || new CatalogClient({ discoveryApi: options.discovery! }); + options.catalog || new CatalogClient({ discoveryApi: options.discovery }); const badgeBuilder = options.badgeBuilder || new DefaultBadgeBuilder(options.badgeFactories || {}); @@ -56,70 +53,84 @@ export async function createRouter( ); } - const context: BadgeContext = { - badgeUrl: '', - config: options.config, - entity, - }; - const specs = []; - for (const badgeId of await badgeBuilder.getBadgeIds()) { - context.badgeUrl = [ - `${req.protocol}://`, - req.headers.host, - req.originalUrl.replace(/badge-specs$/, badgeId), - ].join(''); - const badge = await badgeBuilder.createBadge({ - badgeId, - context, - format: 'json', - }); + for (const badgeInfo of await badgeBuilder.getBadges()) { + const context: BadgeContext = { + badgeUrl: await getBadgeUrl( + namespace, + kind, + name, + badgeInfo.id, + options, + ), + config: options.config, + entity, + }; + const badge = await badgeBuilder.createBadgeJson({ badgeInfo, context }); if (badge) { specs.push(badge); } } res.setHeader('Content-Type', 'application/json'); - res.status(200).send(`[${specs.join(',\n')}]`); + res.status(200).send(JSON.stringify(specs, null, 2)); }); - router.get('/entity/:namespace/:kind/:name/:badgeId', async (req, res) => { - const { namespace, kind, name, badgeId } = req.params; - const entity = await catalog.getEntityByName({ namespace, kind, name }); - if (!entity) { - throw new NotFoundError( - `No ${kind} entity in ${namespace} named "${name}"`, - ); - } + router.get( + '/entity/:namespace/:kind/:name/badge/:badgeId', + async (req, res) => { + const { namespace, kind, name, badgeId } = req.params; + const entity = await catalog.getEntityByName({ namespace, kind, name }); + if (!entity) { + throw new NotFoundError( + `No ${kind} entity in ${namespace} named "${name}"`, + ); + } - let format = - req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml'; - if (req.query.format === 'json') { - format = 'application/json'; - } + let format = + req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml'; + if (req.query.format === 'json') { + format = 'application/json'; + } - const badgeUrl = [ - `${req.protocol}://`, - req.headers.host, - req.originalUrl, - ].join(''); + const badgeOptions = { + badgeInfo: { id: badgeId }, + context: { + badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId, options), + config: options.config, + entity, + }, + }; - const data = await badgeBuilder.createBadge({ - badgeId, - context: { badgeUrl, config: options.config, entity }, - format: format === 'application/json' ? 'json' : 'svg', - }); + let data: string; + if (format === 'application/json') { + data = JSON.stringify( + await badgeBuilder.createBadgeJson(badgeOptions), + null, + 2, + ); + } else { + data = await badgeBuilder.createBadgeSvg(badgeOptions); + } - if (!data) { - throw new NotFoundError(`Unknown badge "${badgeId}" for ${kind} entity.`); - } - - res.setHeader('Content-Type', format); - res.status(200).send(data); - }); + res.setHeader('Content-Type', format); + res.status(200).send(data); + }, + ); router.use(errorHandler()); return router; } + +async function getBadgeUrl( + namespace: string, + kind: string, + name: string, + badgeId: string, + options: RouterOptions, +): Promise { + const baseUrl = await options.discovery.getExternalBaseUrl('badges'); + return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`; +} diff --git a/plugins/badges-backend/src/types.ts b/plugins/badges-backend/src/types.ts index ee4876b90e..69fc275f3f 100644 --- a/plugins/badges-backend/src/types.ts +++ b/plugins/badges-backend/src/types.ts @@ -55,7 +55,7 @@ export interface BadgeContext { } export interface BadgeFactory { - createBadge(context: BadgeContext): Badge | null; + createBadge(context: BadgeContext): Badge; } export interface BadgeFactories { diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 51aae31621..1b26ed69dd 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.3", "@backstage/core": "^0.7.1", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.1.1", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 4160278d10..927a16a7eb 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -16,6 +16,7 @@ import { generatePath } from 'react-router'; import { DiscoveryApi } from '@backstage/core'; +import { ResponseError } from '@backstage/errors'; import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { entityRoute } from '@backstage/plugin-catalog-react'; import { BadgesApi, BadgeSpec } from './types'; @@ -29,11 +30,13 @@ export class BadgesClient implements BadgesApi { public async getEntityBadgeSpecs(entity: Entity): Promise { const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity); - const specs = (await ( - await fetch(entityBadgeSpecsUrl) - ).json()) as BadgeSpec[]; + const response = await fetch(entityBadgeSpecsUrl); - return specs; + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return await response.json(); } private async getEntityBadgeSpecsUrl(entity: Entity): Promise { diff --git a/plugins/badges/src/components/EntityBadgesDialog.tsx b/plugins/badges/src/components/EntityBadgesDialog.tsx index ac0d46f114..74a244e698 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.tsx @@ -15,7 +15,12 @@ */ import { Entity } from '@backstage/catalog-model'; -import { CodeSnippet, Progress, useApi } from '@backstage/core'; +import { + CodeSnippet, + Progress, + ResponseErrorPanel, + useApi, +} from '@backstage/core'; import { Button, Dialog, @@ -28,7 +33,6 @@ import { useTheme, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { useAsync } from 'react-use'; import { badgesApiRef } from '../api'; @@ -80,17 +84,11 @@ export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => { return ( - Entity Badges + Entity Badges - {loading ? : null} - - {error ? ( - - {error.toString()} - - ) : null} - + {loading && } + {error && } {content}