badges: refactoring after review from @freben.
Signed-off-by: Andreas Stenius <andreas.stenius@svenskaspel.se>
This commit is contained in:
committed by
Fredrik Adelöw
parent
97836e51f1
commit
c46ab4afba
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-badges': patch
|
||||
'@backstage/plugin-badges-backend': patch
|
||||
---
|
||||
|
||||
New plugin to serve badges.
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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<Config>;
|
||||
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[^>]*>.*<\/svg>$/));
|
||||
const img = await builder.createBadgeSvg(options);
|
||||
expect(img).toEqual(expect.stringMatching(/^<svg[^>]*>.*<\/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',
|
||||
|
||||
@@ -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<string[]> {
|
||||
return Object.keys(this.factories);
|
||||
public async getBadges(): Promise<BadgeInfo[]> {
|
||||
return Object.keys(this.factories).map(id => ({ id }));
|
||||
}
|
||||
|
||||
public async createBadge(options: BadgeOptions): Promise<string> {
|
||||
const factory = this.factories[options.badgeId];
|
||||
public async createBadgeJson(options: BadgeOptions): Promise<BadgeSpec> {
|
||||
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<string> {
|
||||
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 = ``;
|
||||
const img = ``;
|
||||
return params.link ? `[${img}](${params.link})` : img;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { DefaultBadgeBuilder } from './DefaultBadgeBuilder';
|
||||
export * from './types';
|
||||
export type { BadgeBuilder, BadgeInfo, BadgeOptions, BadgeSpec } from './types';
|
||||
|
||||
@@ -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<string>;
|
||||
getBadgeIds(): Promise<string[]>;
|
||||
getBadges(): Promise<BadgeInfo[]>;
|
||||
createBadgeJson(options: BadgeOptions): Promise<BadgeSpec>;
|
||||
createBadgeSvg(options: BadgeOptions): Promise<string>;
|
||||
};
|
||||
|
||||
@@ -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<BadgeBuilder>;
|
||||
let catalog: jest.Mocked<CatalogApi>;
|
||||
let config: jest.Mocked<Config>;
|
||||
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 = '<svg>...</svg>';
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,17 +31,14 @@ export interface RouterOptions {
|
||||
badgeFactories?: BadgeFactories;
|
||||
catalog?: CatalogApi;
|
||||
config: Config;
|
||||
discovery?: PluginEndpointDiscovery;
|
||||
discovery: PluginEndpointDiscovery;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
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<string> {
|
||||
const baseUrl = await options.discovery.getExternalBaseUrl('badges');
|
||||
return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface BadgeContext {
|
||||
}
|
||||
|
||||
export interface BadgeFactory {
|
||||
createBadge(context: BadgeContext): Badge | null;
|
||||
createBadge(context: BadgeContext): Badge;
|
||||
}
|
||||
|
||||
export interface BadgeFactories {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<BadgeSpec[]> {
|
||||
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<string> {
|
||||
|
||||
@@ -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 (
|
||||
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
|
||||
<DialogTitle id="badges-dialog-title">Entity Badges</DialogTitle>
|
||||
<DialogTitle>Entity Badges</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{loading ? <Progress /> : null}
|
||||
|
||||
{error ? (
|
||||
<Alert severity="error" style={{ wordBreak: 'break-word' }}>
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{loading && <Progress />}
|
||||
{error && <ResponseErrorPanel error={error} />}
|
||||
{content}
|
||||
</DialogContent>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user