diff --git a/package.json b/package.json index 4cbf9d238d..e1cc9609bf 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test:circleci": "yarn run test:ci --maxWorkers=2", "test:ci": "JEST_JUNIT_OUTPUT_DIR=\"./coverage\" jest --ci --coverage", "lint": "yarn tsc && yarn prettier && yarn eslint", + "lint:fix": "yarn tsc && yarn prettier:fix && yarn eslint:fix", "eslint": "eslint '{src,cypress}/**/*.{ts,tsx}'", "eslint:circleci": "eslint", "eslint:fix": "eslint --fix '{src,cypress}/**/*.{ts,tsx}'", @@ -48,7 +49,7 @@ "cy": "CYPRESS_baseUrl=http://localhost:9999 cypress open", "cy:dev": "source ../monitor-ci/.env && CYPRESS_baseUrl=http://kubernetes.docker.internal:$PORT_HTTPS cypress open --config testFiles='{cloud,shared}/**/*.*'", "cy:dev-oss": "source ../monitor-ci/.env && CYPRESS_baseUrl=http://kubernetes.docker.internal:$PORT_HTTPS cypress open --config testFiles='{oss,shared}/**/*.*'", - "generate": "oats https://raw.githubusercontent.com/influxdata/influxdb/master/http/swagger.yml > ./src/client/generatedRoutes.ts && yarn unity", + "generate": "echo todo: revert swagger URL && oats https://raw.githubusercontent.com/Sciator/influxdb/feat-new-plot-types/http/swagger.yml > ./src/client/generatedRoutes.ts && yarn unity", "unity": "oats ./src/client/unity.yml > ./src/client/unityRoutes.ts" }, "author": "", diff --git a/src/shared/components/ColorDropdown.tsx b/src/shared/components/ColorDropdown.tsx index 30f8992a0c..99dae8c881 100644 --- a/src/shared/components/ColorDropdown.tsx +++ b/src/shared/components/ColorDropdown.tsx @@ -75,7 +75,6 @@ const ColorDropdown: FC = props => { ColorDropdown.defaultProps = { disabled: false, - style: {flex: '0 0 120px'}, } export default ColorDropdown diff --git a/src/shared/components/GaugeMini.tsx b/src/shared/components/GaugeMini.tsx new file mode 100644 index 0000000000..1e17e036d8 --- /dev/null +++ b/src/shared/components/GaugeMini.tsx @@ -0,0 +1,552 @@ +// Libraries +import React, {FunctionComponent, useRef, useEffect, useState} from 'react' +import {color as d3Color} from 'd3-color' +import {scaleLinear} from 'd3-scale' + +// Types +import {GaugeMiniLayerConfig} from '@influxdata/giraffe' +import { + gaugeMiniNormalizeThemeMemoized, + GaugeMiniThemeNormalized, +} from '../utils/gaugeMiniThemeNormalize' +import {GaugeMiniColors} from '@influxdata/giraffe/dist/types' +import {GroupedData} from './LatestMultipleValueTransform' + +interface Props { + width: number + height: number + values: GroupedData + theme: Required +} + +const barCssClass = 'gauge-mini-bar' + +// #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: GaugeMiniColors + barWidth: number + getFrac: (x: number) => number + barCenter: number +} + +type BarValueProps = { + theme: Required + barValueWidth: number + colors: GaugeMiniColors + value: number + valueFracFixed: number + barCenter: number +} + +type TextProps = { + theme: Required + barValueWidth: number + colors: GaugeMiniColors + 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 +} + +type BarSegment = { + start: number + end: number + hex: string +} + +// #endregion types + +const BarBackground: FunctionComponent = ({ + theme, + barWidth, + getFrac, + barCenter, +}) => { + const {gaugeHeight, mode, gaugeRounding, colors, colorSecondary} = theme + const {max, min, thresholds = []} = colors + + 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}` + + const segments: BarSegment[] = [] + 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: colorSecondary}) + } + + // 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, colorSecondary} = theme + const {min, max, thresholds = []} = colors + const colorModeGradient = thresholds.length === 0 + + const x = Math.sign(valueFracFixed) === -1 ? barValueWidth : 0 + const y = barCenter - valueHeight / 2 + + const className = 'value-rect' + + const colorValue = + mode === 'bullet' + ? colorSecondary + : d3Color( + (() => { + if (colorModeGradient) { + return scaleLinear() + .range([min.hex, max.hex] as any) + .domain([min.value, max.value])(value) as any + } else { + const sortedColors = [min, ...thresholds, 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, colors} = 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, colors} = theme + + if (axesSteps === undefined) { + return <> + } + + const {min, max} = colors + const axesLineStyle: React.CSSProperties = { + stroke: axesFontColor, + strokeWidth: 2, + strokeLinecap: 'round', + } + + const points: { + anchor: string + value: number + lineLength: number + text: string + posX: number + }[] = axesSteps + .map(value => ({ + value, + anchor: 'middle', + lineLength: 5, + })) + .concat([ + { + value: min.value, + anchor: 'start', + lineLength: 3, + }, + { + value: 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: _theme, + width, + height, +}) => { + const theme = gaugeMiniNormalizeThemeMemoized(_theme) + + const { + gaugeHeight, + sidePaddings, + valueHeight, + barPaddings, + labelMain, + labelMainFontSize, + labelMainFontColor, + labelBarsEnabled, + labelBarsFontColor, + labelBarsFontSize, + colors, + axesSteps, + axesFontSize, + } = theme + const [barLabelsWidth] = useState([]) + + 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 = + Object.keys(values).length * (maxBarHeight + barPaddings) + + // create unified barsDefinition + + const [autocenterToken, setAutocenterToken] = useState(Date.now()) + useEffect(() => { + setAutocenterToken(Date.now()) + }, [ + width, + height, + barLabelWidth, + valueHeight, + gaugeHeight, + barPaddings, + sidePaddings, + labelMainFontSize, + axesSteps, + axesFontSize, + ]) + + /** return value as fraction 0->min 1->max */ + const getFrac = (val: number): number => (val - colors.min.value) / colorLen + + return ( + + + {labelMain && ( + + {labelMain} + + )} + {Object.entries(values).map(([group, value], i) => { + const y = 0 + i * (maxBarHeight + barPaddings) + const textCenter = y + maxBarHeight / 2 + const label = labelBarsEnabled ? group : '' + + // 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..d8e4cdfc0a --- /dev/null +++ b/src/shared/components/GaugeMiniChart.tsx @@ -0,0 +1,48 @@ +// 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' +import {GroupedData} from './LatestMultipleValueTransform' + +// Components + +// Types + +// Constants + +interface Props { + values: GroupedData + theme: GaugeMiniLayerConfig +} + +const GaugeMiniChart: FunctionComponent = (props: Props) => { + const {theme, values} = props + const themeOrDefault: Required = { + ...GAUGE_MINI_THEME_BULLET_DARK, + ...theme, + ...((theme as any) ? {gaugeColors: (theme as any).colors} : {}), + } + + 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..1718c5af9f --- /dev/null +++ b/src/shared/components/LatestMultipleValueTransform.tsx @@ -0,0 +1,76 @@ +// Libraries +import React, {useMemo, FunctionComponent} from 'react' +import {Table} from '@influxdata/giraffe' + +export type GroupedData = {[key: string]: number} + +const getTimeColumn = (table: Table) => { + // 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 [] + } + + return timeColData +} + +export const getLatestValuesGrouped = ( + table: Table, + groupColumnKey: string +): GroupedData => { + const valueCol = table.getColumn('_value', 'number') + const groupCol = table.getColumn(groupColumnKey) + + if (!valueCol.length) { + return {} + } + + return Object.fromEntries( + getTimeColumn(table) + // 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 linear 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}) => [groupCol?.[index] ?? '', valueCol[index]] as const) + // remove invalid data + .filter( + ([_, value]) => typeof value === 'number' && Number.isFinite(value) + ) + ) +} + +interface Props { + table: Table + children: (latestValue: GroupedData) => JSX.Element + quiet?: boolean +} + +export const LatestMultipleValueTransform: FunctionComponent = ({ + table, + quiet = false, + children, +}) => { + const latestValues = useMemo(() => getLatestValuesGrouped(table, '_field'), [ + table, + ]) + + if (Object.keys(latestValues).length === 0) { + return quiet ? null : ( +
+

No latest value found

+
+ ) + } + + return children(latestValues) +} diff --git a/src/shared/components/RefreshingView.tsx b/src/shared/components/RefreshingView.tsx index a69f83f3c9..82cd93e2af 100644 --- a/src/shared/components/RefreshingView.tsx +++ b/src/shared/components/RefreshingView.tsx @@ -122,6 +122,7 @@ class RefreshingView extends PureComponent { switch (properties.type) { case 'single-stat': case 'gauge': + case 'gauge-mini': return [properties.queries[0]] default: return properties.queries diff --git a/src/shared/components/ViewSwitcher.tsx b/src/shared/components/ViewSwitcher.tsx index 043e045713..87b42a3cac 100644 --- a/src/shared/components/ViewSwitcher.tsx +++ b/src/shared/components/ViewSwitcher.tsx @@ -4,6 +4,7 @@ import {Plot, FromFluxResult} 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' @@ -93,6 +95,14 @@ const ViewSwitcher: FunctionComponent = ({ )} ) + case 'gauge-mini': + return ( + + {latestValues => ( + + )} + + ) case 'xy': return ( { w: c.w, i: c.id, } - if (get(view, 'properties.type') === 'gauge') { + const type = get(view, 'properties.type') + if (type === 'gauge') { cell.minW = 5 cell.minH = 2.5 cell.maxW = 20 } + if (type === 'gauge-mini') { + cell.minW = 1 + cell.minH = 1 + cell.maxW = 20 + } return cell }) } diff --git a/src/shared/constants/gaugeMiniSpecs.ts b/src/shared/constants/gaugeMiniSpecs.ts new file mode 100644 index 0000000000..95fd9d2312 --- /dev/null +++ b/src/shared/constants/gaugeMiniSpecs.ts @@ -0,0 +1,100 @@ +import {GaugeMiniLayerConfig, InfluxColors} from '@influxdata/giraffe' +import {Color} from 'src/types/colors' + +export type GaugeMiniThemeString = + | 'GAUGE_MINI_THEME_BULLET_DARK' + | 'GAUGE_MINI_THEME_PROGRESS_DARK' + +export const gaugeMiniThemeStrings: GaugeMiniThemeString[] = [ + 'GAUGE_MINI_THEME_BULLET_DARK', + 'GAUGE_MINI_THEME_PROGRESS_DARK', +] + +export const gaugeMiniGetTheme = (theme: GaugeMiniThemeString) => { + switch (theme) { + case 'GAUGE_MINI_THEME_BULLET_DARK': + return GAUGE_MINI_THEME_BULLET_DARK + case 'GAUGE_MINI_THEME_PROGRESS_DARK': + return GAUGE_MINI_THEME_PROGRESS_DARK + } +} + +export const GAUGE_MINI_THEME_BULLET_DARK: Required = { + type: 'gauge mini', + mode: 'bullet', + textMode: 'follow', + + valueHeight: 18, + gaugeHeight: 25, + valueRounding: 2, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeMiniColors: [ + {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, + + labelBarsEnabled: false, + 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', + + valueHeight: 20, + gaugeHeight: 20, + valueRounding: 3, + gaugeRounding: 3, + barPaddings: 5, + sidePaddings: 20, + oveflowFraction: 0.03, + + gaugeMiniColors: [ + {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, + + labelBarsEnabled: false, + labelBarsFontSize: 11, + labelBarsFontColor: InfluxColors.Forge, + + valuePadding: 5, + valueFontSize: 18, + valueFontColorInside: InfluxColors.Raven, + valueFontColorOutside: InfluxColors.Cloud, + valueFormater: (val: number) => val.toFixed(0), + + axesSteps: undefined, + axesFontSize: 11, + axesFontColor: InfluxColors.Forge, + axesFormater: (val: number) => val.toFixed(0), +} diff --git a/src/shared/utils/formatStatValue.ts b/src/shared/utils/formatStatValue.ts index 2fa2b5cafa..1302f2a688 100644 --- a/src/shared/utils/formatStatValue.ts +++ b/src/shared/utils/formatStatValue.ts @@ -10,7 +10,7 @@ import {MAX_DECIMAL_PLACES} from 'src/dashboards/constants' // Utils import {preventNegativeZero} from 'src/shared/utils/preventNegativeZero' -interface FormatStatValueOptions { +export interface FormatStatValueOptions { decimalPlaces?: DecimalPlaces prefix?: string suffix?: string diff --git a/src/shared/utils/gaugeMiniThemeNormalize.ts b/src/shared/utils/gaugeMiniThemeNormalize.ts new file mode 100644 index 0000000000..e9e5b96380 --- /dev/null +++ b/src/shared/utils/gaugeMiniThemeNormalize.ts @@ -0,0 +1,124 @@ +import {formatStatValue, FormatStatValueOptions} from './formatStatValue' +import {color as d3Color} from 'd3-color' +import {range} from 'd3-array' +import {GaugeMiniLayerConfig} from '@influxdata/giraffe' +import {GaugeMiniColors} from '@influxdata/giraffe/dist/types' +import {useMemo} from 'react' + +export const throwReturn = (msg: string): T => { + throw new Error(msg) +} + +type RestrictedTypesProperties = { + colors?: GaugeMiniColors + gaugeMiniColors?: GaugeMiniColors + valueFormater?: (value: number) => string + + axesSteps: undefined | number[] + axesFormater?: (value: number) => string +} + +export type GaugeMiniThemeNormalized = Omit< + GaugeMiniLayerConfig, + keyof RestrictedTypesProperties +> & + RestrictedTypesProperties + +const getFormater = ( + formater: ((value: number) => string) | FormatStatValueOptions +): ((value: number) => string) => + typeof formater === 'function' + ? formater + : (value: number) => formatStatValue(value, formater) + +const getAxesSteps = ( + axesSteps: number | 'thresholds' | undefined | number[], + colors: GaugeMiniColors +): number[] | undefined => { + if (axesSteps === undefined || axesSteps === null) { + return undefined + } + const { + max: {value: max}, + min: {value: min}, + } = colors + + if (Array.isArray(axesSteps)) { + const steps = axesSteps.filter(x => x > min || x < max) + if (axesSteps.length !== steps.length) { + console.error(`All axes values must be inside range of colors!`) + } + return steps + } + + if (axesSteps === 'thresholds') { + return (colors.thresholds ?? []).map(x => x.value) + } + + if (Number.isInteger(axesSteps)) { + const colorLen = max - min + + return range(axesSteps).map( + x => ((x + 1) * colorLen) / (axesSteps + 1) + min + ) + } + + throw new Error( + `AxesSteps must be number | "thresholds" | number[]` + + ` | undefined. But it's value is ${JSON.stringify(axesSteps)}` + ) +} + +const getColors = ( + colors: Required['gaugeMiniColors'] +): GaugeMiniColors => { + if (!Array.isArray(colors)) { + return { + ...colors, + thresholds: (colors.thresholds ?? []).sort( + ({value: a}, {value: b}) => a - b + ), + } + } + + colors.forEach( + ({hex, name}) => + d3Color(hex) ?? + throwReturn(`Object "${hex}" isn"t valid color for name:${name}`) + ) + + return { + min: + colors.find(x => x.type === 'min') ?? + throwReturn('color of type min must be defined'), + max: + colors.find(x => x.type === 'max') ?? + throwReturn('color of type max must be defined'), + thresholds: colors + .filter(({type}) => type === 'threshold') + .sort(({value: a}, {value: b}) => a - b), + } +} + +export const gaugeMiniNormalizeThemeMemoized = ( + theme: Required +): Required => { + const colors = useMemo(() => getColors(theme.gaugeMiniColors), [ + theme.gaugeMiniColors, + ]) + + const axesSteps = useMemo(() => getAxesSteps(theme.axesSteps, colors), [ + theme.axesSteps, + colors, + ]) + + return { + ...theme, + colors, + gaugeMiniColors: colors, + valueFormater: getFormater(theme.valueFormater), + + axesSteps, + axesFormater: getFormater(theme.axesFormater), + } +} diff --git a/src/shared/visualization/types/GaugeMini/GaugeMiniAxesInputs.tsx b/src/shared/visualization/types/GaugeMini/GaugeMiniAxesInputs.tsx new file mode 100644 index 0000000000..8346ea365c --- /dev/null +++ b/src/shared/visualization/types/GaugeMini/GaugeMiniAxesInputs.tsx @@ -0,0 +1,134 @@ +// Libraries +import React, {FC} from 'react' + +// Components +import { + Button, + ButtonShape, + Input, + InputType, + AlignItems, + AutoInputMode, + ComponentSize, + FlexBox, + FlexDirection, + IconFont, + SquareButton, +} from '@influxdata/clockface' + +// Actions +import {setGaugeMiniProp} from 'src/timeMachine/actions' + +import {SelectGroup as _SelectGroup} from '@influxdata/clockface' +const {SelectGroup, Option: SelectGroupOption} = _SelectGroup + +type Prop = { + axesSteps: number | 'thresholds' | number[] | undefined + onUpdateProp: typeof setGaugeMiniProp +} + +export const GaugeMiniAxesInputs: FC = ({axesSteps, onUpdateProp}) => ( + <> + + { + onUpdateProp({axesSteps: undefined}) + }} + > + None + + { + onUpdateProp({axesSteps: 'thresholds'}) + }} + > + Thresholds + + { + onUpdateProp({axesSteps: 2}) + }} + > + Steps + + { + onUpdateProp({axesSteps: []}) + }} + > + Custom + + + {typeof axesSteps === 'number' && ( + onUpdateProp({axesSteps: +e.target.value})} + /> + )} + {Array.isArray(axesSteps) && ( + <> + +