Merge pull request #440 from hooloovooo/feature/407-infocard

InfoCard TypeScript rewrite
This commit is contained in:
Stefan Ålund
2020-04-03 13:47:51 +02:00
committed by GitHub
7 changed files with 271 additions and 292 deletions
@@ -1,63 +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 React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from '@material-ui/core';
import { Divider, ListItemText, withStyles } from '@material-ui/core';
import { ListItem, ListItemIcon } from '@material-ui/core';
import ArrowIcon from '@material-ui/icons/ArrowForward';
import grey from '@material-ui/core/colors/grey';
import Box from '@material-ui/core/Box';
const styles = theme => ({
root: {
maxWidth: 'fit-content',
padding: theme.spacing(2, 2, 2, 2.5)
},
boxTitle: {
margin: 0,
color: grey[900]
}
})
class BottomLink extends Component {
static propTypes = {
link: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
};
render() {
const { link, title, onClick, classes } = this.props;
return (
<div>
<Divider />
<Link href={link} onClick={onClick} highlight="none">
<ListItem className={classes.root}>
<ListItemText>
<Box className={classes.boxTitle} fontWeight="fontWeightBold" m={1}>{title}</Box>
</ListItemText>
<ListItemIcon>
<ArrowIcon />
</ListItemIcon>
</ListItem>
</Link>
</div>
);
}
}
export default withStyles(styles)(BottomLink);
@@ -0,0 +1,65 @@
/*
* 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 React, { FC } from 'react';
import { Link } from '@material-ui/core';
import { Divider, ListItemText, makeStyles } from '@material-ui/core';
import { ListItem, ListItemIcon } from '@material-ui/core';
import ArrowIcon from '@material-ui/icons/ArrowForward';
import grey from '@material-ui/core/colors/grey';
import Box from '@material-ui/core/Box';
import { BackstageTheme } from '../../theme/theme';
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
maxWidth: 'fit-content',
padding: theme.spacing(2, 2, 2, 2.5),
},
boxTitle: {
margin: 0,
color: grey[900],
},
}));
export type Props = {
link: string;
title: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
};
const BottomLink: FC<Props> = ({ link, title, onClick }) => {
const classes = useStyles();
return (
<div>
<Divider />
<Link href={link} onClick={onClick}>
<ListItem className={classes.root}>
<ListItemText>
<Box className={classes.boxTitle} fontWeight="fontWeightBold" m={1}>
{title}
</Box>
</ListItemText>
<ListItemIcon>
<ArrowIcon />
</ListItemIcon>
</ListItem>
</Link>
</div>
);
};
export default BottomLink;
@@ -1,229 +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 React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Card,
CardActions,
CardContent,
CardHeader,
Divider,
withStyles,
} from '@material-ui/core';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import BottomLink from './BottomLink';
import textContent from 'react-addons-text-content';
const styles = theme => ({
header: {
padding: theme.spacing(2, 2, 2, 2.5)
}
});
const BoldHeader = withStyles({
title: { fontWeight: '700' },
subheader: { paddingTop: '2px' },
})(CardHeader);
const CardActionsTopRight = withStyles({
root: {
display: 'inline-block',
paddingRight: '16px',
paddingTop: '16px',
float: 'right',
},
})(CardActions);
const VARIANT_STYLES = {
card: {
flex: {
display: 'flex',
flexDirection: 'column',
},
widget: {
height: 430,
},
fullHeight: {
height: '100%',
},
height100: {
display: 'flex',
flexDirection: 'column',
height: 'calc(100% - 10px)', // for pages without content header
marginBottom: '10px',
},
contentheader: {
height: 'calc(100% - 40px)', // for pages with content header
},
contentheadertabs: {
height: 'calc(100% - 97px)', // for pages with content header and tabs (Tingle)
},
noShrink: {
flexShrink: 0,
},
minheight300: {
minHeight: 300,
overflow: 'initial',
},
},
cardContent: {
widget: {
overflowY: 'auto',
height: 332,
width: '100%',
},
fullHeight: {
height: 'calc(100% - 50px)',
},
height100: {
height: 'calc(100% - 50px)',
},
contentRow: {
display: 'flex',
flexDirection: 'row',
},
},
};
/**
* InfoCard is used to display a paper-styled block on the screen, similar to a panel.
*
* You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container)
* styles.
*
* The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this
* specifies the channel to display in the error component that is displayed if an error occurs
* in any descendent components.
*
* By default the InfoCard has no custom layout of its children, but is treated as a block element. A
* couple common variants are provided and can be specified via the variant property:
*
* Display the card full height suitable for DataGrid:
*
* <InfoCard variant="height100">...</InfoCard>
*
* Variants can be combined in a whitespace delimited list like so:
*
* <InfoCard variant="noShrink">...</InfoCard>
*/
class InfoCard extends Component {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
divider: PropTypes.bool,
deepLink: PropTypes.object,
slackChannel: PropTypes.string,
variant: PropTypes.string,
};
render() {
const {
title,
subheader,
divider,
deepLink,
children,
actions,
actionsTopRight,
headerStyle,
headerProps,
classes,
slackChannel,
variant,
} = this.props;
if (this.props.style) {
// eslint-disable-next-line no-console
console.warn(
'InfoCard: using `style` property directly, consider migrating your style to variant in InfoCard',
);
}
if (this.props.cardStyle) {
// eslint-disable-next-line no-console
console.warn(
'InfoCard: using `cardStyle` property directly, consider migrating your style to variant in InfoCard',
);
}
/**
* If variant is specified, we build up styles for that particular variant for both
* the Card and the CardContent (since these need to be synced)
*/
let calculatedStyle = {};
let calculatedCardStyle = {};
if (variant) {
const variants = variant.split(/[\s]+/g);
variants.forEach(name => {
calculatedStyle = { ...calculatedStyle, ...VARIANT_STYLES.card[name] };
calculatedCardStyle = {
...calculatedCardStyle,
...VARIANT_STYLES.cardContent[name],
};
});
}
// Apply the passed styles on top
const computedStyle = { ...calculatedStyle, ...this.props.style };
const computedCardStyle = {
...calculatedCardStyle,
...this.props.cardStyle,
};
return (
<Card
style={computedStyle}
classes={classes}
gacontext={textContent(title)}
>
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<>
<BoldHeader
className={classes.header}
title={title}
subheader={subheader}
style={{ display: 'inline-block', ...headerStyle }}
{...headerProps}
/>
<Divider />
</>
)}
{actionsTopRight && (
<CardActionsTopRight>{actionsTopRight}</CardActionsTopRight>
)}
{divider && <Divider />}
<CardContent
className={this.props.cardClassName}
style={computedCardStyle}
>
{children}
</CardContent>
{actions && (
<CardActions className={this.props.actionsClassName}>
{actions}
</CardActions>
)}
{deepLink && <BottomLink {...deepLink} />}
</ErrorBoundary>
</Card>
);
}
}
export default withStyles(styles)(InfoCard);
@@ -0,0 +1,206 @@
/*
* 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 React, { FC, ReactNode } from 'react';
import {
Card,
CardActions,
CardContent,
CardHeader,
Divider,
withStyles,
makeStyles,
} from '@material-ui/core';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import BottomLink, { Props as BottomLinkProps } from './BottomLink';
import { BackstageTheme } from '../../theme/theme';
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
padding: theme.spacing(2, 2, 2, 2.5),
},
}));
const BoldHeader = withStyles(theme => ({
title: { fontWeight: 700 },
subheader: { paddingTop: theme.spacing(1) },
}))(CardHeader);
const CardActionsTopRight = withStyles(theme => ({
root: {
display: 'inline-block',
padding: theme.spacing(8, 8, 0, 0),
float: 'right',
},
}))(CardActions);
const VARIANT_STYLES = {
card: {
flex: {
display: 'flex',
flexDirection: 'column',
},
widget: {
height: 430,
},
fullHeight: {
height: '100%',
},
height100: {
display: 'flex',
flexDirection: 'column',
height: 'calc(100% - 10px)', // for pages without content header
marginBottom: '10px',
},
contentheader: {
height: 'calc(100% - 40px)', // for pages with content header
},
contentheadertabs: {
height: 'calc(100% - 97px)', // for pages with content header and tabs (Tingle)
},
noShrink: {
flexShrink: 0,
},
minheight300: {
minHeight: 300,
overflow: 'initial',
},
},
cardContent: {
widget: {
overflowY: 'auto',
height: 332,
width: '100%',
},
fullHeight: {
height: 'calc(100% - 50px)',
},
height100: {
height: 'calc(100% - 50px)',
},
contentRow: {
display: 'flex',
flexDirection: 'row',
},
},
};
/**
* InfoCard is used to display a paper-styled block on the screen, similar to a panel.
*
* You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container)
* styles.
*
* The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this
* specifies the channel to display in the error component that is displayed if an error occurs
* in any descendent components.
*
* By default the InfoCard has no custom layout of its children, but is treated as a block element. A
* couple common variants are provided and can be specified via the variant property:
*
* Display the card full height suitable for DataGrid:
*
* <InfoCard variant="height100">...</InfoCard>
*
* Variants can be combined in a whitespace delimited list like so:
*
* <InfoCard variant="noShrink">...</InfoCard>
*/
type Props = {
title?: ReactNode;
subheader?: ReactNode;
divider?: boolean;
deepLink?: BottomLinkProps;
slackChannel?: string;
variant?: string;
style?: object;
cardStyle?: object;
children?: ReactNode;
headerStyle?: object;
headerProps?: object;
actionsClassName?: string;
actions?: ReactNode;
cardClassName?: string;
actionsTopRight?: ReactNode;
};
const InfoCard: FC<Props> = ({
title,
subheader,
divider,
deepLink,
slackChannel = '#backstage',
variant,
children,
headerStyle,
headerProps,
actionsClassName,
actions,
cardClassName,
actionsTopRight,
}) => {
const classes = useStyles();
/**
* If variant is specified, we build up styles for that particular variant for both
* the Card and the CardContent (since these need to be synced)
*/
let calculatedStyle = {};
let calculatedCardStyle = {};
if (variant) {
const variants = variant.split(/[\s]+/g);
variants.forEach(name => {
calculatedStyle = { ...calculatedStyle, ...VARIANT_STYLES.card[name] };
calculatedCardStyle = {
...calculatedCardStyle,
...VARIANT_STYLES.cardContent[name],
};
});
}
return (
<Card style={calculatedStyle}>
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<>
<BoldHeader
className={classes.header}
title={title}
subheader={subheader}
style={{ display: 'inline-block', ...headerStyle }}
{...headerProps}
/>
<Divider />
</>
)}
{actionsTopRight && (
<CardActionsTopRight>{actionsTopRight}</CardActionsTopRight>
)}
{divider && <Divider />}
<CardContent className={cardClassName} style={calculatedCardStyle}>
{children}
</CardContent>
{actions && (
<CardActions className={actionsClassName}>{actions}</CardActions>
)}
{deepLink && <BottomLink {...deepLink} />}
</ErrorBoundary>
</Card>
);
};
export default InfoCard;