From f60625c143c8c8f7acba18c2049125e142df5eeb Mon Sep 17 00:00:00 2001 From: TheRWe Date: Sun, 29 Nov 2020 17:37:54 +0100 Subject: [PATCH 01/14] feat: view working --- src/shared/components/GaugeMini.tsx | 608 ++++++++++++++++++ src/shared/components/GaugeMiniChart.tsx | 47 ++ .../LatestMultipleValueTransform.tsx | 115 ++++ src/shared/components/ViewSwitcher.tsx | 28 +- src/shared/constants/gaugeMiniSpecs.ts | 82 +++ .../types/GaugeMini/GaugeMiniOptions.tsx | 160 +++++ .../types/GaugeMini/ThresholdsSettings.tsx | 103 +++ .../visualization/types/GaugeMini/icon.tsx | 103 +++ .../visualization/types/GaugeMini/index.tsx | 13 + .../types/GaugeMini/properties.ts | 17 + .../view_options/GaugeMiniOptions.tsx | 111 ++++ .../view_options/OptionsSwitcher.tsx | 6 +- src/views/helpers/index.ts | 18 + 13 files changed, 1407 insertions(+), 4 deletions(-) create mode 100644 src/shared/components/GaugeMini.tsx create mode 100644 src/shared/components/GaugeMiniChart.tsx create mode 100644 src/shared/components/LatestMultipleValueTransform.tsx create mode 100644 src/shared/constants/gaugeMiniSpecs.ts create mode 100644 src/shared/visualization/types/GaugeMini/GaugeMiniOptions.tsx create mode 100644 src/shared/visualization/types/GaugeMini/ThresholdsSettings.tsx create mode 100644 src/shared/visualization/types/GaugeMini/icon.tsx create mode 100644 src/shared/visualization/types/GaugeMini/index.tsx create mode 100644 src/shared/visualization/types/GaugeMini/properties.ts create mode 100644 src/timeMachine/components/view_options/GaugeMiniOptions.tsx diff --git a/src/shared/components/GaugeMini.tsx b/src/shared/components/GaugeMini.tsx new file mode 100644 index 0000000000..f47ad3fdea --- /dev/null +++ b/src/shared/components/GaugeMini.tsx @@ -0,0 +1,608 @@ +// Libraries +import React, {FunctionComponent, useRef, useEffect, useState} from 'react' +import {color as d3Color} from 'd3-color' +import {range} from 'd3-array' +import {scaleLinear} from 'd3-scale' + +// Types +import { GaugeMiniLayerConfig } from "@influxdata/giraffe" +import {Color} from 'src/types/colors' + + +const throwReturn = (msg: string): T => { + throw new Error(msg) +} + +interface Props { + width: number + height: number + values: {colsMString: string; value: number}[] + theme: Required +} + +/** create merged string for given column string values. String is same for all columns with same values and unique for different ones */ +export const createColsMString = ( + groupedBy: T, + col: {[key in keyof T]: string} +): string => { + const columns = Object.keys(groupedBy).sort() + const columnValues = columns.map(x => col[x]) + /** + * replacing - with -- will ensures that rows + * { a: '0-1', b: '2' } and { a: '0', b: '1-2' } + * will not have same string (0-1-2 instead they will be 0--1-2 and 0-1--2) + */ + return columnValues.map(x => x.split('-').join('--')).join('-') +} + +const barCssClass = 'gauge-mini-bar' + +//#region colors + +export type Colors = { + min: Color + max: Color + secondary: string + thresholds: Color[] +} + +export const getColors = (theme: Required): Colors => { + const {colorSecondary: secondary, gaugeColors} = theme + + gaugeColors.forEach( + ({hex, name}) => + d3Color(hex) ?? + throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) + ) + + return { + min: + gaugeColors.find(x => x.type === 'min') ?? + throwReturn('color of type min must be defined'), + max: + gaugeColors.find(x => x.type === 'max') ?? + throwReturn('color of type max must be defined'), + thresholds: gaugeColors + .filter(({type}) => type === 'threshold') + .sort(({value: a}, {value: b}) => a - b), + secondary, + } +} + +//#endregion colors + +//#region svg helpers + +type TSvgTextRectProps = { + onRectChanged?: (rect: DOMRect) => void +} & React.SVGProps + +/** + * Helper component that returns rect when children changes. Usefull for calculating text box size. + * !onRectChanged called only when children changes! + */ +export const SvgTextRect: React.FC = props => { + const {onRectChanged = () => {}} = props + + const textRef = useRef(null) + + useEffect(() => { + const rect = textRef.current?.getBBox() + if (!rect) { + return + } + + onRectChanged(rect) + }, [props.children, onRectChanged]) + + return ( + <> + + + ) +} + +/** + * Helper component for centering content. + * !Doesn't react on content size changed. Recententering is done manualy by changing refreshToken! + */ +const AutoCenterGroup: FunctionComponent<{ + enabled?: boolean + refreshToken?: number | string +} & React.SVGProps> = props => { + const {children, enabled = true, refreshToken = 0} = props + const ref = useRef(null) + + const [x, setX] = useState(0) + const [y, setY] = useState(0) + + useEffect(() => { + if (!enabled) { + setX(0) + setY(0) + return + } + + const g = ref.current + // we use this function because we want to know how much we are in negative direction + const box = g?.getBBox() + // we use this function because we want to have fixed parent width/height + const boxParent = ((g?.parentElement as any) as + | SVGGraphicsElement + | undefined)?.getBoundingClientRect() + + if (!box || !boxParent) { + return + } + + setX((boxParent.width - box.width) / 2 - box.x) + setY((boxParent.height - box.height) / 2 - box.y) + }, [refreshToken, enabled]) + + return ( + + {children} + + ) +} + +//#endregion svg helpers + +//#region subcomponents + +//#region types + +type BarBackgroundProps = { + theme: Required + colors: Colors + barWidth: number + getFrac: (x: number) => number + barCenter: number +} + +type BarValueProps = { + theme: Required + barValueWidth: number + colors: Colors + value: number + valueFracFixed: number + barCenter: number +} + +type TextProps = { + theme: Required + barValueWidth: number + colors: Colors + value: number +} + +type BarProps = { + value: number + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +} + +type AxesProps = { + theme: Required + barWidth: number + y: number + getFrac: (x: number) => number +} + +//#endregion types + +const BarBackground: FunctionComponent = ({ + theme, + colors: {max, min, secondary, thresholds}, + barWidth, + getFrac, + barCenter, +}) => { + const {gaugeHeight, mode, gaugeRounding} = theme + + const y = barCenter - gaugeHeight / 2 + // todo: invalid HTML -> multiple same ID attribute possible + const roundingDefId = `rounded-bar-w-${barWidth}-h-${gaugeHeight}-r-${gaugeRounding}` + const gradientDefId = `gradient-${min.hex}-${max.hex}` + + type Segment = {start: number; end: number; hex: string} + const segments: Segment[] = [] + if (mode === 'bullet') { + // thresholds are already sorted by getColors + const allColors = [min, ...thresholds, max] + + for ( + let i = 0, start = 0, end = 0; + i + 1 < allColors.length; + i++, start = end + ) { + const {hex} = allColors[i] + const next = allColors[i + 1].value + + end = getFrac(next) + segments.push({start, end, hex}) + } + } else { + segments.push({start: 0, end: 1, hex: secondary}) + } + + // todo: dont't render def linear gradient when is not used + return ( + <> + + + + + + + + + + {thresholds.length === 0 && mode === 'bullet' ? ( + + ) : ( + segments + .reverse() + .map(({hex: col, end, start}, i) => ( + + )) + )} + + ) +} + +const BarValue: FunctionComponent = ({ + colors, + barValueWidth, + value, + theme, + valueFracFixed, + barCenter, +}) => { + const {valueHeight, mode, valueRounding} = theme + const colorModeGradient = colors.thresholds.length === 0 + + const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 + const y = barCenter - valueHeight / 2 + + const className = 'value-rect' + + const colorValue = + mode === 'bullet' + ? colors.secondary + : d3Color( + (() => { + if (colorModeGradient) { + return scaleLinear() + .range([colors.min.hex, colors.max.hex] as any) + .domain([colors.min.value, colors.max.value])(value) as any + } else { + const sortedColors = [ + colors.min, + ...colors.thresholds, + colors.max, + ] + let i = 0 + while ( + i < sortedColors.length && + value >= sortedColors[i].value + ) { + i++ + } + return sortedColors[ + Math.max(Math.min(i - 1, sortedColors.length - 1), 0) + ].hex + } + })() + ) + ?.brighter(1) + .hex() + + // todo: move styling out -> styling is now multiple times inserted + return ( + <> + + + + ) +} + +const Text: FunctionComponent = ({value, barValueWidth, theme}) => { + const { + valueFontColorInside, + valueFontColorOutside, + textMode, + valueFormater, + valueFontSize: fontSize, + valuePadding, + } = theme + const textValue = valueFormater(value) + const follow = textMode === 'follow' + + const [textBBox, setTextBBox] = useState(null) + + const textWidth = textBBox?.width ? textBBox?.width + valuePadding * 2 : 0 + const textInside = textWidth < barValueWidth + + const textColor = textInside ? valueFontColorInside : valueFontColorOutside + + const textAnchor = textInside && follow ? 'end' : 'start' + + const x = follow + ? Math.max( + barValueWidth + (textInside ? -1 : +1) * valuePadding, + valuePadding + ) + : valuePadding + + return ( + <> + + {textValue} + + + ) +} + +const Bar: FunctionComponent = ({ + value, + theme, + y, + barWidth, + getFrac, +}) => { + const {gaugeHeight, valueHeight, oveflowFraction} = theme + + const colors = getColors(theme) + + const valueFracFixed = Math.max( + -oveflowFraction, + Math.min(oveflowFraction + 1, getFrac(value)) + ) + + const barValueWidth = barWidth * valueFracFixed + const maxBarHeight = Math.max(gaugeHeight, valueHeight) + const barCenter = maxBarHeight / 2 + + return ( + + + + + + + + + + + ) +} + +const Axes: FunctionComponent = ({theme, barWidth, y, getFrac}) => { + const {axesSteps, axesFormater, axesFontColor, axesFontSize} = theme + + if (axesSteps === undefined || axesSteps === null) { + return <> + } + + const colors = getColors(theme) + const colorLen = colors.max.value - colors.min.value + const axesLineStyle: React.CSSProperties = { + stroke: axesFontColor, + strokeWidth: 2, + strokeLinecap: 'round', + } + + const axesValuesArray = Array.isArray(axesSteps) + ? axesSteps + : axesSteps === 'thresholds' + ? colors.thresholds.map(x => x.value) + : Number.isInteger(axesSteps) + ? range(axesSteps).map( + x => ((x + 1) * colorLen) / (axesSteps + 1) + colors.min.value + ) + : throwReturn( + `${JSON.stringify( + axesSteps + )} axesSteps must be number | "thresholds" | number[] | undefined.` + ) + + const points: { + anchor: string + value: number + lineLength: number + text: string + posX: number + }[] = axesValuesArray + .map(value => ({ + value, + anchor: 'middle', + lineLength: 5, + })) + .concat([ + { + value: colors.min.value, + anchor: 'start', + lineLength: 3, + }, + { + value: colors.max.value, + anchor: 'end', + lineLength: 3, + }, + ]) + .map(x => ({ + ...x, + posX: getFrac(x.value) * barWidth, + text: axesFormater(x.value), + })) + + return ( + <> + + + {points.map(({posX, lineLength, anchor, text}, i) => ( + + + + {text} + + + ))} + + + ) +} + +//#endregion subcomponents + +export const GaugeMini: FunctionComponent = ({ + values, + theme, + width, + height, +}) => { + const { + gaugeHeight, + sidePaddings, + valueHeight, + barsDefinitions, + barPaddings, + labelMain, + labelMainFontSize, + labelMainFontColor, + labelBarsFontColor, + labelBarsFontSize, + } = theme + const [barLabelsWidth] = useState([]) + + const colors = getColors(theme) + const colorLen = colors.max.value - colors.min.value + const barLabelWidth = Math.max(...barLabelsWidth) || 0 + const barWidth = width - sidePaddings * 2 - barLabelWidth + const maxBarHeight = Math.max(gaugeHeight, valueHeight) + const allBarsHeight = values.length * (maxBarHeight + barPaddings) + + const {groupByColumns} = barsDefinitions + const labelMapping: any = {} + barsDefinitions?.bars?.forEach(x => { + if (!x.label) return + const mstring = createColsMString(groupByColumns, x.barDef) + labelMapping[mstring] = x.label + }) + + const [autocenterToken, setAutocenterToken] = useState(0) + useEffect(() => { + setAutocenterToken(x => x + 1) + }, [barLabelWidth, sidePaddings, valueHeight, width, height]) + + /** return value as fraction 0->min 1->max */ + const getFrac = (val: number): number => (val - colors.min.value) / colorLen + + return ( + + + {labelMain && ( + + {labelMain} + + )} + {values.map(({colsMString, value}, i) => { + const y = 0 + i * (maxBarHeight + barPaddings) + const label = labelMapping?.[colsMString] + + const textCenter = y + maxBarHeight / 2 + + // todo: no rerender ? + const onRectChanged = (r: DOMRect) => { + barLabelsWidth[i] = r.width + } + + return ( + <> + + + {label} + + + ) + })} + + + + ) +} diff --git a/src/shared/components/GaugeMiniChart.tsx b/src/shared/components/GaugeMiniChart.tsx new file mode 100644 index 0000000000..257704239e --- /dev/null +++ b/src/shared/components/GaugeMiniChart.tsx @@ -0,0 +1,47 @@ +// Libraries +import React, { FunctionComponent } from 'react' +import _ from 'lodash' +import {AutoSizer} from 'react-virtualized' +import { GaugeMiniLayerConfig } from "@influxdata/giraffe" +import { GAUGE_MINI_THEME_BULLET_DARK } from "../constants/gaugeMiniSpecs" +import { GaugeMini } from "./GaugeMini" + +// Components + +// Types + +// Constants + + +interface Props { + values: {colsMString: string; value: number}[] + theme: GaugeMiniLayerConfig +} + +const GaugeMiniChart: FunctionComponent = (props: Props) => { + const {theme, values} = props + const themeOrDefault: Required = { + ...GAUGE_MINI_THEME_BULLET_DARK, + ...theme, + } + + return ( + + {({width, height}) => ( +
+ +
+ )} +
+ ) +} + +export default GaugeMiniChart diff --git a/src/shared/components/LatestMultipleValueTransform.tsx b/src/shared/components/LatestMultipleValueTransform.tsx new file mode 100644 index 0000000000..a8dc9eaf33 --- /dev/null +++ b/src/shared/components/LatestMultipleValueTransform.tsx @@ -0,0 +1,115 @@ +// Libraries +import React, { useMemo, FunctionComponent } from 'react' +import { Table } from '@influxdata/giraffe' + +// Utils +import { createColsMString } from "./GaugeMini" + +interface SelectedColumns { + [key: string]: true +} + +export const getLatestValuesGrouped = ( + table: Table, + columnsObj: SelectedColumns +) => { + const columns = Object.keys(columnsObj).sort() + + columns.forEach(x => { + if (table.getColumnType(x) !== 'string') { + throw new Error( + `Data can be grouped only by string columns. But column ${x} is typeof ${table.getColumnType( + x + )}` + ) + } + }) + + const valueColumn = table.getColumn('_value', 'number') as number[] + + if (!valueColumn.length) { + return [] + } + + // Fallback to `_stop` column if `_time` column missing otherwise return empty array. + let timeColData: number[] = [] + + if (table.columnKeys.includes('_time')) { + timeColData = table.getColumn('_time', 'number') as number[] + } else if (table.columnKeys.includes('_stop')) { + timeColData = table.getColumn('_stop', 'number') as number[] + } + if (!timeColData && table.length !== 1) { + return [] + } + + const groupColsData = columns.map(k => + table.getColumn(k, 'string') + ) as string[][] + + const result: {[key: string]: number} = {} + + timeColData + // merge time with it's index + .map((time, index) => ({time, index})) + // remove entries without time + .filter(({time}) => time) + // todo: sort time complexity too high ... replace with another solution + // from low time to high time (last is last) + .sort(({time: t1}, {time: t2}) => t1 - t2) + // get relevant data from index (we don't need time anymore) + .map(({index}) => ({ + value: valueColumn[index], + groupRow: groupColsData.map(x => x[index]), + })) + // remove invalid data + .filter(({value}) => Number.isFinite(value) && typeof value === 'number') + // create result + .forEach(({value, groupRow}) => { + const grupObj = {} + groupRow.forEach((x, i) => (grupObj[columns[i]] = x)) + const strKey = createColsMString(columnsObj, grupObj) + // data is inserted from last to first so latest data is first + result[strKey] = value + }) + + return result +} + +interface Props { + table: Table + columns: SelectedColumns + children: (latestValue: {colsMString: string; value: number}[]) => JSX.Element + quiet?: boolean +} + +// todo: can return string ? +export const LatestMultipleValueTransform: FunctionComponent = ({ + table, + columns, + quiet = false, + children, +}) => { + const latestValues = useMemo(() => getLatestValuesGrouped(table, columns), [ + table, + ]) + + if (latestValues.length === 0 && quiet) { + return null + } + + if (latestValues.length === 0) { + return ( +
+

