Merge pull request #4581 from kaos/kaos/4457_badges

Kaos/4457 badges
This commit is contained in:
Patrik Oldsberg
2021-03-19 11:01:44 +01:00
committed by GitHub
38 changed files with 1622 additions and 3 deletions
+1
View File
@@ -9,6 +9,7 @@
"@backstage/core": "^0.7.2",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-api-docs": "^0.4.9",
"@backstage/plugin-badges": "^0.1.1",
"@backstage/plugin-catalog": "^0.5.0",
"@backstage/plugin-catalog-import": "^0.5.0",
"@backstage/plugin-catalog-react": "^0.1.2",
+1
View File
@@ -44,3 +44,4 @@ export { plugin as Search } from '@backstage/plugin-search';
export { plugin as Org } from '@backstage/plugin-org';
export { plugin as Kafka } from '@backstage/plugin-kafka';
export { todoPlugin } from '@backstage/plugin-todo';
export { badgesPlugin } from '@backstage/plugin-badges';
+1
View File
@@ -33,6 +33,7 @@
"@backstage/config": "^0.1.4",
"@backstage/plugin-app-backend": "^0.3.10",
"@backstage/plugin-auth-backend": "^0.3.5",
"@backstage/plugin-badges-backend": "^0.1.1",
"@backstage/plugin-catalog-backend": "^0.6.6",
"@backstage/plugin-graphql-backend": "^0.1.6",
"@backstage/plugin-kubernetes-backend": "^0.3.1",
+3
View File
@@ -46,6 +46,7 @@ import techdocs from './plugins/techdocs';
import todo from './plugins/todo';
import graphql from './plugins/graphql';
import app from './plugins/app';
import badges from './plugins/badges';
import { PluginEnvironment } from './types';
function makeCreateEnv(config: Config) {
@@ -83,6 +84,7 @@ async function main() {
const kafkaEnv = useHotMemoize(module, () => createEnv('kafka'));
const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const badgesEnv = useHotMemoize(module, () => createEnv('badges'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -95,6 +97,7 @@ async function main() {
apiRouter.use('/kafka', await kafka(kafkaEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/graphql', await graphql(graphqlEnv));
apiRouter.use('/badges', await badges(badgesEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
+32
View File
@@ -0,0 +1,32 @@
/*
* 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 {
createRouter,
createDefaultBadgeFactories,
} from '@backstage/plugin-badges-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
config,
discovery,
}: PluginEnvironment) {
return await createRouter({
config,
discovery,
badgeFactories: createDefaultBadgeFactories(),
});
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
};
+98
View File
@@ -0,0 +1,98 @@
# Badges Backend
Backend plugin for serving badges to the `@backstage/plugin-badges` plugin.
Default implementation uses
[badge-maker](https://www.npmjs.com/package/badge-maker) for creating the
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 an entity specific
endpoint.
## Installation
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`:
```ts
import {
createRouter,
createDefaultBadgeFactories,
} from '@backstage/plugin-badges-backend';
import { PluginEnvironment } from '../types';
export default async function createPlugin({
config,
discovery,
}: PluginEnvironment) {
return await createRouter({
config,
discovery,
badgeFactories: createDefaultBadgeFactories(),
});
}
```
The `createDefaultBadgeFactories()` returns an object with badge factories to
the badges-backend `createRouter()` to forward to the default badge builder. To
customize the available badges, provide a custom set of badge factories. See
further down for an example of a custom badge factories function.
## Badge builder
Badges are created by classes implementing the `BadgeBuilder` type. The default
badge builder uses badge factories to turn a `BadgeContext` into a `Badge` spec
for the `badge-maker` to create the SVG image.
### Default badges
A set of default badge factories are defined in
[badges.ts](https://github.com/backstage/backstage/tree/master/plugins/badges-backend/src/badges.ts)
as examples.
Additional badges may be provided in your application by defining custom badge
factories, and provide them to the badge builder.
### Custom badges
To provide custom badges, create a badge factories function, and use that when
creating the badges backend router.
```ts
import type { Badge, BadgeContext, BadgeFactories } from '@backstage/plugin-badges-backend';
export const createMyCustomBadgeFactories = (): BadgeFactories => ({
<custom-badge-id>: {
createBadge: (context: BadgeContext): Badge | null => {
// ...
return {
label: 'my-badge',
message: 'custom stuff',
// ...
};
},
},
// optional: include the default badges
// ...createDefaultBadgeFactories(),
});
```
## API
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
[BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts)
from the frontend plugin for a type declaration.
- `/badges/entity/:namespace/:kind/:name/:badgeId` Get the entity badge as an
SVG image. If the `accept` request header prefers `application/json` the badge
spec as JSON will be returned instead of the image.
## Links
- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/badges)
- [The Backstage homepage](https://backstage.io)
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@backstage/plugin-badges-backend",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/badges-backend"
},
"keywords": [
"backstage",
"badges"
],
"scripts": {
"start": "backstage-cli backend:dev",
"build": "backstage-cli backend:build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.6.0",
"@backstage/catalog-client": "^0.3.6",
"@backstage/catalog-model": "^0.7.3",
"@backstage/config": "^0.1.3",
"@backstage/errors": "^0.1.1",
"@types/express": "^4.17.6",
"badge-maker": "^3.3.0",
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.6.3",
"@types/supertest": "^2.0.8",
"supertest": "^4.0.2"
},
"files": [
"dist"
]
}
+100
View File
@@ -0,0 +1,100 @@
/*
* 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 { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { InputError } from '@backstage/errors';
import { Badge, BadgeContext, BadgeFactories } from './types';
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}`.toLowerCase();
}
export const createDefaultBadgeFactories = (): BadgeFactories => ({
pingback: {
createBadge: (context: BadgeContext): Badge => {
if (!context.entity) {
throw new InputError('"pingback" badge only defined for entities');
}
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',
};
},
},
lifecycle: {
createBadge: (context: BadgeContext): Badge => {
if (!context.entity) {
throw new InputError('"lifecycle" badge only defined for entities');
}
return {
description: 'Entity lifecycle badge',
kind: 'entity',
label: 'lifecycle',
link: entityUrl(context),
message: `${context.entity.spec?.lifecycle || 'unknown'}`,
style: 'flat-square',
};
},
},
owner: {
createBadge: (context: BadgeContext): Badge => {
if (!context.entity) {
throw new InputError('"owner" badge only defined for entities');
}
return {
description: 'Entity owner badge',
kind: 'entity',
label: 'owner',
link: entityUrl(context),
message: `${context.entity.spec?.owner || 'unknown'}`,
style: 'flat-square',
};
},
},
docs: {
createBadge: (context: BadgeContext): Badge => {
if (!context.entity) {
throw new InputError('"docs" badge only defined for entities');
}
return {
description: 'Entity docs badge',
kind: 'entity',
label: 'docs',
link: `${entityUrl(context)}/docs`,
message: context.entity.metadata.name,
style: 'flat-square',
};
},
},
});
+20
View File
@@ -0,0 +1,20 @@
/*
* 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 * from './badges';
export * from './lib';
export * from './service/router';
export * from './types';
@@ -0,0 +1,105 @@
/*
* 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 { 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: Config;
let factories: BadgeFactories;
const badge = {
description: 'a test badge',
label: 'test',
message: 'ok',
link: 'http://example.com/badgelink',
};
beforeAll(() => {
config = new ConfigReader({
backend: { baseUrl: 'http://127.0.0.1' },
});
factories = {
testbadge: {
createBadge: () => badge,
},
};
});
beforeEach(() => {
jest.resetAllMocks();
builder = new DefaultBadgeBuilder(factories);
});
it('getBadges() returns all badge factory ids', async () => {
expect(await builder.getBadges()).toEqual([{ id: 'testbadge' }]);
});
describe('createBadge[Json|Svg]', () => {
const context: BadgeContext = {
badgeUrl: 'http://127.0.0.1/badge/url',
config,
};
it('badge spec', async () => {
const options: BadgeOptions = {
badgeInfo: { id: 'testbadge' },
context,
};
const spec = await builder.createBadgeJson(options);
expect(spec).toEqual({
badge,
id: 'testbadge',
url: context.badgeUrl,
markdown: `[![a test badge, test: ok](${context.badgeUrl} "a test badge")](${badge.link})`,
});
});
it('badge image', async () => {
const options: BadgeOptions = {
badgeInfo: { id: 'testbadge' },
context,
};
const img = await builder.createBadgeSvg(options);
expect(img).toEqual(expect.stringMatching(/^<svg[^>]*>.*<\/svg>$/));
});
it('returns "unknown" badge for missing factory', async () => {
const options: BadgeOptions = {
badgeInfo: { id: 'other-id' },
context,
};
const spec = await builder.createBadgeJson(options);
expect(spec).toEqual({
badge: {
label: 'unknown badge',
message: 'other-id',
color: 'red',
},
id: 'other-id',
url: context.badgeUrl,
markdown: `![unknown badge: other-id](${context.badgeUrl})`,
});
});
});
});
@@ -0,0 +1,82 @@
/*
* 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 { InputError } from '@backstage/errors';
import { makeBadge, Format } from 'badge-maker';
import { BadgeBuilder, BadgeInfo, BadgeOptions, BadgeSpec } from './types';
import { Badge, BadgeFactories } from '../../types';
export class DefaultBadgeBuilder implements BadgeBuilder {
constructor(private readonly factories: BadgeFactories) {}
public async getBadges(): Promise<BadgeInfo[]> {
return Object.keys(this.factories).map(id => ({ id }));
}
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.badgeInfo.id,
color: 'red',
} as Badge);
if (!badge) {
throw new InputError(
`The badge factory failed to produce a "${options.badgeInfo.id}" badge with the provided context`,
);
}
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, badgeUrl: string): string {
let altText = `${params.label}: ${params.message}`;
if (params.description && params.description !== params.label) {
altText = `${params.description}, ${altText}`;
}
const tooltip = params.description ? ` "${params.description}"` : '';
const img = `![${altText}](${badgeUrl}${tooltip})`;
return params.link ? `[${img}](${params.link})` : img;
}
}
@@ -0,0 +1,18 @@
/*
* 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 { DefaultBadgeBuilder } from './DefaultBadgeBuilder';
export type { BadgeBuilder, BadgeInfo, BadgeOptions, BadgeSpec } from './types';
@@ -0,0 +1,46 @@
/*
* 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, BadgeContext } from '../../types';
export type BadgeInfo = {
id: string;
};
export type BadgeOptions = {
badgeInfo: BadgeInfo;
context: BadgeContext;
};
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 = {
getBadges(): Promise<BadgeInfo[]>;
createBadgeJson(options: BadgeOptions): Promise<BadgeSpec>;
createBadgeSvg(options: BadgeOptions): Promise<string>;
};
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 * from './BadgeBuilder';
+33
View File
@@ -0,0 +1,33 @@
/*
* 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 { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,171 @@
/*
* 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 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, ConfigReader } from '@backstage/config';
import { createRouter } from './router';
import { BadgeBuilder } from '../lib';
describe('createRouter', () => {
let app: express.Express;
let badgeBuilder: jest.Mocked<BadgeBuilder>;
let catalog: jest.Mocked<CatalogApi>;
let config: Config;
let discovery: PluginEndpointDiscovery;
const entity: Entity = {
apiVersion: 'v1',
kind: 'service',
metadata: {
name: 'test',
},
};
const badge = {
id: 'test-badge',
badge: {
label: 'test',
message: 'badge',
},
url: '/...',
markdown: '[![...](...)]',
};
beforeAll(async () => {
badgeBuilder = {
getBadges: jest.fn(),
createBadgeJson: jest.fn(),
createBadgeSvg: jest.fn(),
};
catalog = {
addLocation: jest.fn(),
getEntities: jest.fn(),
getEntityByName: jest.fn(),
getLocationByEntity: jest.fn(),
getLocationById: jest.fn(),
removeEntityByUid: jest.fn(),
};
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);
});
beforeEach(() => {
jest.resetAllMocks();
});
it('works', async () => {
const router = await createRouter({
badgeBuilder,
catalog,
config,
discovery,
});
expect(router).toBeDefined();
});
describe('GET /entity/:namespace/:kind/:name/badge-specs', () => {
it('returns all badge specs for entity', async () => {
catalog.getEntityByName.mockResolvedValueOnce(entity);
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(catalog.getEntityByName).toHaveBeenCalledTimes(1);
expect(catalog.getEntityByName).toHaveBeenCalledWith({
namespace: 'default',
kind: 'service',
name: 'test',
});
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\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
),
config,
entity,
},
});
});
});
describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
it('returns badge for entity', async () => {
catalog.getEntityByName.mockResolvedValueOnce(entity);
const image = '<svg>...</svg>';
badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
const response = await request(app).get(
'/entity/default/service/test/badge/test-badge',
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(Buffer.from(image));
expect(catalog.getEntityByName).toHaveBeenCalledTimes(1);
expect(catalog.getEntityByName).toHaveBeenCalledWith({
namespace: 'default',
kind: 'service',
name: 'test',
});
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\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
),
config,
entity,
},
});
});
});
});
@@ -0,0 +1,136 @@
/*
* 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 express from 'express';
import Router from 'express-promise-router';
import {
errorHandler,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder';
import { BadgeContext, BadgeFactories } from '../types';
export interface RouterOptions {
badgeBuilder?: BadgeBuilder;
badgeFactories?: BadgeFactories;
catalog?: CatalogApi;
config: Config;
discovery: PluginEndpointDiscovery;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const catalog =
options.catalog || new CatalogClient({ discoveryApi: options.discovery });
const badgeBuilder =
options.badgeBuilder ||
new DefaultBadgeBuilder(options.badgeFactories || {});
const router = Router();
router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => {
const { namespace, kind, name } = req.params;
const entity = await catalog.getEntityByName({ namespace, kind, name });
if (!entity) {
throw new NotFoundError(
`No ${kind} entity in ${namespace} named "${name}"`,
);
}
const specs = [];
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(JSON.stringify(specs, null, 2));
});
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';
}
const badgeOptions = {
badgeInfo: { id: badgeId },
context: {
badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId, options),
config: options.config,
entity,
},
};
let data: string;
if (format === 'application/json') {
data = JSON.stringify(
await badgeBuilder.createBadgeJson(badgeOptions),
null,
2,
);
} else {
data = await badgeBuilder.createBadgeSvg(badgeOptions);
}
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}`;
}
@@ -0,0 +1,51 @@
/*
* 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 { Server } from 'http';
import { Logger } from 'winston';
import {
createServiceBuilder,
loadBackendConfig,
SingleHostDiscovery,
} from '@backstage/backend-common';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'badges-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
logger.debug('Creating application...');
const router = await createRouter({ config, discovery });
const service = createServiceBuilder(module)
.enableCors({ origin: 'http://localhost:3000' })
.addRouter('/badges', router);
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 {};
+63
View File
@@ -0,0 +1,63 @@
/*
* 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 { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
export const BADGE_STYLES = [
'plastic',
'flat',
'flat-square',
'for-the-badge',
'social',
] as const;
export type BadgeStyle = typeof BADGE_STYLES[number];
export interface Badge {
/** Badge message background color. */
color?: string;
/** Badge description (tooltip text) */
description?: string;
/** Kind of badge */
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;
}
export interface BadgeContext {
badgeUrl: string;
config: Config;
entity?: Entity; // for entity badges
}
export interface BadgeFactory {
createBadge(context: BadgeContext): Badge;
}
export interface BadgeFactories {
[id: string]: BadgeFactory;
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+19
View File
@@ -0,0 +1,19 @@
# @backstage/plugin-badges
The badges plugin offers a set of badges that can be used outside of
your backstage deployment, showing information related to data from
the catalog, such as entity owner and lifecycle data for instance.
The available badges are setup in the `badges-backend` plugin, see
link below for more details.
## Entity badges
To get markdown code for the entity badges, access the `Badges` context menu
(three dots in the upper right corner) of an entity page, which will popup a
badges dialog showing all available badges for that entity.
## Links
- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/badges-backend)
- [The Backstage homepage](https://backstage.io)
+19
View File
@@ -0,0 +1,19 @@
/*
* 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 { createDevApp } from '@backstage/dev-utils';
import { badgesPlugin } from '../src/plugin';
createDevApp().registerPlugin(badgesPlugin).render();
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@backstage/plugin-badges",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.7.3",
"@backstage/core": "^0.7.2",
"@backstage/errors": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.5",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.9",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
+58
View File
@@ -0,0 +1,58 @@
/*
* 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 { 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';
export class BadgesClient implements BadgesApi {
private readonly discoveryApi: DiscoveryApi;
constructor(options: { discoveryApi: DiscoveryApi }) {
this.discoveryApi = options.discoveryApi;
}
public async getEntityBadgeSpecs(entity: Entity): Promise<BadgeSpec[]> {
const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
const response = await fetch(entityBadgeSpecsUrl);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
private async getEntityBadgeSpecsUrl(entity: Entity): Promise<string> {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(entityRoute.path, routeParams);
return `${await this.discoveryApi.getBaseUrl(
'badges',
)}/entity/${path}/badge-specs`;
}
private getEntityRouteParams(entity: Entity) {
return {
kind: entity.kind.toLowerCase(),
namespace:
entity.metadata.namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE,
name: entity.metadata.name,
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* 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 * from './BadgesClient';
export * from './types';
+59
View File
@@ -0,0 +1,59 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { createApiRef } from '@backstage/core';
export const badgesApiRef = createApiRef<BadgesApi>({
id: 'plugin.badges.client',
description: 'Used to make requests to the badges backend',
});
export type BadgeStyle =
| 'plastic'
| 'flat'
| 'flat-square'
| 'for-the-badge'
| 'social';
interface Badge {
color?: string;
description?: string;
kind?: 'entity';
label: string;
labelColor?: string;
link?: string;
message: string;
style?: BadgeStyle;
}
export interface 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 interface BadgesApi {
getEntityBadgeSpecs(entity: Entity): Promise<BadgeSpec[]>;
}
@@ -0,0 +1,61 @@
/*
* 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 React from 'react';
import { Entity } from '@backstage/catalog-model';
import {
ApiProvider,
ApiRegistry,
ErrorApi,
errorApiRef,
} from '@backstage/core';
import { renderWithEffects } from '@backstage/test-utils';
import { BadgesApi, badgesApiRef } from '../api';
import { EntityBadgesDialog } from './EntityBadgesDialog';
describe('EntityBadgesDialog', () => {
it('should render', async () => {
const mockApi: jest.Mocked<BadgesApi> = {
getEntityBadgeSpecs: jest.fn().mockResolvedValue([
{
id: 'testbadge',
badge: {
label: 'test',
message: 'badge',
},
url: 'http://127.0.0.1/badges/entity/.../testbadge',
markdown: '![test: badge](http://127.0.0.1/catalog/...)',
},
]),
};
const mockEntity = { metadata: { name: 'mock' } } as Entity;
const rendered = await renderWithEffects(
<ApiProvider
apis={ApiRegistry.with(badgesApiRef, mockApi).with(
errorApiRef,
{} as ErrorApi,
)}
>
<EntityBadgesDialog open entity={mockEntity} />
</ApiProvider>,
);
await expect(
rendered.findByText('testbadge badge'),
).resolves.toBeInTheDocument();
});
});
@@ -0,0 +1,102 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import {
CodeSnippet,
Progress,
ResponseErrorPanel,
useApi,
} from '@backstage/core';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
useMediaQuery,
useTheme,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { useAsync } from 'react-use';
import { badgesApiRef } from '../api';
type Props = {
open: boolean;
onClose?: () => any;
entity: Entity;
};
const useStyles = makeStyles({
codeBlock: {
'& code': {
whiteSpace: 'pre-wrap',
},
},
});
export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const badgesApi = useApi(badgesApiRef);
const classes = useStyles();
const { value: badges, loading, error } = useAsync(async () => {
if (open) {
return await badgesApi.getEntityBadgeSpecs(entity);
}
return [];
}, [badgesApi, entity, open]);
const content = (badges || []).map(
({ badge: { description }, id, url, markdown }) => (
<div key={id}>
<DialogContentText>
{description || `${id} badge`}
<br />
<img alt={description || id} src={url} />
</DialogContentText>
<Typography component="div" className={classes.codeBlock}>
Copy the following snippet of markdown code for the badge:
<CodeSnippet language="markdown" text={markdown} showCopyCodeButton />
</Typography>
<hr />
</div>
),
);
return (
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
<DialogTitle>Entity Badges</DialogTitle>
<DialogContent>
{loading && <Progress />}
{error && <ResponseErrorPanel error={error} />}
{content}
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 * from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 { badgesPlugin } from './plugin';
describe('badges', () => {
it('should export plugin', () => {
expect(badgesPlugin).toBeDefined();
});
});
+44
View File
@@ -0,0 +1,44 @@
/*
* 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 {
createApiFactory,
createComponentExtension,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { badgesApiRef, BadgesClient } from './api';
export const badgesPlugin = createPlugin({
id: 'badges',
apis: [
createApiFactory({
api: badgesApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new BadgesClient({ discoveryApi }),
}),
],
});
export const EntityBadgesDialog = badgesPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/EntityBadgesDialog').then(
m => m.EntityBadgesDialog,
),
},
}),
);
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+1
View File
@@ -35,6 +35,7 @@
"@backstage/core": "^0.7.2",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-badges": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.2",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
@@ -35,10 +35,14 @@ const useStyles = makeStyles({
});
type Props = {
onShowBadgesDialog?: () => void;
onUnregisterEntity: () => void;
};
export const EntityContextMenu = ({ onUnregisterEntity }: Props) => {
export const EntityContextMenu = ({
onShowBadgesDialog,
onUnregisterEntity,
}: Props) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
const classes = useStyles();
@@ -70,6 +74,16 @@ export const EntityContextMenu = ({ onUnregisterEntity }: Props) => {
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList>
{onShowBadgesDialog && (
<MenuItem
onClick={() => {
onClose();
onShowBadgesDialog();
}}
>
<Typography variant="inherit">Badges</Typography>
</MenuItem>
)}
<MenuItem
onClick={() => {
onClose();
@@ -40,6 +40,7 @@ import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity';
import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import { Tabbed } from './Tabbed';
const EntityPageTitle = ({
@@ -108,6 +109,7 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
entity!,
);
const [badgesDialogOpen, setBadgesDialogOpen] = useState(false);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const navigate = useNavigate();
const cleanUpAfterRemoval = async () => {
@@ -128,7 +130,10 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
{entity && (
<>
<EntityLabels entity={entity} />
<EntityContextMenu onUnregisterEntity={showRemovalDialog} />
<EntityContextMenu
onShowBadgesDialog={() => setBadgesDialogOpen(true)}
onUnregisterEntity={showRemovalDialog}
/>
</>
)}
</Header>
@@ -159,6 +164,14 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
</Content>
)}
{entity && (
<EntityBadgesDialog
open={badgesDialogOpen}
entity={entity}
onClose={() => setBadgesDialogOpen(false)}
/>
)}
<UnregisterEntityDialog
open={confirmationDialogOpen}
entity={entity!}
+49 -1
View File
@@ -1992,6 +1992,7 @@
"@backstage/core" "^0.7.2"
"@backstage/integration" "^0.5.1"
"@backstage/integration-react" "^0.1.1"
"@backstage/plugin-badges" "^0.1.1"
"@backstage/plugin-catalog-react" "^0.1.2"
"@backstage/theme" "^0.2.4"
"@material-ui/core" "^4.11.0"
@@ -2016,6 +2017,7 @@
"@backstage/core" "^0.7.2"
"@backstage/integration" "^0.5.1"
"@backstage/integration-react" "^0.1.1"
"@backstage/plugin-badges" "^0.1.1"
"@backstage/plugin-catalog-react" "^0.1.2"
"@backstage/theme" "^0.2.4"
"@material-ui/core" "^4.11.0"
@@ -7699,6 +7701,13 @@ alphanum-sort@^1.0.0:
resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
anafanafo@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/anafanafo/-/anafanafo-1.0.0.tgz#2e67190aed5dc67f5f0043b710146e7276c1afb8"
integrity sha512-pDPbI7SFRHu0byMXHAf+v74+LCcHSxnLYkcbfiV91XRlE+NSLpFCpEQdVUy9ZxZw/LuhTrOin4r8wlR3OFrKBA==
dependencies:
char-width-table-consumer "^1.0.0"
ansi-align@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
@@ -8835,6 +8844,14 @@ backoff@^2.5.0:
dependencies:
precond "0.2"
badge-maker@^3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/badge-maker/-/badge-maker-3.3.0.tgz#996cb2beeb458cbc9f559c4276f0d285758e5106"
integrity sha512-jYOlYss1BqwrjHnV6cIFD7G6XDdJ6rbZrzXxJulee3WhUjmXmHWOBm4xhtq10C6rrM5vEeWOB8WpZCD/LfI9CA==
dependencies:
anafanafo "^1.0.0"
css-color-converter "^2.0.0"
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -8956,6 +8973,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
binary-search@^1.3.5:
version "1.3.6"
resolved "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c"
integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==
binary@~0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
@@ -9710,6 +9732,13 @@ char-regex@^1.0.2:
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
char-width-table-consumer@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/char-width-table-consumer/-/char-width-table-consumer-1.0.0.tgz#bb44ccd1ba3ed4fcdb062e22876721858a7697a8"
integrity sha512-Fz4UD0LBpxPgL9i29CJ5O4KANwaMnX/OhhbxzvNa332h+9+nRKyeuLw4wA51lt/ex67+/AdsoBQJF3kgX2feYQ==
dependencies:
binary-search "^1.3.5"
character-entities-legacy@^1.0.0:
version "1.1.4"
resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
@@ -10084,6 +10113,11 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^0.5.2:
version "0.5.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
color-convert@^1.9.0, color-convert@^1.9.1:
version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -10103,7 +10137,7 @@ color-name@1.1.3:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0, color-name@~1.1.4:
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -10832,6 +10866,15 @@ css-box-model@^1.2.0:
dependencies:
tiny-invariant "^1.0.6"
css-color-converter@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/css-color-converter/-/css-color-converter-2.0.0.tgz#70c00fa451a19675e2808f28de9be360c84db5fb"
integrity sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==
dependencies:
color-convert "^0.5.2"
color-name "^1.1.4"
css-unit-converter "^1.1.2"
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -10920,6 +10963,11 @@ css-tree@^1.0.0-alpha.28:
mdn-data "2.0.6"
source-map "^0.6.1"
css-unit-converter@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
css-vendor@^2.0.7:
version "2.0.7"
resolved "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.7.tgz#4e6d53d953c187981576d6a542acc9fb57174bda"