badges: load default badges from code rather than config. #4457.
Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
committed by
Fredrik Adelöw
parent
ce4897ff37
commit
ac17aeb35d
Vendored
+14
-13
@@ -16,7 +16,10 @@
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Define all badges.
|
||||
* Define custom badges. By default, the badges are declared in
|
||||
* code, and passed to the badges backend `createRouter`, which
|
||||
* merges them with any additional badges defined in this
|
||||
* configuration.
|
||||
*
|
||||
* The `label` and `message` fields may use templating to support
|
||||
* dynamic content, based on context. Use same syntax as for
|
||||
@@ -33,7 +36,9 @@ export interface Config {
|
||||
* Useful when using templating for label and/or message if they
|
||||
* use context data only available for a certain kind of badge.
|
||||
*
|
||||
* Available badge kinds:
|
||||
* Context per badge kind.
|
||||
*
|
||||
* Entity badges:
|
||||
*
|
||||
* * `entity` The entity data is available as `entity` in the template.
|
||||
* * `entity_url` The (frontend) URL to view the entity in Backstage.
|
||||
@@ -41,14 +46,13 @@ export interface Config {
|
||||
* Default context for all badges:
|
||||
*
|
||||
* * `app.title` As configured or defaults to "Backstage".
|
||||
* * `badge_url` The URL to the badge image.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
kind?: 'entity';
|
||||
|
||||
/**
|
||||
* The badge label.
|
||||
*
|
||||
*/
|
||||
label: string;
|
||||
|
||||
@@ -70,21 +74,17 @@ export interface Config {
|
||||
/**
|
||||
* Visual design of the badge. One of: 'plastic', 'flat', 'flat-square',
|
||||
* 'for-the-badge' or 'social'.
|
||||
*
|
||||
* Default: 'flat'
|
||||
*
|
||||
*/
|
||||
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social';
|
||||
|
||||
/**
|
||||
* Badge title, used as tooltip text in the markdown code.
|
||||
* Badge description, used as prefix on the alt text in the markdown code.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Badge description, used as alt text in the markdown code.
|
||||
* Defaults to badge id.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
@@ -94,7 +94,8 @@ export interface Config {
|
||||
* For `entity` badges, there is a `entity_url` in the context
|
||||
* that could be appropriate to use here.
|
||||
*
|
||||
* @visibility frontend
|
||||
* Defaults to the `entity_url`, set to falsey value to disable the link.
|
||||
*
|
||||
*/
|
||||
link?: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Badge } from './types';
|
||||
|
||||
export const badges: Badge[] = [
|
||||
{
|
||||
id: 'pingback',
|
||||
kind: 'entity',
|
||||
description: 'Link back to _{app.title}',
|
||||
label: '_{app.title}',
|
||||
message: '_{entity.kind}: _{entity.metadata.name}',
|
||||
style: 'flat-square',
|
||||
},
|
||||
{
|
||||
id: 'lifecycle',
|
||||
kind: 'entity',
|
||||
description: 'Entity lifecycle badge',
|
||||
label: 'lifecycle',
|
||||
message: '_{entity.spec.lifecycle}',
|
||||
style: 'flat-square',
|
||||
},
|
||||
{
|
||||
id: 'owner',
|
||||
kind: 'entity',
|
||||
description: 'Entity owner badge',
|
||||
label: 'owner',
|
||||
message: '_{entity.spec.owner}',
|
||||
color: 'blue',
|
||||
style: 'flat-square',
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
kind: 'entity',
|
||||
link: '_{entity_url}/docs',
|
||||
label: 'docs',
|
||||
message: '_{entity.metadata.name}',
|
||||
color: 'navyblue',
|
||||
style: 'flat-square',
|
||||
},
|
||||
];
|
||||
@@ -15,4 +15,6 @@
|
||||
*/
|
||||
|
||||
export * from './service/router';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './badges';
|
||||
|
||||
@@ -17,28 +17,31 @@
|
||||
import { Logger } from 'winston';
|
||||
import { makeBadge } from 'badge-maker';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import {
|
||||
BadgeBuilder,
|
||||
BadgeConfig,
|
||||
BadgeOptions,
|
||||
BadgeStyle,
|
||||
BadgeStyles,
|
||||
} from './types';
|
||||
import { BadgeBuilder, BadgeOptions } from './types';
|
||||
import { Badge, BadgeStyle, BadgeStyles } from '../../types';
|
||||
import { interpolate } from '../../utils';
|
||||
|
||||
export class DefaultBadgeBuilder implements BadgeBuilder {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly config: JsonObject,
|
||||
) {}
|
||||
) {
|
||||
for (const [badgeId, badge] of Object.entries(config)) {
|
||||
badge.id = badgeId;
|
||||
}
|
||||
}
|
||||
|
||||
public async getBadgeConfig(badgeId: string): Promise<BadgeConfig> {
|
||||
public async getAllBadgeConfigs(): Promise<Badge[]> {
|
||||
return Object.values(this.config) as Badge[];
|
||||
}
|
||||
|
||||
public async getBadgeConfig(badgeId: string): Promise<Badge> {
|
||||
return ((this.config[badgeId] as unknown) ||
|
||||
(this.config.default as unknown) || {
|
||||
label: 'Unknown badge ID',
|
||||
message: badgeId,
|
||||
color: 'red',
|
||||
}) as BadgeConfig;
|
||||
}) as Badge;
|
||||
}
|
||||
|
||||
public async createBadge(options: BadgeOptions): Promise<string> {
|
||||
@@ -47,7 +50,7 @@ export class DefaultBadgeBuilder implements BadgeBuilder {
|
||||
label: this.render(badge.label, context),
|
||||
message: this.render(badge.message, context),
|
||||
color: badge.color || '#36BAA2',
|
||||
} as BadgeConfig;
|
||||
} as Badge;
|
||||
|
||||
if (badge.labelColor) {
|
||||
params.labelColor = badge.labelColor;
|
||||
@@ -59,15 +62,15 @@ export class DefaultBadgeBuilder implements BadgeBuilder {
|
||||
|
||||
switch (options.format) {
|
||||
case 'json':
|
||||
if (badge.title) {
|
||||
params.title = this.render(badge.title, context);
|
||||
}
|
||||
if (badge.description) {
|
||||
params.description = this.render(badge.description, context);
|
||||
}
|
||||
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,
|
||||
@@ -101,4 +104,11 @@ export class DefaultBadgeBuilder implements BadgeBuilder {
|
||||
return `${err} [${template}]`;
|
||||
}
|
||||
}
|
||||
|
||||
private getMarkdownCode(params: Badge, badge_url: string): string {
|
||||
const alt_text = `${params.description}, ${params.label}: ${params.message}`;
|
||||
const tooltip = params.description ? ` "${params.description}"` : '';
|
||||
const img = ``;
|
||||
return params.link ? `[${img}](${params.link})` : img;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,34 +14,16 @@
|
||||
* 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?: BadgeStyle;
|
||||
title?: string;
|
||||
description?: string;
|
||||
link?: string;
|
||||
}
|
||||
import { Badge } from '../../types';
|
||||
|
||||
export type BadgeOptions = {
|
||||
context: object;
|
||||
config: BadgeConfig;
|
||||
config: Badge;
|
||||
format: 'svg' | 'json';
|
||||
};
|
||||
|
||||
export type BadgeBuilder = {
|
||||
createBadge(options: BadgeOptions): Promise<string>;
|
||||
getBadgeConfig(badgeId: string): Promise<BadgeConfig>;
|
||||
getBadgeConfig(badgeId: string): Promise<Badge>;
|
||||
getAllBadgeConfigs(): Promise<Badge[]>;
|
||||
};
|
||||
|
||||
@@ -24,15 +24,12 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config, JsonObject } from '@backstage/config';
|
||||
import {
|
||||
BadgeBuilder,
|
||||
BadgeStyle,
|
||||
BadgeStyles,
|
||||
DefaultBadgeBuilder,
|
||||
} from '../lib/BadgeBuilder';
|
||||
import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder';
|
||||
import { Badge, BadgeStyle, BadgeStyles } from '../types';
|
||||
|
||||
export interface RouterOptions {
|
||||
badgeBuilder?: BadgeBuilder;
|
||||
badges?: Badge[];
|
||||
logger: Logger;
|
||||
config: Config;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
@@ -47,10 +44,58 @@ export async function createRouter(
|
||||
const catalogUrl = `${options.config.getString('app.baseUrl')}/catalog`;
|
||||
const badgesConfig = (options.config.getOptional('badges') ??
|
||||
{}) as JsonObject;
|
||||
|
||||
for (const badge of options.badges || []) {
|
||||
if (!badge.id) {
|
||||
logger.warning(`badge without "id": ${JSON.stringify(badge, null, 2)}`);
|
||||
} else if (!badgesConfig[badge.id]) {
|
||||
badgesConfig[badge.id] = badge;
|
||||
logger.info(`register builtin badge: ${badge.id}`);
|
||||
} else {
|
||||
logger.info(`builtin badge replaced from configuration: ${badge.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const badgeBuilder =
|
||||
options.badgeBuilder || new DefaultBadgeBuilder(logger, badgesConfig);
|
||||
|
||||
logger.debug(`loading badges`);
|
||||
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 },
|
||||
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',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.status(200).send(`[${specs.join(',\n')}]`);
|
||||
});
|
||||
|
||||
router.get('/entity/:namespace/:kind/:name/:badgeId', async (req, res) => {
|
||||
const { badgeId } = req.params;
|
||||
@@ -66,7 +111,7 @@ export async function createRouter(
|
||||
const entityUri = getEntityUri(req.params);
|
||||
const entity = await getEntity(options.discovery, entityUri);
|
||||
if (!entity) {
|
||||
res.status(400).send(`Unknown entity`);
|
||||
res.status(404).send(`Unknown entity`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,12 +125,21 @@ export async function createRouter(
|
||||
badge.style = req.query.style as BadgeStyle;
|
||||
}
|
||||
|
||||
const badge_url = [
|
||||
`${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: `${catalogUrl}/${entityUri}`,
|
||||
entity_url,
|
||||
},
|
||||
format: format === 'application/json' ? 'json' : 'svg',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* 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 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?: 'entity';
|
||||
/**
|
||||
* Badge label (should be a rather static value)
|
||||
* ref. shields spec https://github.com/badges/shields/blob/master/spec/SPECIFICATION.md
|
||||
*/
|
||||
label: string;
|
||||
/** Badge label background color */
|
||||
labelColor?: string;
|
||||
/** Custom badge link */
|
||||
link?: string;
|
||||
/** Badge message */
|
||||
message: string;
|
||||
/** Badge style (apperance). One of "plastic", "flat", "flat-square", "for-the-badge" and "social" */
|
||||
style?: BadgeStyle;
|
||||
}
|
||||
Reference in New Issue
Block a user