Merge pull request #13662 from MiniMarker/merge_upstream
Add support for links on Template Card
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user