import React, { PureComponent, ChangeEvent } from 'react'; import { css } from 'emotion'; import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, SelectableValue, GrafanaTheme, } from '@grafana/data'; import { colors } from '../../utils'; import { ThemeContext } from '../../themes/ThemeContext'; import { Input } from '../Input/Input'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { stylesFactory } from '../../themes'; import { Icon } from '../Icon/Icon'; import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup'; import { Button } from '../Button'; import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer'; import { Label } from '../Forms/Label'; import { isNumber } from 'lodash'; const modes: Array> = [ { value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' }, { value: ThresholdsMode.Percentage, label: 'Percentage', description: 'Pick threshold based on the percent between min/max', }, ]; export interface Props { thresholds: ThresholdsConfig; onChange: (thresholds: ThresholdsConfig) => void; } interface State { steps: ThresholdWithKey[]; } export class ThresholdsEditor extends PureComponent { private latestThresholdInputRef: React.RefObject; constructor(props: Props) { super(props); const steps = toThresholdsWithKey(props.thresholds!.steps); steps[0].value = -Infinity; this.state = { steps }; this.latestThresholdInputRef = React.createRef(); } onAddThreshold = () => { const { steps } = this.state; let nextValue = 0; if (steps.length > 1) { nextValue = steps[steps.length - 1].value + 10; } const color = colors.filter((c) => !steps.some((t) => t.color === c))[1]; const add = { value: nextValue, color: color, key: counter++, }; const newThresholds = [...steps, add]; sortThresholds(newThresholds); this.setState({ steps: newThresholds }, () => { if (this.latestThresholdInputRef.current) { this.latestThresholdInputRef.current.focus(); } this.onChange(); }); }; onRemoveThreshold = (threshold: ThresholdWithKey) => { const { steps } = this.state; if (!steps.length) { return; } // Don't remove index 0 if (threshold.key === steps[0].key) { return; } this.setState({ steps: steps.filter((t) => t.key !== threshold.key) }, this.onChange); }; onChangeThresholdValue = (event: ChangeEvent, threshold: ThresholdWithKey) => { const cleanValue = event.target.value.replace(/,/g, '.'); const parsedValue = parseFloat(cleanValue); const value = isNaN(parsedValue) ? '' : parsedValue; const steps = this.state.steps.map((t) => { if (t.key === threshold.key) { t = { ...t, value: value as number }; } return t; }); if (steps.length) { steps[0].value = -Infinity; } sortThresholds(steps); this.setState({ steps }); }; onChangeThresholdColor = (threshold: ThresholdWithKey, color: string) => { const { steps } = this.state; const newThresholds = steps.map((t) => { if (t.key === threshold.key) { t = { ...t, color: color }; } return t; }); this.setState({ steps: newThresholds }, this.onChange); }; onBlur = () => { const steps = [...this.state.steps]; sortThresholds(steps); this.setState({ steps }, this.onChange); }; onChange = () => { this.props.onChange(thresholdsWithoutKey(this.props.thresholds, this.state.steps)); }; onModeChanged = (value?: ThresholdsMode) => { this.props.onChange({ ...this.props.thresholds, mode: value!, }); }; renderInput(threshold: ThresholdWithKey, styles: ThresholdStyles, idx: number) { const isPercent = this.props.thresholds.mode === ThresholdsMode.Percentage; if (!isFinite(threshold.value)) { return ( this.onChangeThresholdColor(threshold, color)} enableNamedColors={true} /> ) } /> ); } return ( ) => this.onChangeThresholdValue(event, threshold)} value={threshold.value} ref={idx === 0 ? this.latestThresholdInputRef : null} onBlur={this.onBlur} prefix={
{threshold.color && (
this.onChangeThresholdColor(threshold, color)} enableNamedColors={true} />
)} {isPercent &&
%
}
} suffix={ this.onRemoveThreshold(threshold)} /> } /> ); } render() { const { thresholds } = this.props; const { steps } = this.state; return ( {(theme) => { const styles = getStyles(theme); return (
{steps .slice(0) .reverse() .map((threshold, idx) => (
{this.renderInput(threshold, styles, idx)}
))}
); }}
); } } interface ThresholdWithKey extends Threshold { key: number; } let counter = 100; function toThresholdsWithKey(steps?: Threshold[]): ThresholdWithKey[] { if (!steps || steps.length === 0) { steps = [{ value: -Infinity, color: 'green' }]; } return steps .filter((t, i) => isNumber(t.value) || i === 0) .map((t) => { return { color: t.color, value: t.value === null ? -Infinity : t.value, key: counter++, }; }); } export function thresholdsWithoutKey(thresholds: ThresholdsConfig, steps: ThresholdWithKey[]): ThresholdsConfig { const mode = thresholds.mode ?? ThresholdsMode.Absolute; return { mode, steps: steps.map((t) => { const { key, ...rest } = t; return rest; // everything except key }), }; } interface ThresholdStyles { wrapper: string; thresholds: string; item: string; colorPicker: string; addButton: string; percentIcon: string; inputPrefix: string; trashIcon: string; } const getStyles = stylesFactory( (theme: GrafanaTheme): ThresholdStyles => { return { wrapper: css` display: flex; flex-direction: column; `, thresholds: css` display: flex; flex-direction: column; margin-bottom: ${theme.spacing.formSpacingBase * 2}px; `, item: css` margin-bottom: ${theme.spacing.sm}; &:last-child { margin-bottom: 0; } `, colorPicker: css` padding: 0 ${theme.spacing.sm}; `, addButton: css` margin-bottom: ${theme.spacing.sm}; `, percentIcon: css` font-size: ${theme.typography.size.sm}; color: ${theme.colors.textWeak}; `, inputPrefix: css` display: flex; align-items: center; `, trashIcon: css` color: ${theme.colors.textWeak}; cursor: pointer; &:hover { color: ${theme.colors.text}; } `, }; } );