No latest value found

+
+ ) + } + + const entries = Object.keys(latestValues).map(x => ({ + colsMString: x, + value: latestValues[x], + })) + + return children(entries) +} diff --git a/src/shared/components/ViewSwitcher.tsx b/src/shared/components/ViewSwitcher.tsx index 043e045713..81f982be8c 100644 --- a/src/shared/components/ViewSwitcher.tsx +++ b/src/shared/components/ViewSwitcher.tsx @@ -1,9 +1,10 @@ // Libraries -import React, {FunctionComponent} from 'react' -import {Plot, FromFluxResult} from '@influxdata/giraffe' +import React, { FunctionComponent } from 'react' +import { Plot, FromFluxResult, GaugeMiniLayerConfig } from '@influxdata/giraffe' // Components import GaugeChart from 'src/shared/components/GaugeChart' +import GaugeMiniChart from "./GaugeMiniChart" import SingleStat from 'src/shared/components/SingleStat' import TableGraphs from 'src/shared/components/tables/TableGraphs' import HistogramPlot from 'src/shared/components/HistogramPlot' @@ -13,6 +14,7 @@ import FluxTablesTransform from 'src/shared/components/FluxTablesTransform' import XYPlot from 'src/shared/components/XYPlot' import ScatterPlot from 'src/shared/components/ScatterPlot' import LatestValueTransform from 'src/shared/components/LatestValueTransform' +import { LatestMultipleValueTransform } from "./LatestMultipleValueTransform" import CheckPlot from 'src/shared/components/CheckPlot' import BandPlot from 'src/shared/components/BandPlot' @@ -46,7 +48,7 @@ const ViewSwitcher: FunctionComponent = ({ properties, timeRange, files, - giraffeResult: {table, fluxGroupKeyUnion}, + giraffeResult: { table, fluxGroupKeyUnion }, timeZone, statuses, checkType = null, @@ -93,6 +95,26 @@ const ViewSwitcher: FunctionComponent = ({ )} ) + // todo: regenerate swagger, remove 'as any's + case 'gauge-mini' as any: + console.log({table, theme, properties}) + return ( + ) + .barsDefinitions.groupByColumns + } + > + {latestValues => ( + //
{JSON.stringify(latestValues)}
+ + )} +
+ ) case 'xy': return ( = { + type: 'gauge mini', + mode: 'bullet', + textMode: 'follow', + barsDefinitions: {groupByColumns: {_field: true}}, + + valueHeight: 18, + gaugeHeight: 25, + valueRounding: 2, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 50, type: 'threshold', hex: InfluxColors.Sulfur}, + {value: 75, type: 'threshold', hex: InfluxColors.Topaz}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 12, + valueFontColorOutside: InfluxColors.Raven, + valueFontColorInside: InfluxColors.Cloud, + valueFormater: (num: number) => num.toFixed(0), + + axesSteps: 'thresholds', + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (num: number) => num.toFixed(0), +} + +export const GAUGE_MINI_THEME_PROGRESS_DARK: Required = { + type: 'gauge mini', + mode: 'progress', + textMode: 'follow', + barsDefinitions: {groupByColumns: {_field: true}}, + + valueHeight: 20, + gaugeHeight: 20, + valueRounding: 3, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeColors: [ + {value: 0, type: 'min', hex: InfluxColors.Krypton}, + {value: 100, type: 'max', hex: InfluxColors.Topaz}, + ] as Color[], + colorSecondary: InfluxColors.Kevlar, + + labelMain: '', + labelMainFontSize: 13, + labelMainFontColor: InfluxColors.Ghost, + + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 18, + valueFontColorInside: InfluxColors.Raven, + valueFontColorOutside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: undefined as any, + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), +} diff --git a/src/shared/visualization/types/GaugeMini/GaugeMiniOptions.tsx b/src/shared/visualization/types/GaugeMini/GaugeMiniOptions.tsx new file mode 100644 index 0000000000..c7cdf8a2a8 --- /dev/null +++ b/src/shared/visualization/types/GaugeMini/GaugeMiniOptions.tsx @@ -0,0 +1,160 @@ +import React, {FC} from 'react' + +import { + Grid, + Columns, + Form, + Toggle, + AutoInput, + AutoInputMode, + Input, + InputType, + InputToggleType, + InputLabel, + FlexBox, + AlignItems, + ComponentSize, +} from '@influxdata/clockface' + +import ThresholdsSettings from './ThresholdsSettings' +import {MIN_DECIMAL_PLACES, MAX_DECIMAL_PLACES} from 'src/dashboards/constants' +import {convertUserInputToNumOrNaN} from 'src/shared/utils/convertUserInput' + +import {GaugeViewProperties, VisOptionProps} from 'src/types' + +interface Props extends VisOptionProps { + properties: GaugeViewProperties +} + +const GaugeMiniOptions: FC = ({properties, update}) => { + const setDigits = (digits: number | null) => { + update({ + decimalPlaces: { + ...properties.decimalPlaces, + digits, + }, + }) + } + const handleChangeMode = (mode: AutoInputMode): void => { + if (mode === AutoInputMode.Auto) { + setDigits(null) + } else { + setDigits(2) + } + } + return ( + <> + +

Customize Mini-Gauge

+ + + + { + update({prefix: evt.target.value}) + }} + placeholder="%, MPH, etc." + /> + + + + + { + update({suffix: evt.target.value}) + }} + placeholder="%, MPH, etc." + /> + + + + + + + { + update({tickPrefix: val === 'false' || !!!val}) + }} + size={ComponentSize.ExtraSmall} + /> + + Optional Prefix + + + + + + { + update({tickSuffix: val === 'false' || !!!val}) + }} + size={ComponentSize.ExtraSmall} + /> + + Optional Suffix + + + + + {properties.decimalPlaces && ( + + { + setDigits(convertUserInputToNumOrNaN(evt)) + }} + value={properties.decimalPlaces.digits} + min={MIN_DECIMAL_PLACES} + max={MAX_DECIMAL_PLACES} + type={InputType.Number} + /> + } + /> + + )} +
+ +

