Merge pull request #13662 from MiniMarker/merge_upstream

Add support for links on Template Card
This commit is contained in:
Patrik Oldsberg
2022-09-26 21:53:12 +02:00
committed by GitHub
8 changed files with 265 additions and 56 deletions
+8
View File
@@ -396,6 +396,14 @@ export const scaffolderPlugin: BackstagePlugin<
},
{
registerComponent: ExternalRouteRef<undefined, true>;
viewTechDoc: ExternalRouteRef<
{
name: string;
kind: string;
namespace: string;
},
true
>;
},
{}
>;
@@ -14,12 +14,24 @@
* limitations under the License.
*/
import {
DEFAULT_NAMESPACE,
Entity,
EntityLink,
parseEntityRef,
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import {
Button,
ItemCardHeader,
MarkdownContent,
} from '@backstage/core-components';
import {
IconComponent,
useApi,
useApp,
useRouteRef,
} from '@backstage/core-plugin-api';
import {
ScmIntegrationIcon,
scmIntegrationsApiRef,
@@ -30,6 +42,7 @@ import {
getEntityRelations,
getEntitySourceLocation,
} from '@backstage/plugin-catalog-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { BackstageTheme } from '@backstage/theme';
import {
Box,
@@ -45,16 +58,10 @@ import {
Typography,
useTheme,
} from '@material-ui/core';
import LanguageIcon from '@material-ui/icons/Language';
import WarningIcon from '@material-ui/icons/Warning';
import React from 'react';
import { selectedTemplateRouteRef } from '../../routes';
import {
Button,
ItemCardHeader,
MarkdownContent,
} from '@backstage/core-components';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
import { selectedTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
const useStyles = makeStyles(theme => ({
cardHeader: {
@@ -69,7 +76,6 @@ const useStyles = makeStyles(theme => ({
display: '-webkit-box',
'-webkit-line-clamp': 10,
'-webkit-box-orient': 'vertical',
paddingBottom: '0.8em',
},
label: {
color: theme.palette.text.secondary,
@@ -80,6 +86,14 @@ const useStyles = makeStyles(theme => ({
lineHeight: 1,
paddingBottom: '0.2rem',
},
linksLabel: {
padding: '0 16px',
},
description: {
'& p': {
margin: '0px',
},
},
leftButton: {
marginRight: 'auto',
},
@@ -92,6 +106,8 @@ const useStyles = makeStyles(theme => ({
},
}));
const MuiIcon = ({ icon: Icon }: { icon: IconComponent }) => <Icon />;
const useDeprecationStyles = makeStyles(theme => ({
deprecationIcon: {
position: 'absolute',
@@ -115,6 +131,7 @@ type TemplateProps = {
title: string;
type: string;
name: string;
links: EntityLink[];
};
const getTemplateCardProps = (
@@ -127,6 +144,7 @@ const getTemplateCardProps = (
type: template.spec.type ?? '',
description: template.metadata.description ?? '-',
tags: (template.metadata?.tags as string[]) ?? [],
links: template.metadata.links ?? [],
};
};
@@ -155,6 +173,7 @@ const DeprecationWarning = () => {
};
export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
const app = useApp();
const backstageTheme = useTheme<BackstageTheme>();
const templateRoute = useRouteRef(selectedTemplateRouteRef);
const templateProps = getTemplateCardProps(template);
@@ -170,6 +189,22 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
const { name, namespace } = parseEntityRef(stringifyEntityRef(template));
const href = templateRoute({ templateName: name, namespace });
// TechDocs Link
const viewTechDoc = useRouteRef(viewTechDocRouteRef);
const viewTechDocsAnnotation =
template.metadata.annotations?.['backstage.io/techdocs-ref'];
const viewTechDocsLink =
!!viewTechDocsAnnotation &&
!!viewTechDoc &&
viewTechDoc({
namespace: template.metadata.namespace || DEFAULT_NAMESPACE,
kind: template.kind,
name: template.metadata.name,
});
const iconResolver = (key?: string): IconComponent =>
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const sourceLocation = getEntitySourceLocation(template, scmIntegrationsApi);
@@ -184,12 +219,17 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
classes={{ root: classes.title }}
/>
</CardMedia>
<CardContent style={{ display: 'grid' }}>
<CardContent
style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}
>
<Box className={classes.box}>
<Typography variant="body2" className={classes.label}>
Description
</Typography>
<MarkdownContent content={templateProps.description} />
<MarkdownContent
className={classes.description}
content={templateProps.description}
/>
</Box>
<Box className={classes.box}>
<Typography variant="body2" className={classes.label}>
@@ -198,7 +238,11 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="Group" />
</Box>
<Box>
<Typography variant="body2" className={classes.label}>
<Typography
style={{ marginBottom: '4px' }}
variant="body2"
className={classes.label}
>
Tags
</Typography>
{templateProps.tags?.map(tag => (
@@ -206,15 +250,47 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => {
))}
</Box>
</CardContent>
<Typography
variant="body2"
className={[classes.label, classes.linksLabel].join(' ')}
>
Links
</Typography>
<CardActions>
{sourceLocation && (
<IconButton
className={classes.leftButton}
href={sourceLocation.locationTargetUrl}
>
<ScmIntegrationIcon type={sourceLocation.integrationType} />
</IconButton>
)}
<div className={classes.leftButton}>
{sourceLocation && (
<Tooltip
title={
sourceLocation.integrationType ||
sourceLocation.locationTargetUrl
}
>
<IconButton
className={classes.leftButton}
href={sourceLocation.locationTargetUrl}
>
<ScmIntegrationIcon type={sourceLocation.integrationType} />
</IconButton>
</Tooltip>
)}
{viewTechDocsLink && (
<Tooltip title="View TechDocs">
<IconButton
className={classes.leftButton}
href={viewTechDocsLink}
>
<MuiIcon icon={iconResolver('docs')} />
</IconButton>
</Tooltip>
)}
{templateProps.links?.map((link, i) => (
<Tooltip key={`${link.url}_${i}`} title={link.title || link.url}>
<IconButton size="medium" href={link.url}>
<MuiIcon icon={iconResolver(link.icon)} />
</IconButton>
</Tooltip>
))}
</div>
<Button
color="primary"
to={href}
@@ -0,0 +1,45 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 { IconComponent } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import { Link, makeStyles } from '@material-ui/core';
import React from 'react';
interface CardLinkProps {
icon: IconComponent;
text: string;
url: string;
}
const useStyles = makeStyles<BackstageTheme>(() => ({
linkText: {
display: 'inline-flex',
alignItems: 'center',
},
}));
export const CardLink = ({ icon: Icon, text, url }: CardLinkProps) => {
const styles = useStyles();
return (
<div className={styles.linkText}>
<Icon fontSize="small" />
<Link style={{ marginLeft: '8px' }} href={url}>
{text || url}
</Link>
</div>
);
};
@@ -13,8 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
DEFAULT_NAMESPACE,
parseEntityRef,
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Button, MarkdownContent, UserIcon } from '@backstage/core-components';
import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api';
import {
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { BackstageTheme } from '@backstage/theme';
import {
Box,
Card,
@@ -22,22 +34,17 @@ import {
CardContent,
Chip,
Divider,
Grid,
makeStyles,
} from '@material-ui/core';
import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
import {
nextSelectedTemplateRouteRef,
viewTechDocRouteRef,
} from '../../../routes';
import { CardHeader } from './CardHeader';
import { MarkdownContent, UserIcon, Button } from '@backstage/core-components';
import {
parseEntityRef,
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { useRouteRef } from '@backstage/core-plugin-api';
import { nextSelectedTemplateRouteRef } from '../../../routes';
import { BackstageTheme } from '@backstage/theme';
import { CardLink } from './CardLink';
const useStyles = makeStyles<BackstageTheme>(theme => ({
box: {
@@ -50,7 +57,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
markdown: {
/** to make the styles for React Markdown not leak into the description */
'& :first-child': {
marginTop: 0,
margin: 0,
},
},
label: {
@@ -61,9 +68,6 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
lineHeight: 1,
fontSize: '0.75rem',
},
margin: {
marginBottom: theme.spacing(2),
},
footer: {
display: 'flex',
justifyContent: 'space-between',
@@ -104,34 +108,96 @@ export const TemplateCard = (props: TemplateCardProps) => {
namespace: namespace,
});
const app = useApp();
const iconResolver = (key?: string): IconComponent =>
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
// TechDocs Link
const viewTechDoc = useRouteRef(viewTechDocRouteRef);
const viewTechDocsAnnotation =
template.metadata.annotations?.['backstage.io/techdocs-ref'];
const viewTechDocsLink =
!!viewTechDocsAnnotation &&
!!viewTechDoc &&
viewTechDoc({
namespace: template.metadata.namespace || DEFAULT_NAMESPACE,
kind: template.kind,
name: template.metadata.name,
});
return (
<Card>
<CardHeader template={template} />
<CardContent>
<Box className={styles.box}>
<MarkdownContent
className={styles.markdown}
content={template.metadata.description ?? 'No description'}
/>
</Box>
{(template.metadata.tags?.length ?? 0) > 0 && (
<>
<Divider className={styles.margin} />
<Box>
{template.metadata.tags?.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box className={styles.box}>
<MarkdownContent
className={styles.markdown}
content={template.metadata.description ?? 'No description'}
/>
</Box>
</>
)}
</Grid>
{(template.metadata.tags?.length ?? 0) > 0 && (
<>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{template.metadata.tags?.map(tag => (
<Grid item>
<Chip
style={{ margin: 0 }}
size="small"
label={tag}
key={tag}
/>
</Grid>
))}
</Grid>
</Grid>
</>
)}
{(!!viewTechDocsLink || template.metadata.links?.length) && (
<>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{viewTechDocsLink && (
<Grid className={styles.linkText} item xs={6}>
<CardLink
icon={iconResolver('docs')}
text="View TechDocs"
url={viewTechDocsLink}
/>
</Grid>
)}
{template.metadata.links?.map(({ url, icon, title }) => (
<Grid className={styles.linkText} item xs={6}>
<CardLink
icon={iconResolver(icon)}
text={title || url}
url={url}
/>
</Grid>
))}
</Grid>
</Grid>
</>
)}
</Grid>
</CardContent>
<CardActions>
<CardActions style={{ padding: '16px' }}>
<div className={styles.footer}>
<div className={styles.ownedBy}>
{ownedByRelations.length > 0 && (
<>
<UserIcon />
<UserIcon fontSize="small" />
<EntityRefLinks
style={{ marginLeft: '8px' }}
entityRefs={ownedByRelations}
defaultKind="Group"
/>
+2
View File
@@ -27,6 +27,7 @@ import {
nextRouteRef,
registerComponentRouteRef,
rootRouteRef,
viewTechDocRouteRef,
} from './routes';
import {
createApiFactory,
@@ -68,6 +69,7 @@ export const scaffolderPlugin = createPlugin({
},
externalRoutes: {
registerComponent: registerComponentRouteRef,
viewTechDoc: viewTechDocRouteRef,
},
});
+6
View File
@@ -24,6 +24,12 @@ export const registerComponentRouteRef = createExternalRouteRef({
optional: true,
});
export const viewTechDocRouteRef = createExternalRouteRef({
id: 'view-techdoc',
optional: true,
params: ['namespace', 'kind', 'name'],
});
export const rootRouteRef = createRouteRef({
id: 'scaffolder',
});