badges: refactoring frontend plugin.
Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
committed by
Fredrik Adelöw
parent
323ec6f7ca
commit
8bf248799d
@@ -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 = ``;
|
||||
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',
|
||||
});
|
||||
@@ -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 = ``;
|
||||
if (!badge.link) {
|
||||
return img;
|
||||
}
|
||||
return `[${img}](${badge.link})`;
|
||||
}
|
||||
}
|
||||
@@ -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,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>
|
||||
);
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user