diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts index cea58d8737..befca76542 100644 --- a/packages/backend/src/plugins/badges.ts +++ b/packages/backend/src/plugins/badges.ts @@ -16,19 +16,17 @@ import { createRouter, - createDefaultBadges, + createDefaultBadgeFactories, } from '@backstage/plugin-badges-backend'; import { PluginEnvironment } from '../types'; export default async function createPlugin({ - logger, config, discovery, }: PluginEnvironment) { return await createRouter({ - logger, config, discovery, - badges: createDefaultBadges(), + badgeFactories: createDefaultBadgeFactories(), }); } diff --git a/plugins/badges-backend/src/badges.ts b/plugins/badges-backend/src/badges.ts index da1dad6bce..03e32814fd 100644 --- a/plugins/badges-backend/src/badges.ts +++ b/plugins/badges-backend/src/badges.ts @@ -14,39 +14,85 @@ * limitations under the License. */ -import { Badge } from './types'; +import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Badge, BadgeContext, BadgeFactories } from './types'; -export const createDefaultBadges = (): Badge[] => [ - { - id: 'pingback', - kind: 'entity', - description: 'Link to _{entity.metadata.name} in _{app.title}', - label: '_{entity.kind}', - message: '_{entity.metadata.name}', - style: 'flat-square', +function appTitle(context: BadgeContext): string { + return context.config.getString('app.title') || 'Backstage'; +} + +function entityUrl(context: BadgeContext): string { + const e = context.entity!; + const entityUri = `${e.kind}/${ + e.metadata.namespace || ENTITY_DEFAULT_NAMESPACE + }/${e.metadata.name}`; + const catalogUrl = `${context.config.getString('app.baseUrl')}/catalog`; + return `${catalogUrl}/${entityUri}`; +} + +export const createDefaultBadgeFactories = (): BadgeFactories => ({ + pingback: { + createBadge: (context: BadgeContext): Badge | null => { + if (!context.entity) { + return null; + } + return { + description: `Link to ${context.entity.metadata.name} in ${appTitle( + context, + )}`, + kind: 'entity', + label: context.entity.kind, + link: entityUrl(context), + message: context.entity.metadata.name, + style: 'flat-square', + }; + }, }, - { - id: 'lifecycle', - kind: 'entity', - description: 'Entity lifecycle badge', - label: 'lifecycle', - message: '_{entity.spec.lifecycle}', - style: 'flat-square', + + lifecycle: { + createBadge: (context: BadgeContext): Badge | null => { + if (!context.entity) { + return null; + } + return { + description: 'Entity lifecycle badge', + kind: 'entity', + label: 'lifecycle', + link: entityUrl(context), + message: `${context.entity.spec?.lifecycle || 'unknown'}`, + style: 'flat-square', + }; + }, }, - { - id: 'owner', - kind: 'entity', - description: 'Entity owner badge', - label: 'owner', - message: '_{entity.spec.owner}', - style: 'flat-square', + + owner: { + createBadge: (context: BadgeContext): Badge | null => { + if (!context.entity) { + return null; + } + return { + description: 'Entity owner badge', + kind: 'entity', + label: 'owner', + link: entityUrl(context), + message: `${context.entity.spec?.owner || 'unknown'}`, + style: 'flat-square', + }; + }, }, - { - id: 'docs', - kind: 'entity', - link: '_{entity_url}/docs', - label: 'docs', - message: '_{entity.metadata.name}', - style: 'flat-square', + + docs: { + createBadge: (context: BadgeContext): Badge | null => { + if (!context.entity) { + return null; + } + return { + kind: 'entity', + label: 'docs', + link: `${entityUrl(context)}/docs`, + message: context.entity.metadata.name, + style: 'flat-square', + }; + }, }, -]; +}); diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts index c9c9021ddd..ce905d8e9d 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.ts @@ -14,83 +14,56 @@ * limitations under the License. */ -import { Logger } from 'winston'; -import { makeBadge } from 'badge-maker'; +import { makeBadge, Format } from 'badge-maker'; import { BadgeBuilder, BadgeOptions } from './types'; -import { Badge, BadgeConfig, BadgeStyle, BADGE_STYLES } from '../../types'; -import { interpolate } from '../../utils'; +import { Badge, BadgeFactories } from '../../types'; export class DefaultBadgeBuilder implements BadgeBuilder { - private readonly badges: BadgeConfig = {}; + constructor(private readonly factories: BadgeFactories) {} - constructor(private readonly logger: Logger, initBadges: Badge[]) { - for (const badge of initBadges) { - if (!badge.id) { - logger.warning(`badge without "id": ${JSON.stringify(badge, null, 2)}`); - } else { - this.badges[badge.id] = badge; - logger.info(`register ${badge.kind || 'entity'} badge: "${badge.id}"`); - } - } - } - - public async getAllBadgeConfigs(): Promise { - return Object.values(this.badges); - } - - public async getBadgeConfig(badgeId: string): Promise { - return ( - this.badges[badgeId] || - this.badges.default || - ({ - label: 'Unknown badge ID', - message: badgeId, - color: 'red', - } as Badge) - ); + public async getBadgeIds(): Promise { + return Object.keys(this.factories); } 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 Badge; + const factory = this.factories[options.badgeId]; + const badge = factory + ? factory.createBadge(options.context) + : ({ + label: 'unknown badge', + message: options.badgeId, + color: 'red', + } as Badge); - if (badge.labelColor) { - params.labelColor = badge.labelColor; - } - - if (BADGE_STYLES.includes(badge.style as BadgeStyle)) { - params.style = badge.style as BadgeStyle; + if (!badge) { + return ''; } switch (options.format) { case 'json': - if (badge.link) { - params.link = this.render(badge.link, context); - } - - params.description = badge.description - ? this.render(badge.description, context) - : badge.id; - params.markdown = this.getMarkdownCode(params, context.badge_url!); - return JSON.stringify( { - badge: params, - ...options, + badge, + id: options.badgeId, + url: options.context.badgeUrl, + markdown: this.getMarkdownCode(badge, options.context.badgeUrl), }, null, 2, ); case 'svg': try { - return makeBadge(params); + 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 parameters', + label: 'invalid badge', message: `${err}`, color: 'red', }); @@ -100,20 +73,9 @@ export class DefaultBadgeBuilder implements BadgeBuilder { } } - private render(template: string, context: object): string { - try { - return interpolate(template.replace(/_{/g, '${'), context).toLowerCase(); - } catch (err) { - this.logger.info( - `badge template error: ${err}. In template: "${template}"`, - ); - return `${err} [${template}]`; - } - } - private getMarkdownCode(params: Badge, badge_url: string): string { let alt_text = `${params.label}: ${params.message}`; - if (params.description !== params.label) { + if (params.description && params.description !== params.label) { alt_text = `${params.description}, ${alt_text}`; } const tooltip = params.description ? ` "${params.description}"` : ''; diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts index e6cb3213e9..9c17d8e3c3 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/types.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/types.ts @@ -14,24 +14,15 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { Badge } from '../../types'; +import { BadgeContext } from '../../types'; export type BadgeOptions = { - context: { - app?: { - title: string; - }; - entity?: Entity; - entity_url?: string; - badge_url?: string; - }; - config: Badge; + badgeId: string; + context: BadgeContext; format: 'svg' | 'json'; }; export type BadgeBuilder = { createBadge(options: BadgeOptions): Promise; - getBadgeConfig(badgeId: string): Promise; - getAllBadgeConfigs(): Promise; + getBadgeIds(): Promise; }; diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 90d91eb6f3..72400b50b5 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -26,11 +26,7 @@ describe('createRouter', () => { const logger = winston.createLogger(); const config = await loadBackendConfig({ logger, argv: [] }); const discovery = SingleHostDiscovery.fromConfig(config); - const router = await createRouter({ - config, - logger, - discovery, - }); + const router = await createRouter({ config, discovery }); expect(router).toBeDefined(); }); }); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index 31eb96ceb1..0289f39148 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -17,7 +17,6 @@ import express from 'express'; import fetch from 'cross-fetch'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { errorHandler, PluginEndpointDiscovery, @@ -25,12 +24,11 @@ import { import { Entity } from '@backstage/catalog-model'; import { Config, JsonObject } from '@backstage/config'; import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder'; -import { Badge, BadgeStyle, BADGE_STYLES } from '../types'; +import { BadgeContext, BadgeFactories } from '../types'; export interface RouterOptions { badgeBuilder?: BadgeBuilder; - badges?: Badge[]; - logger: Logger; + badgeFactories?: BadgeFactories; config: Config; discovery: PluginEndpointDiscovery; } @@ -39,45 +37,39 @@ export async function createRouter( options: RouterOptions, ): Promise { const router = Router(); - const logger = options.logger.child({ plugin: 'badges' }); - const title = options.config.getString('app.title') || 'Backstage'; - const catalogUrl = `${options.config.getString('app.baseUrl')}/catalog`; const badgeBuilder = options.badgeBuilder || - new DefaultBadgeBuilder(logger, options.badges || []); + new DefaultBadgeBuilder(options.badgeFactories || {}); router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => { const entityUri = getEntityUri(req.params); - const entity_url = `${catalogUrl}/${entityUri}`; const entity = await getEntity(options.discovery, entityUri); if (!entity) { res.status(404).send(`Unknown entity`); return; } - const context = { - app: { title }, - badge_url: '', + const context: BadgeContext = { + badgeUrl: '', + config: options.config, entity, - entity_url, }; const specs = []; - for (const badge of await badgeBuilder.getAllBadgeConfigs()) { - if (!badge.kind || badge.kind === 'entity') { - badge.link = badge.link ?? '_{entity_url}'; - context.badge_url = [ - `${req.protocol}://`, - req.headers.host, - req.originalUrl.replace(/badge-specs$/, badge.id!), - ].join(''); - specs.push( - await badgeBuilder.createBadge({ - config: badge, - context, - format: 'json', - }), - ); + 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', + }); + + if (badge) { + specs.push(badge); } } @@ -87,14 +79,6 @@ export async function createRouter( router.get('/entity/:namespace/:kind/:name/:badgeId', async (req, res) => { const { badgeId } = req.params; - let badge = await badgeBuilder.getBadgeConfig(badgeId); - if (badge.kind && badge.kind !== 'entity') { - badge = { - label: 'Badge kind error', - message: `${badgeId} is for ${badge.kind} not entity`, - color: 'red', - }; - } const entityUri = getEntityUri(req.params); const entity = await getEntity(options.discovery, entityUri); @@ -109,31 +93,24 @@ export async function createRouter( format = 'application/json'; } - if (BADGE_STYLES.includes(req.query.style as BadgeStyle)) { - badge.style = req.query.style as BadgeStyle; - } - - const badge_url = [ + const badgeUrl = [ `${req.protocol}://`, req.headers.host, req.originalUrl, ].join(''); - const entity_url = `${catalogUrl}/${entityUri}`; - badge.link = badge.link ?? '_{entity_url}'; const data = await badgeBuilder.createBadge({ - config: badge, - context: { - app: { title }, - badge_url, - entity, - entity_url, - }, + badgeId, + context: { badgeUrl, config: options.config, entity }, format: format === 'application/json' ? 'json' : 'svg', }); - res.setHeader('Content-Type', format); - res.status(200).send(data); + if (!data) { + res.status(404).send(`Unknown entity badge "${badgeId}"`); + } else { + res.setHeader('Content-Type', format); + res.status(200).send(data); + } }); router.use(errorHandler()); diff --git a/plugins/badges-backend/src/service/standaloneServer.ts b/plugins/badges-backend/src/service/standaloneServer.ts index d358edcbdc..c210efa249 100644 --- a/plugins/badges-backend/src/service/standaloneServer.ts +++ b/plugins/badges-backend/src/service/standaloneServer.ts @@ -38,7 +38,7 @@ export async function startStandaloneServer( logger.debug('Creating application...'); - const router = await createRouter({ logger, config, discovery }); + const router = await createRouter({ config, discovery }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) diff --git a/plugins/badges-backend/src/types.ts b/plugins/badges-backend/src/types.ts index 0943589422..ee4876b90e 100644 --- a/plugins/badges-backend/src/types.ts +++ b/plugins/badges-backend/src/types.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { Config } from '@backstage/config'; +import { Entity } from '@backstage/catalog-model'; + export const BADGE_STYLES = [ 'plastic', 'flat', @@ -24,13 +27,11 @@ export const BADGE_STYLES = [ export type BadgeStyle = typeof BADGE_STYLES[number]; export interface Badge { - /** Unique name for the badge. */ - id?: string; /** Badge message background color. */ color?: string; /** Badge description (tooltip text) */ description?: string; - /** Kind of badge (in what context may it be used) */ + /** Kind of badge */ kind?: 'entity'; /** * Badge label (should be a rather static value) @@ -45,10 +46,18 @@ export interface Badge { message: string; /** Badge style (apperance). One of "plastic", "flat", "flat-square", "for-the-badge" and "social" */ style?: BadgeStyle; - /** (generated) markdown code */ - markdown?: string; } -export interface BadgeConfig { - [id: string]: Badge; +export interface BadgeContext { + badgeUrl: string; + config: Config; + entity?: Entity; // for entity badges +} + +export interface BadgeFactory { + createBadge(context: BadgeContext): Badge | null; +} + +export interface BadgeFactories { + [id: string]: BadgeFactory; } diff --git a/plugins/badges/src/api/types.ts b/plugins/badges/src/api/types.ts index d3ba234e44..9765af2c93 100644 --- a/plugins/badges/src/api/types.ts +++ b/plugins/badges/src/api/types.ts @@ -29,7 +29,7 @@ export type BadgeStyle = | 'for-the-badge' | 'social'; -interface BadgeParams { +interface Badge { color?: string; description?: string; kind?: 'entity'; @@ -40,29 +40,18 @@ interface BadgeParams { style?: BadgeStyle; } -interface Badge extends BadgeParams { - markdown: string; -} - -interface BadgeConfig extends BadgeParams { - id: string; -} - export interface BadgeSpec { - /** The rendered fields, markdown code */ + /** Badge id */ + id: string; + + /** Badge data */ badge: Badge; - /** The configuration data, with placeholders and all */ - config: BadgeConfig; + /** The URL to the badge image */ + url: string; - /** The context used when rendering config -> badge */ - context: { - // here is more, but only badge_url we care about - badge_url: string; - }; - - format: 'json'; // or 'svg', but we'll never see that as structured - // data, only as an svg element + /** The markdown code to use the badge */ + markdown: string; } export interface BadgesApi { diff --git a/plugins/badges/src/components/EntityBadgesDialog.tsx b/plugins/badges/src/components/EntityBadgesDialog.tsx index f30278ca4d..9d4661f520 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.tsx @@ -65,16 +65,12 @@ export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => { }, [badgesApi, entity, open]); const content = (badges || []).map( - ({ - badge: { description, markdown }, - config: { id }, - context: { badge_url }, - }) => ( + ({ badge: { description }, id, url, markdown }) => (
- {description} + {description || `${id} badge`}
- {description} + {description
Copy the following snippet of markdown code for the badge: