1import React, { HTMLProps } from 'react'; 2import { css, cx } from '@emotion/css'; 3import { GrafanaTheme } from '@grafana/data'; 4import { stylesFactory, useTheme } from '../../themes'; 5 6enum Orientation { 7 Horizontal, 8 Vertical, 9} 10type Spacing = 'none' | 'xs' | 'sm' | 'md' | 'lg'; 11type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center'; 12type Align = 'normal' | 'flex-start' | 'flex-end' | 'center'; 13 14export interface LayoutProps extends Omit<HTMLProps<HTMLDivElement>, 'align' | 'children' | 'wrap'> { 15 children: React.ReactNode[] | React.ReactNode; 16 orientation?: Orientation; 17 spacing?: Spacing; 18 justify?: Justify; 19 align?: Align; 20 width?: string; 21 wrap?: boolean; 22} 23 24export interface ContainerProps { 25 padding?: Spacing; 26 margin?: Spacing; 27 grow?: number; 28 shrink?: number; 29} 30 31export const Layout: React.FC<LayoutProps> = ({ 32 children, 33 orientation = Orientation.Horizontal, 34 spacing = 'sm', 35 justify = 'flex-start', 36 align = 'normal', 37 wrap = false, 38 width = '100%', 39 height = '100%', 40 ...rest 41}) => { 42 const theme = useTheme(); 43 const styles = getStyles(theme, orientation, spacing, justify, align, wrap); 44 return ( 45 <div className={styles.layout} style={{ width, height }} {...rest}> 46 {React.Children.toArray(children) 47 .filter(Boolean) 48 .map((child, index) => { 49 return ( 50 <div className={styles.childWrapper} key={index}> 51 {child} 52 </div> 53 ); 54 })} 55 </div> 56 ); 57}; 58 59export const HorizontalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({ 60 children, 61 spacing, 62 justify, 63 align = 'center', 64 wrap, 65 width, 66 height, 67}) => ( 68 <Layout 69 spacing={spacing} 70 justify={justify} 71 orientation={Orientation.Horizontal} 72 align={align} 73 width={width} 74 height={height} 75 wrap={wrap} 76 > 77 {children} 78 </Layout> 79); 80export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation' | 'wrap'>> = ({ 81 children, 82 spacing, 83 justify, 84 align, 85 width, 86 height, 87}) => ( 88 <Layout 89 spacing={spacing} 90 justify={justify} 91 orientation={Orientation.Vertical} 92 align={align} 93 width={width} 94 height={height} 95 > 96 {children} 97 </Layout> 98); 99 100export const Container: React.FC<ContainerProps> = ({ children, padding, margin, grow, shrink }) => { 101 const theme = useTheme(); 102 const styles = getContainerStyles(theme, padding, margin); 103 return ( 104 <div 105 className={cx( 106 styles.wrapper, 107 grow !== undefined && 108 css` 109 flex-grow: ${grow}; 110 `, 111 shrink !== undefined && 112 css` 113 flex-shrink: ${shrink}; 114 ` 115 )} 116 > 117 {children} 118 </div> 119 ); 120}; 121 122const getStyles = stylesFactory( 123 (theme: GrafanaTheme, orientation: Orientation, spacing: Spacing, justify: Justify, align, wrap) => { 124 const finalSpacing = spacing !== 'none' ? theme.spacing[spacing] : 0; 125 // compensate for last row margin when wrapped, horizontal layout 126 const marginCompensation = 127 (orientation === Orientation.Horizontal && !wrap) || orientation === Orientation.Vertical 128 ? 0 129 : `-${finalSpacing}`; 130 131 const label = orientation === Orientation.Vertical ? 'vertical-group' : 'horizontal-group'; 132 133 return { 134 layout: css` 135 label: ${label}; 136 display: flex; 137 flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'}; 138 flex-wrap: ${wrap ? 'wrap' : 'nowrap'}; 139 justify-content: ${justify}; 140 align-items: ${align}; 141 height: 100%; 142 max-width: 100%; 143 // compensate for last row margin when wrapped, horizontal layout 144 margin-bottom: ${marginCompensation}; 145 `, 146 childWrapper: css` 147 label: layoutChildrenWrapper; 148 margin-bottom: ${orientation === Orientation.Horizontal && !wrap ? 0 : finalSpacing}; 149 margin-right: ${orientation === Orientation.Horizontal ? finalSpacing : 0}; 150 display: flex; 151 align-items: ${align}; 152 153 &:last-child { 154 margin-bottom: ${orientation === Orientation.Vertical && 0}; 155 margin-right: ${orientation === Orientation.Horizontal && 0}; 156 } 157 `, 158 }; 159 } 160); 161 162const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => { 163 const paddingSize = (padding && padding !== 'none' && theme.spacing[padding]) || 0; 164 const marginSize = (margin && margin !== 'none' && theme.spacing[margin]) || 0; 165 return { 166 wrapper: css` 167 label: container; 168 margin: ${marginSize}; 169 padding: ${paddingSize}; 170 `, 171 }; 172}); 173