diff --git a/packages/app/package.json b/packages/app/package.json index 967f893da4..df2ecdb277 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 29d19785bf..13b0daf7e3 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -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'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 98a69f6ff6..34f9f7c6de 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 23739596e8..bdd4131e0e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -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) diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts new file mode 100644 index 0000000000..961c94e1ce --- /dev/null +++ b/packages/backend/src/plugins/badges.ts @@ -0,0 +1,25 @@ +/* + * 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 } from '@backstage/plugin-badges-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index b171977cf6..26c56f7f86 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -37,8 +37,6 @@ export async function createRouter( const badgesApi = options.badgesApi || new BadgesApi(logger); - // router.use(express.json()); - router.get('/entity/:namespace/:kind/:name', async (req, res) => { const { namespace, kind, name } = req.params; const badge = badgesApi.getPoweredByBadge(); diff --git a/plugins/badges/package.json b/plugins/badges/package.json index ebc4c4305e..65e09e2524 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -20,6 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.7.1", "@backstage/core": "^0.6.1", "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", @@ -28,6 +29,7 @@ "@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": { diff --git a/plugins/badges/src/BadgesClientApi.ts b/plugins/badges/src/BadgesClientApi.ts new file mode 100644 index 0000000000..dfb1205784 --- /dev/null +++ b/plugins/badges/src/BadgesClientApi.ts @@ -0,0 +1,62 @@ +/* + * 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 { createApiRef, ConfigApi, DiscoveryApi } from '@backstage/core'; +import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { entityRoute } from '@backstage/plugin-catalog-react'; + +export class BadgesClientApi { + private readonly configApi: ConfigApi; + private readonly discoveryApi: DiscoveryApi; + + constructor(options: { configApi: ConfigApi; discoveryApi: DiscoveryApi }) { + this.configApi = options.configApi; + this.discoveryApi = options.discoveryApi; + } + + async getEntityPoweredByMarkdownCode(entity: Entity): string { + const badge = await this.getEntityPoweredByBadgeURL(entity); + const target = this.getEntityLink(entity); + return `[![Powered by Backstage](${badge})](${target})`; + } + + async getEntityPoweredByBadgeURL(entity: Entity): string { + const routeParams = this.getEntityRouteParams(entity); + const path = generatePath(entityRoute.path, routeParams); + return `${await this.discoveryApi.getBaseUrl('badges')}/entity/${path}`; + } + + private getEntityLink(entity: Entity): string { + const routeParams = this.getEntityRouteParams(entity); + const path = generatePath(entityRoute.path, routeParams); + return `${this.configApi.getString('app.baseUrl')}/catalog/${path}`; + } + + private getEntityRouteParams(entity: Entity) { + return { + kind: entity.kind.toLowerCase(), + namespace: + entity.metadata.namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, + name: entity.metadata.name, + }; + } +} + +export const badgesClientApiRef = createApiRef({ + id: 'plugin.badges.client', + description: 'Used by the badges plugin to create badge URLs', +}); diff --git a/plugins/badges/src/components/BadgesDrawer.tsx b/plugins/badges/src/components/BadgesDrawer.tsx new file mode 100644 index 0000000000..10ae0f861f --- /dev/null +++ b/plugins/badges/src/components/BadgesDrawer.tsx @@ -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 React, { useState } from 'react'; +import { Button, Drawer, Grid, Typography } from '@material-ui/core'; +import { CodeSnippet, Content, InfoCard, useApi } from '@backstage/core'; + +const BadgeCard = ({ title, badgeUrl, markdownCode }) => ( + + + + Paste the following snippet in your README.md or other + markdown file for this badge: + + Badge preview + + + +); + +const BadgesDrawerContent = ({ badges, toggleDrawer }) => ( + + + {badges.map((badge, idx) => ( + + ))} + + + + + +); + +export const BadgesDrawer = ({ badges }) => { + const [isOpen, toggleDrawer] = useState(false); + + return ( + <> + + toggleDrawer(false)}> + + + + ); +}; diff --git a/plugins/badges/src/components/EntityBadgesCard.tsx b/plugins/badges/src/components/EntityBadgesCard.tsx index 334575e9db..5fc0f47556 100644 --- a/plugins/badges/src/components/EntityBadgesCard.tsx +++ b/plugins/badges/src/components/EntityBadgesCard.tsx @@ -14,27 +14,22 @@ * limitations under the License. */ import React from 'react'; +import { useAsync } from 'react-use'; import { Typography } from '@material-ui/core'; -import { CodeSnippet, InfoCard } from '@backstage/core'; +import { CodeSnippet, InfoCard, useApi } from '@backstage/core'; +import { ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; +import { badgesClientApiRef } from '../BadgesClientApi'; export const EntityBadgesCard = () => { + const badgesClientApi = useApi(badgesClientApiRef); const { entity } = useEntity(); - - // TODO: extract kind, namespace, entity name from entity data... - const { kind, namespace, name } = { - kind: 'Component', - namespace: 'default', - name: 'demo', - }; - - // TODO: get url for backstage and the badges api - const backstage_ui = 'http://localhost:3000'; - const backstage_api = 'http://localhost:7000'; - - const badge = `${backstage_api}/badges/entity/${namespace}/${kind}/${name}`.toLowerCase(); - const target = `${backstage_ui}/catalog/${namespace}/${kind}/${name}`.toLowerCase(); - const markdown_code = `[![Powered by Backstage](${badge})](${target})`; + const { value, loading, error } = useAsync(async () => { + return { + badge: await badgesClientApi.getEntityPoweredByBadgeURL(entity), + markdown: await badgesClientApi.getEntityPoweredByMarkdownCode(entity), + }; + }, [badgesClientApi, entity]); return ( @@ -42,8 +37,8 @@ export const EntityBadgesCard = () => { Paste the following snippet in your README.md or other markdown file, to get a powered by backstage badge: - Powered by Backstage badge - + {value && Powered by Backstage badge} + {value && } ); }; diff --git a/plugins/badges/src/components/EntityBadgesField.tsx b/plugins/badges/src/components/EntityBadgesField.tsx new file mode 100644 index 0000000000..586f9d366d --- /dev/null +++ b/plugins/badges/src/components/EntityBadgesField.tsx @@ -0,0 +1,52 @@ +/* + * 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 { useAsync } from 'react-use'; +import { Progress, useApi, WarningPanel } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { badgesClientApiRef } from '../BadgesClientApi'; +import { BadgesDrawer } from './BadgesDrawer'; + +export const EntityBadgesField = () => { + const badgesClientApi = useApi(badgesClientApiRef); + const { entity } = useEntity(); + const { value, loading, error } = useAsync(async () => { + return { + badge: await badgesClientApi.getEntityPoweredByBadgeURL(entity), + markdown: await badgesClientApi.getEntityPoweredByMarkdownCode(entity), + }; + }, [badgesClientApi, entity]); + + const badges = []; + + if (value) { + badges.push({ + title: 'Powered by Backstage', + badgeUrl: value.badge, + markdownCode: value.markdown, + }); + } + + return ( +
+ {loading && } + {error && ( + + )} + {badges.length > 0 && } +
+ ); +}; diff --git a/plugins/badges/src/index.ts b/plugins/badges/src/index.ts index 63129bcbc2..4de5957426 100644 --- a/plugins/badges/src/index.ts +++ b/plugins/badges/src/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { badgesPlugin, EntityBadgesCard } from './plugin'; + +export * from './plugin'; diff --git a/plugins/badges/src/plugin.ts b/plugins/badges/src/plugin.ts index 5829dd50eb..7acaaeaeef 100644 --- a/plugins/badges/src/plugin.ts +++ b/plugins/badges/src/plugin.ts @@ -13,12 +13,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin, createComponentExtension } from '@backstage/core'; +import { + createApiFactory, + createComponentExtension, + createPlugin, + configApiRef, + discoveryApiRef, +} from '@backstage/core'; +import { badgesClientApiRef, BadgesClientApi } from './BadgesClientApi'; export const badgesPlugin = createPlugin({ id: 'badges', + apis: [ + createApiFactory({ + api: badgesClientApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new BadgesClientApi({ configApi, discoveryApi }), + }), + ], }); +export const EntityBadgesField = badgesPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/EntityBadgesField').then(m => m.EntityBadgesField), + }, + }), +); + export const EntityBadgesCard = badgesPlugin.provide( createComponentExtension({ component: { diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 8c05c376f9..d2c5085035 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -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", diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 695595285b..d799668433 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -25,6 +25,7 @@ import { } from '@backstage/plugin-catalog-react'; import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; import React from 'react'; +import { EntityBadgesField } from '@backstage/plugin-badges'; import { AboutField } from './AboutField'; const useStyles = makeStyles({ @@ -127,6 +128,9 @@ export const AboutContent = ({ entity }: Props) => { ))} + + + ); };