Merge pull request #27699 from kalleericson/template-card-meta-data

Feat: Additional test coverage and component structure for TemplateCard
This commit is contained in:
Ben Lambert
2024-12-17 11:42:17 +01:00
committed by GitHub
7 changed files with 318 additions and 138 deletions
@@ -19,7 +19,7 @@ import { Link } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
interface CardLinkProps {
export interface CardLinkProps {
icon: IconComponent;
text: string;
url: string;
@@ -15,67 +15,26 @@
*/
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { MarkdownContent, UserIcon } from '@backstage/core-components';
import {
IconComponent,
useAnalytics,
useApp,
} from '@backstage/core-plugin-api';
import {
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { IconComponent, useAnalytics } from '@backstage/core-plugin-api';
import { getEntityRelations } from '@backstage/plugin-catalog-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import Box from '@material-ui/core/Box';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Chip from '@material-ui/core/Chip';
import Divider from '@material-ui/core/Divider';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { makeStyles, Theme } from '@material-ui/core/styles';
import LanguageIcon from '@material-ui/icons/Language';
import React, { useCallback } from 'react';
import { CardHeader } from './CardHeader';
import { CardLink } from './CardLink';
import { usePermission } from '@backstage/plugin-permission-react';
import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha';
import { TemplateCardContent } from './TemplateCardContent';
import { TemplateCardTags } from './TemplateCardTags';
import { TemplateCardLinks } from './TemplateCardLinks';
import { TemplateCardActions } from './TemplateCardActions';
const useStyles = makeStyles<Theme>(theme => ({
box: {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
'-webkit-line-clamp': 10,
'-webkit-box-orient': 'vertical',
},
markdown: {
/** to make the styles for React Markdown not leak into the description */
'& :first-child': {
margin: 0,
},
},
label: {
color: theme.palette.text.secondary,
textTransform: 'uppercase',
fontWeight: 'bold',
letterSpacing: 0.5,
lineHeight: 1,
fontSize: '0.75rem',
},
footer: {
display: 'flex',
justifyContent: 'space-between',
flex: 1,
alignItems: 'center',
},
ownedBy: {
display: 'flex',
alignItems: 'center',
flex: 1,
color: theme.palette.link,
},
const useStyles = makeStyles<Theme>(() => ({
actionContainer: { padding: '16px', flex: 1, alignItems: 'flex-end' },
}));
/**
@@ -89,7 +48,6 @@ export interface TemplateCardProps {
text: string;
url: string;
}[];
onSelected?: (template: TemplateEntityV1beta3) => void;
}
@@ -98,16 +56,13 @@ export interface TemplateCardProps {
* @alpha
*/
export const TemplateCard = (props: TemplateCardProps) => {
const { onSelected, template } = props;
const { additionalLinks, onSelected, template } = props;
const styles = useStyles();
const analytics = useAnalytics();
const ownedByRelations = getEntityRelations(template, RELATION_OWNED_BY);
const app = useApp();
const iconResolver = (key?: string): IconComponent =>
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
const hasTags = !!template.metadata.tags?.length;
const hasLinks =
!!props.additionalLinks?.length || !!template.metadata.links?.length;
!!additionalLinks?.length || !!template.metadata.links?.length;
const displayDefaultDivider = !hasTags && !hasLinks;
const { allowed: canCreateTask } = usePermission({
@@ -120,98 +75,33 @@ export const TemplateCard = (props: TemplateCardProps) => {
return (
<Card>
<CardHeader template={template} />
<CardHeader template={template} data-testid="template-card-header" />
<CardContent>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box className={styles.box}>
<MarkdownContent
className={styles.markdown}
content={template.metadata.description ?? 'No description'}
/>
</Box>
</Grid>
<Grid container spacing={2} data-testid="template-card-content">
<TemplateCardContent template={template} />
{displayDefaultDivider && (
<Grid item xs={12}>
<Divider data-testid="template-card-separator" />
</Grid>
)}
{hasTags && (
<>
<Grid item xs={12}>
<Divider data-testid="template-card-separator--tags" />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{template.metadata.tags?.map(tag => (
<Grid key={`grid-${tag}`} item>
<Chip
style={{ margin: 0 }}
size="small"
label={tag}
key={tag}
/>
</Grid>
))}
</Grid>
</Grid>
</>
)}
{hasTags && <TemplateCardTags template={template} />}
{hasLinks && (
<>
<Grid item xs={12}>
<Divider data-testid="template-card-separator--links" />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2}>
{props.additionalLinks?.map(({ icon, text, url }, index) => (
<Grid className={styles.linkText} item xs={6} key={index}>
<CardLink icon={icon} text={text} url={url} />
</Grid>
))}
{template.metadata.links?.map(
({ url, icon, title }, index) => (
<Grid className={styles.linkText} item xs={6} key={index}>
<CardLink
icon={iconResolver(icon)}
text={title || url}
url={url}
/>
</Grid>
),
)}
</Grid>
</Grid>
</>
<TemplateCardLinks
template={template}
additionalLinks={additionalLinks}
/>
)}
</Grid>
</CardContent>
<CardActions style={{ padding: '16px', flex: 1, alignItems: 'flex-end' }}>
<div className={styles.footer}>
<div className={styles.ownedBy}>
{ownedByRelations.length > 0 && (
<>
<UserIcon fontSize="small" />
<EntityRefLinks
style={{ marginLeft: '8px' }}
entityRefs={ownedByRelations}
defaultKind="Group"
hideIcons
/>
</>
)}
</div>
{canCreateTask ? (
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleChoose}
>
Choose
</Button>
) : null}
</div>
<CardActions
className={styles.actionContainer}
data-testid="template-card-actions"
>
<TemplateCardActions
canCreateTask={canCreateTask}
handleChoose={handleChoose}
ownedByRelations={ownedByRelations}
/>
</CardActions>
</Card>
);
@@ -0,0 +1,86 @@
/*
* 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 { UserIcon } from '@backstage/core-components';
import { EntityRefLinks } from '@backstage/plugin-catalog-react';
import Button from '@material-ui/core/Button';
import { makeStyles, Theme } from '@material-ui/core/styles';
import React from 'react';
const useStyles = makeStyles<Theme>(theme => ({
footer: {
display: 'flex',
justifyContent: 'space-between',
flex: 1,
alignItems: 'center',
},
ownedBy: {
display: 'flex',
alignItems: 'center',
flex: 1,
color: theme.palette.link,
},
actionContainer: { padding: '16px', flex: 1, alignItems: 'flex-end' },
}));
/**
* The Props for the {@link TemplateCardActions} component
* @alpha
*/
export interface TemplateCardActionsProps {
ownedByRelations: any;
canCreateTask: boolean;
handleChoose: () => void;
}
export const TemplateCardActions = ({
canCreateTask,
handleChoose,
ownedByRelations,
}: TemplateCardActionsProps) => {
const styles = useStyles();
return (
<div className={styles.footer} data-testid="template-card-actions--footer">
<div
className={styles.ownedBy}
data-testid="template-card-actions--ownedby"
>
{ownedByRelations.length > 0 && (
<>
<UserIcon fontSize="small" />
<EntityRefLinks
style={{ marginLeft: '8px' }}
entityRefs={ownedByRelations}
defaultKind="Group"
hideIcons
/>
</>
)}
</div>
{canCreateTask ? (
<Button
size="small"
variant="outlined"
color="primary"
data-testid="template-card-actions--create"
onClick={handleChoose}
>
Choose
</Button>
) : null}
</div>
);
};
@@ -0,0 +1,59 @@
/*
* 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 { MarkdownContent } from '@backstage/core-components';
import type { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import React from 'react';
const useStyles = makeStyles(() => ({
box: {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
'-webkit-line-clamp': 10,
'-webkit-box-orient': 'vertical',
},
markdown: {
/** to make the styles for React Markdown not leak into the description */
'& :first-child': {
margin: 0,
},
},
}));
/**
* The Props for the {@link TemplateCardContent} component
* @alpha
*/
export interface TemplateCardContentProps {
template: TemplateEntityV1beta3;
}
export const TemplateCardContent = ({ template }: TemplateCardContentProps) => {
const styles = useStyles();
return (
<Grid item xs={12} data-testid="template-card-content-grid">
<Box className={styles.box} data-testid="template-card-content-container">
<MarkdownContent
className={styles.markdown}
content={template.metadata.description ?? 'No description'}
/>
</Box>
</Grid>
);
};
@@ -0,0 +1,85 @@
/*
* 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, useApp } from '@backstage/core-plugin-api';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import Divider from '@material-ui/core/Divider';
import Grid from '@material-ui/core/Grid';
import { makeStyles, Theme } from '@material-ui/core/styles';
import LanguageIcon from '@material-ui/icons/Language';
import React from 'react';
import { CardLink } from './CardLink';
const useStyles = makeStyles<Theme>({});
/**
* The Props for the {@link TemplateCardLinks} component
* @alpha
*/
export interface TemplateCardLinksProps {
template: TemplateEntityV1beta3;
additionalLinks?: {
icon: IconComponent;
text: string;
url: string;
}[];
}
export const TemplateCardLinks = ({
template,
additionalLinks,
}: TemplateCardLinksProps) => {
const styles = useStyles();
const app = useApp();
const iconResolver = (key?: string): IconComponent =>
key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon;
return (
<>
<Grid item xs={12}>
<Divider data-testid="template-card-separator--links" />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2} data-testid="template-card-links">
{additionalLinks?.map(({ icon, text, url }, index) => (
<Grid
className={styles.linkText}
item
xs={6}
key={index}
data-testid="template-card-links--item"
>
<CardLink icon={icon} text={text} url={url} />
</Grid>
))}
{template.metadata.links?.map(({ url, icon, title }, index) => (
<Grid
className={styles.linkText}
item
xs={6}
key={index}
data-testid="template-card-links--metalink"
>
<CardLink
icon={iconResolver(icon)}
text={title || url}
url={url}
/>
</Grid>
))}
</Grid>
</Grid>
</>
);
};
@@ -0,0 +1,55 @@
/*
* 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 type { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import Chip from '@material-ui/core/Chip';
import Divider from '@material-ui/core/Divider';
import Grid from '@material-ui/core/Grid';
import React from 'react';
/**
* The Props for the {@link TemplateCardTags} component
* @alpha
*/
export interface TemplateCardTagsProps {
template: TemplateEntityV1beta3;
}
export const TemplateCardTags = ({ template }: TemplateCardTagsProps) => (
<>
<Grid item xs={12}>
<Divider data-testid="template-card-separator--tags" />
</Grid>
<Grid item xs={12}>
<Grid container spacing={2} data-testid="template-card-tags">
{template.metadata.tags?.map(tag => (
<Grid
key={`grid-${tag}`}
item
data-testid={`template-card-tag-item-${tag}`}
>
<Chip
style={{ margin: 0 }}
size="small"
data-testid={`template-card-tag-chip-${tag}`}
label={tag}
key={tag}
/>
</Grid>
))}
</Grid>
</Grid>
</>
);