badges: refactoring to use badge factories to create dynamic badges.

Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se>
This commit is contained in:
Andreas Stenius
2021-03-04 14:05:31 +01:00
committed by Fredrik Adelöw
parent ad95bebc8f
commit 5a09c681d7
10 changed files with 171 additions and 207 deletions
+77 -31
View File
@@ -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',
};
},
},
];
});
@@ -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<Badge[]> {
return Object.values(this.badges);
}
public async getBadgeConfig(badgeId: string): Promise<Badge> {
return (
this.badges[badgeId] ||
this.badges.default ||
({
label: 'Unknown badge ID',
message: badgeId,
color: 'red',
} as Badge)
);
public async getBadgeIds(): Promise<string[]> {
return Object.keys(this.factories);
}
public async createBadge(options: BadgeOptions): Promise<string> {
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}"` : '';
@@ -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<string>;
getBadgeConfig(badgeId: string): Promise<Badge>;
getAllBadgeConfigs(): Promise<Badge[]>;
getBadgeIds(): Promise<string[]>;
};
@@ -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();
});
});
+29 -52
View File
@@ -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<express.Router> {
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());
@@ -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' })
+16 -7
View File
@@ -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;
}