import React, { memo, cloneElement, FC, HTMLAttributes, ReactNode, useCallback } from 'react'; import { css, cx } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; import { useTheme, styleMixins, stylesFactory } from '../../themes'; import { Tooltip, PopoverContent } from '../Tooltip/Tooltip'; /** * @public */ export interface ContainerProps extends HTMLAttributes { /** Content for the card's tooltip */ tooltip?: PopoverContent; } const CardContainer: FC = ({ children, tooltip, ...props }) => { return tooltip ? (
{children}
) : (
{children}
); }; /** * @public */ export interface CardInnerProps { href?: string; } const CardInner: FC = ({ children, href }) => { const theme = useTheme(); const styles = getCardStyles(theme); return href ? ( {children} ) : ( <>{children} ); }; /** * @public */ export interface Props extends ContainerProps { /** Main heading for the Card **/ heading: ReactNode; /** Card description text */ description?: string; /** Indicates if the card and all its actions can be interacted with */ disabled?: boolean; /** Link to redirect to on card click. If provided, the Card inner content will be rendered inside `a` */ href?: string; /** On click handler for the Card */ onClick?: () => void; } export interface CardInterface extends FC { Tags: typeof Tags; Figure: typeof Figure; Meta: typeof Meta; Actions: typeof Actions; SecondaryActions: typeof SecondaryActions; } /** * Generic card component * * @public */ export const Card: CardInterface = ({ heading, description, disabled, tooltip, href, onClick, className, children, ...htmlProps }) => { const theme = useTheme(); const styles = getCardStyles(theme); const [tags, figure, meta, actions, secondaryActions] = ['Tags', 'Figure', 'Meta', 'Actions', 'SecondaryActions'].map( (item) => { const found = React.Children.toArray(children as React.ReactElement[]).find((child) => { return child?.type && (child.type as any).displayName === item; }); if (found) { return React.cloneElement(found, { disabled, styles, ...found.props }); } return found; } ); const hasActions = Boolean(actions || secondaryActions); const disableHover = disabled || (!onClick && !href); const disableEvents = disabled && !actions; const containerStyles = getContainerStyles(theme, disableEvents, disableHover); const onCardClick = useCallback(() => (disableHover ? () => {} : onClick?.()), [disableHover, onClick]); return ( {figure}
{heading}
{meta} {description &&

{description}

}
{tags}
{hasActions && (
{actions} {secondaryActions}
)}
); }; /** * @public */ export const getContainerStyles = stylesFactory((theme: GrafanaTheme, disabled = false, disableHover = false) => { return css` display: flex; width: 100%; color: ${theme.colors.textStrong}; background: ${theme.colors.bg2}; border-radius: ${theme.border.radius.sm}; padding: ${theme.spacing.md}; position: relative; pointer-events: ${disabled ? 'none' : 'auto'}; margin-bottom: ${theme.spacing.sm}; &::after { content: ''; display: ${disabled ? 'block' : 'none'}; position: absolute; top: 1px; left: 1px; right: 1px; bottom: 1px; background: linear-gradient(180deg, rgba(75, 79, 84, 0.5) 0%, rgba(82, 84, 92, 0.5) 100%); width: calc(100% - 2px); height: calc(100% - 2px); border-radius: ${theme.border.radius.sm}; } &:hover { background: ${disableHover ? theme.colors.bg2 : styleMixins.hoverColor(theme.colors.bg2, theme)}; cursor: ${disableHover ? 'default' : 'pointer'}; } &:focus { ${styleMixins.focusCss(theme)}; } `; }); /** * @public */ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => { return { inner: css` display: flex; justify-content: space-between; align-items: center; width: 100%; flex-wrap: wrap; `, heading: css` display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 0; font-size: ${theme.typography.size.md}; line-height: ${theme.typography.lineHeight.xs}; color: ${theme.colors.text}; font-weight: ${theme.typography.weight.semibold}; `, info: css` display: flex; flex-direction: row; justify-content: space-between; align-items: center; width: 100%; `, metadata: css` display: flex; align-items: center; width: 100%; font-size: ${theme.typography.size.sm}; color: ${theme.colors.textSemiWeak}; margin: ${theme.spacing.xs} 0 0; line-height: ${theme.typography.lineHeight.xs}; `, description: css` width: 100%; margin: ${theme.spacing.sm} 0 0; color: ${theme.colors.textSemiWeak}; line-height: ${theme.typography.lineHeight.md}; `, media: css` margin-right: ${theme.spacing.md}; width: 40px; display: flex; align-items: center; & > * { width: 100%; } &:empty { display: none; } `, actionRow: css` display: flex; justify-content: space-between; align-items: center; width: 100%; margin-top: ${theme.spacing.md}; `, actions: css` & > * { margin-right: ${theme.spacing.sm}; } `, secondaryActions: css` display: flex; align-items: center; color: ${theme.colors.textSemiWeak}; // align to the right margin-left: auto; & > * { margin-right: ${theme.spacing.sm} !important; } `, separator: css` margin: 0 ${theme.spacing.sm}; `, innerLink: css` display: flex; width: 100%; `, tagList: css` max-width: 50%; `, }; }); interface ChildProps { styles?: ReturnType; disabled?: boolean; } const Tags: FC = ({ children, styles }) => { return
{children}
; }; Tags.displayName = 'Tags'; const Figure: FC = ({ children, styles, align = 'top' }) => { return (
{children}
); }; Figure.displayName = 'Figure'; const Meta: FC = memo(({ children, styles, separator = '|' }) => { let meta = children; // Join meta data elements by separator if (Array.isArray(children) && separator) { const filtered = React.Children.toArray(children).filter(Boolean); if (!filtered.length) { return null; } meta = filtered.reduce((prev, curr, i) => [ prev, {separator} , curr, ]); } return
{meta}
; }); Meta.displayName = 'Meta'; interface ActionsProps extends ChildProps { children: JSX.Element | JSX.Element[]; variant?: 'primary' | 'secondary'; } const BaseActions: FC = ({ children, styles, disabled, variant }) => { const css = variant === 'primary' ? styles?.actions : styles?.secondaryActions; return (
{Array.isArray(children) ? React.Children.map(children, (child) => cloneElement(child, { disabled })) : cloneElement(children, { disabled })}
); }; const Actions: FC = ({ children, styles, disabled }) => { return ( {children} ); }; Actions.displayName = 'Actions'; const SecondaryActions: FC = ({ children, styles, disabled }) => { return ( {children} ); }; SecondaryActions.displayName = 'SecondaryActions'; Card.Tags = Tags; Card.Figure = Figure; Card.Meta = Meta; Card.Actions = Actions; Card.SecondaryActions = SecondaryActions;