diff --git a/changelog/unreleased/issue-20965.toml b/changelog/unreleased/issue-20965.toml new file mode 100644 index 000000000000..945c311ad85e --- /dev/null +++ b/changelog/unreleased/issue-20965.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Add unit handling to a single number widget" + +issues = ["20965"] +pulls = ["21339"] diff --git a/graylog2-web-interface/src/views/components/visualizations/number/NumberVisualization.tsx b/graylog2-web-interface/src/views/components/visualizations/number/NumberVisualization.tsx index 1c1559a5357f..aaee11fd9cfa 100644 --- a/graylog2-web-interface/src/views/components/visualizations/number/NumberVisualization.tsx +++ b/graylog2-web-interface/src/views/components/visualizations/number/NumberVisualization.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React, { useContext, useEffect, useRef } from 'react'; +import React, { useContext, useEffect, useRef, useMemo } from 'react'; import styled, { css } from 'styled-components'; import type { Rows } from 'views/logic/searchtypes/pivot/PivotHandler'; @@ -27,9 +27,13 @@ import NumberVisualizationConfig from 'views/logic/aggregationbuilder/visualizat import type { VisualizationComponentProps } from 'views/components/aggregationbuilder/AggregationBuilder'; import { makeVisualization, retrieveChartData } from 'views/components/aggregationbuilder/AggregationBuilder'; import ElementDimensions from 'components/common/ElementDimensions'; +import useWidgetUnits from 'views/components/visualizations/hooks/useWidgetUnits'; +import useFeature from 'hooks/useFeature'; +import { UNIT_FEATURE_FLAG } from 'views/components/visualizations/Constants'; +import { parseSeries } from 'views/logic/aggregationbuilder/Series'; -import Trend from './Trend'; import AutoFontSizer from './AutoFontSizer'; +import Trend from './Trend'; const Container = styled.div<{ $height: number }>(({ $height }) => css` height: ${$height}px; @@ -89,6 +93,8 @@ const _extractFirstSeriesName = (config) => { const NumberVisualization = ({ config, fields, data, height: heightProp }: VisualizationComponentProps) => { const targetRef = useRef(); + const unitFeatureEnabled = useFeature(UNIT_FEATURE_FLAG); + const widgetUnits = useWidgetUnits(config); const onRenderComplete = useContext(RenderCompletionCallback); const visualizationConfig = (config.visualizationConfig as NumberVisualizationConfig) ?? NumberVisualizationConfig.create(); @@ -99,6 +105,13 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua const trendRows = data.trend; const { value } = _extractValueAndField(chartRows); const { value: previousValue } = _extractValueAndField(trendRows || []); + const unit = useMemo(() => { + if (!unitFeatureEnabled) return undefined; + + const fieldNameKey = parseSeries(field).field; + + return widgetUnits.getFieldUnit(fieldNameKey); + }, [field, unitFeatureEnabled, widgetUnits]); if (!field || (value !== 0 && !value)) { return <>N/A; @@ -115,7 +128,8 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua + render={DecoratedValue} + unit={unit} /> )} @@ -127,7 +141,8 @@ const NumberVisualization = ({ config, fields, data, height: heightProp }: Visua + trendPreference={visualizationConfig.trendPreference} + unit={unit} /> )} diff --git a/graylog2-web-interface/src/views/components/visualizations/number/Trend.tsx b/graylog2-web-interface/src/views/components/visualizations/number/Trend.tsx index cc8682f03189..c632d99b32f4 100644 --- a/graylog2-web-interface/src/views/components/visualizations/number/Trend.tsx +++ b/graylog2-web-interface/src/views/components/visualizations/number/Trend.tsx @@ -21,6 +21,13 @@ import numeral from 'numeral'; import Icon from 'components/common/Icon'; import type { TrendPreference } from 'views/logic/aggregationbuilder/visualizations/NumberVisualizationConfig'; +import type FieldUnit from 'views/logic/aggregationbuilder/FieldUnit'; +import { + getPrettifiedValue, + convertValueToUnit, +} from 'views/components/visualizations/utils/unitConverters'; +import formatValueWithUnitLabel from 'views/components/visualizations/utils/formatValueWithUnitLabel'; +import getUnitTextLabel from 'views/components/visualizations/utils/getUnitTextLabel'; type TrendDirection = 'good' | 'bad' | 'neutral'; @@ -28,6 +35,7 @@ type Props = { current: number, previous: number | undefined | null, trendPreference: TrendPreference, + unit?: FieldUnit, }; const background = (theme: DefaultTheme, trend: TrendDirection = 'neutral') => ({ @@ -98,19 +106,53 @@ const diff = (current: number | undefined, previous: number | undefined): [numbe return [NaN, NaN]; }; -const Trend = React.forwardRef(({ current, previous, trendPreference }: Props, ref) => { +const getTrendConvertedValues = (current: number, previous: number, fieldUNit: FieldUnit): { + previousConverted: number | string, + differenceConverted: number, + differencePercent: number, + unitAbbrevString: string, +} => { const [difference, differencePercent] = diff(current, previous); - const backgroundTrend = _trendDirection(difference, trendPreference); - const trendIcon = _trendIcon(difference); + if (!fieldUNit?.isDefined) { + return ({ + previousConverted: previous, + differenceConverted: difference, + differencePercent, + unitAbbrevString: '', + }); + } + + const originalParams = { unitType: fieldUNit?.unitType, abbrev: fieldUNit?.abbrev }; + const { unit: currentPrettyUnit } = getPrettifiedValue(current, originalParams); + const currentPrettyParams = { unitType: currentPrettyUnit?.unitType, abbrev: currentPrettyUnit?.abbrev }; + const { value: prettyDiff } = convertValueToUnit(difference, originalParams, currentPrettyParams); + const { value: previousPretty } = convertValueToUnit(previous, originalParams, currentPrettyParams); + + return ({ + previousConverted: `${formatValueWithUnitLabel(previousPretty, currentPrettyUnit.abbrev)} (${previous})`, + unitAbbrevString: ` ${getUnitTextLabel(currentPrettyUnit.abbrev)}`, + differenceConverted: prettyDiff, + differencePercent, + }); +}; + +const Trend = React.forwardRef(({ current, previous, trendPreference, unit = undefined }: Props, ref) => { + const { differenceConverted, differencePercent, unitAbbrevString, previousConverted } = getTrendConvertedValues(current, previous, unit); + + const backgroundTrend = _trendDirection(differenceConverted, trendPreference); + const trendIcon = _trendIcon(differenceConverted); - const absoluteDifference = Number.isFinite(difference) ? numeral(difference).format('+0,0[.]0[000]') : '--'; + const absoluteDifference = Number.isFinite(differenceConverted) ? `${numeral(differenceConverted).format('+0,0[.]0[000]')}${unitAbbrevString}` : '--'; const relativeDifference = Number.isFinite(differencePercent) ? numeral(differencePercent).format('+0[.]0[0]%') : '--'; return ( - {absoluteDifference} / {relativeDifference} + {' '} + + {absoluteDifference} / {relativeDifference} + ); diff --git a/graylog2-web-interface/src/views/components/visualizations/utils/unitConverters.ts b/graylog2-web-interface/src/views/components/visualizations/utils/unitConverters.ts index a8caa7ea4087..2772c2437a37 100644 --- a/graylog2-web-interface/src/views/components/visualizations/utils/unitConverters.ts +++ b/graylog2-web-interface/src/views/components/visualizations/utils/unitConverters.ts @@ -130,20 +130,28 @@ const _convertValueToUnit = (units: FieldUnitTypes, value: number, fromParams: C export const _getPrettifiedValue = (units: FieldUnitTypes, initValue: number | string, params: ConversionParams): ConvertedResult => { const currentUnit = units?.[params?.unitType] ?? null; - const value = initValue === null ? null : toNumber(initValue); - if (!(value && currentUnit)) return ({ value, unit: currentUnit ? currentUnit.find(({ abbrev }) => abbrev === params.abbrev) : null }); + const _value = initValue === null ? null : toNumber(initValue); + if (!(_value && currentUnit)) return ({ value: _value, unit: currentUnit ? currentUnit.find(({ abbrev }) => abbrev === params.abbrev) : null }); - const allConvertedValues = Object.values(currentUnit).map((unit: Unit) => _convertValueToUnit(units, value, params, { abbrev: unit.abbrev, unitType: unit.unitType })); + const sign = Math.sign(_value); + const absolutValue = Math.abs(_value); + + const allConvertedValues = Object.values(currentUnit).map((unit: Unit) => _convertValueToUnit(units, absolutValue, params, { abbrev: unit.abbrev, unitType: unit.unitType })); const filtratedValues = allConvertedValues.filter(({ value: val, unit }) => val >= 1 && unit.useInPrettier); + let result: ConvertedResult; + if (filtratedValues.length > 0) { - return minBy(filtratedValues, ({ value: val }) => val); + result = minBy(filtratedValues, ({ value: val }) => val); + } else { + const filtratedValuesLower = allConvertedValues.filter(({ value: val, unit }) => val < 1 && unit.useInPrettier); + result = maxBy(filtratedValuesLower, ({ value: val }) => val); } - const filtratedValuesLower = allConvertedValues.filter(({ value: val, unit }) => val < 1 && unit.useInPrettier); + result.value *= sign; - return maxBy(filtratedValuesLower, ({ value: val }) => val); + return result; }; export type ConvertValueToUnit = (value: number, fromParams: ConversionParams, toParams: ConversionParams) => ConvertedResult