badges: refactoring frontend plugin.

Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
Andreas Stenius
2021-02-22 16:23:53 +01:00
committed by Fredrik Adelöw
parent 323ec6f7ca
commit 8bf248799d
16 changed files with 213 additions and 417 deletions
-119
View File
@@ -1,119 +0,0 @@
/*
* 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';
// TODO: place interpolate code someplace re-usable...
// import { interpolate } from '@backstage/plugin-badges-backend';
function interpolate(template, params) {
const names = Object.keys(params);
const vals = Object.values(params);
// eslint-disable-next-line no-new-func
return new Function(...names, `return \`${template}\`;`)(...vals);
}
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 getDefaultContext(entity: Entity) {
return {
app: {
title: this.configApi.getString('app.title'),
},
entity,
entity_url: entity && (await this.getEntityLink(entity)),
};
}
async getDefinedBadges(badgeKind: string, context: object) {
const badges = [];
const badgesConfig = this.configApi.getOptional('badges') ?? {};
for (const [badgeId, badge] of Object.entries(badgesConfig)) {
if (!badgeKind || !badge.kind || badgeKind === badge.kind) {
badges.push(await this.renderBadgeInfo(badgeId, badge, context));
}
}
return badges;
}
private async renderBadgeInfo(id, badge, context) {
const info = {
id,
url: context.entity && (await this.getEntityBadgeURL(id, context.entity)),
title: badge.title && this.render(badge.title, context),
description: this.render(badge.description || id, context),
target: badge.target && this.render(badge.target, context),
};
info.markdown = this.getBadgeMarkdownCode(info);
return info;
}
private async getEntityBadgeURL(badgeId: string, entity: Entity): string {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(entityRoute.path, routeParams);
return `${await this.discoveryApi.getBaseUrl(
'badges',
)}/entity/${path}/${badgeId}`;
}
private getBadgeMarkdownCode(badge: object): string {
const title = badge.title ? ` "${badge.title}"` : '';
const img = `![${badge.description}](${badge.url}${title})`;
if (!badge.target) {
return img;
}
return `[${img}](${badge.target})`;
}
private render(template, context) {
try {
return interpolate(template.replace('$$', '$'), context);
} catch (err) {
return `${err} [${template}]`;
}
}
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<typeof BadgesClientApi>({
id: 'plugin.badges.client',
description: 'Used by the badges plugin to create badge URLs',
});
+86
View File
@@ -0,0 +1,86 @@
/*
* 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 { ConfigApi, DiscoveryApi } from '@backstage/core';
import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { entityRoute } from '@backstage/plugin-catalog-react';
import { BadgesApi, BadgeConfig, BadgeSpec } from './types';
export class BadgesClient implements BadgesApi {
private readonly configApi: ConfigApi;
private readonly discoveryApi: DiscoveryApi;
constructor(options: { configApi: ConfigApi; discoveryApi: DiscoveryApi }) {
this.configApi = options.configApi;
this.discoveryApi = options.discoveryApi;
}
public async getDefinedEntityBadges(entity: Entity): Badge[] {
const badges = [];
const badgesConfig = this.configApi.getOptional('badges') ?? {};
const entityBadgeUri = await this.getEntityBadgeUri(entity);
for (const [badgeId, badge] of Object.entries(badgesConfig)) {
if (!badge.kind || badge.kind === 'entity') {
const info = await this.getBadgeInfo(entityBadgeUri, badgeId);
if (info) {
badges.push(info);
}
}
}
return badges;
}
private async getBadgeInfo(entityBadgeUri: string, badgeId: string): Badge {
const badgeUrl = `${entityBadgeUri}/${badgeId}`;
const spec = (await (
await fetch(`${badgeUrl}?format=json`)
).json()) as BadgeSpec;
return {
id: badgeId,
markdown: this.getBadgeMarkdownCode(badgeUrl, spec.badge),
spec,
url: badgeUrl,
};
}
private async getEntityBadgeUri(entity: Entity): string {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(entityRoute.path, routeParams);
return `${await this.discoveryApi.getBaseUrl('badges')}/entity/${path}`;
}
private getEntityRouteParams(entity: Entity) {
return {
kind: entity.kind.toLowerCase(),
namespace:
entity.metadata.namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE,
name: entity.metadata.name,
};
}
private getBadgeMarkdownCode(badgeUrl: string, badge: BadgeConfig): string {
const title = badge.title ? ` "${badge.title}"` : '';
const img = `![${badge.description || 'badge'}](${badgeUrl}${title})`;
if (!badge.link) {
return img;
}
return `[${img}](${badge.link})`;
}
}
+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';
+56
View File
@@ -0,0 +1,56 @@
/*
* 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',
});
// should probably have this in a "badges-common" package
interface BadgeConfig {
kind?: 'entity';
label: string;
message: string;
color?: string;
labelColor?: string;
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social';
title?: string;
description?: string;
link?: string;
}
export interface BadgeSpec {
/** The rendered data */
badge: BadgeConfig;
/** The configuration data, with placeholders and all */
config: BadgeConfig;
/** The context used when rendering config -> badge */
context: object;
}
export interface Badge {
id: string;
markdown: string;
spec: BadgeSpec;
url: string;
}
export interface BadgesApi {
getDefinedEntityBadges(entity: Entity): Promise<Badge[]>;
}
@@ -1,63 +0,0 @@
/*
* 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 }) => (
<Grid item>
<InfoCard title={title}>
<Typography paragraph>
Paste the following snippet in your <code>README.md</code> or other
markdown file for this badge:
</Typography>
<img src={badgeUrl} alt="Badge preview" />
<CodeSnippet text={markdownCode} showCopyCodeButton />
</InfoCard>
</Grid>
);
const BadgesDrawerContent = ({ badges, toggleDrawer }) => (
<Content>
<Grid container direction="column" spacing={4}>
{badges.map((badge, idx) => (
<BadgeCard key={idx} {...badge} />
))}
<Grid item>
<Button color="primary" onClick={() => toggleDrawer(false)}>
Close
</Button>
</Grid>
</Grid>
</Content>
);
export const BadgesDrawer = ({ badges }) => {
const [isOpen, toggleDrawer] = useState(false);
return (
<>
<Button onClick={() => toggleDrawer(!isOpen)}>
{badges.map(({ badgeUrl, title }, idx) => (
<img key={idx} src={badgeUrl} alt={title} />
))}
</Button>
<Drawer anchor="right" open={isOpen} onClose={() => toggleDrawer(false)}>
<BadgesDrawerContent badges={badges} toggleDrawer={toggleDrawer} />
</Drawer>
</>
);
};
@@ -1,44 +0,0 @@
/*
* 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 { Typography } from '@material-ui/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();
const { value, loading, error } = useAsync(async () => {
return {
badge: await badgesClientApi.getEntityPoweredByBadgeURL(entity),
markdown: await badgesClientApi.getEntityPoweredByMarkdownCode(entity),
};
}, [badgesClientApi, entity]);
return (
<InfoCard title="Badges">
<Typography paragraph>
Paste the following snippet in your <code>README.md</code> or other
markdown file, to get a powered by backstage badge:
</Typography>
{value && <img src={value.badge} alt="Powered by Backstage badge" />}
{value && <CodeSnippet text={value.markdown} showCopyCodeButton />}
</InfoCard>
);
};
@@ -31,7 +31,7 @@ import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useAsync } from 'react-use';
import { badgesClientApiRef } from '../BadgesClientApi';
import { badgesApiRef } from '../api';
type Props = {
open: boolean;
@@ -50,30 +50,31 @@ const useStyles = makeStyles(theme => ({
export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const badgesClientApi = useApi(badgesClientApiRef);
const badgesApi = useApi(badgesApiRef);
const classes = useStyles();
const { value: badges, loading, error } = useAsync(async () => {
const context = await badgesClientApi.getDefaultContext(entity);
return await badgesClientApi.getDefinedBadges('entity', context);
});
if (open) {
return await badgesApi.getDefinedEntityBadges(entity);
}
const content = (badges || []).map(
({ id, title, description, url, target, markdown }) => (
<div key={id}>
<DialogContentText>
{title || description}
<br />
<img alt={description} src={url} />
</DialogContentText>
<Typography component="div" className={classes.codeBlock}>
Copy the following snippet of markdown code for the badge:
<CodeSnippet text={markdown} showCopyCodeButton />
</Typography>
<hr />
</div>
),
);
return [];
}, [badgesApi, entity, open]);
const content = (badges || []).map(({ id, markdown, spec, url }) => (
<div key={id}>
<DialogContentText>
{spec.badge.title || spec.badge.description || id}
<br />
<img alt={spec.badge.description} src={url} />
</DialogContentText>
<Typography component="div" className={classes.codeBlock}>
Copy the following snippet of markdown code for the badge:
<CodeSnippet text={markdown} showCopyCodeButton />
</Typography>
<hr />
</div>
));
return (
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
@@ -1,52 +0,0 @@
/*
* 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 (
<div>
{loading && <Progress />}
{error && (
<WarningPanel title="Failed to get badge" message={`Error: ${error}`} />
)}
{badges.length > 0 && <BadgesDrawer badges={badges} />}
</div>
);
};
+3 -21
View File
@@ -20,38 +20,20 @@ import {
configApiRef,
discoveryApiRef,
} from '@backstage/core';
import { badgesClientApiRef, BadgesClientApi } from './BadgesClientApi';
import { badgesApiRef, BadgesClient } from './api';
export const badgesPlugin = createPlugin({
id: 'badges',
apis: [
createApiFactory({
api: badgesClientApiRef,
api: badgesApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new BadgesClientApi({ configApi, discoveryApi }),
new BadgesClient({ configApi, discoveryApi }),
}),
],
});
export const EntityBadgesField = badgesPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/EntityBadgesField').then(m => m.EntityBadgesField),
},
}),
);
export const EntityBadgesCard = badgesPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/EntityBadgesCard').then(m => m.EntityBadgesCard),
},
}),
);
export const EntityBadgesDialog = badgesPlugin.provide(
createComponentExtension({
component: {