diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 3d77acbe98..ef9e23e16f 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts index 80bca52470..1e96b6ce5b 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts @@ -15,41 +15,46 @@ */ import { Logger } from 'winston'; -import { makeBadge, ValidationError } from 'badge-maker'; -import { BadgeBuilder, BadgeConfig, BadgeOptions } from './types'; +import { makeBadge } from 'badge-maker'; +import { JsonObject } from '@backstage/config'; +import { + BadgeBuilder, + BadgeConfig, + BadgeOptions, + BadgeStyle, + BadgeStyles, +} from './types'; import { interpolate } from '../../utils'; export class DefaultBadgeBuilder implements BadgeBuilder { constructor( private readonly logger: Logger, - private readonly config: { [id: string]: BadgeConfig }, + private readonly config: JsonObject, ) {} - public async getBadgeConfig(badgeId: string): BadgeConfig { - return ( - this.config[badgeId] || - this.config.default || { + public async getBadgeConfig(badgeId: string): Promise { + return ((this.config[badgeId] as unknown) || + (this.config.default as unknown) || { label: 'Unknown badge ID', message: badgeId, color: 'red', - } - ); + }) as BadgeConfig; } - public async createBadge(options: BadgeOptions): string { + public async createBadge(options: BadgeOptions): Promise { const { context, config: badge } = options; const params = { label: this.render(badge.label, context), message: this.render(badge.message, context), color: badge.color || '#36BAA2', - }; + } as BadgeConfig; if (badge.labelColor) { params.labelColor = badge.labelColor; } - if (badge.style) { - params.style = badge.style; + if (BadgeStyles.includes(badge.style as BadgeStyle)) { + params.style = badge.style as BadgeStyle; } switch (options.format) { @@ -90,6 +95,9 @@ export class DefaultBadgeBuilder implements BadgeBuilder { try { return interpolate(template.replace(/_{/g, '${'), context); } catch (err) { + this.logger.info( + `badge template error: ${err}. In template: "${template}"`, + ); return `${err} [${template}]`; } } diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts index ae898d645d..0b0bc3d3b0 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts @@ -14,13 +14,22 @@ * limitations under the License. */ +export const BadgeStyles = [ + 'plastic', + 'flat', + 'flat-square', + 'for-the-badge', + 'social', +] as const; +export type BadgeStyle = typeof BadgeStyles[number]; + export interface BadgeConfig { kind?: 'entity'; label: string; message: string; color?: string; labelColor?: string; - style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'; + style?: BadgeStyle; title?: string; description?: string; link?: string; diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 94a0b099c0..90d91eb6f3 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -14,12 +14,23 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { ConfigReader } from '@backstage/config'; -import express from 'express'; -import request from 'supertest'; -import { BadgesApi } from '../api'; +import * as winston from 'winston'; +import { + loadBackendConfig, + SingleHostDiscovery, +} from '@backstage/backend-common'; import { createRouter } from './router'; -import {} from '../api/types'; -describe('createRouter', () => {}); +describe('createRouter', () => { + it('works', async () => { + const logger = winston.createLogger(); + const config = await loadBackendConfig({ logger, argv: [] }); + const discovery = SingleHostDiscovery.fromConfig(config); + const router = await createRouter({ + config, + logger, + discovery, + }); + expect(router).toBeDefined(); + }); +}); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index d0013d8318..81e9be069a 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -22,11 +22,17 @@ import { errorHandler, PluginEndpointDiscovery, } from '@backstage/backend-common'; -import { Config } from '@backstage/config'; -import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder'; +import { Entity } from '@backstage/catalog-model'; +import { Config, JsonObject } from '@backstage/config'; +import { + BadgeBuilder, + BadgeStyle, + BadgeStyles, + DefaultBadgeBuilder, +} from '../lib/BadgeBuilder'; export interface RouterOptions { - badgeBuilder: BadgeBuilder; + badgeBuilder?: BadgeBuilder; logger: Logger; config: Config; discovery: PluginEndpointDiscovery; @@ -39,10 +45,13 @@ export async function createRouter( const logger = options.logger.child({ plugin: 'badges' }); const title = options.config.getString('app.title') || 'Backstage'; const catalogUrl = `${options.config.getString('app.baseUrl')}/catalog`; - const badgesConfig = options.config.getOptional('badges') ?? {}; + const badgesConfig = (options.config.getOptional('badges') ?? + {}) as JsonObject; const badgeBuilder = options.badgeBuilder || new DefaultBadgeBuilder(logger, badgesConfig); + logger.debug(`loading badges`); + router.get('/entity/:namespace/:kind/:name/:badgeId', async (req, res) => { const { badgeId } = req.params; let badge = await badgeBuilder.getBadgeConfig(badgeId); @@ -55,7 +64,7 @@ export async function createRouter( } const entityUri = getEntityUri(req.params); - const entity = await getEntity(logger, options.discovery, entityUri); + const entity = await getEntity(options.discovery, entityUri); if (!entity) { res.status(400).send(`Unknown entity`); return; @@ -67,8 +76,8 @@ export async function createRouter( format = 'application/json'; } - if (req.query.style) { - badge.style = req.query.style; + if (BadgeStyles.includes(req.query.style as BadgeStyle)) { + badge.style = req.query.style as BadgeStyle; } const data = await badgeBuilder.createBadge({ @@ -90,23 +99,20 @@ export async function createRouter( return router; } -function getEntityUri(params) { +function getEntityUri(params: JsonObject): string { const { kind, namespace, name } = params; return `${kind}/${namespace}/${name}`; } -async function getEntity(logger, discovery, entityUri) { +async function getEntity( + discovery: PluginEndpointDiscovery, + entityUri: string, +): Promise { const catalogUrl = await discovery.getBaseUrl('catalog'); - try { - const entity = (await ( - await fetch(`${catalogUrl}/entities/by-name/${entityUri}`) - ).json()) as Entity; + const entity = (await ( + await fetch(`${catalogUrl}/entities/by-name/${entityUri}`) + ).json()) as Entity; - return entity; - } catch (err) { - const msg = `Unable to get entity ${entityUri}, error ${err}`; - logger.info(msg); - return null; - } + return entity; } diff --git a/plugins/badges-backend/src/utils.ts b/plugins/badges-backend/src/utils.ts index e9090905e3..0f4773170e 100644 --- a/plugins/badges-backend/src/utils.ts +++ b/plugins/badges-backend/src/utils.ts @@ -17,7 +17,7 @@ /** * adapted from https://stackoverflow.com/a/41015840/444060 */ -export function interpolate(template, params) { +export function interpolate(template: string, params: object): string { const names = Object.keys(params); const vals = Object.values(params); // eslint-disable-next-line no-new-func diff --git a/plugins/badges/dev/index.tsx b/plugins/badges/dev/index.tsx index 1f545a777e..1616edacb4 100644 --- a/plugins/badges/dev/index.tsx +++ b/plugins/badges/dev/index.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { badgesPlugin } from '../src/plugin'; diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 9c0387b58e..2cd68ec909 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -18,7 +18,7 @@ import { generatePath } from 'react-router'; import { ConfigApi, DiscoveryApi } from '@backstage/core'; import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { entityRoute } from '@backstage/plugin-catalog-react'; -import { BadgesApi, BadgeConfig, BadgeSpec } from './types'; +import { Badge, BadgesApi, BadgeConfig, BadgeSpec } from './types'; export class BadgesClient implements BadgesApi { private readonly configApi: ConfigApi; @@ -29,7 +29,7 @@ export class BadgesClient implements BadgesApi { this.discoveryApi = options.discoveryApi; } - public async getDefinedEntityBadges(entity: Entity): Badge[] { + public async getDefinedEntityBadges(entity: Entity): Promise { const badges = []; const badgesConfig = this.configApi.getOptional('badges') ?? {}; const entityBadgeUri = await this.getEntityBadgeUri(entity); @@ -46,7 +46,10 @@ export class BadgesClient implements BadgesApi { return badges; } - private async getBadgeInfo(entityBadgeUri: string, badgeId: string): Badge { + private async getBadgeInfo( + entityBadgeUri: string, + badgeId: string, + ): Promise { const badgeUrl = `${entityBadgeUri}/${badgeId}`; const spec = (await ( await fetch(`${badgeUrl}?format=json`) @@ -60,7 +63,7 @@ export class BadgesClient implements BadgesApi { }; } - private async getEntityBadgeUri(entity: Entity): string { + private async getEntityBadgeUri(entity: Entity): Promise { const routeParams = this.getEntityRouteParams(entity); const path = generatePath(entityRoute.path, routeParams); return `${await this.discoveryApi.getBaseUrl('badges')}/entity/${path}`; diff --git a/plugins/badges/src/api/types.ts b/plugins/badges/src/api/types.ts index 5b03f43a58..00f5f62a3b 100644 --- a/plugins/badges/src/api/types.ts +++ b/plugins/badges/src/api/types.ts @@ -22,14 +22,21 @@ export const badgesApiRef = createApiRef({ description: 'Used to make requests to the badges backend', }); +export type BadgeStyle = + | 'plastic' + | 'flat' + | 'flat-square' + | 'for-the-badge' + | 'social'; + // should probably have this in a "badges-common" package -interface BadgeConfig { +export interface BadgeConfig { kind?: 'entity'; label: string; message: string; color?: string; labelColor?: string; - style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'; + style?: BadgeStyle; title?: string; description?: string; link?: string; diff --git a/plugins/badges/src/components/EntityBadgesDialog.tsx b/plugins/badges/src/components/EntityBadgesDialog.tsx index ec9bd35221..3a667fb338 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.tsx @@ -39,13 +39,13 @@ type Props = { entity: Entity; }; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles({ codeBlock: { '& code': { whiteSpace: 'pre-wrap', }, }, -})); +}); export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => { const theme = useTheme(); @@ -70,7 +70,7 @@ export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => { Copy the following snippet of markdown code for the badge: - +
diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index a8da917fd8..b9af9b307e 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -35,7 +35,7 @@ const useStyles = makeStyles({ }); type Props = { - onShowBadgesDialog: () => void; + onShowBadgesDialog?: () => void; onUnregisterEntity: () => void; };