badges: move to context menu. support for multiple badges from config. #4457.
Signed-off-by: Andreas Stenius <git@astekk.se>
This commit is contained in:
committed by
Fredrik Adelöw
parent
798002ee03
commit
b99231c0bc
@@ -401,8 +401,26 @@ homepage:
|
||||
timezone: 'Asia/Tokyo'
|
||||
pagerduty:
|
||||
eventsBaseUrl: 'https://events.pagerduty.com/v2'
|
||||
|
||||
# sample badges
|
||||
badges:
|
||||
pingback:
|
||||
kind: 'entity'
|
||||
description: 'Link back to $${app.title}'
|
||||
target: '$${window.location.href}'
|
||||
label: 'View $${entity.metadata.name} in'
|
||||
message: '$${app.title}'
|
||||
|
||||
lifecycle:
|
||||
kind: 'entity'
|
||||
description: 'Entity lifecycle badge'
|
||||
target: '$${window.location.href}'
|
||||
label: 'Lifecycle'
|
||||
message: '$${entity.spec.lifecycle}'
|
||||
|
||||
owner:
|
||||
kind: 'entity'
|
||||
description: 'Entity owner badge'
|
||||
target: '$${window.location.href}'
|
||||
label: 'Owner'
|
||||
message: '$${entity.spec.owner}'
|
||||
|
||||
Vendored
+2
@@ -40,11 +40,13 @@ export interface Config {
|
||||
*
|
||||
* * `app.title` As configured or defaults to "Backstage".
|
||||
*
|
||||
* @visibility: frontend
|
||||
*/
|
||||
kind?: 'entity';
|
||||
|
||||
/**
|
||||
* The badge label.
|
||||
*
|
||||
*/
|
||||
label: string;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class BadgesApi {
|
||||
private readonly config: { [id: string]: BadgeConfig },
|
||||
) {}
|
||||
|
||||
public getBadge(badgeKind: string, badgeId: string, context: any) {
|
||||
public getBadge(badgeKind: string, badgeId: string, context: object) {
|
||||
const badge = this.config[badgeId] || this.config.default;
|
||||
|
||||
if (!badge) {
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './api';
|
||||
export * from './service/router';
|
||||
export * from './utils';
|
||||
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 interface Config {
|
||||
// defined with doc in badges-backend
|
||||
badges?: {
|
||||
[badgeId: string]: {
|
||||
/**
|
||||
* Badge title, used as tooltip text in the markdown code.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Badge description, used as alt text in the markdown code.
|
||||
* Defaults to badge id.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Badge target URL, turns badge into a link in the markdown code.
|
||||
*
|
||||
* For `entity` badges, there is a `entity_url` in the context
|
||||
* that could be appropriate to use here.
|
||||
*
|
||||
* @visibility frontend
|
||||
*/
|
||||
target?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -45,6 +45,8 @@
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,15 @@ 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;
|
||||
@@ -28,16 +37,64 @@ export class BadgesClientApi {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
async getEntityPoweredByMarkdownCode(entity: Entity): string {
|
||||
const badge = await this.getEntityPoweredByBadgeURL(entity);
|
||||
const target = this.getEntityLink(entity);
|
||||
return `[](${target})`;
|
||||
async getDefaultContext(entity: Entity) {
|
||||
return {
|
||||
app: {
|
||||
title: this.configApi.getString('app.title'),
|
||||
},
|
||||
entity,
|
||||
entity_url: entity && (await this.getEntityLink(entity)),
|
||||
};
|
||||
}
|
||||
|
||||
async getEntityPoweredByBadgeURL(entity: Entity): string {
|
||||
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}`;
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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, 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 Alert from '@material-ui/lab/Alert';
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { badgesClientApiRef } from '../BadgesClientApi';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => any;
|
||||
entity: Entity;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
codeBlock: {
|
||||
'& code': {
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const EntityBadgesDialog = ({ open, onClose, entity }: Props) => {
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const badgesClientApi = useApi(badgesClientApiRef);
|
||||
const classes = useStyles();
|
||||
|
||||
const { value: badges, loading, error } = useAsync(async () => {
|
||||
const context = await badgesClientApi.getDefaultContext(entity);
|
||||
return await badgesClientApi.getDefinedBadges('entity', context);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Dialog fullScreen={fullScreen} open={open} onClose={onClose}>
|
||||
<DialogTitle id="badges-dialog-title">Entity Badges</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{loading ? <Progress /> : null}
|
||||
|
||||
{error ? (
|
||||
<Alert severity="error" style={{ wordBreak: 'break-word' }}>
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{content}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -51,3 +51,14 @@ export const EntityBadgesCard = badgesPlugin.provide(
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityBadgesDialog = badgesPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/EntityBadgesDialog').then(
|
||||
m => m.EntityBadgesDialog,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,21 +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 { createRouteRef } from '@backstage/core';
|
||||
|
||||
export const badgeRouteRef = createRouteRef({
|
||||
title: 'Powered By Badge',
|
||||
path: 'badges/:kind/:namespace/:entityname',
|
||||
});
|
||||
@@ -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,9 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
{entity && (
|
||||
<>
|
||||
<EntityLabels entity={entity} />
|
||||
<EntityContextMenu onUnregisterEntity={showRemovalDialog} />
|
||||
<EntityContextMenu
|
||||
onShowBadgesDialog={() => setBadgesDialogOpen(true)}
|
||||
onUnregisterEntity={showRemovalDialog} />
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
@@ -159,6 +163,13 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
</Content>
|
||||
)}
|
||||
|
||||
{entity && (
|
||||
<EntityBadgesDialog
|
||||
open={badgesDialogOpen}
|
||||
entity={entity}
|
||||
onClose={() => setBadgesDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<UnregisterEntityDialog
|
||||
open={confirmationDialogOpen}
|
||||
entity={entity!}
|
||||
|
||||
Reference in New Issue
Block a user