Colorized Thresholds

+
+ + { + update({colors}) + }} + /> + + + ) +} + +export default GaugeMiniOptions diff --git a/src/shared/visualization/types/GaugeMini/ThresholdsSettings.tsx b/src/shared/visualization/types/GaugeMini/ThresholdsSettings.tsx new file mode 100644 index 0000000000..3fdd7a62ab --- /dev/null +++ b/src/shared/visualization/types/GaugeMini/ThresholdsSettings.tsx @@ -0,0 +1,103 @@ +import React, {useState, FC} from 'react' + +// Components +import ThresholdSetting from 'src/shared/components/ThresholdSetting' +import { + Button, + ButtonShape, + IconFont, + FlexBox, + ComponentSize, + FlexDirection, + AlignItems, +} from '@influxdata/clockface' + +// Utils +import { + sortThresholds, + validateThresholds, + addThreshold, +} from 'src/shared/utils/thresholds' + +// Types +import {Color} from 'src/types' + +interface Props { + thresholds: Color[] + onSetThresholds: (thresholds: Color[]) => void +} + +const ThresholdsSettings: FC = ({thresholds, onSetThresholds}) => { + const [errors, setErrors] = useState({}) + + const appendThreshold = () => { + const defaultThreshold = addThreshold(thresholds) + + onSetThresholds([...thresholds, defaultThreshold]) + } + + const updateThreshold = (id: string, value: Partial) => { + onSetThresholds( + thresholds.map(threshold => { + if (threshold.id !== id) { + return threshold + } + + return { + ...threshold, + ...value, + } + }) + ) + } + + const removeThreshold = (id: string) => { + onSetThresholds(thresholds.filter(threshold => threshold.id !== id)) + } + + const onBlur = () => { + setErrors(validateThresholds(thresholds)) + } + + return ( + +