import React, { PureComponent, CSSProperties } from 'react'; import { VizOrientation } from '@grafana/data'; import { calculateGridDimensions } from '../../utils/squares'; interface Props<V, D> { /** * Optionally precalculate dimensions to support consistent behavior between repeated * values. Two typical patterns are: * 1) Calculate raw values like font size etc and pass them to each vis * 2) find the maximum input values and pass that to the vis */ getAlignmentFactors?: (values: V[], width: number, height: number) => D; /** * Render a single value */ renderValue: (props: VizRepeaterRenderValueProps<V, D>) => JSX.Element; height: number; width: number; source: any; // If this changes, new values will be requested getValues: () => V[]; renderCounter: number; // force update of values & render orientation: VizOrientation; itemSpacing?: number; /** When orientation is set to auto layout items in a grid */ autoGrid?: boolean; minVizHeight?: number; } export interface VizRepeaterRenderValueProps<V, D = {}> { value: V; width: number; height: number; orientation: VizOrientation; alignmentFactors: D; /** * Total number of values being shown in repeater */ count: number; } interface DefaultProps { itemSpacing: number; } type PropsWithDefaults<V, D> = Props<V, D> & DefaultProps; interface State<V> { values: V[]; } export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>> { static defaultProps: DefaultProps = { itemSpacing: 8, }; constructor(props: Props<V, D>) { super(props); this.state = { values: props.getValues(), }; } componentDidUpdate(prevProps: Props<V, D>) { const { renderCounter, source } = this.props; if (renderCounter !== prevProps.renderCounter || source !== prevProps.source) { this.setState({ values: this.props.getValues() }); } } getOrientation(): VizOrientation { const { orientation, width, height } = this.props; if (orientation === VizOrientation.Auto) { if (width > height) { return VizOrientation.Vertical; } else { return VizOrientation.Horizontal; } } return orientation; } renderGrid() { const { renderValue, height, width, itemSpacing, getAlignmentFactors, orientation } = this .props as PropsWithDefaults<V, D>; const { values } = this.state; const grid = calculateGridDimensions(width, height, itemSpacing, values.length); const alignmentFactors = getAlignmentFactors ? getAlignmentFactors(values, grid.width, grid.height) : ({} as D); let xGrid = 0; let yGrid = 0; let items: JSX.Element[] = []; for (let i = 0; i < values.length; i++) { const value = values[i]; const isLastRow = yGrid === grid.yCount - 1; const itemWidth = isLastRow ? grid.widthOnLastRow : grid.width; const itemHeight = grid.height; const xPos = xGrid * itemWidth + itemSpacing * xGrid; const yPos = yGrid * itemHeight + itemSpacing * yGrid; const itemStyles: CSSProperties = { position: 'absolute', left: xPos, top: yPos, width: `${itemWidth}px`, height: `${itemHeight}px`, }; items.push( <div key={i} style={itemStyles}> {renderValue({ value, width: itemWidth, height: itemHeight, alignmentFactors, orientation, count: values.length, })} </div> ); xGrid++; if (xGrid === grid.xCount) { xGrid = 0; yGrid++; } } return <div style={{ position: 'relative' }}>{items}</div>; } render() { const { renderValue, height, width, itemSpacing, getAlignmentFactors, autoGrid, orientation, minVizHeight } = this .props as PropsWithDefaults<V, D>; const { values } = this.state; if (autoGrid && orientation === VizOrientation.Auto) { return this.renderGrid(); } const itemStyles: React.CSSProperties = { display: 'flex', }; const repeaterStyle: React.CSSProperties = { display: 'flex', overflow: minVizHeight ? 'hidden auto' : 'hidden', }; let vizHeight = height; let vizWidth = width; let resolvedOrientation = this.getOrientation(); switch (resolvedOrientation) { case VizOrientation.Horizontal: repeaterStyle.flexDirection = 'column'; repeaterStyle.height = `${height}px`; itemStyles.marginBottom = `${itemSpacing}px`; vizWidth = width; vizHeight = Math.max(height / values.length - itemSpacing + itemSpacing / values.length, minVizHeight ?? 0); break; case VizOrientation.Vertical: repeaterStyle.flexDirection = 'row'; repeaterStyle.justifyContent = 'space-between'; itemStyles.marginRight = `${itemSpacing}px`; vizHeight = height; vizWidth = width / values.length - itemSpacing + itemSpacing / values.length; } itemStyles.width = `${vizWidth}px`; itemStyles.height = `${vizHeight}px`; const alignmentFactors = getAlignmentFactors ? getAlignmentFactors(values, vizWidth, vizHeight) : ({} as D); return ( <div style={repeaterStyle}> {values.map((value, index) => { return ( <div key={index} style={getItemStylesForIndex(itemStyles, index, values.length)}> {renderValue({ value, width: vizWidth, height: vizHeight, alignmentFactors, orientation: resolvedOrientation, count: values.length, })} </div> ); })} </div> ); } } /* * Removes any padding on the last item */ function getItemStylesForIndex(itemStyles: CSSProperties, index: number, length: number): CSSProperties { if (index === length - 1) { return { ...itemStyles, marginRight: 0, marginBottom: 0, }; } return itemStyles; }