ItemCard: deprecate and replace with composable pieces
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user