ItemCard: deprecate and replace with composable pieces

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-03-04 11:40:13 +01:00
parent 10f5ded753
commit 2089de76be
20 changed files with 593 additions and 344 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core': patch
'@backstage/theme': patch
---
Deprecated `ItemCard`. Added `ItemCardGrid` and `ItemCardHeader` instead, that can be used to compose functionality around regular Material-UI `Card` components instead.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-explore': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-techdocs': patch
---
Make use of the new core `ItemCardGrid` and `ItemCardHeader` instead of the deprecated `ItemCard`.
@@ -87,8 +87,8 @@ export const Breadcrumbs = ({ children, ...props }: Props) => {
}}
>
<List>
{expandablePages.map(pageLink => (
<ListItem button>
{expandablePages.map((pageLink, index) => (
<ListItem key={index} button>
<StyledBox clone>{pageLink}</StyledBox>
</ListItem>
))}
@@ -13,86 +13,101 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Grid } from '@material-ui/core';
import {
Card,
CardActions,
CardContent,
CardMedia,
makeStyles,
Typography,
} from '@material-ui/core';
import React from 'react';
import { MemoryRouter } from 'react-router';
import { ItemCard } from '.';
import { Button } from '../../components';
import { ItemCardGrid } from './ItemCardGrid';
import { ItemCardHeader } from './ItemCardHeader';
export default {
title: 'Layout/Item Card',
component: ItemCard,
title: 'Layout/Item Cards',
};
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
const useStyles = makeStyles({
grid: {
gridTemplateColumns: 'repeat(auto-fill, 12em)',
},
header: {
color: 'black',
backgroundImage: 'linear-gradient(to bottom right, red, yellow)',
},
});
export const Default = () => (
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
subtitle="Pretitle"
onClick={() => {}}
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card"
label="Button"
subtitle="Pretitle"
onClick={() => {}}
/>
</Grid>
</Grid>
);
export const Tags = () => (
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card with Tags"
tags={['one tag', 'one tag']}
label="Button"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card with Tags"
tags={['one tag', 'two tag']}
label="Button"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Item Card"
description="This is the description of an Item Card without Tags"
label="Button"
/>
</Grid>
</Grid>
);
export const Link = () => (
<MemoryRouter>
<Grid container spacing={4}>
<Grid item xs={12} md={3}>
<ItemCard
title="Backstage"
description="This is the description of an Item Card with link"
label="Read More"
href="https://backstage.io"
/>
</Grid>
<Grid item xs={12} md={3}>
<ItemCard
title="Backstage @ GitHub"
description="This is the description of an Item Card with link"
label="Read More"
href="https://github.com/backstage/backstage"
/>
</Grid>
</Grid>
<Typography paragraph>
The most basic setup is to place a bunch of cards into a large grid,
leaving styling to the defaults. Try to resize the window to see how they
rearrange themselves to fit the viewport.
</Typography>
<ItemCardGrid>
{[...Array(10).keys()].map(index => (
<Card key={index}>
<CardMedia>
<ItemCardHeader title={`Card #${index}`} subtitle="Subtitle" />
</CardMedia>
<CardContent>
{text
.split(' ')
.slice(0, 5 + Math.floor(Math.random() * 30))
.join(' ')}
</CardContent>
<CardActions>
<Button color="primary" to="/catalog">
Go There!
</Button>
</CardActions>
</Card>
))}
</ItemCardGrid>
</MemoryRouter>
);
export const Styling = () => {
const classes = useStyles();
return (
<MemoryRouter>
<Typography paragraph>
Both the grid and the header can be styled, using the{' '}
<Typography variant="caption">classes</Typography> property. This lets
you for example tweak the column sizes and the background of the header.
</Typography>
<ItemCardGrid classes={{ root: classes.grid }}>
{[...Array(10).keys()].map(index => (
<Card key={index}>
<CardMedia>
<ItemCardHeader
title={`Card #${index}`}
subtitle="Subtitle"
classes={{ root: classes.header }}
/>
</CardMedia>
<CardContent>
{text
.split(' ')
.slice(0, 5 + Math.floor(Math.random() * 30))
.join(' ')}
</CardContent>
<CardActions>
<Button color="primary" to="/catalog">
Go There!
</Button>
</CardActions>
</Card>
))}
</ItemCardGrid>
</MemoryRouter>
);
};
+59 -60
View File
@@ -13,33 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core';
import clsx from 'clsx';
import {
Box,
Card,
CardActions,
CardContent,
CardMedia,
Chip,
} from '@material-ui/core';
import React, { ReactNode } from 'react';
import { Link } from '../../components';
const useStyles = makeStyles(theme => ({
header: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 6),
backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)',
},
content: {
padding: theme.spacing(2),
},
description: {
height: 175,
overflow: 'hidden',
textOverflow: 'ellipsis',
},
withTags: {
height: 'calc(175px - 32px - 8px)',
},
footer: {
display: 'flex',
flexDirection: 'row-reverse',
},
}));
import { Button } from '../../components';
import { ItemCardHeader } from './ItemCardHeader';
type ItemCardProps = {
description?: string;
@@ -53,6 +37,31 @@ type ItemCardProps = {
href?: string;
};
/**
* This card type has been deprecated. Instead use plain MUI Card and helpers
* where appropriate.
*
* <code>
* <!--
* <Card>
* <CardMedia>
* <ItemCardHeader title="My Card" subtitle="neat!" />
* </CardMedia>
* <CardContent>
* Some text
* </CardContent>
* <CardActions>
* <Button color="primary" to="https://backstage.io">
* Get Started
* </Button>
* </CardActions>
* </Card>
* -->
* </code>
*
* @deprecated Use plain MUI <Card> and composable helpers instead.
* @see https://material-ui.com/components/cards/
*/
export const ItemCard = ({
description,
tags,
@@ -63,43 +72,33 @@ export const ItemCard = ({
onClick,
href,
}: ItemCardProps) => {
const classes = useStyles();
return (
<Card>
<div className={classes.header}>
{(subtitle || type) && (
<Typography variant="subtitle2">{subtitle ?? type}</Typography>
<CardMedia>
<ItemCardHeader title={title} subtitle={subtitle || type} />
</CardMedia>
<CardContent>
{tags?.length ? (
<Box>
{tags.map((tag, i) => (
<Chip size="small" label={tag} key={i} />
))}
</Box>
) : null}
{description}
</CardContent>
<CardActions>
{!href && (
<Button to="#" onClick={onClick} color="primary">
{label}
</Button>
)}
<Typography variant="h6">{title}</Typography>
</div>
<div className={classes.content}>
{tags?.map((tag, i) => (
<Chip label={tag} key={`tag-${i}`} />
))}
<Typography
variant="body2"
paragraph
className={clsx(
classes.description,
tags && tags.length > 0 && classes.withTags,
)}
>
{description}
</Typography>
<div className={classes.footer}>
{!href && (
<Button onClick={onClick} color="primary">
{label}
</Button>
)}
{href && (
<Button component={Link} to={href} color="primary">
{label}
</Button>
)}
</div>
</div>
{href && (
<Button to={href} color="primary">
{label}
</Button>
)}
</CardActions>
</Card>
);
};
@@ -0,0 +1,50 @@
/*
* Copyright 2020 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 { renderInTestApp } from '@backstage/test-utils';
import { Card } from '@material-ui/core';
import { screen } from '@testing-library/react';
import React from 'react';
import { ItemCardGrid } from './ItemCardGrid';
describe('<ItemCardGrid />', () => {
it('renders default without exploding', async () => {
await renderInTestApp(
<ItemCardGrid>
<Card>Hello!</Card>
</ItemCardGrid>,
);
expect(screen.getByRole('grid')).toBeInTheDocument();
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
it('renders custom styles', async () => {
await renderInTestApp(
<>
<ItemCardGrid>
<Card>Hello!</Card>
</ItemCardGrid>
<ItemCardGrid classes={{ root: 'my-css-class' }}>
<Card>Goodbye!</Card>
</ItemCardGrid>
</>,
);
expect(screen.getAllByRole('grid')[0]).toHaveStyle({
gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))',
});
expect(screen.getAllByRole('grid')[1]).toHaveClass('my-css-class');
});
});
@@ -0,0 +1,62 @@
/*
* 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 { createStyles, makeStyles, Theme, WithStyles } from '@material-ui/core';
import React from 'react';
const styles = (theme: Theme) =>
createStyles({
root: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))',
gridAutoRows: '1fr',
gridGap: theme.spacing(2),
},
});
const useStyles = makeStyles(styles);
export type ItemCardGridProps = Partial<WithStyles<typeof styles>> & {
/**
* The Card items of the grid.
*/
children?: React.ReactNode;
};
/**
* A default grid to use when arranging "item cards" - cards that let users
* select among several options.
*
* The immediate children are expected to be MUI Card components.
*
* Styles for the grid can be overridden using the `classes` prop, e.g.:
*
* <code>
* <ItemCardGrid title="Hello" classes={{ root: myClassName }} />
* </code>
*
* This can be useful for e.g. overriding gridTemplateColumns to adapt the
* minimum size of the cells to fit the content better.
*/
export const ItemCardGrid = (props: ItemCardGridProps) => {
const { children, ...otherProps } = props;
const classes = useStyles(otherProps);
return (
<div role="grid" className={classes.root} {...otherProps}>
{children}
</div>
);
};
@@ -0,0 +1,60 @@
/*
* Copyright 2020 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 { renderInTestApp } from '@backstage/test-utils';
import { Card, CardMedia } from '@material-ui/core';
import { screen } from '@testing-library/react';
import React from 'react';
import { ItemCardHeader } from './ItemCardHeader';
describe('<ItemCardHeader />', () => {
it('renders default without exploding', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader title="My Title" subtitle="My Subtitle" />
</CardMedia>
</Card>,
);
expect(screen.getByText('My Title')).toBeInTheDocument();
expect(screen.getByText('My Subtitle')).toBeInTheDocument();
});
it('renders custom children', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader title="My Title">My Custom Text</ItemCardHeader>
</CardMedia>
</Card>,
);
expect(screen.getByText('My Title')).toBeInTheDocument();
expect(screen.getByText('My Custom Text')).toBeInTheDocument();
});
it('renders custom styles', async () => {
await renderInTestApp(
<Card>
<CardMedia>
<ItemCardHeader classes={{ root: 'my-css-class' }}>
My Custom Text
</ItemCardHeader>
</CardMedia>
</Card>,
);
expect(screen.getByText('My Custom Text')).toHaveClass('my-css-class');
});
});
@@ -0,0 +1,85 @@
/*
* 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 {
createStyles,
makeStyles,
Theme,
Typography,
WithStyles,
} from '@material-ui/core';
import React from 'react';
const styles = (theme: Theme) =>
createStyles({
root: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 3),
backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)',
backgroundPosition: 0,
backgroundSize: 'inherit',
},
});
const useStyles = makeStyles(styles);
export type ItemCardHeaderProps = Partial<WithStyles<typeof styles>> & {
/**
* A large title to show in the header, providing the main heading.
*
* Use this if you want to have the default styling and placement of a title.
*/
title?: React.ReactNode;
/**
* A slightly smaller title to show in the header, providing additional
* details.
*
* Use this if you want to have the default styling and placement of a
* subtitle.
*/
subtitle?: React.ReactNode;
/**
* Custom children to draw in the header.
*
* If the title and/or subtitle were specified, the children are drawn below
* those.
*/
children?: React.ReactNode;
};
/**
* A simple card header, rendering a default look for "item cards" - cards that
* are arranged in a grid for users to select among several options.
*
* This component expects to be placed within a MUI <CardMedia>.
*
* Styles for the header can be overridden using the `classes` prop, e.g.:
*
* <code>
* <ItemCardHeader title="Hello" classes={{ root: myClassName }} />
* </code>
*/
export const ItemCardHeader = (props: ItemCardHeaderProps) => {
const { title, subtitle, children } = props;
const classes = useStyles(props);
return (
<div className={classes.root}>
{subtitle && <Typography variant="subtitle2">{subtitle}</Typography>}
{title && <Typography variant="h6">{title}</Typography>}
{children}
</div>
);
};
@@ -15,3 +15,7 @@
*/
export { ItemCard } from './ItemCard';
export { ItemCardGrid } from './ItemCardGrid';
export type { ItemCardGridProps } from './ItemCardGrid';
export { ItemCardHeader } from './ItemCardHeader';
export type { ItemCardHeaderProps } from './ItemCardHeader';
+19
View File
@@ -234,12 +234,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`,
},
},
MuiCard: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
// etc) end up at the bottom of the card instead of just below the body
// contents.
display: 'flex',
flexDirection: 'column',
},
},
MuiCardHeader: {
root: {
// Reduce padding between header and content
paddingBottom: 0,
},
},
MuiCardContent: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
// etc) end up at the bottom of the card instead of just below the body
// contents.
flexGrow: 1,
},
},
MuiCardActions: {
root: {
// We default to putting the card actions at the end
@@ -14,12 +14,20 @@
* limitations under the License.
*/
import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { ItemCard, useRouteRef } from '@backstage/core';
import { Button, ItemCardHeader, useRouteRef } from '@backstage/core';
import {
EntityRefLinks,
entityRouteParams,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import {
Box,
Card,
CardActions,
CardContent,
CardMedia,
Chip,
} from '@material-ui/core';
import React from 'react';
import { catalogEntityRouteRef } from '../../routes';
@@ -28,23 +36,39 @@ type DomainCardProps = {
};
export const DomainCard = ({ entity }: DomainCardProps) => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
const catalogEntityRoute = useRouteRef(catalogEntityRouteRef);
return (
<ItemCard
title={entity.metadata.name}
description={entity.metadata.description}
tags={entity.metadata.tags}
subtitle={
<EntityRefLinks
entityRefs={ownedByRelations}
defaultKind="group"
color="inherit"
/>
}
label="Explore"
href={catalogEntityRoute(entityRouteParams(entity))}
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
const url = catalogEntityRoute(entityRouteParams(entity));
const owner = (
<EntityRefLinks
entityRefs={ownedByRelations}
defaultKind="group"
color="inherit"
/>
);
return (
<Card>
<CardMedia>
<ItemCardHeader title={entity.metadata.name} subtitle={owner} />
</CardMedia>
<CardContent>
{entity.metadata.tags?.length ? (
<Box>
{entity.metadata.tags.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
</Box>
) : null}
{entity.metadata.description}
</CardContent>
<CardActions>
<Button to={url} color="primary">
Explore
</Button>
</CardActions>
</Card>
);
};
@@ -1,59 +0,0 @@
/*
* Copyright 2020 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 { DomainEntity } from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { catalogEntityRouteRef } from '../../routes';
import { DomainCardGrid } from './DomainCardGrid';
describe('<DomainCardGrid />', () => {
it('renders a grid of domain cards', async () => {
const entities: DomainEntity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
metadata: {
name: 'playback',
},
spec: {
owner: 'guest',
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
metadata: {
name: 'artists',
},
spec: {
owner: 'guest',
},
},
];
const { getByText } = await renderInTestApp(
<DomainCardGrid entities={entities} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': catalogEntityRouteRef,
},
},
);
expect(getByText('artists')).toBeInTheDocument();
expect(getByText('playback')).toBeInTheDocument();
});
});
@@ -1,33 +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 { DomainEntity } from '@backstage/catalog-model';
import { Grid } from '@material-ui/core';
import React from 'react';
import { DomainCard } from '.';
type DomainCardGridProps = {
entities: DomainEntity[];
};
export const DomainCardGrid = ({ entities }: DomainCardGridProps) => (
<Grid container spacing={4}>
{entities.map((e, i) => (
<Grid item xs={12} md={3} key={i}>
<DomainCard entity={e} />
</Grid>
))}
</Grid>
);
@@ -14,4 +14,3 @@
* limitations under the License.
*/
export { DomainCard } from './DomainCard';
export { DomainCardGrid } from './DomainCardGrid';
@@ -18,6 +18,7 @@ import {
Content,
ContentHeader,
EmptyState,
ItemCardGrid,
Progress,
SupportButton,
useApi,
@@ -27,47 +28,64 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Button } from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { DomainCardGrid } from '../DomainCard';
import { DomainCard } from '../DomainCard';
export const DomainExplorerContent = () => {
const Body = () => {
const catalogApi = useApi(catalogApiRef);
const { value: entities, loading, error } = useAsync(async () => {
const response = await catalogApi.getEntities({
filter: { kind: 'domain' },
});
return response.items as DomainEntity[];
}, [catalogApi]);
if (loading) {
return <Progress />;
}
if (error) {
return (
<WarningPanel severity="error" title="Could not load domains.">
{error.message}
</WarningPanel>
);
}
if (!entities?.length) {
return (
<EmptyState
missing="info"
title="No domains to display"
description="You haven't added any domains yet."
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-domain"
>
Read more
</Button>
}
/>
);
}
return (
<ItemCardGrid>
{entities.map((entity, index) => (
<DomainCard key={index} entity={entity} />
))}
</ItemCardGrid>
);
};
export const DomainExplorerContent = () => {
return (
<Content noPadding>
<ContentHeader title="Domains">
<SupportButton>Discover the domains in your ecosystem.</SupportButton>
</ContentHeader>
{loading && <Progress />}
{error && (
<WarningPanel severity="error" title="Could not load domains.">
{error.message}
</WarningPanel>
)}
{!loading && !error && (!entities || entities.length === 0) && (
<EmptyState
missing="info"
title="No domains to display"
description={`You haven't added any domains yet.`}
action={
<Button
variant="contained"
color="primary"
href="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-domain"
>
Read more
</Button>
}
/>
)}
{!loading && entities && <DomainCardGrid entities={entities} />}
<Body />
</Content>
);
};
@@ -14,14 +14,13 @@
* limitations under the License.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
configApiRef,
Content,
ContentHeader,
Header,
ItemCardGrid,
Lifecycle,
Page,
Progress,
@@ -30,14 +29,16 @@ import {
WarningPanel,
} from '@backstage/core';
import { useStarredEntities } from '@backstage/plugin-catalog-react';
import { Box, Button, Link, makeStyles, Typography } from '@material-ui/core';
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
import StarIcon from '@material-ui/icons/Star';
import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import { ScaffolderFilter } from '../ScaffolderFilter';
import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter';
import SearchToolbar from '../SearchToolbar/SearchToolbar';
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
const useStyles = makeStyles(theme => ({
contentWrapper: {
@@ -46,12 +47,6 @@ const useStyles = makeStyles(theme => ({
gridTemplateColumns: '250px 1fr',
gridColumnGap: theme.spacing(2),
},
templateGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(22em, 1fr))',
gridAutoRows: '1fr',
gridGap: theme.spacing(2),
},
}));
const getTemplateCardProps = (
@@ -183,13 +178,13 @@ export const ScaffolderPageContents = () => {
</Typography>
)}
<Box className={styles.templateGrid}>
<ItemCardGrid>
{matchingEntities &&
matchingEntities?.length > 0 &&
matchingEntities.map(template => (
<TemplateCard {...getTemplateCardProps(template)} />
))}
</Box>
</ItemCardGrid>
</div>
</div>
</Content>
@@ -13,29 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, useRouteRef } from '@backstage/core';
import { Button, ItemCardHeader, useRouteRef } from '@backstage/core';
import { BackstageTheme, pageTheme } from '@backstage/theme';
import {
Box,
Card,
CardActions,
CardContent,
CardMedia,
Chip,
makeStyles,
Typography,
useTheme,
} from '@material-ui/core';
import React from 'react';
import { generatePath } from 'react-router';
import { rootRouteRef } from '../../routes';
const useStyles = makeStyles(theme => ({
header: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 3),
backgroundImage: (props: { backgroundImage: string }) =>
props.backgroundImage,
backgroundPosition: 0,
const useStyles = makeStyles({
title: {
backgroundImage: ({ backgroundImage }: any) => backgroundImage,
},
description: {
overflow: 'hidden',
@@ -44,14 +40,7 @@ const useStyles = makeStyles(theme => ({
'-webkit-line-clamp': 10,
'-webkit-box-orient': 'vertical',
},
card: {
display: 'flex',
flexDirection: 'column',
},
cardContent: {
flexGrow: 1,
},
}));
});
export type TemplateCardProps = {
description: string;
@@ -79,18 +68,21 @@ export const TemplateCard = ({
});
return (
<Card className={classes.card}>
<CardMedia className={classes.header}>
<Typography variant="subtitle2">{type}</Typography>
<Typography variant="h6">{title}</Typography>
<Card>
<CardMedia>
<ItemCardHeader
title={title}
subtitle={type}
classes={{ root: classes.title }}
/>
</CardMedia>
<CardContent className={classes.cardContent}>
{tags?.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
<Typography variant="body2" className={classes.description}>
{description}
</Typography>
<CardContent>
<Box>
{tags?.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
</Box>
<Box className={classes.description}>{description}</Box>
</CardContent>
<CardActions>
<Button color="primary" to={href}>
@@ -16,34 +16,32 @@
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import { TechDocsHome } from './TechDocsHome';
describe('TechDocs Home', () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () => Promise.resolve({ items: [] }),
getEntities: async () => ({ items: [] }),
};
const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]);
const apiRegistry = ApiRegistry.with(catalogApiRef, catalogApi);
it('should render a TechDocs home page', async () => {
const { findByTestId, findByText } = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsHome />
</ApiProvider>,
),
await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsHome />
</ApiProvider>,
);
// Header
expect(await findByText('Documentation')).toBeInTheDocument();
expect(await screen.findByText('Documentation')).toBeInTheDocument();
expect(
await findByText(/Documentation available in Backstage/i),
await screen.findByText(/Documentation available in Backstage/i),
).toBeInTheDocument();
// Explore Content
expect(await findByTestId('docs-explore')).toBeDefined();
expect(await screen.findByTestId('docs-explore')).toBeDefined();
});
});
@@ -15,17 +15,19 @@
*/
import {
Button,
CodeSnippet,
Content,
Header,
ItemCard,
ItemCardGrid,
ItemCardHeader,
Page,
Progress,
useApi,
WarningPanel,
} from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Grid } from '@material-ui/core';
import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core';
import React from 'react';
import { generatePath } from 'react-router-dom';
import { useAsync } from 'react-use';
@@ -81,24 +83,30 @@ export const TechDocsHome = () => {
subtitle="Documentation available in Backstage"
/>
<Content>
<Grid container data-testid="docs-explore">
{value?.length
? value.map((entity, index: number) => (
<Grid key={index} item xs={12} sm={6} md={3}>
<ItemCard
href={generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
})}
title={entity.metadata.name}
label="Read Docs"
description={entity.metadata.description}
/>
</Grid>
))
: null}
</Grid>
<ItemCardGrid data-testid="docs-explore">
{!value?.length
? null
: value.map((entity, index: number) => (
<Card key={index}>
<CardMedia>
<ItemCardHeader title={entity.metadata.name} />
</CardMedia>
<CardContent>{entity.metadata.description}</CardContent>
<CardActions>
<Button
to={generatePath(rootDocsRouteRef.path, {
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
name: entity.metadata.name,
})}
color="primary"
>
Read Docs
</Button>
</CardActions>
</Card>
))}
</ItemCardGrid>
</Content>
</Page>
);