@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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: `[](${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: ``,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = ``;
|
||||
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>;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 {};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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: '',
|
||||
},
|
||||
]),
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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!}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user